9 Joining mit dplyr

Laden Sie die folgenden Packages un Data Frames:

library(tidyverse)
library(magrittr)
url <- "http://www.phonetik.uni-muenchen.de/~jmh/lehre/Rdf"
vcv <- read.table(file.path(url, "vcvC.txt"))

Vielleicht kennen Sie von der Arbeit mit relationalen Daten(banken) und/oder SQL den sogenannten join. Das Prinzip relationaler Daten beruht darauf, alle Informationen und Messwerte, die man gesammelt und erhoben hat, in thematisch sinnvoll aufgeteilten Tabellen abzuspeichern. So könnte man nach einem Experiment zum Beispiel eine Tabelle mit den Messwerten (Formantwerte, Grundfrequenz, etc.) und eine Tabelle mit den Metadaten (Alter, Herkunft, Bildungsstand, etc.) zu den Sprechern erstellen. Zusätzlich bietet es sich vielleicht an, eine Tabelle mit Informationen über das erhobene Material (Wörter, Betonung, Phoneme, Stimuli, etc.) und über die getesteten Bedingungen (Sprechtempo, welcher Gesprächspartner, etc.) zu haben. Wenn nötig, kann man jederzeit (und ggf. nur temporär) zwei Tabellen über einen key, d.h. eine Identifikationsspalte, miteinander verbinden. Bei dplyr unterscheiden wir zwischen mutating joins und filtering joins.

9.1 Mutating Joins

Wie auch bei mutate() werden einem Data Frame x durch einen mutating join weitere Spalten hinzugefügt. Im Gegensatz zu mutate() stammen die neuen Spalten aber aus einem anderen Data Frame y. In beiden Data Frames x und y muss es mindestens eine Spalte geben, die einen identifier oder key enthält, über den die Tabellen verbunden werden können.

9.1.1 Inner Join

Die einfachste Form des mutating join ist der sogenannte inner join, der mittels der Funktion inner_join() durchgeführt wird. Die Funktion bekommt als Argumente zwei Data Frames x und y und den Identifier im Argument by. Das Ergebnis eines inner join enthält alle Spalten von x und y sowie alle Zeilen, für die es einen Match in beiden Data Frames gibt. Fehlende Werte (NA) tauchen nicht im Ergebnis eines inner join auf, daher ist hier immer Vorsicht angebracht.

Als einfaches Beispiel verwenden wir hier eine Tabelle mit Grundfrequenzwerten von zehn Sprechern und eine Tabelle mit Metadaten über die zehn Sprecher:

measures <- tibble(subject = rep(paste0("s", 1:10), each = 10),
                   F0 = rnorm(100, 120, 15))
measures
## # A tibble: 100 × 2
##    subject    F0
##    <chr>   <dbl>
##  1 s1      124. 
##  2 s1      112. 
##  3 s1      124. 
##  4 s1      109. 
##  5 s1      119. 
##  6 s1      134. 
##  7 s1      136. 
##  8 s1      104. 
##  9 s1      131. 
## 10 s1       99.4
## # ℹ 90 more rows
meta <- tibble(subject = paste0("s", 1:10),
               age = rep(c("old", "young"), each = 5))
meta
## # A tibble: 10 × 2
##    subject age  
##    <chr>   <chr>
##  1 s1      old  
##  2 s2      old  
##  3 s3      old  
##  4 s4      old  
##  5 s5      old  
##  6 s6      young
##  7 s7      young
##  8 s8      young
##  9 s9      young
## 10 s10     young

Beide tibbles haben die Spalte subject, die wir als key bei unserem inner join benutzen werden:

inner_join(x = measures, y = meta, by = "subject")
## # A tibble: 100 × 3
##    subject    F0 age  
##    <chr>   <dbl> <chr>
##  1 s1      124.  old  
##  2 s1      112.  old  
##  3 s1      124.  old  
##  4 s1      109.  old  
##  5 s1      119.  old  
##  6 s1      134.  old  
##  7 s1      136.  old  
##  8 s1      104.  old  
##  9 s1      131.  old  
## 10 s1       99.4 old  
## # ℹ 90 more rows

