9 Der Pipe-Operator %>%
Neben vielen praktischen Funktionen und der Datenstruktur Tibbles führt das Tidyverse auch ein neuens Syntax-Konzept in R ein: den sogenannten Pipe-Operator %>%
, mit dem Argumente auf eine andere Art und Weise an Funktionen übergeben werden.18
9.1 Lesbarkeit verschachtelter Funktionen
Hierzu rufen wir uns zunächst noch einmal in Erinnerung, wie Funktionen in R grundsätzlich aufgerufen werden:
Wir haben außerdem bereits gesehen, dass wir Funktionen ineinander verschachteln können, wenn wir mehrere Funktionen hintereinander aufrufen möchten:
## [1] 5.84
Das wird jedoch irgendwann sehr unübersichtlich und anfällig für Fehler – bereits bei diesem Beispiel müssen wir darauf achten, dass die Klammern zur richtigen Zeit geöffnet und vor allem wieder geschlossen werden, und wir müssen “Klammern zählen”, wenn wir wissen wollen, zu welcher der aufgerufenen Funktionen das Argument 2
gehört. Zusätzlich ergibt sich durch die Verschachtelung die unnatürliche Lesereihenfolge von innen nach außen, was komplexeren Code schwer nachvollziehbar macht.
9.2 Ein Beispiel in Pseudo-Code
Um dies zu verdeutlichen, stellen wir uns einmal vor, eine typische Morgenroutine bestünde aus “Funktionen”, die wir der Reihe nach “aufrufen”:
- Aufstehen
- Frühstücken
- Zähne putzen
- Duschen
- Anziehen
In R-Code ausgedrückt würde das also wie folgt aussehen:
einsatzbereit <- anziehen(duschen(zaehne_putzen(fruehstuecken(aufstehen(ich), food = "muesli")), wasser_temperatur = "warm"))
Wir könnten das ganze durch Einrückungen zumindest etwas lesbarer gestalten:
einsatzbereit <- anziehen(
duschen(
zaehne_putzen(
fruehstuecken(
aufstehen(ich),
food = "muesli")
), wasser_temperatur = "warm")
)
Das ist schon etwas besser, aber immer noch nicht sonderlich intuitiv zu lesen – und schließen wir nur eine Klammer an der falschen Stelle oder vergessen sie gar ganz, fliegt uns der gesamte Code um die Ohren.
Natürlich könnten wir die Schritte der Morgenroutine auch einzeln durchgehen und jeweils einem neuen “Objekt” zuweisen:
wach <- aufstehen(ich)
satt <- fruehstuecken(wach, food = "muesli")
sauber1 <- zaehe_putzen(satt)
sauber2 <- duschen(sauber1, wasser_temperatur = "warm")
einsatzbereit <- anziehen(sauber2)
Das erzeugt aber viele Objekte, die wir gar nicht weiter benötigen, da wir nur an einsatzbereit
interessiert sind. Wir könnten natürlich auch immer das gleiche Objekt wieder und wieder überschreiben, darunter leidet dann aber erneut die Lesbarkeit.
Mit dem Pipe-Operator %>%
können wir diese Schritte in einer logischen Lesereihenfolge ohne die Erstellung von redundaten Objekten durchführen:
9.3 Formale Definition
Formal ausgedrückt übergibt der Pipe-Operator %>%
das links von ihm stehende Objekt als erstes Argument an die rechts von ihm stehende Funktion:
# Die folgenden beiden Zeilen sind analog
f(x)
x %>% f()
# Oder anhand einer echten Funktion
mean(x) # ist analog zu
x %>% mean()
Weitere Funktionsargumente können regulär entweder positional oder explizit durch Namensnennung an die Funktion übergeben werden:
9.4 Einsatz von Pipes im Tidyverse
Besonders sinnvoll sind Pipes dann, wenn wir viele Funktionen hintereinander am gleichen Ausgangsobjekt aufrufen wollen, z. B. wenn wir unterschiedliche Schritte der Datenmodifikation an einem Datensatz vornehmen möchten. Bei den Tidyverse-Funktionen wissen wir, dass
- das erste Argument immer der Datensatz, also ein Tibble, ist und
- das Resultat der Funktion auch immer ein Datensatz, also ein Tibble ist.
Daher können wir diese Schritte schnell aneinanderreihen. Nutzen wir als Beispiel nochmals den Datensatz aus Kapitel 8:
# Wir laden den Datensatz
df_fb_eu <- read_csv("data/facebook_europawahl.csv")
# Wir erstellen einen modifizierten Datensatz, indem wir:
# 1. nur die Video-Posts auswählen
# 2. nur die Variablen id, party und comment_count auswählen
# 3. Nach Partei gruppieren
# 4. Eine neue Variable erstellen, die für jeden Post angibt,
# welchen Anteil dieser an allen Kommentaren unter Post der
# jeweiligen Partei hatte
# 5. heben die Gruppierung wieder auf und
# 6. weisen das Resultat dieser 'Pipe' dem Objekt modified_df zu
modified_df <- df_fb_eu %>% # Wir definieren die Zuweisung und übergeben df_fb_eu an
filter(type == "video") %>% # die filter()-Funktion; der resultierende Datensatz wird an
select(id, party, comments_count) %>% # select() übergeben; das Resultat wiederum wird
group_by(party) %>% # gruppiert etc.
mutate(comment_percentage = comments_count / sum(comments_count)) %>%
ungroup()
modified_df
## # A tibble: 272 x 4
## id party comments_count comment_percentage
## <dbl> <chr> <dbl> <dbl>
## 1 1 oedp.de 0 NA
## 2 3 B90DieGruenen 70 NA
## 3 6 CDU 239 NA
## 4 8 Piratenpartei 0 0
## 5 11 DiePARTEI 61 0.0371
## 6 14 FDP 358 0.117
## 7 16 DiePARTEI 15 0.00912
## 8 18 CDU 140 NA
## 9 26 SPD 174 0.0599
## 10 31 CDU 274 NA
## # ... with 262 more rows
Auch für schnelle deskriptive Auswertungen können wir Pipes gut nutzen – z. B. um uns schnell die Mittelwerte bestimmter Variablen gruppiert nach anderen Variablen anzuzeigen:
df_fb_eu %>%
group_by(party, type) %>%
summarize(mean_comments = mean(comments_count, na.rm = TRUE),
mean_shares = mean(shares_count, na.rm = TRUE),
mean_reactions = mean(reactions_count, na.rm = TRUE))
## # A tibble: 46 x 5
## # Groups: party [14]
## party type mean_comments mean_shares mean_reactions
## <chr> <chr> <dbl> <dbl> <dbl>
## 1 alternativefuerde link 826. 1341. 2953
## 2 alternativefuerde photo 860. 2466. 5269.
## 3 alternativefuerde video 875. 847. 2340.
## 4 B90DieGruenen photo 134. 183. 902.
## 5 B90DieGruenen video 77.9 184. 425.
## 6 CDU photo 394. 114. 810.
## 7 CDU video 293. 53.0 400.
## 8 CSU link 25 11 132.
## 9 CSU photo 143. 63.8 566.
## 10 CSU status 416. 112. 1106
## # ... with 36 more rows
Praktisch, oder? Bleibt noch die eine Hürde, dass %>%
eher kompliziert zu tippen ist – dankenswerterweise stellt RStudio aber auch hier eine Tastenkombination zur Verfügung: Strg/Cmd + Shift + M
fügt den gesamten Operator ein.
9.5 Übungsaufgaben
Erstellen Sie für die folgende Übungsaufgabe eine eigene Skriptdatei oder eine R-Markdown-Datei und speichern diese als ue9_nachname.R
bzw. ue9_nachname.Rmd
ab.
Lösen Sie die Übungsaufgaben 8.2 und 8.3 erneut, aber verwenden Sie Pipes, um den Code lesbarer und mit weniger redundanten Zwischenobjekten zu gestalten. An welchen Stellen ist es sinnvoll bzw. weniger sinnvoll, Pipes zu verwenden?
Das Konzept ist aus anderen Programmiersprachen entlehnt und wurde ursprünglich durch das Package
magrittr
in R eingeführt; soll der Pipe-Operator%>%
ohne das Tidyverse-Package genutzt werden, kann also dieses Package geladen werden:library(magrittr)
.↩︎