4  Tidyverse

Programmeertalen hebben alvast dit gemeen met natuurlijke talen: gebruikers ontwikkelen steeds variëteiten die na verloop van tijd kunnen evolueren naar nieuwe dochtertalen.

In R is er momenteel een “variant” genaamd tidyverse (Wickham et al. 2019), dat heel populair geworden is vanwege zijn consistentie en (relatieve) eenvoud. Een van de kernideeën is dat alle datastructuren en output weergegeven worden als tidy data, waarbij elke rij een observatie weergeeft.

Hier maken we een “tibble” – een vereenvoudigede vorm van een dataframe. De code heb ik overgenomen uit het tidyr-vignette. Een vignette is een uitgewerkt voorbeeld van de functionaliteiten van een packages; probeer bvb. vignette("tidy-data").

1library(tibble)
2classroom <- tribble(
  ~name,    ~quiz1, ~quiz2, ~test1,
  "Billy",  NA,     "D",    "C",
  "Suzy",   "F",    NA,     NA,
  "Lionel", "B",    "C",    "B",
  "Jenny",  "A",    "A",    "B"
  )
classroom
1
Een tibble is een vereenvoudigde vorm van een dataframe.
2
een tribble is een tibble die gemaakt wordt aan de hand van rijen. Vergelijk dit met de manier waarop we in het vorige hoofdstuk een dataframe gemaakt hebben en waarbij we vectoren samen hebben gevoegd tot een dataframe. Hier hoeven we enkel rijen in te geven. De tilde \(\sim\) duidt de naam van de variabelen aan.
# A tibble: 4 × 4
  name   quiz1 quiz2 test1
  <chr>  <chr> <chr> <chr>
1 Billy  <NA>  D     C    
2 Suzy   F     <NA>  <NA> 
3 Lionel B     C     B    
4 Jenny  A     A     B    

Elke kolom is één variabele, elke rij is één observatie, elke cel is een unieke waarde. Nu zou je kunnen denken dat dit niets speciaals is, maar dezelfde datastructuur wordt consistent gebruikt in doorheen de analyses en output. Dit wordt naderhand duidelijker.

Alhoewel de functionaliteit van base-R door niemand betwist wordt, kiezen veel handboeken en onderzoekers tegenwoordig voor de tidyverse-aanpak. Een bondig hoofdstuk over deze aanpak kan dan ook niet ontbreken.

We openen tidyverse.

library(tidyverse)

Hierdoor openen we meerdere packages tegelijkertijd. In dit hoofdstuk bekijken we twee pakketten in het bijzonder:

  1. dplyr: om data te verkennen en te manipuleren (Wickham et al. 2023)
  2. tidyr: om het dataformaat te transformeren van het lange naar het wijde dataformaat of omgekeerd.

4.1 Dataset: Falsebeginners

Om de functionaliteiten van dplyr te illustreren gebruiken we de dataset van De Wilde et al. (De Wilde, Brysbaert, and Eyckmans 2019) De dataset is beschikbaar via: https://osf.io/ndr47/. Ik heb voor dit hoofdstuk een aantal variabelen geselecteerd en alles vertaald naar het Nederlands. Table 4.1 biedt een overzicht van de variabelen.

Table 4.1: Codeboek Falsebeginners
ID Variabele Toelichting
1 Leerling Een uniek nummer voor elke leerling
2 School Een uniek nummer voor elke school
3 Klas Een uniek nummer voor elke klas
4 PPVT Score voor PPVT. Totaal = 120. PPVT = “Peabody Picture Vocabulary Test”
5 Spreken Score voor de spreektest. Totaal = 20
6 Luisteren Score voor de luistertest. Totaal = 25
7 LezenSchrijven Score voor de testen lezen en schrijven. Totaal = 50
8 Attitude Attitude tegenover Engels. Positief vs. Negatief
9 Thuistaal Nederlands vs. Meertalig
10 Geslacht Man vs. Vrouw

