5 Datenaufbereitung mit dplyr

5.1 Einführung

Dplyr ist Teil des \(tidyverse\) Packages und ermöglicht es, Daten sehr einfach zu manipulieren und in eine Form zu bringen, um diese zu analysieren. Der größte Vorteil dabei ist die einfache Syntax des Packages. Diese ermöglicht es, komplexe Operationen und Umformungen mit relativ wenigen Codezeilen zu realisieren. Um dplyr kennenzulernen, werden wir mit dem Starwars Datensatz arbeiten. Dieser enthält verschiedenen Informationen zu unterschiedlichen Charakteren der Star Wars Saga, wie zum Beispiel das Alter, Geschlecht, Heimatplanet oder Alienrasse. Zunächst lesen wir den Datensatz mit readRDS() ein und verschaffen uns dann einen ersten Überblick über den Datensatz:

# Der Datensatz befindet sich im Buch im Verzeichnis "Data" (unter https://github.com/AMD-Lab/R-Kurs-Buch/blob/main/data/starwars.RDS?raw=true). 
# Daher muss der Pfad beim Einlesen des Datensatzes mit angegeben werden.

starwars <- readRDS("starwars.RDS") %>% drop_na()

Wir benutzen den head() Befehl, um uns die ersten 5 Zeilen des Datensatzes anzeigen zu lassen.

# Wir lassen uns zunächst die ersten 5 Zeilen des Datensatzes ausgeben. 

head(starwars, 5)
## # A tibble: 5 × 11
##   name      height  mass hair_…¹ skin_…² eye_c…³   Age sex  
##   <chr>      <int> <dbl> <fct>   <fct>   <fct>   <dbl> <fct>
## 1 Luke Sky…    172    77 blond   fair    blue     19   male 
## 2 Darth Va…    202   136 none    white   yellow   41.9 male 
## 3 Leia Org…    150    49 brown   light   brown    19   fema…
## 4 Owen Lars    178   120 brown,… light   blue     52   male 
## 5 Beru Whi…    165    75 brown   light   blue     47   fema…
## # … with 3 more variables: gender <fct>, homeworld <chr>,
## #   species <chr>, and abbreviated variable names
## #   ¹​hair_color, ²​skin_color, ³​eye_color
# Analog können wir auch mit den Befehl tail(), die letzten n Zeilen eines Datensatzes anzeigen.

tail(starwars, 5)
## # A tibble: 5 × 11
##   name      height  mass hair_…¹ skin_…² eye_c…³   Age sex  
##   <chr>      <int> <dbl> <fct>   <fct>   <fct>   <dbl> <fct>
## 1 Luminara…    170  56.2 black   yellow  blue       58 fema…
## 2 Barriss …    166  50   black   yellow  blue       40 fema…
## 3 Dooku        193  80   white   fair    brown     102 male 
## 4 Jango Fe…    183  79   black   tan     brown      66 male 
## 5 Padmé Am…    165  45   brown   light   brown      46 fema…
## # … with 3 more variables: gender <fct>, homeworld <chr>,
## #   species <chr>, and abbreviated variable names
## #   ¹​hair_color, ²​skin_color, ³​eye_color

Nachdem wir nun eine erste “Augapfeldiagnostik” des Datensatzes betrieben haben, sollten wir uns nun die einzelnen Variablen genauer ansehen. Um einen ersten Überblick in die unterschiedlichen Parameter der Variablen zu bekommen, eignet sich der summary() Befehl. Dieser berechnet die wichtigstens Kennwerte der im Datensatz enthaltenen Variablen wie Mittelwert, Median, Quantile, Minimum, Maximum und fehlende Werte.

