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:

funktionsname(argument1 = wert1, argument2 = wert2, argument3 = wert3, ...)

Wir haben außerdem bereits gesehen, dass wir Funktionen ineinander verschachteln können, wenn wir mehrere Funktionen hintereinander aufrufen möchten:

round(mean(iris$Sepal.Length), 2)
## [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”:

  1. Aufstehen
  2. Frühstücken
  3. Zähne putzen
  4. Duschen
  5. 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:

einsatzbereit <- ich %>%
  aufstehen() %>% 
  fruehstuecken(food = "muesli") %>% 
  zaehne_putzen() %>% 
  duschen(wasser_temperatur = "warm") %>% 
  anziehen()

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:

# Die folgenden beiden Zeilen sind wiederum analog
f(x, y, z)
x %>% f(y, z)

# Und wieder am Beispiel der mean()-Funktion
mean(x, na.rm = TRUE) # ist analog zu
x %>% mean(na.rm = TRUE)

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

  1. das erste Argument immer der Datensatz, also ein Tibble, ist und
  2. 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.


Übungsaufgabe 9.1 Pipes:

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?


  1. 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).↩︎