Die Familie der apply-Funktionen in R Teil 1: Verarbeitung von Listen mit lapply(), sapply(), vapply() und rapply()

In der Familie der apply-Funktionen gibt es mehrere Vertreter, mit den über die Elemente einer Liste iteriert werden kann, wobei auf jede Komponente eine Funktion f() angewendet wird. Besprochen werden lapply(), sapply(), vapply() und rapply(). Die Funktion lapply() ist dabei der grundlegende Vertreter, der die bei der Iteration entstehenden Rückgabewerte wieder zu einer Liste zusammensetzt. Dagegen versucht sapply() einen möglichst einfachen Rückgabewert zu erzeugen (Vektor oder Feld). Der Funktion vapply() kann eine Vorlage für den Rückgabewert übergeben werden, so dass man bessere Kontrolle für weitere Berechnungen hat. Mit rapply() können bestimmte Datentypen aus einer Liste selektiert werden und nur auf diese wird die Funktion f() angewendet; zudem wird die Anwendung von f() rekursiv an die Komponenten der Liste weitergereicht.

Einordnung des Artikels

  • Einführung in die Informatik
    • Einführung in die Programmiersprache R
      • Funktionen in R
        • Die Familie der apply-Funktionen in R Teil 1: Verarbeitung von Listen mit lapply(), sapply(), vapply() und rapply()

In Die Funktion apply() in R: Iteration über die Zeilen oder Spalten einer Matrix wurde die Unterteilung der Arbeitsweise der apply-Funktionen in die Phasen split-apply-combine vorgestellt. Diese wird hier auf die Funktionen übertragen, mit denen über die Komponenten von Listen iteriert wird. Die Kenntnis dieser Phasen wird hier vorausgesetzt. Ebenso die Kenntnisse, wie man Listen erzeugt und auf ihre Komponenten zugreift (siehe Listen in R: der Datentyp list und Listen in R: Anwendungen).

Die Funktion lapply()

Einführung

So wie apply() zur Iteration über die Zeilen oder Spalten einer Matrix verwendet wird, kann man mit lapply() über die Komponenten einer Liste iterieren. Die Funktion lapply() besitzt die Eingabewerte:

lapply(X, FUN, ...)

Dabei ist X eine Liste und FUN eine Funktion, die auf die Komponenten der Liste angewendet wird; mit Hilfe von ... kann man weitere Argumente an FUN weiterreichen.

Abbildung 1 zeigt die drei Phasen split-apply-combine für FUN = f , wenn lapply() auf eine Liste lst angewendet wird.

Abbildung 1: Die drei Phasen split-apply-combine bei der Anwendung von lapply() auf eine Liste lst: Die Liste wird in ihre Komponenten zerlegt, auf jede Komponente wird die Funktion f() angewendet, die Komponenten werden wieder zu einer Liste zusammengesetzt.Abbildung 1: Die drei Phasen split-apply-combine bei der Anwendung von lapply() auf eine Liste lst: Die Liste wird in ihre Komponenten zerlegt, auf jede Komponente wird die Funktion f() angewendet, die Komponenten werden wieder zu einer Liste zusammengesetzt.

Auf den ersten Blick ist Abbildung 1 identisch mit der Anwendung einer Funktion auf einen Vektor. Der große Unterschied liegt darin, dass eine Liste als rekursive Struktur Komponenten mit unterschiedlichem Datentyp besitzen kann. Es können Komplikationen dadurch entstehen, dass die Funktion FUN nicht auf alle Komponenten angewendet werden kann.

Kann sie auf alle Komponenten angewendet werden, werden die Rückgabewerte wieder zu einer Liste zusammengesetzt. (Wie sich lapply() verhält, wenn FUN nicht auf alle Komponenten angewendet werden kann, wird im Zusammenhang mit der Funktion rapply() diskutiert.)

Und es gibt weitere Fragen, die sich durch Abbildung 1 aufdrängen:

  • Gibt es andere Datentypen, auf die lapply() angewendet werden kann? Insbesondere sind Dataframes eng verwandt mit Listen; ebenso scheint nichts dagegen zu sprechen, lapply() auf Vektoren anzuwenden.
  • Gibt es einfache Fälle, in denen anstelle einer Liste ein Vektor zurückgegeben wird?
  • Welche Fehlermeldung wird erzeugt, wenn FUN nicht auf alle Komponenten anwendbar ist?

Die typische Anwendung von lapply()

Im folgenden Skript wird eine Liste definiert, die aus 4 Vektoren unterschiedlicher Länge besteht – es handelt sich um die ersten 4 Zeilen des Pascalschen Dreiecks. Mit Hilfe von lapply() wird die Summe der Zeilen des Pascalschen Dreiecks berechnet:

# Binomialkoeffizienten:
bc <- list(row0 = 1, row1 = c(1, 1), row2 = c(1, 2, 1), row3 = c(1, 3, 3, 1))

# Zeilensummen:
sum.bc <- lapply(X = bc, FUN = sum)

str(sum.bc)
# List of 4
# $ row0: num 1
# $ row1: num 2
# $ row2: num 4
# $ row3: num 8

Zeile 2: Die Binomialkoeffizienten werden in einer Liste bc mit 4 Komponenten abgespeichert. Für die Komponenten werden Namen gesetzt. Würde man eine Matrix verwenden, müsste man eine Konvention einführen, wie die Matrix mit Nullen aufgefüllt wird, da die Zeilen unterschiedlich lang sind.

Zeile 5: Anwendung der Funktion lapply() auf die Liste bc mit der Funktion FUN = sum . Damit werden im Pascalschen Dreieck die Zeilensummen berechnet, die mit 2n übereinstimmen müssen, wenn man die Zeilen beginnend bei 0 durchnummeriert.

