Kapitel 6 Einführung in dplyr

6.1 Einstieg

dplyr ist ein Paket zur Datenmanipulation, entwickelt von Hadley Wickham und Romain Francois. Das Paket ist Teil des tidyverse und gehört als Kernpaket zu den Paketen, die über library(tidyverse) geladen werden.

Die Autoren des Pakets verstehen dplyr als eine Grammatik der Datenmanipulation. Daher werden die wichtigsten dplyr Funktionen auch oft als Verben bezeichnet. Diese Verben sollen euch helfen, die häufigsten Herausforderungen bei der Datenmanipulation zu lösen:

  • mutate(): fügt neue Variablen zum Datensatz hinzu, die Funktionen von bestehenden Variablen sind

  • select(): wählt Variablen (Spalten) basierend auf ihren Namen aus

  • filter(): wählt Zeilen basierend auf anzugebenden Bedingungen aus

  • summarise(): reduziert mehrere Werte auf eine einzige Zusammenfassung

  • arrange(): ändert die Reihenfolge der Zeilen

Der Ursprung von dplyr liegt in einem früheren Paket mit dem Namen plyr, das zum Ziel hat die “split-apply-combine”-Strategie der Datenanalyse (Wickham 2011) umzusetzen. Wo plyr noch einen vielfältigen Satz von Ein- und Ausgabetypen abdeckt (z.B. Arrays, data frames, Listen), hat dplyr einen klaren Fokus auf data frames oder tibbles (wenn man sich im tidyverse befindet).

dplyr bietet schnelle Alternativen zu den R Standardfunktionen:

und mehr. Ferner bietet dplyr die Möglichkeit schnell über Zeilen oder Gruppen von Zeilen zu iterieren, was eine schnelle Alternative zur Nutzung von for Schleifen darstellt.

Wie immer, laden wir zu Beginn

Der Fokus liegt in diesem Abschnitt auf dplyr. Aber da wir immer wieder auch Funktionen aus anderen “tidyverse-Paketen” nutzen, laden wir stets tidyverse.

library(tidyverse)
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr     1.1.4     ✔ readr     2.1.5
## ✔ forcats   1.0.0     ✔ stringr   1.5.1
## ✔ ggplot2   3.5.1     ✔ tibble    3.2.1
## ✔ lubridate 1.9.3     ✔ tidyr     1.3.1
## ✔ purrr     1.0.2     
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag()    masks stats::lag()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors

Zusätzlich laden wir auch noch wieder das gapminder Paket, da wir erneut mit dem gapminder Datensatz arbeiten wollen.

6.2 filter(): Indizieren von Zeilen

Die Funktion filter() erwartet neben dem Datensatz logische Ausdrücke als Input und gibt die Zeilen des Datensatzes zurück, für die die Kombination der verwendeten logischen Ausdrücke ein TRUE ergibt.

# beobachtungen mit einer lebenserwartung unter 29 jahren
filter(gapminder, lifeExp < 29)
## # A tibble: 2 × 6
##   country     continent  year lifeExp     pop gdpPercap
##   <fct>       <fct>     <int>   <dbl>   <int>     <dbl>
## 1 Afghanistan Asia       1952    28.8 8425333      779.
## 2 Rwanda      Africa     1992    23.6 7290203      737.
# beobachtungen aus ruanda nach dem jahr 1979
filter(gapminder, country == "Rwanda", year > 1979)
## # A tibble: 6 × 6
##   country continent  year lifeExp     pop gdpPercap
##   <fct>   <fct>     <int>   <dbl>   <int>     <dbl>
## 1 Rwanda  Africa     1982    46.2 5507565      882.
## 2 Rwanda  Africa     1987    44.0 6349365      848.
## 3 Rwanda  Africa     1992    23.6 7290203      737.
## 4 Rwanda  Africa     1997    36.1 7212583      590.
## 5 Rwanda  Africa     2002    43.4 7852401      786.
## 6 Rwanda  Africa     2007    46.2 8860588      863.