Het bestand dat we gebruiken is een tab delimited .txt-bestand, dat we openen met read.delim().

1fb <- read.delim("Falsebeginners.txt",
2                 header = TRUE,
3                 na.strings = NA,
4                 stringsAsFactors=TRUE)
1
de naam van het bestand dat we openen. Dit bestand moet in dezelfde map zitten als je R-script of Notebook. Hier gebruiken we read.delim () omdat het een .txt-bestand is.
2
de eerste rij van de dataset bevat de namen van de variabelen
3
ontbrekende waarden zijn aangeduid als NA in de dataset
4
strings (\(\approx\) “woorden”) worden geïnterpreteerd als levels van een factorvariabele

4.2 Dataverkenning

We vatten alle variabelen samen voor een eerste kennismaking.

summary(fb)
    Leerling         School         Klas          PPVT          Spreken      
 Min.   :  2.0   S12    : 51   9A     : 29   Min.   : 31.0   Min.   : 0.000  
 1st Qu.:219.8   S34    : 49   12A    : 27   1st Qu.: 69.0   1st Qu.: 2.000  
 Median :445.5   S33    : 37   11A    : 24   Median : 78.0   Median : 5.000  
 Mean   :441.1   S38    : 36   12B    : 24   Mean   : 78.6   Mean   : 6.786  
 3rd Qu.:660.2   S51    : 36   42A    : 24   3rd Qu.: 88.0   3rd Qu.:10.000  
 Max.   :867.0   S8     : 35   21A    : 21   Max.   :116.0   Max.   :20.000  
                 (Other):536   (Other):631   NA's   :1       NA's   :13      
   Luisteren     SchrijvenLezen      Attitude        Thuistaal    Geslacht  
 Min.   : 0.00   Min.   : 0.00   negatief: 27   Meertalig :207   man  :402  
 1st Qu.:10.00   1st Qu.:13.00   positief:733   Nederlands:567   vrouw:378  
 Median :15.00   Median :18.00   NA's    : 20   NA's      :  6              
 Mean   :14.95   Mean   :21.16                                              
 3rd Qu.:20.00   3rd Qu.:29.00                                              
 Max.   :25.00   Max.   :50.00                                              
 NA's   :2       NA's   :1                                                  

We zien enkele NA-waarden. Die waarden laten we voorlopig in de dataset. Bij de variabelen Spreken en SchrijvenLezen zijn er minimale waarden van \(0\). Dit is eigenaardig want je zou op zijn minst verwachten dat er toch iets correct was op de testen. Laten we die nulwaarden voorlopig ook behouden in onze data.

De functies die we tot nu toe gebruikt hebben in dit hoofdstuk behoren allemaal tot base-R. In de volgende paragrafen exploreren we onze dataset met dplyr-functies.

4.3 Data samenvatten met dplyr

Een van kernideeën achter de tidyverse-aanpak is om geneste functies te vermijden en om met sequentiële code te werken. Ook het dollarteken als verwijzing naar een variabele in een dataset (data$variabele) wordt vermeden. Daarnaast beoogt tidyverse meer consistentie in de codesyntax.

Als eerste kennismaking met deze filosofie vergelijken we de berekening van het gemiddelde van PPVT in base-R en in tidyverse.

  • Base-R:
mean(fb$PPVT, na.rm = TRUE)
[1] 78.5982
  • Tidyverse:
1fb |>
2  summarise(MEAN = mean(PPVT, na.rm = TRUE))
1
Kies de fb-dataset. Opgepast: kiezen veronderstelt dat je de data al geopend hebt. Als je meerdere datasets geopend hebt kun je er een kiezen.
2
we gebruiken summarize() om data samen te vatten, en meer specifiek de mean() functie. MEAN is een nieuwe naam voor het resultaat.
     MEAN
1 78.5982