Zeile 7: Die Ausgabe der Struktur des Rückgabewertes von lapply() bestätigt, dass die 4 Summenwerte zu einer Liste zusammengefasst werden und die Namen der ürsprünglichen Liste weitergegeben werden; und man erkennt die erwarteten Zahlenwerte. Dies alles ist keine Selbstverständlichkeit, denn die Funktion FUN = sum wurde hier so einfach gewählt, dass als Rückgabewert auch ein Vektor in Frage kommt.

Die folgende Abbildung zeigt die nötigen Formeln zum Pascalschen Dreieck, die hier nicht erklärt werden.

Abbildung 2: Definition der Fakultät und des Binomialkoeffizienten (Gleichung 1 und 2) und der wichtigsten Formeln zur Berechnung des Pascalschen Dreiecks.Abbildung 2: Definition der Fakultät und des Binomialkoeffizienten (Gleichung 1 und 2) und der wichtigsten Formeln zur Berechnung des Pascalschen Dreiecks.

Anwendung von lapply() auf andere Datentypen

Zuerst soll die Frage beantwortet werden, welche Datentypen X im Aufruf von lapply() annehmen darf.

Im folgenden Skript wird versucht, lapply() auf

  • einen Vektor,
  • eine Matrix und
  • ein Dataframe anzuwenden:

1. Beispiel: Anwendung von lapply() auf einen Vektor

Das folgende Skript zeigt ein Beispiel für eine vektorisierte Anwendung einer Funktion. Hier wird nchar() auf einen Vektor mit drei Komponenten angewendet; die Funktion nchar() zählt de Zeichen in einer Zeichenkette.

names <- c("Anna", "Berta", "Carla")

nchar(names)
# [1] 4 5 5

Das folgende Skript versucht diese Aufgabe mit lapply() zu lösen:

names <- c("Anna", "Berta", "Carla")

nchars <- lapply(X = names, FUN = nchar)
nchars
# [[1]]
# [1] 4
# 
# [[2]]
# [1] 5
# 
# [[3]]
# [1] 5

as.list(names)
# [[1]]
# [1] "Anna"
# 
# [[2]]
# [1] "Berta"
# 
# [[3]]
# [1] "Carla"

Zeile 1: Es wird wie oben der Vektor der Namen gebildet.

Zeile 3: Auf diesen Vektor wird die Funktion lapply() angewendet, wobei FUN = nchar gesetzt wird.

Zeile 4 bis 12: Die Ausgabe des Ergebnisses mag unerwartet sein; es wird eine Liste mit 3 Komponenten gebildet. Naheliegender wäre es vielleicht gewesen, dass bei Anwendung von lapply() auf einen Vektor wieder ein Vektor gebildet wird (dies würde der Vorgehensweise split-apply-combine einer vektorisierten Funktion entsprechen).

Zeile 14: Gibt die Erklärung für das Verhalten von lapply(). Verwandelt man den Vektor names mit as.list() in eine Liste, ensteht die LIste, die ab Zeile 14 ausgegeben wird. Und wendet man auf diese Liste die Funktion lapply() wie in Zeile 3 an, entsteht die Liste aus Zeile 4 bis 12.

Das erste Beispiel zeigt somit:

Die Funktion lapply() kann auf Vektoren angewendet werden; diese werden aber zuerst (also noch in der Phase split) in eine Liste verwandelt.

2. Beispiel: Anwendung von lapply() auf eine Matrix

Zuerst wird die zweidimensionale Einheitsmatrix erzeugt und anschließend wird darauf lapply() angewendet mit der FunktionFUN = "-" . Hier wird das Minuszeichen als unärer Operator interpretiert, das heißt jede Zahl wird mit -1 multipliziert:

m <- diag(x = 1, nrow = 2)
m
#     [,1] [,2]
# [1,]    1    0
# [2,]    0    1

lst <- lapply(X = m, FUN = "-")
lst
# [[1]]
# [1] -1
# 
# [[2]]
# [1] 0
# 
# [[3]]
# [1] 0
# 
# [[4]]
# [1] -1

as.list(m)
# [[1]]
# [1] 1
# 
# [[2]]
# [1] 0
# 
# [[3]]
# [1] 0
# 
# [[4]]
# [1] 1

Zeile 1 bis 5: Die Einheitsmatrix wird erzeugt und ausgegeben.

Zeile 7: Die Funktion lapply() wird auf die Einheitsmatrix angewendet.

Zeile 8: Die Ausgabe zeigt, dass die 4 Elemente der Matrix mit -1 multipliziert wurden und als Liste verpackt werden.

Zeile 21: Zeigt wieder die Erklärung für das Verhalten von lapply(). Die Matrix wird (wie der Vektor im ersten Beispiel) zuerst mit as.list() in eine Liste verwandelt und dann erst wird lapply() angewendet.

3. Beispiel: Anwendung von lapply() auf ein Dataframe

Zuletzt wird gezeigt, wie die Funktion lapply() wirkt, wenn sie auf ein Dataframe angewendet wird:

v1 <- c(1, 2, 3)
v2 <- c(4, 5, 6)

df <- data.frame(x = v1, y = v2)
df
#   x y
# 1 1 4
# 2 2 5
# 3 3 6

lst <- lapply(X = df, FUN = function(x){return(x * x)})
lst
# $x
# [1] 1 4 9
# 
# $y
# [1] 16 25 36

is.list(lst)
# [1] TRUE
is.data.frame(lst)
# [1] FALSE

as.list(df)
# $x
# [1] 1 2 3
# 
# $y
# [1] 4 5 6