Am letzten Befehl erkennt man, dass die verschiedenen logischen Ausdrücke mit einem & verknüpft werden. Will man eine “oder Abfrage” gestalten, so muss diese in einem logischen Ausdruck enthalten sein. So kann man mit nachfolgendem Befehl beispielsweise nach allen Beobachtungen aus Ruanda oder Beobachachtungen nach 1979 fragen:

filter(gapminder, country == "Rwanda" | year > 1979)
## # A tibble: 858 × 6
##    country     continent  year lifeExp      pop gdpPercap
##    <fct>       <fct>     <int>   <dbl>    <int>     <dbl>
##  1 Afghanistan Asia       1982    39.9 12881816      978.
##  2 Afghanistan Asia       1987    40.8 13867957      852.
##  3 Afghanistan Asia       1992    41.7 16317921      649.
##  4 Afghanistan Asia       1997    41.8 22227415      635.
##  5 Afghanistan Asia       2002    42.1 25268405      727.
##  6 Afghanistan Asia       2007    43.8 31889923      975.
##  7 Albania     Europe     1982    70.4  2780097     3631.
##  8 Albania     Europe     1987    72    3075321     3739.
##  9 Albania     Europe     1992    71.6  3326498     2497.
## 10 Albania     Europe     1997    73.0  3428038     3193.
## # ℹ 848 more rows

Will man einen Vergleich mit mehr als einem Wert durchführen, so kann man natürlich alle Abfragen mit einem | verknüpfen, oder gleich den %in% Operator verwenden.

# beobachtungen aus ruanda und afghanistan
filter(gapminder, country %in% c("Rwanda", "Afghanistan"))
## # A tibble: 24 × 6
##    country     continent  year lifeExp      pop gdpPercap
##    <fct>       <fct>     <int>   <dbl>    <int>     <dbl>
##  1 Afghanistan Asia       1952    28.8  8425333      779.
##  2 Afghanistan Asia       1957    30.3  9240934      821.
##  3 Afghanistan Asia       1962    32.0 10267083      853.
##  4 Afghanistan Asia       1967    34.0 11537966      836.
##  5 Afghanistan Asia       1972    36.1 13079460      740.
##  6 Afghanistan Asia       1977    38.4 14880372      786.
##  7 Afghanistan Asia       1982    39.9 12881816      978.
##  8 Afghanistan Asia       1987    40.8 13867957      852.
##  9 Afghanistan Asia       1992    41.7 16317921      649.
## 10 Afghanistan Asia       1997    41.8 22227415      635.
## # ℹ 14 more rows

Wir erkennen sofort, dass wir mithilfe von dplyr sehr leicht den Datensatz aufteilen können, basierend auf der Tatsache ob Bedingungen erfüllt werden oder eben nicht.

Daher solltet ihr unter keinen Umständen mit Befehlen wie diesem

auswahl <- gapminder[241:252, ]

arbeiten.

Warum ist das eine blöde Idee?

  • Der Befehl dokumentiert sich nicht selbst. Was ist das Besondere an den Zeilen 241 bis 252?

  • Der Befehl ist fehleranfällig. Diese Codezeile wird zu anderen Ergebnissen führen, wenn jemand die Zeilenreihenfolge von gapminder ändert, z.B. die Daten vor diesem Befehl erst sortiert.

Ganz anders verhält es sich mit diesem Befehl

filter(gapminder, country == "Canada")

Er erklärt sich von selbst und ist ziemlich robust.

6.3 Der Pipe-Operator

Bevor es weitergeht, wollen wir aber den Pipe-Operator vorstellen. Dafür gibt es zwei Optionen. Zuerst wurde der Pipe-Operator %>% eingeführt, den das Tidyverse aus dem magrittr-Paket von Stefan Bache importiert. In Version 4.1 von R wurde auch der native Pipe-Operator |> eingeführt. Zwischen den beiden Operatoren gibt es einige Unterschiede. Da der neue Pipe-Operator |> schneller und nicht von einem Pakett abhängig ist, werden wir ihn bevorzugen.

