Die Funktion apply() in R: Iteration über die Zeilen oder Spalten einer Matrix

Die Funktion apply() erlaubt es, über die Zeilen beziehungsweise Spalten einer Matrix zu iterieren und dabei eine Funktion FUN auf die Zeilen oder Spalten anzuwenden. Dabei entstehen leichter verständliche Quelltexte als bei den gleichwertigen Schleifen. Die Arbeitsweise der Funktion apply() kann man in drei Phasen unterteilen: split, apply, combine (Aufspalten der Matrix, Anwenden der Funktion FUN auf die Teile, Zusammensetzen der einzelnen Rückgabewerte zum Rückgabewert von apply()). Diese drei Phasen werden ausführlich erklärt und damit die Diskussion weiterer mit apply() verwandter Funktionen vorbereitet.

Einordnung des Artikels

  • Einführung in die Informatik
    • Einführung in die Programmiersprache R
      • Funktionen in R
        • Die Funktion apply() in R: Iteration über die Zeilen oder Spalten einer Matrix
        • Die Familie der apply-Funktionen in R

Da mit der Funktion apply() Matrizen verarbeitet werden, werden hier Kenntnisse aus Matrizen in R: der Datentyp matrix vorausgesetzt, nämlich:

  • wie Matrizen erzeugt werden,
  • wie man auf Zeilen, Spalten beziehungsweise einzelne Elemente einer Matrix zugreift.

Und da Dataframes als spezielle Matrizen aufgefasst werden können und Felder (Datentyp array) die mehrdimensionale Verallgemeinerung von Matrizen sind, können mit apply() auch Dataframes und Felder verarbeitet werden; dazu werden die entsprechenden Kenntnisse aus Dataframes in R: der Datentyp data frame und Felder in R: der Datentyp array benötigt.

Einführung

In den bisherigen Kapiteln wurden ausführlich behandelt:

  1. Die in R vorbereiteten Datentypen (wie Vektor, Matrix, Feld, Liste, Dataframe und so weiter).
  2. Die Eigenschaften von Funktionen und wie man selber Funktionen implementiert.

Die Familie der apply()-Funktionen führt die Kenntnisse über Datentypen und Funktionen zusammen und bildet damit einen wichtigen Meilenstein, um R souverän einzusetzen.

Man kann die apply()-Funktionen folgendermaßen kurz (und etwas oberflächlich) charakterisieren:

Sämtliche Operationen in R sind vektorisiert, so dass man eine Funktion, von der man vielleicht erwarten würde, dass sie nur einen Eingabewert besitzt, problemlos auf einen Vektor anwenden kann. Die Funktion wird dann auf jede Komponente des Vektors angewendet.

Die Familie der apply()-Funktionen versuchen dieses Verhalten auf komplexere Datentypen zu übertragen, indem etwa eine Funktion auf die Zeilen (oder Spalten) einer Matrix, auf die Komponenten einer Liste oder dergleichen angewendet wird.

Was dem Anfänger dabei große Schwierigkeiten macht ist, dass es

  • Eine Vielzahl von apply()-Funktionen gibt; darunter sind auch welche, die man dem Namen nach nicht zur Familie rechnen würde.
  • Diese Funktionen auf den ersten Blick nahezu identische Funktionalitäten anbieten, bei genauerer Betrachtung sich aber deutlich unterscheiden.
  • Man daher bei einer konkreten Anwendung erst lange in der Dokumentation stöbern muss bis man die geeignete apply()-Funktion gefunden hat.

Dieses Kapitel soll:

  1. Den Zugang zu den apply()-Funktionen erleichtern, indem es methodische Vorgehensweisen zeigt, wie man die Arbeitsweise und Systematik der Familie besser verstehen kann.
  2. Dadurch dass die Funktion apply() sehr detailliert vorgestellt wird, werden schon fast alle typischen Probleme diskutiert, die im Zusammenhang mit den apply()-Funktionen auftreten.
  3. In den Kapiteln Diagnose-Funktionen für Funktionen in R und Spezielle selbstdefinierte Funktionen in R wurde schon ein Ausblick auf die funktionale Programmierung gegeben. Die Arbeit mit den apply()-Funktionen entspricht genau der Denkweise der funktionalen Programmierung und daher kann man dieses Kapitel zugleich als einen ersten Einstieg in die funktionale Programmierung verstehen.

Im nächsten Kapitel Die Familie der apply-Funktionen in R werden dann zwar nicht alle apply()-Funktionen im Detail und mit all ihren Spitzfindigkeiten vorgestellt. Vielmehr werden die wichtigsten unter ihnen ausgewählt und erläutert sowie einige Anwendungen gezeigt.

Methodische Hinweise: Der Zugang zu den Funktionen der apply-Familie

Es sollen jetzt einige Vorgehensweisen vorgestellt werden, wie man sich der verwirrenden Vielfalt der apply()-Funktionen nähern kann. Dies wird aber zugleich zeigen, zu welchem Zweck man die die apply()-Funktionen einsetzen kann und wie sie intern arbeiten. Je nach Vorkenntnissen wird man den einen oder anderen Zugang bevorzugen.

Die apply()-Funktionen als Ersatz für Schleifen

Das folgende Skript zeigt, wie man in R eine Funktion vektorisiert einsetzen kann, etwa um eine Wertetabelle einer mathematischen Funktion zu erstellen:

v <- seq(from = 0, to = 1, by = 0.1)
v
# [1] 0.0 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1.0

print(sin(v), digits = 3)
# [1] 0.0000 0.0998 0.1987 0.2955 0.3894 0.4794 0.5646 0.6442 0.7174 0.7833 0.8415

Wer schon eine andere Programmiersprache kennt, die nicht in diesem Sinne vektorisiert ist, wird über einen Ausdruck wie sin(v) staunen, wenn v ein Vektor ist und keine Zahl. In vielen anderen Programmiersprachen ist es naheliegend eine Wertetabelle wie oben mit einer Schleife (Iteration) zu erzeugen: Der x-Wert durchläuft in der Schleife die gewünschte Wertemenge (mit einem Konstrukt wie for(x in v) ) und für jeden einzelnen x-Wert wird sin(x) berechnet.

Man kann die Funktionen der apply-Familie dann auch folgendermaßen verstehen: sie erlauben es, über komplexe Datentypen zu iterieren und dabei in jedem Schritt eine Teilmenge herauszuschneiden und auf diese Teilmenge eine Funktion anzuwenden.

Kurz: apply()-Funktionen ersetzen Schleifen.

Wer also schon mit Schleifen gut vertraut ist, sollte sich daher stets fragen: Wie würde man die Aufgabe einer apply()-Funktion durch eine Schleife ersetzen?