Het fbobject is een dataframe met 1 kolom en 1 rij, waarbij we zelf een naam gegeven hebben aan de variabele (MEAN). Op het eerste gezicht is de dplyr-code langer en omslachtiger. Het voordeel is echter dat we dezelfde code gemakkelijk kunnen uitbreiden met extra samenvattingen, en dat we de output in een dataframe krijgen.

1fb |>
2  summarise(mean = mean(PPVT, na.rm = TRUE),
            median = median(PPVT, na.rm = TRUE),
            min = min(PPVT, na.rm = TRUE),
            max = max(PPVT, na.rm = TRUE),
            SD = sd(PPVT, na.rm = TRUE))
1
kies fb
2
een lijst met functies om de data samen te vatten.
     mean median min max       SD
1 78.5982     78  31 116 13.85053

Summarize() is een van de basisfunctie in dplyr. Daarnaast zijn er nog een viertal functies die samen gebruikt worden en die we hieronder achtereenvolgens toelichten.

  • select() selecteert variabelen/kolommen
  • filter() selecteert rijen
  • mutate() transformeert variabelen naar nieuwe variabelen
  • group_by() groepeert data volgens een (categorische) variabele
  • summarise() vat variabelen samen

4.3.1 group_by()

Wat is de gemiddelde PPVT van beide geslachten?

1fb |>
2  group_by(Geslacht) |>
3  summarise(mean = mean(PPVT, na.rm = TRUE))
1
Kies fb
2
Groepeer volgens Geslacht
3
Geef de gemiddelde PPVT-score (volgens Geslacht dus)
# A tibble: 2 × 2
  Geslacht  mean
  <fct>    <dbl>
1 man       81.7
2 vrouw     75.3
1fb |>
2  group_by(Geslacht) |>
3  summarise(mean = mean(PPVT, na.rm = TRUE),
            median = median(PPVT, na.rm = TRUE),
            min = min(PPVT, na.rm = TRUE),
            max = max(PPVT, na.rm = TRUE),
            SD = sd(PPVT, na.rm = TRUE))
1
Kies fb als dataset
2
groepeer volgens Geslacht
3
bereken enkele samenvattende waarden voor PPVT.
# A tibble: 2 × 6
  Geslacht  mean median   min   max    SD
  <fct>    <dbl>  <dbl> <int> <int> <dbl>
1 man       81.7     82    31   116  15.1
2 vrouw     75.3     75    45   108  11.5

We voegen Attitude toe als extra groeperende variabele:

1fb |>
2  group_by(Geslacht, Attitude) |>
3  summarise(mean = mean(PPVT, na.rm = TRUE),
            median = median(PPVT, na.rm = TRUE),
            min = min(PPVT, na.rm = TRUE),
            max = max(PPVT, na.rm = TRUE),
            SD = sd(PPVT, na.rm = TRUE))
1
Kies fb als dataset
2
groepeer volgens Geslacht en Attitude
3
bereken enkele samenvattende waarden voor PPVT.
`summarise()` has grouped output by 'Geslacht'. You can override using the
`.groups` argument.
# A tibble: 6 × 7
# Groups:   Geslacht [2]
  Geslacht Attitude  mean median   min   max    SD
  <fct>    <fct>    <dbl>  <dbl> <int> <int> <dbl>
1 man      negatief  68.6   67.5    36    99 14.4 
2 man      positief  82.3   82      31   116 14.8 
3 man      <NA>      78.5   71      55   110 18.3 
4 vrouw    negatief  67.5   64      49    81  9.78
5 vrouw    positief  75.7   75      45   108 11.4 
6 vrouw    <NA>      70.1   68      54    97 14.3 

We verwijderen gemakshalve alle NAs. We voegen daarvoor drop_na() toe aan de pijplijn.

1fb |>
2  drop_na() |>
3  group_by(Geslacht, Attitude) |>
4  summarise(mean = mean(PPVT),
            median = median(PPVT),
            min = min(PPVT),
            max = max(PPVT),
            SD = sd(PPVT))