summary(starwars)
##      name               height         mass       
##  Length:29          Min.   : 88   Min.   : 20.00  
##  Class :character   1st Qu.:170   1st Qu.: 75.00  
##  Mode  :character   Median :180   Median : 79.00  
##                     Mean   :178   Mean   : 77.77  
##                     3rd Qu.:188   3rd Qu.: 83.00  
##                     Max.   :228   Max.   :136.00  
##                                                   
##          hair_color   skin_color   eye_color 
##  none         :9    fair   :7    brown  :10  
##  brown        :7    light  :6    blue   : 8  
##  black        :6    dark   :2    yellow : 4  
##  blond        :2    orange :2    hazel  : 2  
##  white        :2    pale   :2    orange : 2  
##  auburn, white:1    yellow :2    black  : 1  
##  (Other)      :2    (Other):8    (Other): 2  
##       Age                     sex           gender  
##  Min.   :  8.00   female        : 6   feminine : 6  
##  1st Qu.: 31.00   hermaphroditic: 0   masculine:23  
##  Median : 46.00   male          :23                 
##  Mean   : 51.29   none          : 0                 
##  3rd Qu.: 57.00                                     
##  Max.   :200.00                                     
##                                                     
##   homeworld           species         
##  Length:29          Length:29         
##  Class :character   Class :character  
##  Mode  :character   Mode  :character  
##                                       
##                                       
##                                       
## 

Wir sehen nun die unterschiedlichen Verteilungsinformationen jeder Variable. Für factor bzw. character Variablen, werden hier die Häufigkeiten der einzelnen Kategorien angezeigt.

5.2 dplyr: Die wichtigsten Befehle

Wie am Anfang des Kapitels bereits erwähnt, ermöglicht es dplyr mit relativ einfachen Mitteln, komplexe Operationen und Transformationen in Datensätzen vorzunehmen. Hierzu hat dplyr eine eigene Syntax entwickelt, die sich sehr stark von der ursprünglichen R-Syntax unterscheidet. Diese baut auf wenigen, relativ intuitiven Befehlen auf, welche verkettet werden können. Zunächst eine Übersicht der wichtigsten Befehle:

  • filter() – Filtern von Beobachtungen nach einem bestimmten Kriterium

  • arrange() – Reihen neu Sortieren

  • select() – Auswahl von Variablen nach deren Name

  • mutate() – Erstellen von neuen Variablen aus bereits existierenden

  • summarise() – Viele Werte zu einem einzelnen Wert zusammenfassen

  • group_by() – Gruppieren von Daten nach bestimmten Variablen in Kombination mit anderen Funktionen

Der vielleicht wichtigste Befehl ist group_by(), mit dem die oben genannten Befehle auf einzelne Gruppen innerhalb eines Datensatzes anwendbar sind.

Diese sechs sogennaten "Verben" bilden die Grundlage von dplyr. Mit ihnen ist es möglich mehrere einfache Operationen miteinander zu verketten, um ein komplexes Ergebnis zu erzielen. Alle Befehle funktionieren auf die gleiche Art und Weise. Jede Operation ist durch die gleiche Struktur gekennzeichnet:

  1. Das erste Argument ist ein Dataframe.

  2. Die nachfolgenden Argumente beschreiben, was mit dem Dataframe geschehen soll, wobei die Variablennamen (ohne Anführungszeichen) verwendet werden.

  3. Das Ergebnis ist ein neuer Dataframe

5.3 Beispiel: Filtern von Beobachtungen mit filter()

Um effektiv nach bestimmten Werten zu filtern, müssen für jede Operation die Kriterien, nach denen gefiltert werden soll, definiert werden. Dies geschiet mit Hilfe der bereits eingeführten logischen Operatoren. Im ersten Beispiel sollen alle Beobachtungen, in welchen die Variablen height und mass größer als 190 bzw. 90 sind gefiltert werden:

# Es werden hier nun alle Helden aus dem Datensatz ausgegeben, die größer als 1,90 m und schwerer als 90 Kilogramm sind

filter(starwars, height > 190 & mass > 90)
## # A tibble: 2 × 11
##   name      height  mass hair_…¹ skin_…² eye_c…³   Age sex  
##   <chr>      <int> <dbl> <fct>   <fct>   <fct>   <dbl> <fct>
## 1 Darth Va…    202   136 none    white   yellow   41.9 male 
## 2 Chewbacca    228   112 brown   unknown blue    200   male 
## # … with 3 more variables: gender <fct>, homeworld <chr>,
## #   species <chr>, and abbreviated variable names
## #   ¹​hair_color, ²​skin_color, ³​eye_color