Liest man in der Literatur über apply()-Funktionen, wird stets gesagt, dass Schleifen zu vermeiden sind und stattdessen die geeignete apply()-Funktion einzusetzen ist. Warum eigentlich?

Die apply()-Funktionen sind primitive Funktionen und arbeiten daher sehr effektiv; eine selbst implementierte, gleichwertige Schleife wird immer langsamer sein und vermutlich mehr Speicherplatz beanspruchen.

Aber es gibt noch einen weiteren Grund, der schon in die funktionale Programmierung hineinführt: Schleifen – insbesondere wenn sie nicht kommentiert sind – sind immer nur down to top zu verstehen. Damit ist gemeint: Der Leser des Quelltextes erkennt zwar schnell die einfachen Operationen, die beim Durchlauf der Schleife ausgeführt werden. Aber – dies gilt vor allem für verschachtelte Schleifen – es ist schwer abzulesen, zu welchem Zweck die Schleife ausgeführt wird.

Dagegen erlauben die apply()-Funktionen den Zugang top to down: Es ist meist sehr viel leichter die entsprechenden Anweisungen auf einer abstrakten Ebene zu verstehen als die konkreten Einzelschritte. Und Anweisungen, die mit apply()-Funktionen formuliert sind, lassen sich auf der abstrakten Ebene leichter verstehen.

Die Phasen: split – apply – combine

Das kleine Skript oben zum Erstellen einer Wertetabelle kann man auch – zugegeben wirkt dies sehr umständlich – folgendermaßen lesen: Die Berechnung der Wertetabelle erfolgt in drei Phasen:

  1. split: Der Vektor der Eingabewerte wird in einzelne Zahlen zerlegt.
  2. apply: Auf jede der Zahlen wird die Sinus-Funktion angewendet.
  3. combine: Die Sinus-Werte werden wieder zu einem Vektor zusammengesetzt.

Es ist klar: dadurch dass Operationen vektorisiert sind, muss man diese Unterscheidung der Phasen nicht vornehmen. Wenn man sich aber vorstellt, dass der R-Quelltext irgendwann in eine elementare Sprache übersetzt werden muss, kommt man nicht an dieser Phaseneinteilung vorbei.

Und für alle apply()-Funktionen kann man genau diese drei Phasen identifizieren; möchte man sie verstehen, muss man nur die richtigen Fragen stellen:

  1. split:
    • Welche Datenstruktur wird aufgespalten?
    • In welche Teilmengen wird sie zerlegt?
  2. apply:
    • Welche Funktion wird auf die Teilmengen angewendet?
    • Welchen Rückgabewert liefert dies?
  3. combine:
    • Wie werden die Rückgabewerte wieder zusammengesetzt?
    • Welchen Datentyp hat der Wert, der insgesamt berechnet wird?

Abbildung 1: Ein Vektor wird in seine Komponenten zerlegt (split). Auf jede der Komponenten wird eine Funktion angewendet (apply). Die Komponenten werden wieder zu einem Vektor zusammengesetzt (combine). Dass bei Anwendung der Funktion f() Eingabewert und Rückgabewert unterschiedlich groß dargestellt sind, soll andeuten, dass deren Datentypen nicht übereinstimmen müssen.Abbildung 1: Ein Vektor wird in seine Komponenten zerlegt (split). Auf jede der Komponenten wird eine Funktion angewendet (apply). Die Komponenten werden wieder zu einem Vektor zusammengesetzt (combine). Dass bei Anwendung der Funktion f() Eingabewert und Rückgabewert unterschiedlich groß dargestellt sind, soll andeuten, dass deren Datentypen nicht übereinstimmen müssen.

Abbildung 1 versucht diese Phaseneinteilung für die Berechnung der Wertetabelle darzustellen; obige Fragen sollten leicht zu beantworten sein. Es werden später weitere Abbildungen folgen, die die Arbeitsweise der apply()-Funktionen in ähnlichen Diagrammen darstellen.

Klassifikation über verarbeitete Datentypen

In diesem Kapitel wird nur die Funktion apply() vorgestellt, mit der man Schleifen über die Zeilen oder Spalten einer Matrix ersetzen kann. Ganz ähnlich kann man auch die anderen Familienmitglieder dadurch charakterisieren, dass sie Iterationen über einen gewissen Datentyp ermöglichen. Um sich einen schnellen Überblick über die apply-Familie zu verschaffen, sollte man versuchen kurz zu charakterisieren:

  • Welcher Datentyp wird verarbeitet?
  • Entspricht die apply()-Funktion einer Iteration oder welche andere Funktionalität hat sie?

Die Funktion apply()

Die typische Anwendung von apply()

Die folgenden zwei Beispiele zeigen den typischen Einsatz von apply():

  1. Berechnung der Zeilen- und Spalten-Mittelwerte einer Matrix.
  2. Interpratation einer 3 × 3 Matrix als Zeilenvektoren und Berechnung der Winkel zwischen einem gegebenen Vektor und den Spalten der Matrix.

Das erste Beispiel ist nicht sehr überzeugend, da man diese Aufgabe mit Funktionen aus dem Paket base lösen kann; es wurde dennoch gewählt, weil man hier die Syntax eines apply()-Aufrufes leicht nachvollziehen kann.

Das zweite Beispiel ist dagegen in seiner Syntax schwerer verständlich, zeigt aber besser den typischen Einsatz von apply() und welche Fragen im Folgenden geklärt werden müssen, um derartige Anwendungen zu meistern.

1. Beispiel: Zeilen- und Spalten-Mittelwerte einer Matrix

Das folgende Skript definiert eine Matrix mit 3 Zeilen und 4 Spalten und berechnet anschließend die Mittelwerte der Zeilen und Spalten; dazu werden die Funktionen rowMeans() und colMeans() verwendet

v <- (1:12)
m.3.4 <- matrix(data = v, nrow = 3, byrow = TRUE)
m.3.4
#      [,1] [,2] [,3] [,4]
# [1,]    1    2    3    4
# [2,]    5    6    7    8
# [3,]    9   10   11   12

rowMeans(x = m.3.4)
# [1]  2.5  6.5 10.5

colMeans(x = m.3.4)
# [1] 5 6 7 8

Kennt man die Funktion matrix() zum Erzeugen einer Matrix und die beiden Funktionen zur Berechnung der Mittelwerte, sollte das Skript keinerlei Fragen aufwerfen.

Dieselbe Berechnung der Mittelwerte wird jetzt mit Hilfe der apply()-Funktion durchgeführt:

# m.3.4 wie oben

row.means <- apply(X = m.3.4, MARGIN =  1, FUN = mean)
row.means
# [1]  2.5  6.5 10.5

col.means <- apply(X = m.3.4, MARGIN =  2, FUN = mean)
col.means
# [1] 5 6 7 8