Es kann vorkommen, dass die key-Spalte in den zwei Data Frames unterschiedlich benannt ist. In diesem Fall sagen wir der Funktion mittels by = c("a"="b"), dass die Spalte a aus dem Data Frame x mit der Spalte b aus dem Data Frame y gematched werden soll (das gilt für alle join Funktionen):

measures %<>% rename(Vpn = subject)
inner_join(x = measures, y = meta, by = c("Vpn"="subject"))
## # A tibble: 100 × 3
##    Vpn      F0 age  
##    <chr> <dbl> <chr>
##  1 s1    124.  old  
##  2 s1    112.  old  
##  3 s1    124.  old  
##  4 s1    109.  old  
##  5 s1    119.  old  
##  6 s1    134.  old  
##  7 s1    136.  old  
##  8 s1    104.  old  
##  9 s1    131.  old  
## 10 s1     99.4 old  
## # ℹ 90 more rows
measures %<>% rename(subject = Vpn)

In diesem Beispiel werden bislang immer alle Zeilen von measures zurückgegeben und alle Spalten beider Data Frames. Das liegt daran, dass es sowohl in measures als auch in meta Informationen zu denselben zehn Probanden gibt. Wenn wir in einem der Data Frames mehr oder weniger Informationen haben, werden plötzlich Zeilen weggelassen.

# Messwerte von 20 statt 10 Sprechern:
measures <- tibble(subject = rep(paste0("s", 1:20), each = 10),
                   F0 = rnorm(200, 120, 15))
inner_join(x = measures, y = meta, by = "subject")
## # A tibble: 100 × 3
##    subject    F0 age  
##    <chr>   <dbl> <chr>
##  1 s1       119. old  
##  2 s1       123. old  
##  3 s1       117. old  
##  4 s1       136. old  
##  5 s1       128. old  
##  6 s1       118. old  
##  7 s1       121. old  
##  8 s1       104. old  
##  9 s1       170. old  
## 10 s1       125. old  
## # ℹ 90 more rows

In diesem Ergebnis gibt es keine Zeilen für die Sprecher 11 bis 20, weil es zu diesen Sprechern keine Informationen im Data Frame meta gibt!

9.1.2 Outer Join

Im Unterschied zum inner join werden beim outer join auch Zeilen mit fehlenden Werten beibehalten. Diese fehlenden Werte werden als NA gekennzeichnet. Die einfachste Version eines outer join ist der full join, bei dem alle Zeilen und alle Spalten aus beiden Data Frames beibehalten werden. Die entsprechende Funktion dazu heißt full_join() (wir frisieren das Ergebnis hier mit slice(), um einen interessanten Teil des Ergebnisses hervorzuheben):

full_join(x = measures, y = meta, by = "subject") %>% 
  slice(95:105)
## # A tibble: 11 × 3
##    subject    F0 age  
##    <chr>   <dbl> <chr>
##  1 s10      122. young
##  2 s10      155. young
##  3 s10      119. young
##  4 s10      108. young
##  5 s10      120. young
##  6 s10      122. young
##  7 s11      129. <NA> 
##  8 s11      106. <NA> 
##  9 s11      116. <NA> 
## 10 s11      123. <NA> 
## 11 s11      116. <NA>

Dieses Ergebnis besteht aus 200 Zeilen (wenn wir nicht slice() darauf anwenden) – als wir oben die Funktion inner_join() auf die exakt selben tibbles ausgeführt haben, hatte das Ergebnis nur 100 Zeilen. Das liegt daran, dass full_join() die 100 Zeilen mit den Messwerten der Sprecher 11 bis 20 beibehalten hat, während inner_join() diese Zeilen gelöscht hat, weil es für diese Sprecher keine Informationen aus meta zu holen gab. Beim Ergebnis des full_join() finden sich deshalb NA-Werte in der angehängten Spalte age für die Sprecher 11 bis 20.

