R Anleitungen

Der %>% Operator: Pipes in R

Pipes sind ein mächtiges Werkzeug, um eine Abfolge von mehreren Operationen klar auszudrücken. Dadurch kann Code erheblich vereinfacht und die Abläufe intuitiver gestaltet werden. Es ist jedoch nicht die einzige Möglichkeit, Code zu schreiben und mehrere Operationen zu kombinieren. Tatsächlich existierte R viele Jahre lang ohne Pipes. Wir lernen die Alternativen zur Pipe, wann Pipes nicht verwendet werden sollten, und einige nützliche Tools hierfür.

Die Pipe %>% stammt aus dem Magrittr-Paket. Pakete in der tidyverse laden %>% automatisch für uns, so dass wir normalerweise magrittr nicht explizit laden müssen. Da wir uns hier jedoch explizit auf die Pipe konzentrieren und keine anderen Pakete laden, laden wir sie explizit.

library(magrittr)

R ist eine funktionale Sprache, was bedeutet, dass der Code oft eine Menge Klammern enthält, ( und ). Bei komplexem Code bedeutet dies oft, dass diese Klammern ineinander verschachtelt werden müssen. Dadurch wird der R-Code schwer zu lesen und zu verstehen. Hier kommt %>% ins Spiel.

Der Sinn der Pipe ist es also, uns zu helfen, Code so zu schreiben, dass er leichter zu lesen und zu verstehen ist. Um zu sehen, warum die Pipe so nützlich ist, werden wir verschiedene Möglichkeiten zum Schreiben desselben Codes durchgehen. In unserem ersten Beispiel backen wir einen Kuchen ;-)

Backe, backe Kuchen,
der Bäcker hat gerufen:
Wer will guten Kuchen backen,
der muss haben sieben Sachen:
Eier und Schmalz,
Zucker und Salz,
Milch und Mehl,
Safran macht den Kuchen gel!
Schieb in den Ofen ’nein.

Jetzt wollen wir mit R den Kuchen backen. Dazu initiieren wir das Rezept im ersten Schritt:

kuchen <- Rezept()

Schrittweises Vorgehen

R Code
kuchen1 <- call(kuchen, from="Bäcker")
kuchen2 <- make(kuchen, quality="good")
kuchen3 <- mix(kuchen, c("Eier", "Schmalz", "Zucker", "Salz", "Milch", "Mehl", "Safran"))
kuchen4 <- bake(kuchen)

Der Hauptnachteil dieses Vorgehensweise ist, dass sie uns zwingt, jedes Zwischenelement zu benennen. Wenn dies zu natürlichen Namen führt: super, weiter so! Aber oft, wie in diesem Beispiel, fügen wir einfach numerische Suffixe hinzu, um die Namen eindeutig zu machen. Das führt zu zwei Problemen:

  1. Wir müssen darauf achten, immer die Elemente korrekt zu benennen. Falls wir noch einen Zwischenschritt einfügen wollen, z.B. zwischen kuchen2 und kuchen3, stört dies entweder unsere ehemals recht harmonische Struktur, oder wir müssen uns die Arbeit machen, die Variablen nach kuchen2 neu zu benennen.
  2. Unser Code wird überflutet mit einer Reihe von (unwichtigen) Variablen, für die wir in der Regel über diesen einen Aufruf keine Verwendung haben.
Für die meisten Befehle führt dies interessanterweise nicht dazu, dass R überproportional mehr Speicher verwendet, auch wenn das Objekt in mehreren Variablen verwendet wird.

Nur eine Variable

Statt immer eine neue Variable zu definieren, können wir auch einfach die ursprüngliche Variable (kuchen) immer wieder überschreiben:

R Code
kuchen <- call(kuchen, from="Bäcker")
kuchen <- make(kuchen, quality="good")
kuchen <- mix(kuchen, c("Eier", "Schmalz", "Zucker", "Salz", "Milch", "Mehl", "Safran"))
kuchen <- bake(kuchen)

Hierdurch vermeiden zwar die beiden Nachteile aus dem schrittweisen Vorgehen, aber dafür gibt es immer noch zwei Nachteile:

  1. Das Debugging ist mühsam: Bei einem Fehler müssen wir alles von Anfang an neu starten, weil die Werte der Variable nicht zwischengespeichert werden.
  2. Dadurch das die Variable immer die gleiche ist, mindert dies die Lesbarkeit des Codes. Es wird schwerer zu erkennen, was wir eigentlich auf jeder Zeile machen.

Verschachteln von Funktionen

Wir können auch gar keine Variablen verwenden und jeden Aufruf direkt machen. Dazu verschachteln wir die Funktionsaufrufe einfach ineinander.