5.3.1 Filtern von Strings / Factors

Logische Operatoren lassen sich sehr gut auf numerische bzw. kontinuierliche Variablen anwenden, um diese nach bestimmten Kriterien zu filtern. Wenn mit Strings oder Factors gearbeitet wird, sucht man häufig nach bestimmen pattern in den Strings, wie hier bei den Namen.

Sollen nun alle Beobachtungen mit “Skywalker” im Namen gefiltert werden, kann die grepl() Funktion aus R genutzt werden. Diese prüft, ob eine Zeichenfolge vorhanden ist oder nicht und gibt dann entsprechend TRUE oder FALSE aus, was ein filtern ermöglicht. Dies ist vor allem bei Strings die aus mehr als einem Wort bestehen und durch ein Leerzeichen getrennt sind, sehr praktisch. Bei Strings die nur aus einem Wort bestehen oder Factors, kann auch mit einem einfach == Vergleich gearbeitet werden.

Beispiel:

# Hier werden alle Beobachtungen mit der Spezies "Human" gefiltert.

filter(starwars, species == "Human")
## # A tibble: 18 × 11
##    name     height  mass hair_…¹ skin_…² eye_c…³   Age sex  
##    <chr>     <int> <dbl> <fct>   <fct>   <fct>   <dbl> <fct>
##  1 Luke Sk…    172  77   blond   fair    blue     19   male 
##  2 Darth V…    202 136   none    white   yellow   41.9 male 
##  3 Leia Or…    150  49   brown   light   brown    19   fema…
##  4 Owen La…    178 120   brown,… light   blue     52   male 
##  5 Beru Wh…    165  75   brown   light   blue     47   fema…
##  6 Biggs D…    183  84   black   light   brown    24   male 
##  7 Obi-Wan…    182  77   auburn… fair    blue-g…  57   male 
##  8 Anakin …    188  84   blond   fair    blue     41.9 male 
##  9 Han Solo    180  80   brown   fair    brown    29   male 
## 10 Wedge A…    170  77   brown   fair    hazel    21   male 
## 11 Palpati…    170  75   grey    pale    yellow   82   male 
## 12 Boba Fe…    183  78.2 black   fair    brown    31.5 male 
## 13 Lando C…    177  79   black   dark    brown    31   male 
## 14 Lobot       175  79   none    light   blue     37   male 
## 15 Mace Wi…    188  84   none    dark    brown    72   male 
## 16 Dooku       193  80   white   fair    brown   102   male 
## 17 Jango F…    183  79   black   tan     brown    66   male 
## 18 Padmé A…    165  45   brown   light   brown    46   fema…
## # … with 3 more variables: gender <fct>, homeworld <chr>,
## #   species <chr>, and abbreviated variable names
## #   ¹​hair_color, ²​skin_color, ³​eye_color
# Analog hierzu kann auch nach bestimmten Faktorstufen gefiltert werden:

filter(starwars, sex == "male")
## # A tibble: 23 × 11
##    name     height  mass hair_…¹ skin_…² eye_c…³   Age sex  
##    <chr>     <int> <dbl> <fct>   <fct>   <fct>   <dbl> <fct>
##  1 Luke Sk…    172    77 blond   fair    blue     19   male 
##  2 Darth V…    202   136 none    white   yellow   41.9 male 
##  3 Owen La…    178   120 brown,… light   blue     52   male 
##  4 Biggs D…    183    84 black   light   brown    24   male 
##  5 Obi-Wan…    182    77 auburn… fair    blue-g…  57   male 
##  6 Anakin …    188    84 blond   fair    blue     41.9 male 
##  7 Chewbac…    228   112 brown   unknown blue    200   male 
##  8 Han Solo    180    80 brown   fair    brown    29   male 
##  9 Wedge A…    170    77 brown   fair    hazel    21   male 
## 10 Palpati…    170    75 grey    pale    yellow   82   male 
## # … with 13 more rows, 3 more variables: gender <fct>,
## #   homeworld <chr>, species <chr>, and abbreviated
## #   variable names ¹​hair_color, ²​skin_color, ³​eye_color
# Um komplexere Strings zu filtern, kann die grepl() Funktion integriert werden. Hier werden alle Beobachtungen gefiltert, welche innerhalb der Variable "name" den String "Skywalker" enthalten. So erhalten wir alle Skywalker Charaktere, die im Datensatz vorhanden sind:

filter(starwars, grepl("Skywalker", name))
## # A tibble: 2 × 11
##   name      height  mass hair_…¹ skin_…² eye_c…³   Age sex  
##   <chr>      <int> <dbl> <fct>   <fct>   <fct>   <dbl> <fct>
## 1 Luke Sky…    172    77 blond   fair    blue     19   male 
## 2 Anakin S…    188    84 blond   fair    blue     41.9 male 
## # … with 3 more variables: gender <fct>, homeworld <chr>,
## #   species <chr>, and abbreviated variable names
## #   ¹​hair_color, ²​skin_color, ³​eye_color

5.4 dplyr: Der Piping Operator %>%

Die vielleicht wichtigste Funktion in dplyr ist der sogenannte “piping operator” %>%. Mit diesem können beliebig viele Befehle kombiniert, oder auch “verkettet” werden, um einen Datensatz umzuformen. Hierbei bleiben die oben vorgestellten Prinzipien gültig:

  1. Das erste Argument ist ein Dataframe.

  2. Die nachfolgenden Argumente beschreiben, was mit dem Dataframe geschehen soll, wobei die Variablennamen (ohne Anführungszeichen) verwendet werden.

  3. Das Ergebnis ist ein neuer Dataframe

In diesem Kapitel werden nur die basis dplyr-Funktionen besprochen und wie diese in einer Pipline integriert werden können. Prinzipiell lassen sich jedoch auch alle anderen R-Befehle in eine “Pipeline” integrieren, wie zum Beispiel statistische Transformationen. Es spielt keine Rolle, welche Befehle innerhalb einer Pipeline ausgeführt werden, solange die oben genannten Prinzipien eingehalten werden.

Hier ein sehr fortgeschrittenes Beispiel, wie dies aussehen kann. Es wurden in diesem Beispiel Funktionen aus unterschiedlichen Paketen verwendet (z.B. fisherz() aus dem psych Package), als auch Funktionen von R (cor()) um aus einem sehr großen Datensatz mit 1 Millionen Beobachtungen, mittlere Korrelationen zwischen unterschiedlichen Variablen zu berechnen:

# df_clean %>% group_by(N, K, Retrievals) %>%

# Hier wird der Datensatz nach den Variablen N, K und Retrievals gruppiert.

#   summarise(corA = cor(mu_est_a, mu_real_a), 
#             corC = cor(mu_est_c, mu_real_c)) %>%

# Dann werden die Korrelationen zwischen den Variablen est_a und real_a,
# so wie die Korrelationen zwischen den Variablen est_c und real_c für alle Gruppenkombinationen berechnet.

#   mutate(z_a = fisherz(corA), z_c = fisherz(corC)) %>% 

# Anschließend werden diese Korrelationen z-transformiert und dann die Mittelwerte nach den Variablen N und K berechnet

#   filter(Retrievals == 100) %>%
#   group_by(N, K) %>%  
#   summarise(mean_a_100 = mean(z_a),
#             mean_c_100 = mean(z_c),
#             range_cor = range(mean_a_100),
#             range_cor = range(mean_a_100)) %>%
#   mutate(meanCorA_100 = fisherz2r(mean_a_100),
#          meanCorC_100 = fisherz2r(mean_c_100)) %>%
#   select(-c(mean_a_100, mean_c_100))

5.5 Beispiel