Mithilfe des Pipe-Operators ist man in der Lage aufeinanderfolgende Befehle von Daten-Operationen strukturiert anzugeben, ohne sie ineinander zu verschachteln. Diese neue Syntax führt zu Code, der viel einfacher zu schreiben und zu lesen ist.

Das entsprechende RStudio Tastenkürzel lautet:
Ctrl+Shift+M (Windows), Cmd+Shift+M (Mac).

Die Standardeinstellung in RStudio ist, dass man mit der obigen Tastenkürzel den Operator %>% bekommt. Um den neuen Operator |> zu bekommen, kann man die Tastenkürzel unter Global Options -> Code umstellen.

Erstmal ein Beispiel

gapminder |>  head()
## # A tibble: 6 × 6
##   country     continent  year lifeExp      pop gdpPercap
##   <fct>       <fct>     <int>   <dbl>    <int>     <dbl>
## 1 Afghanistan Asia       1952    28.8  8425333      779.
## 2 Afghanistan Asia       1957    30.3  9240934      821.
## 3 Afghanistan Asia       1962    32.0 10267083      853.
## 4 Afghanistan Asia       1967    34.0 11537966      836.
## 5 Afghanistan Asia       1972    36.1 13079460      740.
## 6 Afghanistan Asia       1977    38.4 14880372      786.

Man erkennt sofort, der Befehl ist äquivalent zu head(gapminder). Der Pipe-Operator nimmt das Objekt auf der linken Seite und leitet es in den Funktionsaufruf auf der rechten Seite weiter - er gibt es buchstäblich als erstes Argument ein.

Und natürlich kann man der Funktion auf der rechten Seite auch noch weitere Argumente übergeben. Um die ersten 3 Zeilen von gapminder auszugeben, könnte man head(gapminder, 3) nutzen oder:

gapminder |>  head(3)
## # A tibble: 3 × 6
##   country     continent  year lifeExp      pop gdpPercap
##   <fct>       <fct>     <int>   <dbl>    <int>     <dbl>
## 1 Afghanistan Asia       1952    28.8  8425333      779.
## 2 Afghanistan Asia       1957    30.3  9240934      821.
## 3 Afghanistan Asia       1962    32.0 10267083      853.

Der bisherige Einsatz des Pipe-Operators |> war sicherlich noch nicht sehr beeindruckend, aber das sollte sich noch ändern.

6.4 Mit select() Variablen auswählen

Verwendet select(), um aus den Daten verschiedene Variablen (Spalten) auszuwählen. Hier kommt eine typische Verwendung von select():

select(gapminder, year, lifeExp)
## # A tibble: 1,704 × 2
##     year lifeExp
##    <int>   <dbl>
##  1  1952    28.8
##  2  1957    30.3
##  3  1962    32.0
##  4  1967    34.0
##  5  1972    36.1
##  6  1977    38.4
##  7  1982    39.9
##  8  1987    40.8
##  9  1992    41.7
## 10  1997    41.8
## # ℹ 1,694 more rows

und nun noch kombiniert mit head() über den Pipe-Operator:

gapminder |> 
  select(year, lifeExp) |> 
  head(4)
## # A tibble: 4 × 2
##    year lifeExp
##   <int>   <dbl>
## 1  1952    28.8
## 2  1957    30.3
## 3  1962    32.0
## 4  1967    34.0

Der letzte Befehl nochmal in Worten:

“Nimm gapminder, wähle die Variablen year und lifeExp und zeige dann die ersten 4 Zeilen an.”

Natürlich kann man all diese Operationen auch mit R Standardbefehlen ausführen. Die dplyr Befehle haben aber klare Vorteile bei der Lesbarkeit des Codes, wie man im nächsten Beispiel sieht.