Zeile 1 bis 5: Es wird ein Dataframe aus zwei Vektoren erzeugt (so dass es drei Zeilen und zwei Spalten besitzt) und ausgegeben.

Zeile 11: Die Funktion lapply() wird auf das Dataframe angewendet mit der anonymen Funktion, die jeden Wert quadriert.

Zeile 12 bis 17: An der Ausgabe erkennt man, dass vermutlich in der split-Phase das Dataframes spaltenweise zerlegt wird und auf jede dieser Spalten wird die anonyme Funktion (vektorisiert) angewendet. In der combine-Phase werden die bearbeiteten Spalten des Dataframes in Komponenten einer Liste verwandelt.

Zeile 19 bis 22: Man erkennt, das lapply() tatsächlich eine Liste und kein Dataframe zurückgibt.

Zeile 24 bis 29: Die Erklärung des Verhaltens von lapply() in der combine-Phase liefert wieder die Funktion as.list(). Das Dataframe wird zuerst in eine Liste verwandelt, indem as.list() angewendet wird; dabei werden die Spalten des Dataframes zu den Komponenten der Liste und die Spalten-Namen werden zu den Komponenten-Namen. Auf diese Liste wird dann lapply() in der üblichen Art angewendet.

Man kann die Ergebnisse dieser Beispiele etwa wie folgt zusammenfassen:

  1. Die Funktion lapply() erzeugt als Rückgabewert immer eine Liste.
  2. Es ist möglich, lapply() auf Objekte anzuwenden, die keine Listen sind. Diese werden mit as.list() zuerst in eine Liste verwandelt und dann wird lapply() darauf angewendet.
  3. Man sollte sich daher lapply() nur dann auf Objekte anwenden, die keine Listen sind, wenn man als Rückgabewert mit einer Liste weiterarbeiten möchte. Wenn nicht, sind andere apply() Funktionen besser geeignet.
  4. Insbesondere bei Vektoren ist meist die direkte Anwendung der Funktion einfacher als die Verwendung von lapply().

Die graphische Darstellung der Arbeitsweise von lapply() erfolgt dann besser wie in Abbildung 3: Wird ein Objekt obj mit lapply() verarbeitet, das keine Liste ist, wird in der split-Phase zuerst die Funktion as.list() angewendet. In Abbildung 1 war dieser Schritt nicht dargestellt.

Abbildung 3: Im Unterschied zu Abbildung 1 wird jetzt der erste Schritt der split-Phase dargestellt. Wenn ein Objekt obj keine Liste ist, wird es zuerst mit as.list() in eine Liste verwandelt. Dann erst finden die Schritte statt, die schon in Abbildung 1 gezeigt wurden.Abbildung 3: Im Unterschied zu Abbildung 1 wird jetzt der erste Schritt der split-Phase dargestellt. Wenn ein Objekt obj keine Liste ist, wird es zuerst mit as.list() in eine Liste verwandelt. Dann erst finden die Schritte statt, die schon in Abbildung 1 gezeigt wurden.

An der Implementierung von lapply() kann man leicht nachvollziehen, wie die unterschiedlichen Datentypen behandelt werden:

lapply
function (X, FUN, ...) 
{
    FUN <- match.fun(FUN)
    if (!is.vector(X) || is.object(X)) 
        X <- as.list(X)
    .Internal(lapply(X, FUN))
}
<bytecode: 0x000000000287dff8>
<environment: namespace:base>

Man erkennt, dass Objekte, die keine Vektoren sind zuerst in Listen verwandelt werden; Vektoren sind dabei entweder atomare Vektoren (mit Modus logical, numeric und so weiter) oder Listen (eine Liste ist ein Vektor mit Modus list). Die eigentliche Implementierung von lapply() ist natürlich so nicht zugänglich.

Die Funktion sapply(): eine vereinfachte Version von lapply()

Das einführende Beispiel für die Anwendung von lapply() zur Bearbeitung der Binomialkoeffizienten hat gezeigt, dass die Funktion lapply() – auch in diesem einfachen Fall – als Rückgabewert eine Liste erzeugt, obwohl sie dort zur Weiterverarbeitung zu sperrig ist – ein Vektor wäre besser geeignet gewesen. Möchte man einen Vektor erzeugen, kann man dies mit unlist() erreichen:

bc <- list(row0 = 1, row1 = c(1, 1), row2 = c(1, 2, 1), row3 = c(1, 3, 3, 1))

sum.bc <- lapply(X = bc, FUN = sum)

str(sum.bc)
# List of 4
# $ row0: num 1
# $ row1: num 2
# $ row2: num 4
# $ row3: num 8

is.vector(sum.bc)    # TRUE
is.list(sum.bc)      # TRUE

sums <- unlist(sum.bc)
# row0 row1 row2 row3 
# 1    2    4    8 

is.vector(sums)   # TRUE
is.list(sums)     # FALSE

Zeile 1 bis 10: Die ersten 4 Zeilen des Pascalschen Dreiecks werden mit den Binomialkoeffizienten gesetzt, wobei jede Zeile des Dreiecks ein Vektor ist, die zu einer Liste zusammengefasst werden. Mit lapply() in Zeile 3 werden die Summen der Binomialkoeffizienten je einer Zeile berechnet.

Zeile 12 und 13: Man erkennt, dass lapply() hier eine Liste zurückgibt, die ja immer zugleich ein Vektor ist (Modus list).

Zeile 15 bis 20: Wird jetzt die Liste sum.bc mit unlist() weiterverarbeitet, erhält man tatsächlich einen Vektor und keine Liste. Die Namen aus der Liste (Attribut names) werden an den Vektor weitergereicht