1
Kies fb als dataset
2
Laat alle rijen met NAs weg
3
groepeer volgens Geslacht en Attitude
4
bereken enkele samenvattende waarden voor PPVT.
`summarise()` has grouped output by 'Geslacht'. You can override using the
`.groups` argument.
# A tibble: 4 × 7
# Groups:   Geslacht [2]
  Geslacht Attitude  mean median   min   max    SD
  <fct>    <fct>    <dbl>  <dbl> <int> <int> <dbl>
1 man      negatief  67.7     67    36    99 14.6 
2 man      positief  82.0     82    31   116 14.8 
3 vrouw    negatief  67.5     64    49    81  9.78
4 vrouw    positief  75.7     75    45   108 11.3 

We zien onmiddellijk dat een positieve attitude tegenover Engels geassocieerd is met een hogere gemiddelde PPVT-score voor beide geslachten en dat dit iets meer uitgesproken is bij jongens.1

4.3.2 filter() en select()

We selecteren Leerlingen, Scholen en Klassen voor Leerlingen die meer dan \(110/120\) scoren voor PPVT:

1fb |>
2  select(Leerling, School, Klas, PPVT) |>
3  filter(PPVT > 110)
1
Kies fb als dataset
2
Selecteer de gewenste variabelen
3
hou enkel rijen met PPVT groter dan 110.
  Leerling School Klas PPVT
1      183    S11  11A  113
2      208    S12  12A  113
3      304    S19  19A  116
4      364    S21  21A  115
5      416    S24  24B  112
6      427    S25  25A  111
7      846    S51  51B  112
8      847    S51  51B  115

Hoeveel scholen zijn er?

1fb |>
2  select(Leerling, School, Klas, PPVT) |>
3  filter(PPVT > 110) |>
4  count(School)
1
Kies fb als dataset
2
selecteer de gewenste variabelen
3
hou enkel rijen met PPVT groter dan \(110\)
4
tel het aantal scholen.
  School n
1    S11 1
2    S12 1
3    S19 1
4    S21 1
5    S24 1
6    S25 1
7    S51 2

Er zijn zeven scholen en in een daarvan zijn er twee leerlingen die meer dan \(110\) scoren voor PPVT.

4.3.3 mutate()

Met mutate() creëren we een nieuwe variabele die we toevoegen aan de dataset. We kunnen bijvoorbeeld een gestandaardiseerde PPVT score (z-score) berekenen en toevoegen. Waarbij we hieronder onmiddellijk ook de gemiddelde z-score berekenen per Geslacht.

1fb |>
2  drop_na() |>
3  mutate(PPVT_z = (PPVT-mean(PPVT))/sd(PPVT)) |>
4  group_by(Geslacht) |>
5  summarize(AVG = mean(PPVT_z))
1
Kies fb als dataset
2
laat alle rijen met NAs weg
3
bereken de nieuwe variabele PPVT_Z
4
groepeer volgens Geslacht
5
Bereken het gemiddelde
# A tibble: 2 × 2
  Geslacht    AVG
  <fct>     <dbl>
1 man       0.220
2 vrouw    -0.230

Jongens scoren boven het gemiddelde, meisjes eronder.

4.4 Data pivoteren

Vergelijk de twee datasets in Table 4.2. In Table 4.2 (a) krijgt elke observatie (elke score voor elk moment) een eigen rij. Daardoor zijn er twee rijen per leerling. We hebben een binaire categorische variabele Test en een continue variabele Score. In Table 4.2 (b) geeft elke rij de gegevens voor 1 leerling weer. Er zijn twee testmomenten (Pre- en Posttest) die apart worden weergegeven. Dit wijde dataformaat is gebruikelijk bij gepaarde en longitudinale data waarbij er meerdere metingen zijn voor 1 element. Het formaat is belangrijk om de data correct te kunnen analyseren en vooral te visualiseren. Daarom is het noodzakelijk om van het ene naar het andere formaat te kunnen omschakelen. Alles opnieuw overschrijven is een mogelijkheid, maar die is veel te omslachtig. We kunnen code gebruiken om de data te “pivoteren”.