Die Anwendung verwendet drei Argumente von apply():

  1. Die Matrix X, die abgearbeitet werden soll.
  2. Die Angabe MARGIN, die angibt, ob die Matrix zeilenweise (mit MARGIN = 1 ) oder spaltenweise (mit MARGIN = 2 ) abgearbeitet werden soll.
  3. Die Funktion FUN, die auf jeweils eine Zeile beziehungsweise Spalte angewendet werden soll.

Im zweiten Beispiel wird gezeigt, dass apply() als zusätzliches Argument ... besitzt:

apply(X, MARGIN, FUN, ...)

Damit können weitere Argumente an FUN weitergereicht werden.

Als Rückgabewert erhält man in obigen Anwendungen jeweils einen Vektor (mit 3 beziehungsweise 4 Komponenten, entsprechend der Anzahl der Zeilen und Spalten).

Man erkennt auch, dass die apply()-Anweisungen – im Vergleich zur entsprechenden Schleife – leicht verständlich sind: das zu verarbeitende Objekt und die auszuführende Operation sind sofort erkennbar; lediglich das Argument MARGIN erfordert Gewöhnung, um es sofort zu interpretieren.

2. Beispiel: Winkelberechnung

Hier sollen die Spalten einer 3 × 3 Matrix m als drei-dimensionale Vektoren interpretiert werden. Zudem soll ein Vektor v gegeben sein und es sollen die drei Winkel zwischen den Spalten der Matrix und v berechnet werden. Die Berechnung soll mit Hilfe der apply() Funktion durchgeführt werden, wobei m und v als beliebige Matrix beziehungsweise Vektor eingegeben werden.

Vergleicht man diese Aufgabe mit dem letzten Beispiel, fällt sofort auf, dass zwar die Abarbeitung der Matrix keine Schwierigkeiten bereitet, wohl aber die Übergabe des Vektors v.

Das folgende Skript definiert zuerst eine Funktion angle(x, y) , die den Winkel zwischen den Vektoren x und y im Bogenmaß berechnet. Anschließend werden willkürliche Beispiele für m und v definiert und die gesuchten Winkel mit Hilfe von apply() berechnet:

angle <- function(x, y){
  # Prüfung der Eingabewerte:
  stopifnot(is.numeric(x), is.numeric(y))
  stopifnot(identical(length(x), length(y)))
  
  x.norm <- sqrt(sum(x^2))
  y.norm <- sqrt(sum(y^2))
  
  if(x.norm == 0 || y.norm == 0){
    return(NaN)
  }
  
  phi <- acos((x %*% y) / (x.norm * y.norm))
  return(phi)
}

# Test:
v <- c(1, -5, 3)

m.v <- c(2, -3, 12, 0, 3, -5, 2, 0, 8)
m <- matrix(data =  m.v, nrow = 3, byrow =  TRUE)

m
#      [,1] [,2] [,3]
# [1,]    2   -3   12
# [2,]    0    3   -5
# [3,]    2    0    8

angles <- apply(X = m, MARGIN = 2, FUN = angle, y = v)
print(angles, digits = 3)
# [1] 1.072 2.370 0.829

Zeile 1 bis 15: Die Funktion angle() erhält zwei Vektoren als Eingabewerte und berechnet über das Skalarprodukt und die Funktion acos() den Winkel zwischen den Vektoren.

Zeile 18 bis 27: Der Vektor v und die Matrix m werden angelegt und m ausgegeben.

Zeile 29: Die Funktion apply() erhält als Eingabewerte:

  1. Die Matrix m.
  2. Damit m spaltenweise abgearbeitet wird, wird MARGIN = 2 gesetzt.
  3. Die Funktion FUN = angle , die die Spalten als Eingabewert erhält.
  4. Das zweite Argument, das die Funktion angle() benötigt, wird in ... durch y = v gesetzt.

Die ersten drei Eingabewerte sollten nach dem ersten Beispiel leicht verständlich sein. Das vierte Argument lässt sich über ... an FUN weiterreichen; dies ist hier nötig, da angle() zwei Eingabewerte benötigt. Weiter unten wird dies ausführlich erklärt.

Zeile 30 und 31: Die Ausgabe der Ergebnisse.

Die Beispiele müssen jetzt noch nicht im Detail verstanden werden, sie sollen vielmehr zeigen, wie leicht sich Schleifen, in denen Matrizen abgearbeitet werden, durch apply() ersetzen lassen.

Die Sichtweise split-apply-combine

Die Abbildungen 2 und 3 zeigen die drei Phasen split-apply-combine für die Funktion apply(), einmal für MARGIN = 1 und einmal für MARGIN = 2 .

Abbildung 2: Eine Matrix m wird in Zeilen zerlegt (split). Auf jede der Zeilen wird eine Funktion f angewendet (apply). Die Rückgabewerte werden zu einem Vektor zusammengesetzt (combine). Ganz rechts ist die vollständige apply()-Anweisung zu sehen.Abbildung 2: Eine Matrix m wird in Zeilen zerlegt (split). Auf jede der Zeilen wird eine Funktion f angewendet (apply). Die Rückgabewerte werden zu einem Vektor zusammengesetzt (combine). Ganz rechts ist die vollständige apply()-Anweisung zu sehen.

Abbildung 3: Die Phasen split-apply-combine bei spaltenweiser Abarbeitung der Matrix durch apply().Abbildung 3: Die Phasen split-apply-combine bei spaltenweiser Abarbeitung der Matrix durch apply().

Betrachtet man diese Phaseneinteilung, so sollte sie für die einfachen Befehle aus dem einführenden Skript leicht verständlich sein. Allerdings drängen sich sofort weitere Fragen auf:

  1. Die Phase split:
    • Welche Datentypen können anstelle einer Matrix eingesetzt werden? Naheliegend ist es die entsprechenden Befehle mit Feldern und Dataframes auszuführen?
    • Welche Werte kann MARGIN bei anderen Datentypen annehmen?
  2. Die Phase apply:
    • Welche Funktionen können im Argument FUN übergeben werden? Welche Bedingung muss eine Funktion erfüllen, damit sie als Argument von apply() in Frage kommt?
    • Kann man die zulässigen Funktionen sinnvoll klassifizieren?
    • Welche Bedeutung hat das zusätzliche Argument ... , das oben angedeutet wurde?
  3. Die Phase combine:
    • Gibt es Einschränkungen, welche Rückgabewerte FUN liefern darf?
    • Welcher Rückgabewert wird dann insgesamt von apply() erzeugt?

Die folgenden Abschnitte werden diese Fragen ausführlich untersuchen. Da man diese Fragen – zumindest so ähnlich – bei allen Funktionen der apply-Familie stellen kann, werden sie bei anderen Funktionen nicht mehr detailliert besprochen.