Man sieht an diesem Beispiel, dass man bei der Anwendung von lapply() selbst dafür verantwortlich ist, wie die von lapply() zurückgegebene Liste weiterverarbeitet wird. Mit Hilfe der Funktion sapply() kann man dies vereinfachen:

  1. Die Funktion sapply() ruft intern lapply() auf.
  2. Der Rückgabewert von lapply() wird – wenn möglich – vereinfacht, nämlich zu einem Vektor oder zu einem Feld. Nur wenn dies nicht möglich ist, wird die Liste wie bei lapply() zurückgegeben. Dies ist der Fall, wenn die Rückgabewerte von FUN unterschiedliche Längen haben (siehe das Beispiel weiter unten mit FUN = identity ).

Verwendet man also im letzten Beispiel nicht lapply() sondern sapply() (siehe Zeile 3 unten), sollte sofort ein Vektor gebildet werden (siehe Ausgaben):

bc <- list(row0 = 1, row1 = c(1, 1), row2 = c(1, 2, 1), row3 = c(1, 3, 3, 1))

sum.bc <- sapply(X = bc, FUN = sum)

sum.bc
# row0 row1 row2 row3 
# 1    2    4    8 

str(sum.bc)
# Named num [1:4] 1 2 4 8
# - attr(*, "names")= chr [1:4] "row0" "row1" "row2" "row3"

is.vector(sum.bc)    # TRUE
is.list(sum.bc)      # FALSE

Nur wenn kein Vektor (oder Feld) gebildet werden kann, erhält man wieder eine Liste. Im folgenden Beispiel wird anstelle der Summe die Funktion identity() verwendet, wodurch Vektoren unterschiedlicher Länge enstehen:

bc <- list(row0 = 1, row1 = c(1, 1), row2 = c(1, 2, 1), row3 = c(1, 3, 3, 1))

sapply(X = bc, FUN = identity)
# $row0
# [1] 1
# 
# $row1
# [1] 1 1
# 
# $row2
# [1] 1 2 1
# 
# $row3
# [1] 1 3 3 1

Jetzt stimmt die zu verarbeitende Liste X mit dem Rückgabewert von sapply() überein.

Im folgenden Beispiel wird jede Zeile aus dem Pascalschen Dreieck mit Nullen aufgefüllt, so dass alle Zeilen identische Länge haben; für die "identische Länge" wird die längste Zeile aus dem Pascalschen Dreieck genommen:

bc <- list(row0 = 1, row1 = c(1, 1), row2 = c(1, 2, 1), row3 = c(1, 3, 3, 1))

max.lgth <- max(lengths(bc))
                
m <- sapply(X = bc, FUN = function(x){ c( x, rep(0, times = max.lgth - length(x) ) ) })
m
#     row0 row1 row2 row3
# [1,]    1    1    1    1
# [2,]    0    1    2    3
# [3,]    0    0    1    3
# [4,]    0    0    0    1

Zeile 1: Definiert wieder bc als die ersten 4 Zeilen des Pascalschen Dreiecks.

Zeile 3: Als max.lgth wird die Länge der längsten Zeile abgespeichert.

Zeile 5: Die Funktion sapply() wird auf bc angewendet mit der anonymen Funktion, die jede Zeile aus bc um so viele Nullen erweitert, dass alle Vektoren die Länge max.lgth haben. Man beachte, dass die Variable max.lgth innerhalb der anonymen Funktion eingesetzt werden kann.

Zeile 6: An der Ausgabe erkennt man, dass ein Feld (hier eine Matrix) gebildet wird mit folgenden Eigenschaften:

  • Die verlängerten Zeilen aus bc sind jetzt die Spalten der Matrix.
  • Die Namen der Komponenten von bc werden als Spalten-Namen an die Matrix weitergegeben.

Für eine andere Anordnung der Binomialkoeffizienten müsste man jetzt geeignete Matrix-Operationen anwenden.

In den Beispielen wurde sapply() mit den selben Argumenten wie lapply() aufgerufen. Dies ist möglich, da sapply() genau die drei Argumente von lapply() besitzt und zwei weitere Argumente mit default-Wert TRUE:

lapply(X, FUN, ...)

sapply(X, FUN, ..., simplify = TRUE, USE.NAMES = TRUE)

Die ersten drei Eingabewerte von sapply() werden an lapply() weitergereicht. Mit simplify = TRUE wird das oben beschriebene Verhalten gesteuert: Es wird versucht, als Rückgabewert ein Feld zu erzeugen. Das Argument USE.NAMES = TRUE sorgt dafür, dass die Namen aus X an den Rückgabewert weitergegeben werden.

Unten sind die Quelltexte von sapply() und simplify2array() angeführt. Man erkennt:

  • Die Funktion sapply() ruft lapply() auf, wenn möglich wird der Rückgabewert mit Hilfe von simplify2array() verändert.
  • Die Funktion simplify2array() erzeugt zum Eingabewert ein geeignetes Feld und gibt dies zurück.