Wenn Sie nicht alle Zeilen aus beiden Data Frames übernehmen wollen, stehen Ihnen die Funktionen left_join() und right_join() zur Verfügung. Beim left_join() werden alle Zeilen aus Data Frame x und beim right_join() alle Zeilen aus dem Data Frame y übernommen. In unserem Beispiel wollen wir alle Zeilen aus measures erhalten und nur die Altersinformation aus meta hinzufügen:

left_join(x = measures, y = meta, by = "subject")
## # A tibble: 200 × 3
##    subject    F0 age  
##    <chr>   <dbl> <chr>
##  1 s1       119. old  
##  2 s1       123. old  
##  3 s1       117. old  
##  4 s1       136. old  
##  5 s1       128. old  
##  6 s1       118. old  
##  7 s1       121. old  
##  8 s1       104. old  
##  9 s1       170. old  
## 10 s1       125. old  
## # ℹ 190 more rows
right_join(x = meta, y = measures, by = "subject")
## # A tibble: 200 × 3
##    subject age      F0
##    <chr>   <chr> <dbl>
##  1 s1      old    119.
##  2 s1      old    123.
##  3 s1      old    117.
##  4 s1      old    136.
##  5 s1      old    128.
##  6 s1      old    118.
##  7 s1      old    121.
##  8 s1      old    104.
##  9 s1      old    170.
## 10 s1      old    125.
## # ℹ 190 more rows

9.2 Filtering Joins

Die zweite Art von joins in R sind die sogenannten filtering joins, die keine neuen Spalten hinzufügen, sondern nur ausgewählte Zeilen zurückgeben. Hierfür gibt es im tidyverse zwei Funktionen:

  • semi_join(): Gibt alle Beobachtungen des Data Frames x zurück, für die es einen Match im Data Frame y gibt
  • anti_join(): Gibt alle Beobachtungen des Data Frames x zurück, für die es keinen Match im Data Frame y gibt

Wir werden diese beiden Funktionen anhand der folgenden Data Frames demonstrieren:

vcv %<>% as_tibble()
vcv
## # A tibble: 810 × 7
##       RT Subject Vowel Lang  Left  Right Context
##    <dbl> <chr>   <chr> <chr> <chr> <chr> <chr>  
##  1  648. S209    a     AE    f     h     f.h    
##  2  617  S209    a     AE    f     sh    f.sh   
##  3  728. S209    a     AE    f     sh    f.sh   
##  4  629  S209    a     AE    f     th    f.th   
##  5  688. S209    a     AE    f     th    f.th   
##  6  602. S209    a     AE    s     h     s.h    
##  7  632. S209    a     AE    s     sh    s.sh   
##  8  574  S209    a     AE    s     th    s.th   
##  9  719  S209    a     AE    s     h     s.h    
## 10  569  S209    a     AE    s     th    s.th   
## # ℹ 800 more rows
vcv_summary <- vcv %>% 
  group_by(Subject, Vowel) %>% 
  summarise(mean_rt = mean(RT)) %>% 
  ungroup() %>%
  slice_max(mean_rt, n = 5)
## `summarise()` has grouped output by 'Subject'. You can
## override using the `.groups` argument.
vcv_summary
## # A tibble: 5 × 3
##   Subject Vowel mean_rt
##   <chr>   <chr>   <dbl>
## 1 S502    i       1173.
## 2 S502    u       1076.
## 3 S502    a       1000.
## 4 S508    u        816.
## 5 S508    i        780.

Der Data Frame vcv_summary enthält also die fünf höchsten durchschnittlichen Reaktionszeiten und welchem Sprecher und Vokal diese Werte zugeordnet sind. Wenn wir jetzt herausfinden wollen, aus welchen Beobachtungen in vcv diese Mittelwerte berechnet wurden, nutzen wir den semi join. Genau genommen möchten wir alle Zeilen aus vcv zurückbekommen, für die es in vcv_summary einen Match bezüglich der Spalten Subject und Vowel gibt.