Die drei Phasen split-apply-combine

Die Phase split

Es ist klar, dass man für das Argument X eine Matrix einsetzen kann; nicht so klar ist, welche weiteren Datentypen zulässig sind.

Das folgende Skript zeigt, dass auch ein Dataframe verwendet werden kann und dass das Argument MARGIN dann wie bei Matrizen für zeilenweise und spaltenweise Abarbeitung sorgt (siehe Zeile 11 und 14):

x1 <- (1:3)
x2 <- (4:6)

df <- data.frame(x1, x2)
df
#   x1 x2
# 1  1  4
# 2  2  5
# 3  3  6  

apply(X = df, MARGIN = 1, FUN = mean)
# [1] 2.5 3.5 4.5

apply(X = df, MARGIN = 2, FUN = mean)
# x1 x2 
# 2  5

Da für ein Dataframe das Attribut dim gesetzt ist und dies 2 Einträge hat, kann ein Dataframe mit as.matrix() in eine Matrix verwandelt werden (dies geschieht intern) und diese wird wie oben beschrieben abgearbeitet.

Da ein zweidimensionales Feld (array) wie eine Matrix behandelt wird, muss es auch möglich sein für X ein zweidimensionales Feld einzusetzen. Aber kann man auch höher dimensionale Felder einsetzen?

Das folgende Beispiel demonstriert die Anwendung von apply() auf ein drei-dimensionales Feld X und die zusätzlichen Möglichkeiten für die Werte, die MARGIN annehmen kann. Um das Feld anschließend graphisch darzustellen und diskutieren zu können, wird das Attribut dimnames gesetzt:

v <- (1:27)
nms <- list(c("X1", "X2", "X3"), c("Y1", "Y2", "Y3"), c("Z1", "Z2", "Z3"))

a <- array(data = v, dim = c(3, 3, 3), nms)
a
# , , Z1
# 
#     Y1 Y2 Y3
# X1  1  4  7
# X2  2  5  8
# X3  3  6  9
# 
# , , Z2
# 
#     Y1 Y2 Y3
# X1 10 13 16
# X2 11 14 17
# X3 12 15 18
# 
# , , Z3
# 
#     Y1 Y2 Y3
# X1 19 22 25
# X2 20 23 26
# X3 21 24 27

apply(X = a, MARGIN = 1, FUN = mean)
# X1 X2 X3 
# 13 14 15 

apply(X = a, MARGIN = 2, FUN = mean)
# Y1 Y2 Y3 
# 11 14 17

apply(X = a, MARGIN = 3, FUN = mean)
# Z1 Z2 Z3 
# 5 14 23

apply(X = a, MARGIN = c(1, 2), FUN = mean)
#     Y1 Y2 Y3
# X1 10 13 16
# X2 11 14 17
# X3 12 15 18

Zur Erläuterung ist vielleicht die folgende Abbildung 4 hilfreich.

Abbildung 4: Ein drei-dimensionales Feld, das aus den Zahlen von 1 bis 27 aufgebaut wurde. Die Beschriftung der Achsen soll die Zuordnung zu den Ausgaben im Skript erleichtern.Abbildung 4: Ein drei-dimensionales Feld, das aus den Zahlen von 1 bis 27 aufgebaut wurde. Die Beschriftung der Achsen soll die Zuordnung zu den Ausgaben im Skript erleichtern.

Die Zahlen des drei-dimensionale Feld a kann man sich als Würfel angeordnet denken, wobei die Z-Komponente des Dimensions-Vektors in Abbildung 4 der Richtung "in die Zeichenebene" entspricht. Die Ausgaben aus Zeile 6 bis 25, die der Ausgabe von 3 Matrizen entsprechen, sind dann die drei hinereinander liegenden Ebenen im Würfel.

Mit dieser Zuordnung der Ausgaben des Skripts zu Abbildung 4 ist es nicht mehr schwer die Bedeutung von MARGIN zu verstehen:

In Zeile 27 wird die Mittelwert-Bildung bezüglich MARGIN = 1 vorgenommen. Aber dies steht nun nicht mehr für die Zeilen einer Matrix, sondern für die erste Dimension eines Feldes. Das heißt die Mittelwerte werden mit festgehaltenem X1, X2 und X3 berechnet. "Festgehaltenes X1" bedeutet dabei, dass mit den 9 Zahlen aus der oberen Ebene des Würfels der Mittelwert berechnet wird (es sind alle Zahlen in Y- und Z-Richtung zugelassen, die als erste Komponente X1 besitzen).

Bei "festgehaltenem X2 und X3 wird dann die mittlere und die untere Ebene des Würfels zur Mittelwert-Bildung verwendet.

In der obersten Ebene stehen die Zahlen:

1, 4, 7, 10, 13, 16, 19, 22, 25

und ihr Mittelwert ist 13.

Entsprechend erhält man für die mittlere und untere Ebene die Mittelwerte 14 und 15.

Man kann die Vorgehensweise in der Phase split bei MARGIN = 1 auch so formulieren: Das Feld – besser: der Würfel – wird in 3 Ebenen zerlegt, die parallel zur YZ-Ebene sind. Und die Zerlegung findet in fortschreitender X-Richtung statt. Für MARGIN = 2 oder MARGIN = 3 wir die Zerlegung in den anderen Richtungen vorgenommen.

In Zeile 31 wird der Mittelwert bezüglich MARGIN = 2 gebildet; dies sind jetzt nicht mehr die Spalten einer Matrix, sondern die drei Ebenen des Würfels entlang der Y-Achse. In der linken Ebene stehen die Zahlen

1, 2, 3, 10, 11, 12, 19, 20, 21,

deren Mittelwert gleich 11 ist. Die anderen Mittelwerte sind 14 und 17.

Und MARGIN = 3 aus Zeile 35 beschreibt die drei Ebenen entlang der Z-Achse mit den Mittelwerten 5, 14 und 23.

Welche Bedeutung hat dann die Mittelwert-Bildung bezüglich MARGIN = c(1, 2) aus Zeile 39? Jetzt werden für jede Mittelwert-Bildung ein X-Wert und ein Y-Wert festgehalten und Z kann alle möglichen Werte durchlaufen. Somit werden insgesamt 9 Mittelwerte berechnet und jede Mittelwert-Bildung erfasst 3 Zahlenwerte (jeweils entlang der Z-Achse). Diese 9 Mittelwerte werden dann in Form einer Matrix ausgegeben (Zeile 40 bis 43).

Aufgabe:

Versuchen Sie die Ausgaben in Zeile 40 bis 43 nachzuvollziehen und vorherzusagen, welche Ergebnisse die Mittelwert-Bildung mit MARGIN = c(1, 3) beziehungsweise MARGIN = c(2, 3) liefert.