sapply
# function (X, FUN, ..., simplify = TRUE, USE.NAMES = TRUE) 
# {
#   FUN <- match.fun(FUN)
#   answer <- lapply(X = X, FUN = FUN, ...)
#   if (USE.NAMES && is.character(X) && is.null(names(answer))) 
#     names(answer) <- X
#   if (!identical(simplify, FALSE) && length(answer)) 
#     simplify2array(answer, higher = (simplify == "array"))
#   else answer
# }
# <bytecode: 0x0000000013492c88>
#   <environment: namespace:base>
simplify2array
# function (x, higher = TRUE) 
# {
#   if (length(common.len <- unique(lengths(x))) > 1L) 
#     return(x)
#   if (common.len == 1L) 
#     unlist(x, recursive = FALSE)
#   else if (common.len > 1L) {
#     n <- length(x)
#     r <- unlist(x, recursive = FALSE, use.names = FALSE)
#     if (higher && length(c.dim <- unique(lapply(x, dim))) == 
#         1 && is.numeric(c.dim <- c.dim[[1L]]) && prod(d <- c(c.dim, 
#                                                              n)) == length(r)) {
#       iN1 <- is.null(n1 <- dimnames(x[[1L]]))
#       n2 <- names(x)
#       dnam <- if (!(iN1 && is.null(n2))) 
#         c(if (iN1) rep.int(list(n1), length(c.dim)) else n1, 
#           list(n2))
#       array(r, dim = d, dimnames = dnam)
#     }
#     else if (prod(d <- c(common.len, n)) == length(r)) 
#       array(r, dim = d, dimnames = if (!(is.null(n1 <- names(x[[1L]])) & 
#                                          is.null(n2 <- names(x)))) 
#         list(n1, n2))
#     else x
#   }
#   else x
# }
# <bytecode: 0x000000000c152c60>
#   <environment: namespace:base>

In Abbildung 4 wird versucht, die Arbeitsweise von sapply() graphisch darzustellen:

Abbildung 4: Die Darstellung der Arbeitsweise von sapply() mit den frei Phasen split-apply-combine. Man beachte den Unterschied zu Abbildung 3: jetzt wird in der combine-Phase durch simplify2array() zusätzlich dafür gesorgt, dass der Rückgabewert von lapply() in ein Feld verwandelt wird.Abbildung 4: Die Darstellung der Arbeitsweise von sapply() mit den frei Phasen split-apply-combine. Man beachte den Unterschied zu Abbildung 3: jetzt wird in der combine-Phase durch simplify2array() zusätzlich dafür gesorgt, dass der Rückgabewert von lapply() in ein Feld verwandelt wird.

Mit sapply() hat man eine sehr benutzerfreundliche Funktion zur Verfügung, die

  • wie alle apply-Funktionen Schleifen erspart und die
  • einen möglichst einfachen Rückgabewert erzeugt.

Allerdings ist es oft nicht ganz leicht, bei einer Anwendung von sapply() vorherzusagen, welchen Datentyp der Rückgabewert besitzt. Wird daher sapply() innerhalb einer anderen Funktion eingesetzt, wo mit ihrem Rückgabewert weitergerechnet wird, kann es leicht zu Fehlermeldungen kommen. Eine Funktion, die nahezu die identische Funktionalität wie sapply() besitzt, bei der sich aber der Datentyp des Rückgabewertes besser kontrollieren lässt, ist vapply(), die im nächsten Abschnitt vorgestellt wird.

Die Funktion vapply()

Die Funktion vapply() arbeitet ähnlich wie sapply(), hat aber das Argument FUN.VALUE, mit dem man vorgeben kann, welchen Rückgabewert die Funktion FUN hat:

vapply(X, FUN, FUN.VALUE, ..., USE.NAMES = TRUE)

Was auf den ersten Blick wie zusätzlicher Aufwand aussieht – wozu soll man sich Gedanken machen, welchen Rückgabewert FUN hat? – kann das Argument sinnvoll eingesetzt werden, wie in den folgenden Beispielen gezeigt wird.

1. Beispiel: Vorgabe eines speziellen Datentyps für den Rückgabewert (Die Summe der Binomialkoeffizienten)

Es wird wie oben im ersten Beispiel für sapply() über die ersten 4 Zeilen des Pascalschen Dreiecks iteriert. Von jeder Zeile wird die Summe gebildet. Jetzt wird aber vapply() eingesetzt und ausdrücklich gesagt, dass ein numerischer Wert von FUN erzeugt wird:

bc <- list(row0 = 1, row1 = c(1, 1), row2 = c(1, 2, 1), row3 = c(1, 3, 3, 1))

sum.bc <- vapply(X = bc, FUN = sum, FUN.VALUE = numeric(1))
sum.bc
# row0 row1 row2 row3 
# 1    2    4    8

Bei der Verwendung von lapply() entsteht hier eine Liste, die man mit unlist() zu einem Vektor einebnen kann. Mit sapply() ensteht genau der Vektor, der auch mit vapply() erzeugt wird.

Worin soll dann der Vorteil von vapply() gegenüber sapply() liegen? Falls man einen falschen Datentyp in FUN.VALUE angibt, erhält man eine spezifischere Fehlermeldung als wenn man mit sapply() ein "unbekanntes" Objekt erzeugt und dann später Operationen anwendet, die dafür nicht definiert sind. Gerade wenn man vor der Wahl steht, ob man innerhalb einer Funktion sapply() oder vapply() einsetzt, sollte man vapply() wählen, falls mit dem Rückgabewert weitergerechnet werden soll. Falls nicht der gewünschte Rückgabewert erzeugt wird, erhält man eine aussagekräftige Fehlermeldung.

Im folgenden Skript wird für den Rückgabewert FUN.VALUE = integer(1) gesetzt, aber die Eingabewerte werden wie oben verwendet (da sie nicht ausdrücklich als integer definiert wurden, sind alle Zahlen im Pascalschen Dreieck double-Zahlen):

rm(sum.bc)
bc <- list(row0 = 1, row1 = c(1, 1), row2 = c(1, 2, 1), row3 = c(1, 3, 3, 1))

sum.bc <- vapply(X = bc, FUN = sum, FUN.VALUE = integer(1))
# Error in vapply(X = bc, FUN = sum, FUN.VALUE = integer(1)) : 
#   values must be type 'integer',
# but FUN(X[[1]]) result is type 'double'