R Code
kuchen <- bake(
               mix(
                    make(call(kuchen, from="Bäcker"),
                         quality="good"),
                    c("Eier", "Schmalz", "Zucker", "Salz", "Milch", "Mehl", "Safran")
                  )
              )

# Alternative Schreibweise, mit schlechter Lesbarkeit
kuchen <- bake(mix(make(call(kuchen, from="Bäcker"), quality="good"), c("Eier", "Schmalz", "Zucker", "Salz", "Milch", "Mehl", "Safran")))

Der Hauptnachteil hierbei ist, dass wir den Code von innen nach außen lesen müssen und das dadurch oft nicht gleich ersichtlich ist, welches Argument zu welcher Funktion gehört. Noch schlimmer ist es aber, wenn wir unseren Code gar nicht formatieren und alles in einer einzigen Zeile schreiben (wie im Beispiel oben die Alternative Schreibweise).

Die Pipe zur Rettung

Zu guter Letzt können wir auch die Pipe verwenden:

R Code
kuchen <- call(from="Bäcker") %>%
          make(quality="good") %>%
          mix(c("Eier", "Schmalz", "Zucker", "Salz", "Milch", "Mehl", "Safran")) %>%
          bake

Diese Abfolge von Funktionen kann so gelesen werden, als ob es sich um eine Kette von imperativen Aktionen handelt. Der Pipe-Operator ist dabei einfach nur eine einfachere und übersichtlichere Schreibweise für:

  • f(x) kann als x %>% f geschrieben werden.
  • Wenn die Funktion nur ein einziges Argument hat, können die Klammern weggelassen werden: x %>% f ist identisch mit x %>% f()

Platzhalter verwenden

Natürlich gibt es eine ganze Reihe von Funktionen, die nicht nur ein Argument, sondern mehrere Argumente nehmen. In seinem Standardmodus wird %>% immer das erste Argument verketten. Aber manchmal müssen wir ein anderes Argument verketten, als das erste. Dazu brauchen wir einen Platzhalter für das Argument und der ist der Punkt, ..

Auf diese Art und Weise könnten wir π zum Beispiel in einem Rutsch auf 0 bis 4 Nachkommastellen runden und danach dessen Kosinus berechnen:

R Code
0:4 %>% round(pi, digits=.) %>% cos

# -0.9899925 -0.9991352 -0.9999987 -0.9999999 -1.0000000

Den Platzhalter für Attribute wiederverwenden

Der Punkt ist hierbei aber ein allgemeiner Platzhalter, den wir auch wiederverwenden können. Beide Funktionen unten haben die gleiche Ausgabe, aber in der zweiten verwenden wir die Pipe um die Reihe 1:12 durch den Punkt-Operator zwei mal zu verwenden.

R Code
sprintf("%s (%s)", month.name[1:12], 1:12)

# [1] "January (1)" "February (2)" "March (3)" "April (4)" "May (5)" "June (6)"
# [7] "July (7)" "August (8)" "September (9)" "October (10)" "November (11)" "December (12)"

1:12 %>% sprintf("%s (%s)", month.name[.], .)

# [1] "January (1)" "February (2)" "March (3)" "April (4)" "May (5)" "June (6)" 
# [7] "July (7)" "August (8)" "September (9)" "October (10)" "November (11)" "December (12)"

Platzhalter bei verschachtelten Funktionen

Oft haben wir mehrere Funktionen ineinander verschachtelt, wenn wir den Pipe-Operator verwenden. Ein Beispiel hierfür wäre, wenn wir die sqrt(cos(x)) aufrufen. Die Funktionen sqrt und cos sind ineinander verschachtelt. Bei verschachtelten Funktionen ändert sich das Verhalten des Pipe-Operators: bei solchen Aufrufen wird noch zusätzlich ein Platzhalter in der ersten Funktion gesetzt. Betrachten wir zwei Beispiele:

R Code
1:10 %>% c(min(.), max(.))

# 1  2  3  4  5  6  7  8  9 10  1 10

1:10 %>% c(., min(.), max(.))

# 1 2 3 4 5 6 7 8 9 10 1 10

Im ersten Aufruf hätten wir einen Vektor mit zwei Elementen erwartet: 1 und 10. Stattdessen haben wir aber die Zahlen von 1 bis 10 und dazu zusätzlich 1 und 10 erhalten. Was ist passiert? Die Antwort ist einfach: magrittr hat die Funktion intern wie im zweiten Aufruf umgeschrieben und noch einen zusätzlichen Punkt unbemerkt eingefügt. Der erste und zweite Aufruf sind identisch.

Aber dieses Verhalten ist nicht immer erwünscht – und zum Glück gibt es auch eine einfache Möglichkeit dies zu deaktivieren. Wenn wir nicht wollen, dass noch zusätzlich ein Platzhalter in der ersten Funktion eingefügt wird, müssen wir die gesamte Funktion einfach in geschwungenen Klammern { } schreiben, so wie unten:

R Code
1:10 %>% {c(min(.), max(.))}

# 1 10

Lambda-Ausdrücke

Die geschwungenen Klammern können aber noch mehr: wir können mehrere Anweisungen hintereinander schreiben und (temporäre) sogar Variablen zuweisen. Alles, was in den Klammern steht, bezeichnet man als Lambda-Ausdruck. Schauen wir uns dazu das Beispiel unten an:

R Code
iris <- read.csv(url("https://statistikguru.de/iris.data"), header = TRUE)

iris %>% {
  size <- sample(1:5, size = 1)
  rbind(head(., size), tail(., size))
}

#     Kelchlänge Kelchbreite Blütenblattlänge Blütenblattbreite        Gattung
# 1          5.1         3.5              1.4               0.2    Iris-setosa
# 2          4.9         3.0              1.4               0.2    Iris-setosa
# 3          4.7         3.2              1.3               0.2    Iris-setosa
# 4          4.6         3.1              1.5               0.2    Iris-setosa
# 5          5.0         3.6              1.4               0.2    Iris-setosa
# 146        6.7         3.0              5.2               2.3 Iris-virginica
# 147        6.3         2.5              5.0               1.9 Iris-virginica
# 148        6.5         3.0              5.2               2.0 Iris-virginica
# 149        6.2         3.4              5.4               2.3 Iris-virginica
# 150        5.9         3.0              5.1               1.8 Iris-virginica

Hier nehmen wir den Iris-Datensatz und geben ihn über die Pipe an einen Lambda-Ausdruck weiter. In dem Lambda-Ausdruck erstellen wir einen neue Variable, size. Sie existiert aber nur in diesem einem Ausdruck und kann nicht außerhalb verwendet werden. Der Punkt entspricht von seiner Bedeutung her auch dem, was wir kennen, nämlich der Rückgabe der vorigen Funktion oder Variable, in diesem Fall dem Iris-Datensatz.

Andere Pipes

Neben der klassischen Pipe, existieren noch drei weitere Pipes: %<>%, %T>%, %$%. Auch wenn sie in der Regel weniger häufig verwendet werden, gibt etliche Anwendungsfälle, bei denen sie die Schreibweise vereinfachen und verkürzen können.

%<>%

Oft wollen wir eine Variable zuerst mehrere Funktionen durchlaufen lassen und dann diesen Rückgabewert wieder der ursprünglichen Variable zuweisen. Genau das macht der %<>% Operator:

R Code
x <- rnorm(100)

x <- x %>%
     abs %>%
     round(digits=2) %>%
     sort

x %<>%
     abs %>%
     round(digits=2) %>%
     sort

Beide Zuweisungen sind identisch, nur dass wir uns bei der Zweiten noch die Zuweisung zu x sparen können.

Der Operator %<>% muss der erste Pipe-Operator in der Kette sein, damit alles funktioniert.

%T>%

Der T-Operator arbeitet genau wie %>%, aber er gibt den Wert auf der linken Seite zurück und nicht das potenzielle Ergebnis der Operationen auf der rechten Seite.

Das bedeutet, dass der T-Operator in Situationen nützlich sein kann, in denen wir Funktionen eingebunden haben, die für ihre Nebenwirkung verwendet werden, wie z.B. das Plotten mit plot() oder das Schreiben in eine Datei.

Mit anderen Worten: Funktionen wie plot() geben normalerweise nichts zurück. Das bedeutet, dass die Pipeline beispielsweise nach dem Aufruf von plot() enden würde. Im folgenden Beispiel ermöglicht der T-Operator %T>% jedoch, dass die Pipeline auch nach der Verwendung von plot() fortgesetzt werden kann:

R Code
rnorm(200) %>%
  matrix(ncol = 2) %T>%
  plot %>%
  colSums

%$%

Wenn wir mit R arbeiten, stellen wir fest, dass viele Funktionen ein Datenargument benötigen. Zum Beispiel die Funktion lm() oder die Funktion with(). Diese Funktionen sind nützlich in einer Pipeline, in der Daten zunächst verarbeitet und dann an die Funktion übergeben werden.

Bei Funktionen, die kein Datenargument haben, wie z. B. die Funktion cor(), ist es immer noch nützlich, wenn wir die Variablen aus den Daten anderen (nachfolgenden) Funktionen bereitstellen können, und genau da kommt der %$%-Operator ins Spiel. Dazu das folgende Beispiel:

R Code
iris %>%
  subset(Kelchlänge > mean(Kelchlänge)) %$%
  cor(Kelchlänge, Kelchbreite)

# 0.3361992