♦ ♦ ♦ ♦ ♦

Verallgemeinert man die Überlegungen zu Abbildung 4 und dem zugehörigen Skript auf n-dimensionale Felder, so gilt:

  1. Wird für MARGIN eine Zahl (Vektor der Länge 1) gewählt, wird das Feld in (n - 1)-dimensionale Felder zerlegt.
  2. Für MARGIN können Vektoren der Länge 1 bis n - 1 eingesetzt werden.
  3. Wird MARGIN gleich (1:n) gewählt, werden alle Komponenten einzeln ausgewählt – dann benötigt man die apply()-Funktion nicht und kann die Funktion FUN direkt auf das Feld anwenden.
  4. Ist MARGIN ein Vektor der Länge k, dann wird das gegebene Feld in (n - k)-dimensionale Felder zerlegt.

Zuletzt soll noch ein Beispiel angegeben werden, wie man die Ausgabe eines Feldes vereinfachen kann. Denn wie an dem Beispiel oben zu sehen war, führt die Ausgabe eines drei-dimensionalen (oder noch höher-dimensionalen Feldes) zu umfangreichen Ausgaben. Verwendet wird wieder das Feld a:

# a wie im Skript oben
dimnames(a) <- NULL

apply(X = a, MARGIN = 3, FUN = str)
# int [1:3, 1:3] 1 2 3 4 5 6 7 8 9
# int [1:3, 1:3] 10 11 12 13 14 15 16 17 18
# int [1:3, 1:3] 19 20 21 22 23 24 25 26 27

Zeile 2: Damit die Ausgaben möglichst schlank werden, wird das Attribut dimnames gelöscht (benötigt man sie später noch, wäre es besser ein neues Objekt anzulegen).

Zeile 4: Für das Feld a wird über die dritte Dimension iteriert; dabei wird jeweils die Struktur ausgegeben.

Zeile 5 bis 7: Jetzt werden nicht die Matrizen entlang der Z-Achse (aus Abbildung 4) ausgegeben, sondern ihre zugrunde liegenden Vektoren (plus Informationen über den Datentyp).

Zu Beginn dieses Unterabschnittes wurde die beiden naheliegenden Werte für MARGIN bei der Iteration über eine Matrix verwendet, nämlich MARGIN = 1 (Iteration über die Zeilen) und MARGIN = 2 (Iteration über die Spalten). Die Diskussion höher-dimensionaler Felder hat aber gezeigt, dass MARGIN nicht nur einzelne Werte annehmen kann. Was ist dann eine Iteration über eine Matrix mit MARGIN = c(1, 2) ? Vergleicht man dies mit den entsprechenden Beispielen oben zu Feldern, sollte klar sein: Jetzt werden Zeilen und Spalten ausgewählt, das heißt man iteriert über alle Komponenten der Matrix. Das folgende Skript zeigt dies:

v <- (1:12)
m.3.4 <- matrix(data = v, nrow = 3, byrow = TRUE)

means <- apply(X = m.3.4, MARGIN = c(1, 2), FUN = mean)
means
#      [,1] [,2] [,3] [,4]
# [1,]    1    2    3    4
# [2,]    5    6    7    8
# [3,]    9   10   11   12

Wie man sieht, wird jetzt beim Aufruf von apply() (Zeile 4) die Funktion FUN = mean auf jedes Element der Matrix angewendet. Man kann diesen Einsatz von apply() auch mit dem zugrunde liegenden Vektor der Matrix ersetzen: Die Funktion FUN wird auf jede seiner Komponenten angewendet und anschließend wird der Vektor wied in die Matrix-Form gebracht.

Die Phase apply

Nachdem geklärt ist, wie bei der Anwendung der Funktion apply(X, MARGIN, FUN, ...) das Objekt X mittels der Angabe MARGIN in Teilmengen zerlegt wird, kann man konkrete Fragen zur Funktion FUN stellen.

  1. Kann man die für FUN zugelassenen Funktionen geeignet klassifizieren?
  2. Welche Eingabewerte kann FUN besitzen?
  3. Wie kann man mit Hilfe von ... weitere Eingabewerte an FUN weiterreichen?

Im Folgenden werden für X lediglich Matrizen betrachtet; es sollte nicht schwer sein, die Ergebnisse auf Dataframes und Felder zu übertragen.

Da eine Matrix durch apply () entweder zeilen- oder spaltenweise abgearbeitet wird, kann jede Funktion, die als Eingabewert einen Vektor besitzt, an FUN übergeben werden. Natürlich muss der Eingabewert den geeigneten Modus besitzen (oder in diesen automatisch umgewandelt werden können).

Man kann sich die Abarbeitung der apply()-Funktion am Besten durch die gleichwertige Schleife veranschaulichen (siehe auch Abbildung 2 und 3, apply-Phase):

  • In jedem Iterationsschritt wird eine Zeile (oder Spalte) der Matrix ausgewählt; dabei entsteht ein Vektor, dessen Länge mit der Spalten-Anzahl oder Zeilen-Anzahl übereinstimmt.
  • Dieser Vektor ist der Eingabewert für FUN und der Rückgabewert wird berechnet.
  • Bei einer Schleife müsste man jetzt selbst dafür sorgen, dass die Rückgabewerte zu einem Objekt zusammengesetzt werden. In der combine-Phase von apply() geschieht dies automatisch.

Da demnach nur der Eingabewert der Funktion geeignet sein muss, macht es keinen Unterschied, ob die Funktion

  • aus den Basis-Paketen oder anderen installierten Paketen stammt,
  • eine selbstdefinierte Funktion ist oder
  • eine anonyme Funktion ist.

Dazu soll nochmals das einführende Beispiel betrachtet werden:

v <- (1:12)
m.3.4 <- matrix(data = v, nrow = 3, byrow = TRUE)

row.means <- apply(X = m.3.4, MARGIN =  1, FUN = mean)
row.means
# [1]  2.5  6.5 10.5

Man beachte, dass in Zeile 4 für das Argument FUN nur der Name der Funktion angegeben wird. Wie oben beschrieben wird die jeweils ausgewählte Zeile oder Spalte an die Funktion übergeben. Hat die in FUN gesetzte Funktion nur einen Eingabewert, ist die Übergabe der Zeile (oder Spalte) an FUN problemlos; erst bei zusätzlichen Argumenten muss man die eventuell die Namen der Argumente oder ihre Reihenfolge beachten (siehe unten).

Das folgende Beispiel zeigt dies nochmals für eine anonyme Funktion – jetzt wird mean(x) selbst (vereinfacht) implementiert:

# m.3.4 wie oben