Um nun verschiedene Operationen zu einer Pipeline zu verketten, können wir zwischen die einzelnen dplyr Befehle den Piping Operator %>% schalten.

Soll zum Beispiel der Mittelwert des Alters der Helden aus dem Starwars Datensatz für verschiedene Gruppen (hier Heimatwelten und Spezies) berechnet werden, können hierfür der group_by() und der summarise() Befehl kombiniert werden. Zunächst Gruppieren wir den Datensatz nach den Variablen species und homeworld und berechnen dann mit summarise() eine neue Variable mean_Age für jede Gruppenkombination. Hierbei wird innerhalb von summarise() der Mittelwert der Age Variablen berechnet und in der neuen Variablen mean_Age gespeichert. Hierbei gelten die oben genannten Prinzipien, welche dann mit %>% verkettet werden:

  1. Das erste Argument ist ein Dataframe: starwars

  2. Die nachfolgenden Argumente beschreiben, was mit dem Dataframe geschehen soll:

    group_by(species, homeworld) und summarise(mean_Age = mean(Age))

  3. Das Ergebnis ist ein neuer Dataframe, der nur noch die Gruppenvariablen Homeworld und Species, sowie die neu berechnete Variable mean_Ages enthält:

# Dazu benutzen wir den Piping Operator %>%, um die Befehle zu verketten:

starwars %>% 
  group_by(species, homeworld) %>% 
  summarise(mean_Age = mean(Age))
## `summarise()` has grouped output by 'species'. You can
## override using the `.groups` argument.
## # A tibble: 21 × 3
## # Groups:   species [11]
##    species homeworld    mean_Age
##    <chr>   <chr>           <dbl>
##  1 Cerean  Cerea            92  
##  2 Ewok    Endor             8  
##  3 Gungan  Naboo            52  
##  4 Human   Alderaan         19  
##  5 Human   Bespin           37  
##  6 Human   Concord Dawn     66  
##  7 Human   Corellia         25  
##  8 Human   Haruun Kal       72  
##  9 Human   Kamino           31.5
## 10 Human   Naboo            64  
## # … with 11 more rows

Natürlichsprachlich dargestellt werden also folgende Operationen ausgeführt:

  1. Nehme den Datensatz starwars (1. Zuerst der Dataframe):
starwars %>%
  1. Gruppiere diesen nach Spezies und Heimatwelt (1. Verarbeitungsschritt):
group_by(species, homeworld) %>%
  1. Berechne dann für jede dieser Gruppen den Mittelwert für die Variable Age (2. Schritt):
summarise(meanAge = mean(Age))

Da nun der Piping-Operator die einzelnen Elemente miteinander verkettet, ist es nicht mehr notwendig, den Datensatz innerhalb der einzelnen Befehle als Argument anzugeben, so wie im Beipsiel von filter(). Es muss lediglich der Ausgangsdatensatz am Anfang der Pipeline angeben werden.

5.6 dplyr: Neue Variablen mit mutate() berechnen

Der letzte wichtige Befehl in dplyr ist mutate() bzw. across(). Mit mutate() bzw. across() ist es möglich eine Variable bzw. mehrere Variablen umzuformen, oder neu zu berechnen. Dies wird hier anhand einer z-Tranformation erläutert. Dies ermöglicht der Befehl scale(), der standardmäßig in R vorhanden ist.

5.7 Beispiel

starwars %>% 
  select(height, mass) %>% 
  mutate(z_height = scale(height),
         z_mass = scale(mass)) 
## # A tibble: 29 × 4
##    height  mass z_height[,1] z_mass[,1]
##     <int> <dbl>        <dbl>      <dbl>
##  1    172    77     -0.265      -0.0335
##  2    202   136      1.07        2.52  
##  3    150    49     -1.24       -1.25  
##  4    178   120      0.00153     1.83  
##  5    165    75     -0.576      -0.120 
##  6    183    84      0.224       0.270 
##  7    182    77      0.179      -0.0335
##  8    188    84      0.446       0.270 
##  9    228   112      2.22        1.48  
## 10    180    80      0.0904      0.0965
## # … with 19 more rows
starwars %>%
  select(height, mass) %>% 
  mutate(across(c(height, mass), list(z = scale)))