semi_join(x = vcv, y = vcv_summary, by = c("Subject", "Vowel"))
## # A tibble: 75 × 7
##       RT Subject Vowel Lang  Left  Right Context
##    <dbl> <chr>   <chr> <chr> <chr> <chr> <chr>  
##  1 1120. S502    a     D     f     h     f.h    
##  2 1066. S502    a     D     f     sh    f.sh   
##  3  848  S502    a     D     f     sh    f.sh   
##  4 1148. S502    a     D     f     th    f.th   
##  5 1130  S502    a     D     f     th    f.th   
##  6  938  S502    a     D     s     h     s.h    
##  7 1124. S502    a     D     s     sh    s.sh   
##  8  981  S502    a     D     s     th    s.th   
##  9  774  S502    a     D     s     h     s.h    
## 10 1104  S502    a     D     s     th    s.th   
## # ℹ 65 more rows

Das Ergebnis enthält jetzt also alle Beobachtungen, aus denen die gemittelten Reaktionszeiten in vcv_summary berechnet wurden. Wir können das nochmal verdeutlichen, indem wir uns ausgeben lassen, welche einzigartigen Kombinationen von Subject und Vowel es im Ergebnis des semi joins gibt (es sind dieselben fünf Kombinationen wie in vcv_summary):

semi_join(x = vcv, y = vcv_summary, by = c("Subject", "Vowel")) %>% 
  select(Subject, Vowel) %>% 
  unique()
## # A tibble: 5 × 2
##   Subject Vowel
##   <chr>   <chr>
## 1 S502    a    
## 2 S502    i    
## 3 S502    u    
## 4 S508    i    
## 5 S508    u

Mit dem anti join erhalten wir hingegen alle Beobachtungen, aus denen nicht die gemittelten Reaktionszeiten berechnet wurden, oder mit anderen Worten: alle Beobachtungen aus vcv, für die es bzgl. Subject und Vowel keinen Match in vcv_summary gibt.

anti_join(x = vcv, y = vcv_summary, by = c("Subject", "Vowel"))
## # A tibble: 735 × 7
##       RT Subject Vowel Lang  Left  Right Context
##    <dbl> <chr>   <chr> <chr> <chr> <chr> <chr>  
##  1  648. S209    a     AE    f     h     f.h    
##  2  617  S209    a     AE    f     sh    f.sh   
##  3  728. S209    a     AE    f     sh    f.sh   
##  4  629  S209    a     AE    f     th    f.th   
##  5  688. S209    a     AE    f     th    f.th   
##  6  602. S209    a     AE    s     h     s.h    
##  7  632. S209    a     AE    s     sh    s.sh   
##  8  574  S209    a     AE    s     th    s.th   
##  9  719  S209    a     AE    s     h     s.h    
## 10  569  S209    a     AE    s     th    s.th   
## # ℹ 725 more rows

Anti joins eignen sich unter anderem zur Fehlersuche in den eigenen Daten. Nehmen wir nochmal unsere Beispieldaten von vorhin, measures mit Messwerten für 20 Sprecher, und meta mit Metadaten für nur 10 dieser Sprecher. Wenn wir den anti join hier anwenden, finden wir sofort heraus, für welche Zeilen in measures es keinen Match in meta gibt, für welche Sprecher es also keine Metadaten gibt.

anti_join(x = measures, y = meta, by = "subject")
## # A tibble: 100 × 2
##    subject    F0
##    <chr>   <dbl>
##  1 s11     129. 
##  2 s11     106. 
##  3 s11     116. 
##  4 s11     123. 
##  5 s11     116. 
##  6 s11     145. 
##  7 s11      90.0
##  8 s11     133. 
##  9 s11     118. 
## 10 s11     128. 
## # ℹ 90 more rows

Das Ergebnis ist das Gegenstück zum inner join von oben: Wir erhalten hier die 100 Beobachtungen für die Sprecher 11 bis 20, für die es keine Metadaten gibt.

Weiterführende Infos: dplyr

Die joins waren die letzten Funktionen aus dem tidyverse, die wir Ihnen hier vorstellen wollen. Eine Übersicht über alle bisher gelernten und viele weitere Funktionen erhalten Sie in diesem Cheatsheet.