Damit kann man auch besser verstehen, wie vapply() arbeitet: Bevor die Iteration startet, wird der Rückgabewert für vapply() vorbereitet – im letzten Beispiel ist es ein Vektor der Länge 4 mit Speicher-Modus integer. Erst dann wird die Liste bc abgearbeitet. Und da bei der ersten Anwendung von sum() ein double-Wert entsteht, erhält man eine Fehlermeldung und das Objekt sum.bc kann nicht gebildet werden.

Mit dieser Beobachtung sollte auch klar sein, dass man das Argument FUN.VALUE nicht dafür einsetzen kann, eine Typumwandlung zu erzwingen. Dazu muss man erst vapply() korrekt anwenden und anschließend die Typumwandlung an dem erzeugten Objekt selbst vornehmen.

Aufgabe : Definieren Sie die Zahlenwerte in bc ausdrücklich als integer-Zahlen und testen Sie, ob vapply() dann mit FUN.VALUE = integer(1) richtig arbeitet.

2. Beispiel: FUN.VALUE als Vorlage für den Rückgabewert (Die Diagonalen im Pascalschen Dreieck)

Das folgende Beispiel zeigt, wie man das Argument FUN.VALUE einsetzen kann, um eine Vorlage für den Rückgabewert zu definieren.

Dazu wird das Beispiel von oben aufgegriffen, in dem das Pascalsche Dreieck derart mit Nullen aufgefüllt wurde, so dass man eine Matrix bilden konnte. Vergleicht man diese Matrix mit dem ursprünglichen Pascalschen Dreieck, so fällt auf, dass dir Zeilen der Matrix gerade die Diagonalen des Pascalschen Dreiecks sind (siehe Abbildung 2).

bc <- list(row0 = 1, row1 = c(1, 1), row2 = c(1, 2, 1), row3 = c(1, 3, 3, 1))
lgth <- max(lengths(bc))

m <-  vapply( X = bc, FUN = function(x){c(x, rep(0L, times = lgth - length(x)))}, 
              FUN.VALUE = c("diag1" = 0, "diag2" = 0, "diag3" = 0, "diag4" = 0) )
m
#       row0 row1 row2 row3
# diag1    1    1    1    1
# diag2    0    1    2    3
# diag3    0    0    1    3
# diag4    0    0    0    1

t(m)
#      diag1 diag2 diag3 diag4
# row0     1     0     0     0
# row1     1     1     0     0
# row2     1     2     1     0
# row3     1     3     3     1

Zeile 1 und 2: Es werden wieder die ersten 4 Zeilen des Pascalschen Dreiecks als Liste definiert und die maximale Länge der Zeilen bestimmt.

Zeile 4 und 5: Die Funktion vapply() wird eingesetzt, um die Liste bc in eine Matrix umzuwandeln, die durch Nullen aufgefüllt werden. Die Funktionswerte der dabei verwendeten anonymen Funktion sind gerade die Diagonalen des Pascalschen Dreiecks. Daher wird als Vorlage für den Rückgabewert FUN.VALUE ein Vektor mit 4 Komponenten gewählt, der die Zeilen geeignet bezeichnet.

Zeile 6 bis 11: Die Ausgabe zeigt die derart erzeugte Matrix m.

Zeile 13 bis 18: Es ist geschickter die transponierte Matrix zu verwenden, damit nicht ihre Spalten mit row0, row1, ... bezeichnet werden.

Die Funktion rapply()

Die Funktionalitäten von rapply()

Die Verarbeitung von Listen wird oft dadurch erschwert, dass eine Liste als rekursive Struktur Komponenten mit unterschiedlichen Datentypen enthalten kann. Soll über die Komponenten der Liste mit einer Funktion f() iteriert werden, kann es leicht vorkommen, dass die Funktion nicht auf alle Komponenten angewendet werden kann. Bei einer Anwendung von Funktionen wie lapply() ist es daher oft nötig entweder die Funktion f() anzupassen oder aus der Liste zuerst eine geeignete Teil-Liste auszuwählen.

Die Funktion rapply() kann hier sehr hilfreich sein:

  • Sie erlaubt eine Selektion der Komponenten der Liste, indem die Funktion f() nur auf gewisse Datentypen angewendet wird.
  • Es kann konfiguriert werden, wie die anderen Datentypen behandelt werden.
  • Enthält die zu verarbeitende Liste weitere Listen als Komponenten, wird die Funktion rapply() rekursiv an diese Listen weitergereicht.

Wegen dieser Vielzahl an Funktionalitäten hat die Funktion rapply() viele Eingabewerte:

rapply(object, f, classes = "ANY", deflt = NULL,
           how = c("unlist", "replace", "list"), ...)

Zum Verständnis der Funktionalitäten von rapply() müssen diese in der richtigen Reihenfolge erklärt werden:

  1. Die Argumente object und f geben die zu verarbeitende Liste und die Funktion an, die zur Iteration verwendet werden – bis auf die Bezeichnung gilt hier alles, was für die anderen apply-Funktionen gesagt wurde.
  2. Mit dem Argument classes lassen sich die gewünschten Datentypen auswählen, die verarbeitet werden sollen.
  3. Das Argument how gibt an, wie in der combine-Phase der Rückgabewert von rapply() aus den Anwendungen von f() zusammengesetzt wird. Man kann für how drei Werte how = c("unlist", "replace", "list") einsetzen, wobei der erste Wert unlist der default-Wert ist.
  4. Mit dem Argument deflt lässt sich ein default-Wert setzen, der dann verwendet wird, wenn die Funktion f() auf einen nicht ausgewählten Datentyp trifft.
  5. Dass rapply() rekursiv an eventuell in object enthaltene Listen weitergereicht wird, muss nicht konfiguriert werden: wenn möglich wird rapply() immer rekursiv angewendet.