## # A tibble: 29 × 4
##    height  mass height_z[,1] mass_z[,1]
##     <int> <dbl>        <dbl>      <dbl>
##  1    172    77     -0.265      -0.0335
##  2    202   136      1.07        2.52  
##  3    150    49     -1.24       -1.25  
##  4    178   120      0.00153     1.83  
##  5    165    75     -0.576      -0.120 
##  6    183    84      0.224       0.270 
##  7    182    77      0.179      -0.0335
##  8    188    84      0.446       0.270 
##  9    228   112      2.22        1.48  
## 10    180    80      0.0904      0.0965
## # … with 19 more rows

In diesem Beispiel wurden zunächst nur height und mass mit dem select() Befehl ausgewählt, daher werden auch nur diese beiden Spalten am Ende der Pipline im Datensatz angezeigt. Dies kann hilfreich sein, wenn man einen Datensatz mit sehr vielen Variablen analysieren muss, von denen nur einige wenige interessant sind. Dies ist z.B. bei Fragebögen der Fall, die unterschiedliche Facetten erfassen.

Der nächste Befehl mutate() besteht immer aus einer Operation, die mit einer Spalte im Datensatz durchgeführt wird. Im Beispiel oben werden die Spalten z_height und z_mass berechnet, die sich jeweils aus scale(SPALTENNAME) zusammensetzen und die z-Werte der jeweiligen Variablen ausgeben.

Anstatt beide Variablen einzeln zu transformieren, kann den Befehl scale() auch direkt auf mehrere Spalten angewendet werden. Dazu kann der across() Befehl verwendet werden.

Hier muss innerhalb von mutate() einfach mit across(c(SPALTE1, SPALTE2)) ein Vektor der zu tranformierenden Spalten übergeben werden, sowie die Funktion(en), welche auf die Spalten angewandt werden soll. Dies muss dann wie folgt definiert werden:

mutate(across(c(height, mass), list(z = scale)))

Diese Schreibweise hat den Vorteil, dass

  1. In der list() mehrere Befehle übergeben werden können (z.B. könnten neben der z-Standardisierung wie in diesem Fall auch die Wurzel mit sqrt oder der Logarithmus mit log berechnet werden)

  2. Die Originalspalten beibehalten werden

  3. Den neuen Spalten direkt einen Suffix zugewiesen werden kann.
    Dieser wird automatisch als “_suffix” an die neue Variable im Output angehängt (in diesem Fall "_z").

starwars %>% mutate(across(c(height, mass), list(z = scale)))
## # A tibble: 29 × 13
##    name     height  mass hair_…¹ skin_…² eye_c…³   Age sex  
##    <chr>     <int> <dbl> <fct>   <fct>   <fct>   <dbl> <fct>
##  1 Luke Sk…    172    77 blond   fair    blue     19   male 
##  2 Darth V…    202   136 none    white   yellow   41.9 male 
##  3 Leia Or…    150    49 brown   light   brown    19   fema…
##  4 Owen La…    178   120 brown,… light   blue     52   male 
##  5 Beru Wh…    165    75 brown   light   blue     47   fema…
##  6 Biggs D…    183    84 black   light   brown    24   male 
##  7 Obi-Wan…    182    77 auburn… fair    blue-g…  57   male 
##  8 Anakin …    188    84 blond   fair    blue     41.9 male 
##  9 Chewbac…    228   112 brown   unknown blue    200   male 
## 10 Han Solo    180    80 brown   fair    brown    29   male 
## # … with 19 more rows, 5 more variables: gender <fct>,
## #   homeworld <chr>, species <chr>, height_z <dbl[,1]>,
## #   mass_z <dbl[,1]>, and abbreviated variable names
## #   ¹​hair_color, ²​skin_color, ³​eye_color