Table 4.2: Het lange en wijde dataformaat

(a) Het lange dataformaat
Leerling Test Score
1 pretest 8
1 posttest 12
2 pretest 12
2 posttest 14
3 pretest 9
3 posttest 8
(b) Het wijde dataformaat
Leerling Pretest Posttest
1 8 12
2 12 14
3 9 8

We illustreren de twee pivotfuncties op basis van de data Table 4.2. We maken eerste een dataframe op basis van het wijde dataformaat.

df <- data.frame(Leerling = c(1,2,3),            
                 Pretest = c(8,12,9),
                 Posttest = c(12,14,8))
df
  Leerling Pretest Posttest
1        1       8       12
2        2      12       14
3        3       9        8

We tranformeren de data terug naar het lange dataformaat met pivot_longer()

1df_long <- df |>
2  pivot_longer(cols = c(Pretest, Posttest),
3               names_to = "Test",
4               values_to = "Score")
df_long
1
We creëren een nieuwe dataset die we df_long noemen.
2
selecteer de kolommen om te transformeren
3
Test is de naam van de nieuwe variabele met de twee levels “Pretest” en “Posttest”.
4
Score is de naam van de nieuwe variabele met de scores voor de twee testen.
# A tibble: 6 × 3
  Leerling Test     Score
     <dbl> <chr>    <dbl>
1        1 Pretest      8
2        1 Posttest    12
3        2 Pretest     12
4        2 Posttest    14
5        3 Pretest      9
6        3 Posttest     8

We transformeren nu de lange dataset df_long naar het wijde formaat.

1df_wide <- df_long |>
2  pivot_wider(id_cols = Leerling,
3              names_from = Test,
4              values_from = Score)
df_wide
1
De naam voor de getransformeerde dataset.
2
De variabele die de waarden in de dataset clustert.
3
De variabele waarvan de categorieën nieuwe kolommen/variabelen worden.
4
De variabele waar de waarden vandaan komen.
# A tibble: 3 × 3
  Leerling Pretest Posttest
     <dbl>   <dbl>    <dbl>
1        1       8       12
2        2      12       14
3        3       9        8

4.5 Samenvatting en vooruitblik

In dit hoofdstuk hebben we kennis gemaakt met de voornaamste functie in de tidyverse-aanpak, voornamelijk dan met dplyr . Het grootste voordeel om met tidyverse te werken is de consistentie in de code en de relatieve eenvoud van de syntax. Dat alle output als een dataframe gestructureerd wordt, heeft als bijkomend voordeel dat die zich uitstekend leent voor een visualisatie met ggplot2.

Tot slot wil ik hier ook nog opmerken dat er ook kritiek gegeven kan worden op een al te exclusief gebruik van tidyverse. Norm Matloff geeft enkele sterke argumenten waarom ook base-R belangrijk blijft: https://github.com/matloff/TidyverseSkeptic. Zowel base-R als tidyverse gebruik ik in dit handboek en hebben hun plaats in onze gereedschapskist.

4.6 Terminologie

  • tibble
  • tidy data
  • data pivoteren
  • het lange en wijde dataformaat

Functies:

  • read.delim()
  • tibble()
  • tribble()
  • data.frame()
  • drop_na()
  • select()
  • filter()
  • mutate()
  • group_by()
  • summarise()
  • pivot_wider()
  • pivot_longer()

  1. We kunnen hier discussiëren over de kip of het ei. Heeft Attitude een positief effect of is de positieve attitude een gevolg van de betere score? We kunnen dit causaliteitsvraagstuk niet beantwoorden op basis van deze analyse. Vandaar ook de term “geassocieerd”, die beide richtingen toelaat.↩︎