Wir wählen aus dem gapminder Datensatz die Variablen year und lifeExp der Kambodscha Beobachtungen

gapminder |> 
  filter(country == "Cambodia") |> 
  select(year, lifeExp)
## # A tibble: 12 × 2
##     year lifeExp
##    <int>   <dbl>
##  1  1952    39.4
##  2  1957    41.4
##  3  1962    43.4
##  4  1967    45.4
##  5  1972    40.3
##  6  1977    31.2
##  7  1982    51.0
##  8  1987    53.9
##  9  1992    55.8
## 10  1997    56.5
## 11  2002    56.8
## 12  2007    59.7

Das gleiche Ergebnis würde man mit diesem R Standardbefehl erhalten:

gapminder[gapminder$country == "Cambodia", c("year", "lifeExp")]
## # A tibble: 12 × 2
##     year lifeExp
##    <int>   <dbl>
##  1  1952    39.4
##  2  1957    41.4
##  3  1962    43.4
##  4  1967    45.4
##  5  1972    40.3
##  6  1977    31.2
##  7  1982    51.0
##  8  1987    53.9
##  9  1992    55.8
## 10  1997    56.5
## 11  2002    56.8
## 12  2007    59.7

Ich hoffe, ihr stimmt mir zu, dass der dplyr Befehl deutlich leichter zu lesen ist.

6.5 select() Hilfsfunktionen

Der gapminder Datensatz ist klein und damit leicht überschaubar. Daher ist eine strukturierte Auswahl von Variablen hier nicht notwendig. In größeren Datensätzen kann dies aber ganz anders sein. Dort bietet es sich an mit Hilfsfunktionen wie

  • : wählt einen Bereich von Spalten aus

  • - wählt alle Spalten außer …

  • starts_with() wählt alle Spalten, die mit … starten

  • ends_with() wählt alle Spalten, die mit … enden

  • contains() wählt alle Spalten, die … enthalten

  • matches() wählte alle Spalten, die den regulären Ausdruck … enthalten

zu arbeiten.

select(gapminder, 
       matches(        # von beginn ^
         "^.{4}$"      # bis ende $
         )             # enthält der namen irgendwelche . character
       )
## # A tibble: 1,704 × 1
##     year
##    <int>
##  1  1952
##  2  1957
##  3  1962
##  4  1967
##  5  1972
##  6  1977
##  7  1982
##  8  1987
##  9  1992
## 10  1997
## # ℹ 1,694 more rows

6.6 Pure, predictable, pipeable

Bisher haben wir nur etwas an der Oberfläche von dplyr gekratzt, trotzdem möchten wir auf ein Schlüsselprinzip hinweisen, das du mit der Zeit schätzen lernen wirst.

Die Verben (Hauptfunktionen) von dplyr, wie z.B. filter() und select(), sind pure functions. Dazu schreibt Hadley Wickham im Kapitel Functions in seinem Advanced R Buch (2019):

The functions that are the easiest to understand and reason about are pure functions: functions that always map the same input to the same output and have no other impact on the workspace. In other words, pure functions have no side effects: they don’t affect the state of the world in any way apart from the value they return.

Tatsächlich sind diese Verben ein Spezialfall reiner Funktionen: sie nehmen als Input und Output denselben Objekttyp an, i.d.R. ein data frame.

Die Daten sind für all diese Funktionen auch stets das erste Inputargument.

6.7 Aufgabe

Die dplyr Einführung geht weiter im Kapitel Mehr zu dplyr. Bearbeitet aber vorher den letzten Abschnitt des Work with Data Primers:

Deriving Information with dplyr zeigt euch wie ihr über bestehenden Variablen neue Variablen definiert und leicht zusammenfassende Statistiken innerhalb vorab definierter Gruppen berechnet.

learnr::run_tutorial("deriving", package = "idsst.rtutorials")