row.means <- apply( X = m.3.4, MARGIN = 1, FUN = function(x){ return(sum(x) / length(x) } )

Das Argument x der anonymen Funktion hat nur die Aufgabe, die Implementierung der Funktion zu ermöglichen. Es regelt nicht die Übergabe der Matrizen-Zeile an FUN; dies wird intern von apply() erledigt.

Damit zur Frage, wie man an eine Funktion weitere Eingabewerte übergeben kann. Um dies zu klären, wird im folgenden Beispiel wieder die Funktion weightedMean() verwendet, mit der schon viele Konzepte in Selbstdefinierte Funktionen in R (UDF = User Defined Functions) und Spezielle selbstdefinierte Funktionen in R erläutert wurden. Verwendet wird hier die einfache Version:

weightedMean <- function(x, m){
  return(sum(x * m) / sum(m))
}

Man kann diese Funktion für folgende Aufgabenstellung einsetzen:

Gegeben ist eine Matrix, für deren Zeilen ein gewichteter Mittelwert berechnet werden soll; dabei soll der Vektor der Gewichtungsfaktoren immer identisch sein, im Beispiel unten wird der Vektor (1, 1, 2, 2) verwendet.

# m.3.4 wie oben:
v <- (1:12)
m.3.4 <- matrix(data = v, nrow = 3, byrow = TRUE)

weightedMeans <- apply(X = m.3.4, MARGIN = 1, FUN = weightedMean, m = c(1, 1, 2, 2))

print(x = weightedMeans, digits = 3)
# [1]  2.83  6.83 10.83

# Kontrolle mit Schleife:

for( i in (1:nrow(m.3.4)) ){
  print( weightedMean( x = m.3.4[i, ], m =  c(1, 1, 2, 2) ), digits = 3 )
}
# [1] 2.83
# [1] 6.83
# [1] 10.8

Zeile 5: In der Funktion apply() werden jetzt 4 Argumente gesetzt:

  1. Die Matrix m.3.4, die 3 Zeilen und 4 Spalten besitzt.
  2. Durch MARGIN = 1 wird die Matrix zeilenweise abgearbeitet.
  3. Für FUN wird die selbstdefinierte Funktion weightedMean() gesetzt.
  4. Im Argument ... werden mit m = c(1, 1, 2, 2) die Gewichtungsfaktoren gesetzt.

Die Übergabe der Eingabewerte an weightedMean() geschieht jetzt folgendermaßen:

  • In jedem Iterationsschritt von apply() wird die aktuelle Zeile der Matrix als Eingabewert x an weightedMean() übergeben.
  • Zusätzlich wird m aus dem Argument ... als m an weightedMean() übergeben.

Unten wird genauer erklärt, wie die beiden Übergaben von apply() geregelt werden, es ist nicht nötig (oder wäre ein Syntax-Fehler), die Argumente direkt an weightedMean() zu übergeben. Im Argument FUN hat nur der Name der verwendeten Funktion zu stehen, aber keine Eingabewerte.

Zeile 7: Die Ausgabe zeigt drei gewichtete Mittelwerte – die Matrix m.3.4 hat 3 Zeilen.

Zeile 12 bis 14: Zur Kontrolle werden die gewichteten Mittelwerte direkt mit weightedMean() in einer Schleife berechnet.

Um dieses Verhalten von apply() in eigenen Anwendungen auszunutzen, ist natürlich ein wenig Verständnis dafür nötig, wie die Argumente an weightedMean() übergeben werden. Dazu muss daran erinnert werden, wie die aufrufenden Argumente den formalen Argumenten zugeordnet werden (im Englischen wird diese Zuordnung auch als matching bezeichnet):

Die Funktion weightedMean() hat die beiden formalen Argumente x und m: weightedMean(x, m) . Werden bei einem Aufruf die Namen der Argumente gesetzt, kann ihre Reihenfolge auch vertauscht werden, denn das matching wird zuerst über die Namen der Argumente hergestellt. Sind die Argument-Namen beim Aufruf nicht gesetzt, entscheidet die Reihenfolge der Argumente über die Zuordnung: das erste Argument wird als x interpretiert, das zweite Argument als m.

Damit kann man den apply()-Aufruf von oben untersuchen:

apply(X = m.3.4, MARGIN = 1, FUN = weightedMean, m = c(1, 1, 2, 2))

Im Argument ... von apply(), das an weightedMean() weitergereicht wird, ist ausdrücklich der Name m gesetzt. Daher muss die Matrizen-Zeile, die bei jedem Iterationsschritt an weightedMean() übergeben wird, das Argument x sein.

Zum Test kann man den folgenden Aufruf untersuchen:

print( apply(X = m.3.4, MARGIN = 1, FUN = weightedMean, x = c(1, 1, 2, 2) ), digits = 3 )
# [1] 1.70 1.58 1.55

# Kontrolle mit Schleife:

for( i in (1:nrow(m.3.4)) ){
  print( weightedMean( x = c(1, 1, 2, 2), m = m.3.4[i, ]  ), digits = 3 )
}
# [1] 1.7
# [1] 1.58
# [1] 1.55

Zeile 1: Das Argument in ... hat jetzt den Namen x, daher wird die Matrizen-Zeile als Argument m von weightedMean() interpretiert.

Zeile 6 bis 11: Zur Kontrolle wird weightedMean() wieder mit einer Schleife berechnet. Aber im Vergleich zur Schleife oben wird weightedMean() mit vertauschten Argumenten aufgerufen, wodurch die Ergebnisse von apply() reproduziert werden.

Zuletzt kann man noch den Fall untersuchen, bei dem der Name des Argumentes aus ... weggelassen wird:

# m.3.4 wie oben
weightedMeans <- apply(X = m.3.4, MARGIN = 1, FUN = weightedMean, c(1, 1, 2, 2))

print(x = weightedMeans, digits = 3)
# [1]  2.83  6.83 10.83

Jetzt sind die Ausgaben wieder identisch zur ersten Version mit m = c(1, 1, 2, 2) . Denn das matching wird jetzt über die Reihenfolge der Argumente hergestellt: Zuerst wird an weightedMean(x, m) die Zeile der Matrix übergeben, dann wird das Argument aus ... , nämlich der Vektor c(1, 1, 2, 2) übergeben.

Aufgabe:

Beschreiben Sie, wie im einführenden Beispiel der Winkelberechnung das Argument ... eingesetzt wurde und wie das matching der Eingabewerte zustande gekommen ist.

Die Phase combine

In den bisher betrachteten Beispielen sind keine Spitzfindigkeiten aufgefallen, die den Rückgabewert der Funktion FUN betreffen oder die beim Zusammensetzen der einzelnen Rückgabewerte zum Rückgabewert von apply() auftreten. Die Beispiele waren stets so einfach konstruiert, dass man leicht vorhersagen konnte, welches Objekt in der combine-Phase erzeugt wird. Dennoch drängen sich einige Fragen auf:

  1. Gibt es Einschränkungen an den Rückgabewert von FUN?
  2. Ist es möglich, dass FUN in verschiedenen Iterationsschritten unterschiedliche Datentypen des Rückgabewertes erzeugt? Welchen Rückgabewert erzeugt apply() in diesem Fall?
  3. Welcher Rückgabewert wird erzeugt, wenn FUN in den Iterationsschritten Vektoren unterschiedlicher Länge erzeugt?

In allen Beispielen zu apply(), die bisher mit Matrizen X betrachtet wurden, war der Rückgabewert von FUN eine Zahl. In der combine-Phase werden diese Zahlen zu einem Vektor zusammengefasst.

Besondere Beachtung verdienen natürlich die Fälle, in denen FUN Rückgabewerte erzeugt, die in der combine-Phase nicht einfach zu einem Vektor zusammengesetzt werden können. Dazu folgen zwei Beispiele:

  1. Unterschiedliche Datentypen des Rückgabewertes: Es wird eine Funktion FUN verwendet, die manchmal Zahlen und manchmal Zeichen erzeugt.
  2. Vektoren unterschiedlicher Länge: Die Funktion FUN soll Vektoren erzeugen, die zwar identischen Modus, aber unterschiedliche Länge haben.

1. Beispiel: Unterschiedliche Datentypen des Rückgabewertes

Es wird folgende – ungeschickte – Implementierung für die Funktion angle() verwendet (man vergleiche dies mit dem einführenden Beispiel):

angle <- function(x, y){
  # Prüfung der Eingabewerte:
  stopifnot(is.numeric(x), is.numeric(y))
  stopifnot(identical(length(x), length(y)))
  
  x.norm <- sqrt(sum(x^2))
  y.norm <- sqrt(sum(y^2))
  
  if(x.norm == 0 || y.norm == 0){
    return(NaN)
  }
  
  phi <- acos((x %*% y) / (x.norm * y.norm))
  if(phi == 0) return("parallele Vektoren!") else return(phi)
}

Die Funktion angle() kann jetzt 3 verschiedene Werte zurückgeben:

  1. NaN, falls einer der Vektoren die (geometrische) Länge 0 hat.
  2. Die Zeichenkette &quot;parallele Vektoren!&quot; , falls die Vektoren parallel sind.
  3. Den Zwischenwinkel der Vektoren als Zahl.

Das folgende Skript verwendet angle() in apply(), siehe Zeile 9:

v <- c(1, 0, 0, 0, 0, 0, 0, 1, 0)
m <- matrix(data = v, nrow = 3, ncol = 3)
m
#      [,1] [,2] [,3]
# [1,]    1    0    0
# [2,]    0    0    1
# [3,]    0    0    0

angles <- apply(X = m, MARGIN = 2, FUN = angle, y = c(1, 0, 0) )
print(angles, digits = 3)
# [1] "parallele Vektoren!" "NaN"                 "1.5707963267949"  

str(angles)
#  chr [1:3] "parallele Vektoren!" "NaN" "1.5707963267949"

Das Beispiel ist so gewählt, dass tatsächlich die drei verschiedenen Rückgabewerte erzeugt werden.

An Zeile 11 erkennt man, dass die Rückgabewerte zum einzig gemeinsam möglichen Datentyp konvertiert werden: Sie werden zuerst in Zeichenketten verwandelt, damit sie dann zu einem Vektor zusammengefasst werden können – es gibt keinen anderen geeigneten Modus, um einen Vektor zu bilden.

Insbesondere kommt die Anweisung digits = 3 in Zeile 10 zu spät: An dieser Stelle wurden schon die Zeichenketten gebildet, auf die diese Anweisung nicht wirkt.

Die Ausgabe der Struktur in Zeile 13 bestätigt, dass apply() hier einen character-Vektor erzeugt.

2. Beispiel: Zufallsfolgen

Ein Würfel soll 6 mal geworfen werden und die Ergebnisse werden notiert. Dieses Experiment soll 10 mal durchgeführt werden und die Ergebnisse sollen in einer Matrix abgespeichert werden (10 Zeilen für die 10 Experimente und 6 Spalten für die Zahlenfolgen). Das folgende Skript erzeugt diese Matrix mit Hilfe einer for-Schleife und der Funktion sample(), an deren Eingabewerten man leicht erkennen kann, dass sie genau das 6-malige Würfeln simuliert (mit einem Laplace-Würfel).

m <- matrix(data = 0, nrow = 10, ncol = 6)

for(i in (1:10)){
  m[i, ] <- sample(x = (1:6), size = 6, replace = TRUE, prob = rep(1/6, times = 6))
}

m
#      [,1] [,2] [,3] [,4] [,5] [,6]
# [1,]    3    1    1    5    4    1
# [2,]    5    6    4    2    5    4
# [3,]    6    3    5    5    4    4
# [4,]    3    6    4    6    2    5
# [5,]    5    3    4    2    5    6
# [6,]    4    2    4    6    6    4
# [7,]    1    3    2    5    2    6
# [8,]    5    1    1    6    2    1
# [9,]    5    2    3    6    6    4
# [10,]    1    3    3    5    2    2

Das Skript ist natürlich nicht reproduzierbar, da Zufallszahlen erzeugt werden.

Soweit hat dies noch nichts mit der apply()-Funktion zu tun. Aber aus der Matrix soll jetzt eine weitere Information geholt werden, die in der jetzigen Ausgabe nur schwer erkennbar ist: Für jede Zeile sollen die enthaltenen Werte angegeben werden, und zwar aufsteigend sortiert. Also etwa (1, 3, 4, 5) für die erste Zeile.

Ist eine Zeile als Vektor x gegeben, dann kann mit sort(unique(x) der gewünschte Vektor erzeugt werden. Die Funktion sort(unique(x) wird in apply() als anonyme Funktion eingesetzt. Dabei entstehen allerdings Vektoren mit unterschiedlichen Längen und daher ist nicht klar, welchen Rückgabewert die apply()-Funktion liefert. Das folgende Skript führt die Berechnungen aus und untersucht den Rückgabewert:

m.values <- apply(X = m, MARGIN = 1, FUN = function(x){sort(unique(x))}) 
m.values
# [[1]]
# [1] 1 3 4 5
# 
# [[2]]
# [1] 2 4 5 6
# 
# [[3]]
# [1] 3 4 5 6
# 
# [[4]]
# [1] 2 3 4 5 6
# 
# [[5]]
# [1] 2 3 4 5 6
# 
# [[6]]
# [1] 2 4 6
# 
# [[7]]
# [1] 1 2 3 5 6
# 
# [[8]]
# [1] 1 2 5 6
# 
# [[9]]
# [1] 2 3 4 5 6
# 
# [[10]]
# [1] 1 2 3 5

str(m.values)
# List of 10
# $ : num [1:4] 1 3 4 5
# $ : num [1:4] 2 4 5 6
# $ : num [1:4] 3 4 5 6
# $ : num [1:5] 2 3 4 5 6
# $ : num [1:5] 2 3 4 5 6
# $ : num [1:3] 2 4 6
# $ : num [1:5] 1 2 3 5 6
# $ : num [1:4] 1 2 5 6
# $ : num [1:5] 2 3 4 5 6
# $ : num [1:4] 1 2 3 5

Man erkennt, dass die 10 Vektoren unterschiedlicher Länge in eine Liste mit 10 Komponenten verpackt werden.

Aus diesen beiden Beispielen kann man für die Arbeitsweise von apply() in der combine-Phase folgern:

Es werden die in R üblichen Datenumwandlungen vorgenommen, um unterschiedliche Datentypen zu einem Objekt zusammenzusetzen, wobei die Hierarchie logical < integer < double < character < list angewendet wird. Ist es möglich als Rückgabewert einen Vektor zu erzeugen wird gemäß der Hierarchie der "größte" Modus ausgewählt; kann man keinen Vektor bilden, werden die Rückgabewerte aus den Iterationsschritten zu einer Liste zusammengefasst.

Haben die Rückgabe-Vektoren gleiche Länge, muss keine Liste gebildet werden, jetzt kann ein Feld erzeugt werden, wie das folgende Beispiel zeigt. Dazu werden die Zufallsfolgen aus dem letzten Beispiel lediglich sortiert:

m <- matrix(data = 0, nrow = 10, ncol = 6)

for(i in (1:10)){
  m[i, ] <- sample(x = (1:6), size = 6, replace = TRUE, prob = rep(1/6, times = 6))
}

m
#      [,1] [,2] [,3] [,4] [,5] [,6]
# [1,]    4    6    5    3    1    3
# [2,]    3    2    4    6    5    1
# [3,]    6    3    5    6    3    2
# [4,]    5    4    3    5    2    3
# [5,]    2    1    4    2    3    4
# [6,]    4    5    2    4    2    2
# [7,]    4    3    5    4    5    6
# [8,]    4    1    4    1    2    2
# [9,]    2    6    6    2    5    1
# [10,]    5    3    1    3    2    5

m.sort <- apply(X = m, MARGIN = 1, FUN = sort) 
m.sort
#      [,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9] [,10]
# [1,]    1    1    2    2    1    2    3    1    1     1
# [2,]    3    2    3    3    2    2    4    1    2     2
# [3,]    3    3    3    3    2    2    4    2    2     3
# [4,]    4    4    5    4    3    4    5    2    5     3
# [5,]    5    5    6    5    4    4    5    4    6     5
# [6,]    6    6    6    5    4    5    6    4    6     5

str(m.sort)
# num [1:6, 1:10] 1 3 3 4 5 6 1 2 3 4 ...

Zeile 1 bis 5: Die Zufallsfolgen werden neu erzeugt.

Zeile 20: Die Funktion apply() wird so eingesetzt, dass die Zeilen der Matrix m sortiert werden. Der Rückgabewert jedes Funktionsaufrufes von sort() ist also ein Vektor der Länge 6, zum Beispiel (1, 3, 3, 4, 5, 6) für die erste Zeile.

Zeile 21: An der Ausgabe erkennt man, wie die Rückgabe-Vektoren zusammengesetzt werden. Die 10 Rückgabe-Vektoren werden als Spalten aneinandergehängt und es entsteht eine Matrix mit 6 Zeilen und 10 Spalten.

Zeile 30: Die Ausgabe der Struktur bestätigt dies.

Ist der Rückgabewert von FUN ein Feld, wird durch apply() ein höher-dimensionales Feld erzeugt. Und wie die Diskussion der split-Phase gezeigt hat, hängt es von der Länge von MARGIN ab, um wieviele Dimensionen sich diese Felder unterscheiden.

Das letzte Beispiel weist auf eine – vielleicht unerwartete – Eigenschaft von apply() hin: Die Funktion apply() ist nicht idempotent. Damit ist gemeint, dass bei FUN = identity nicht immer das Objekt X zurückgegeben wird, das in apply() als X gesetzt wurde. Zur Erklärung muss gesagt werden, dass die Funktion identity(x) immmer das Objekt x zürückgibt, das als Eingabewert gesetzt wurde:

# x wird als ein beliebiges Objekt definiert
y <- identity(x)
identical(x, y)
# [1] TRUE

Egal wie das Objekt x definiert wird, das in Zeile 2 erzeugte Objekt y ist identisch zu x.

Das hat aber nicht zur Folge, dass im folgenden Skript m.3.4 und m identisch sind – obwohl in jedem Iterationsschritt der apply-Phase ein identisches Objekt erzeugt wird:

v <- (1:12)
m.3.4 <- matrix(data = v, nrow = 3, byrow = TRUE)

m <- apply(X = m.3.4, MARGIN = 1, FUN = identity)

identical(m, m.3.4)
# [1] FALSE

identical(t(m), m.3.4)
# [1] TRUE

m
#      [,1] [,2] [,3]
# [1,]    1    5    9
# [2,]    2    6   10
# [3,]    3    7   11
# [4,]    4    8   12

An der Ausgabe von m erkennt man (Zeile 12), warum m.3.4 und m nicht identisch sind (Zeile 6): In der von apply() zurückgegebenen Matrix werden die in der apply-Phase berechneten Rückgabewerte spaltenweise angeordnet. Und daher liefert der Aufruf von identical() den Wert TRUE, wenn man eine transponierte Matrix einsetzt (Zeile 9).

Verwendet man apply() in obigem Skript mit MARGIN = 2 , ensteht diese Komplikation nicht:

}
v <- (1:12)
m.3.4 <- matrix(data = v, nrow = 3, byrow = TRUE)

m <- apply(X = m.3.4, MARGIN = 2, FUN = identity)

identical(m, m.3.4)
# [1] TRUE

m
#      [,1] [,2] [,3] [,4]
# [1,]    1    2    3    4
# [2,]    5    6    7    8
# [3,]    9   10   11   12
Alle Kommentare
Durch die Nutzung dieser Website erklären Sie sich mit der Verwendung von Cookies einverstanden. Außerdem werden teilweise auch Cookies von Diensten Dritter gesetzt. Genauere Informationen finden Sie in unserer Datenschutzerklärung sowie im Impressum.