Der Selektion von Datentypen durch das Argument classes von rapply()

Bevor der Selektionsmechanismus von rapply() erklärt wird, soll gezeigt werden, wie sich lapply() und sapply() verhalten, wenn man Listen verarbeitet, die unterschiedliche Datentypen enthalten. Dazu wird eine Liste mit 2 Komponenten definiert (Zeile 1); die erste Komponenten ist ein numerischer Vektor, die zweite Komponente ein character-Vektor.

Angewendet wird die Funktion nchar() die die enthaltenen Zeichen zählt.

lst1 <- list(numbers = (11:13), chars = c("a", "b", "c"))

ch <- lapply(X = lst1, FUN =  nchar)
str(ch)
# List of 2
# $ numbers: int [1:3] 2 2 2
# $ chars  : int [1:3] 1 1 1

ch <- sapply(X = lst1, FUN =  nchar)
ch
#      numbers chars
# [1,]       2     1
# [2,]       2     1
# [3,]       2     1

Zeile 3 bis 7: Mit lapply() wird die Funktion nchar() auf die Liste lst1 angewendet: Es entsteht eine Liste mit wieder zwei Komponenten und man erkennt (Zeile 6), dass die Funktion nchar() mit den Zahlen im Vektor numbers zurechtkommt, da sie in Zeichen konvertiert werden. Man kann dies leicht testen: der Aufruf von nchar(11) liefert den Wert 2.

Zeile 9 bis 14: Wird anstelle von lapply() jetzt sapply() aufgerufen, so erhält man die selben Ergebnisse; sie werden lediglich in der combine-Phase nicht zu einer Liste sondern zu einer Matrix zusammengesetzt.

Eine naheligende Fragestellung ist nun:

Wie kann man dafür sorgen, dass die Funktion nchar() nur an character-Vektoren weitergereicht wird?

Der folgende Lösungsvorschlag ist ungeeignet: Man definiert eine Funktion nchar2(), die zuerst prüft, ob der Eingabewert den Modus character hat. Falls ja, wird die Anfrage an nchar() weitergereicht.

nchar2 <- function(x){
  stopifnot(mode(x) == "character")
  return(nchar(x))
}

ch2 <- lapply(X = lst1, FUN =  nchar2)
# Error: mode(x) == "character" ist nicht TRUE

Man erkennt, dass die Bearbeitung abbricht, wenn zum ersten Mal stopifnot() aufgerufen wird; die weiteren Komponenten der Liste lst1 werden dann nicht mehr abgearbeitet. Das obige Problem kann mit der Funktion nchar2() also nicht gelöst werden.

Viel einfacher ist es die Funktion rapply() mit dem Argument classes zu konfigurieren: Man setzt classes = "character" ; dies sorgt dafür, dass die Funktion nchar() nur auf Komponenten mit Modus character angewendet wird.

lst1 <- list(numbers = (11:13), chars = c("a", "b", "c"))

ch <- rapply(object = lst1, f =  nchar, classes = "character")
str(ch)
# Named int [1:3] 1 1 1
# - attr(*, "names")= chr [1:3] "chars1" "chars2" "chars3"

Die Ausgabe der Struktur von ch (Zeile 4 bis 6) zeigt:

  • Die erste Komponente der Liste, der numerische Vektor, wird ignoriert.
  • Die Funktion nchar() wird nur auf die zweite Komponente angewendet.
  • Das der default-Wert von how gleich unlist ist, gibt rapply() einen Vektor zurück; das Verhalten bei anderen Werten von how wird sofort diskutiert.
  • Aus dem Namen chars der zweiten Komponente von lst1 werden Namen für die Komponenten des Rückgabe-Vektors gebildet.

Um diesen Selektion der Datentypen einsetzen zu können, muss noch geklärt werden, welche Werte das Argument classes annehmen kann und wie diese Werte bestimmte Datentypen ansprechen.

Der default-Wert für ist: classes = "ANY" . Dis bedeutet, dass die Funktion f an jedes Objekt weitergereicht wird.

Der Begriff Klasse und das Attribut class gehören zur objekt-orientierten Programmierung, die bisher noch nicht besprochen wurde. In R besitzen aber nicht nur Klassen – die ausdrücklich als solche definiert werden – ein Attribut class, sondern man muss zwei Fälle unterscheiden:

  1. Das Attribut class wurde für ein Objekt ausdrücklich gesetzt, dann ist es ein Vertreter der zugehörigen Klasse im Sinne der objekt-orientierten Programmierung. Das class-Attribut kann sogar mehrere Werte annehmen, nämlich wenn mit Vererbung gearbeitet wird. (So wurde zum Beispiel schon gesagt, dass ein geordneter Faktor das class-Attribut "ordered", "factor" besitzt.)
  2. Objekte, die im Sinne der objekt-orientierten Programmierung keiner Klasse angehören, haben ein implizites Klassen-Attribut gesetzt; zum Beispiel eine Matrix das class-Attribut "matrix" oder generell das Ergebnis von mode(x) (Ausnahme sind lediglich integer-Vektoren mit "integer" .

Mehr zum Attribut class findet man in der Dokumentation im Paket base unter ?class .

Das Argument how

Die folgende Tabelle beschreibt kurz, wie der Wert von how bestimmt, wie der Rückgabewert zusammengesetzt wird.

Wert von how Wirkung
how = "unlist" Der default-Wert von how ist gleich unlist. Es wird versucht, die Rückgabewerte von f zu einem Vektor oder einem Feld zusammenzufassen.
how = "replace" Die ursprüngliche Struktur der Liste bleibt erhalten. Die Funktion f wird auf die ausgewählten Datentypen angewendet; die ursprünglichen Werte werden durch die Funktionswerte ersetzt. Nicht ausgewählte Datentypen bleiben unverändert in der Liste.
how = "list" Auch hier bleibt die Struktur der Liste erhalten. Die Funktion f wird wie im Fall how = "replace" auf die ausgewählten Datentypen angewendet. Die nicht ausgewählten Datentypen werden durch NULL ersetzt oder durch den default-Wert deflt.

Das folgende Skript zeigt einige Anwendungen für die verschiedenen Werte von how; dabei wird immer classes = "character" ausgewählt:

lst1 <- list(numbers = (11:13), chars = c("a", "b", "c"))

# how = "unlist" (default-Wert):
rapply(object = lst1, f = nchar, classes = "character")
# chars1 chars2 chars3 
# 1      1      1 

# how = "replace":
rapply(object = lst1, f = nchar, classes = "character", how = "replace")
# $numbers
# [1] 11 12 13
# 
# $chars
# [1] 1 1 1

# how = "list" ohne deflt:
rapply(object = lst1, f = nchar, classes = "character", how = "list")
# $numbers
# NULL
# 
# $chars
# [1] 1 1 1

# how = "list" mit deflt = 0:
rapply(object = lst1, f = nchar, classes = "character", deflt = 0, how = "list")
# $numbers
# [1] 0
# 
# $chars
# [1] 1 1 1

Zeile 1: Es wird wieder das Versuchs-Objekt lst1 erzeugt.

Zeile 3 bis 6: Die Funktion f = nchar wird nur auf den character-Vektor angewendet und es wird der default-Wert how = "unlist" verwendet. Der numerische Vektor aus lst1 wird ignoriert, die Funktion nchar() wird nur auf den character-Vektor angewendet und die Funktionswerte werden zu einem Vektor zusammengefasst (wobei sogar Namen erzeugt werden).

Zeile 9 bis 14: Jetzt wird mit how = "replace" eine Liste erzeugt, die die Struktur der ursprünglichen Liste besitzt. Die Funktion nchar() wird nicht auf den numerischen Vektor angewendet; er bleibt in der Liste als erste Komponente unverändert enthalten. Dagegen wird nchar() auf den character-Vektor angewendet und aus den Ergebnissen wird die zweite Komponente der Liste gebildet.

Zeile 17 bis 22: Wird how = "list" gesetzt, entsteht wieder eine Liste. Aber anders als bei how = "replace" wird jetzt der Wert der Komponente gleich NULL gesetzt, auf die nchar() nicht angewendet werden kann.

Zeile 25 bis 30: Wird how = "list" und zusätzlich ein Wert für deflt gesetzt, hier gleich 0, wird anstelle von NULL dieser default-Wert in die Liste eingesetzt.

Die rekursive Verarbeitung einer Liste mit rapply()

Nachdem der Selektionsmechanismus von rapply() (mit dem Argument classes) und die Wirkung des Argumentes how erklärt ist, kann man sich der Frage zuwenden, in welchem Sinne eine Liste rekursiv abgearbeitet wird.

Im folgenden Skript werden zwei Listen lst1 und lst2 definiert, die wiederum als Komponenten einer Liste lst eingesetzt werden. Die Liste lst wird in rapply() mit der Funktion nchar() abgearbeitet:

lst1 <- list(numbers = (11:13), chars = c("a", "b", "c"))
lst2 <- list(numbers = (1:3), chars = c("xx", "yy", "zz"))

lst <- list(L1 = lst1, L2 = lst2)

lst.chars <- rapply(object = lst, f = nchar, classes = "character")

lst.chars
# L1.chars1 L1.chars2 L1.chars3 L2.chars1 L2.chars2 L2.chars3 
# 1         1         1         2         2         2 

lst.chars <- rapply(object = lst, f = nchar, classes = "character", deflt = 0, how = "list")
str(lst.chars)
# List of 2
# $ L1:List of 2
# ..$ numbers: num 0
# ..$ chars  : int [1:3] 1 1 1
# $ L2:List of 2
# ..$ numbers: num 0
# ..$ chars  : int [1:3] 2 2 2

Zeile 1 bis 4: Die Listen werden vorbereitet.

Zeile 6 bis 10: Die verschachtelte Liste wird mit classes = "character" abgearbeitet. Man erkennt, dass die Funktion nchar() nicht unmittelbar auf die Komponenten von lst angewendet wird, sondern rekursiv an die beiden Komponenten lst1 und lst2 weitergereicht wird. Wegen how = "unlist" ist der Rückgabewert von rapply() ein Vektor mit dn 6 Anzahlen an Zeichen.

Zeile 12 bis 20: Wird how = "list" und deflt = 0 entsteht eine Liste mit zwei Komponenten. Jeder dieser Listen ist wiederum eine Liste – die ursprüngliche Struktur der Liste lst ist unverändert. Die Komponenten mit dem numerischen Vektor sind jetzt gleich 0 und die Anzahlen der Zeichen werden wie bekannt berechnet.

Aufgabe:

Auch bei Dataframes hat man oft das Problem, dass die Spalten unterschiedliche Datentypen besitzen. Testen Sie, ob rapply() auf Dataframes angewendet werden kann, um über ihre Spalten zu iterieren.

Alle Kommentare
Durch die Nutzung dieser Website erklären Sie sich mit der Verwendung von Cookies einverstanden. Unsere Datenschutzbestimmungen können Sie hier nachlesen