Kapitel 12 Funktionen wiederholt anwenden
Im Zuge der Datenanalyse muss man häufig dieselben Berechnungen für mehrere Szenarien vornehmen. Um sich Zeit zu sparen und die Häufigkeit von Fehlern zu minimieren, existieren in jeder Programmiersprache sogenannte Schleifen. In diesem Kapitel lernst du drei verschiedene Arten kennen, diese sogenannten iterativen Prozesse selbst in R umzusetzen.
12.1 Das Copy & Paste Problem
In diesem Kapitel ist das Installieren und Laden des purrr
Packages aus dem tidyverse
eine Voraussetzung.
In vielen Szenarien ist es nötig, die Funktion nicht nur einmal, sondern mehrmals anzuwenden. Die naheliegende Lösung dafür ist den Funktionsaufruf zu kopieren und leicht modifiziert für den nächsten Anwendungsfall zu verwenden. Ein Beispiel dafür haben wir bereits in Kapitel 9.6 beim Ausgeben der Häufigkeiten kategorialer Variablen gesehen. Wenn ein oder zwei Häufigkeiten ausgegeben werden sollen, ist das Kopieren der Funktion noch kein Problem.
f m
118 82
Jung Mittel Weise
147 39 14
Falls jedoch die Häufigkeiten von 20 bis 30 Variablen gewünscht sind, ist das Kopieren der jeweiligen Funktion nicht nur zeitaufwendig, sondern auch fehleranfällig. Im Verlauf der nächsten Kapitel werden wir drei mögliche Automatisierungen kennenlernen: map()
, for-Schleifen und Einnisten.
Mit der Funktion map()
können wir die Funktion table()
zur Berechnung der Häufigkeiten auf jede ausgewählte Spalte anwenden. Es werden also zuerst die Häufigkeiten der Geschlechter und anschließend die der Altersgruppen berechnet.
$Geschlecht
f m
118 82
$Gruppe
Jung Mittel Weise
147 39 14
Durch einen weiteren Aufruf von map()
dieses Mal mit der Funktion prop.table()
können zusätzlich die Verhältnisse der Merkmale ausgegeben werden.
$Geschlecht
f m
0.59 0.41
$Gruppe
Jung Mittel Weise
0.735 0.195 0.070
Da Häufigkeiten in der Regel nur bei kategorialen Variablen erwünscht sind, könnten wir auch nur Spalten vom Datentyp Character oder Factor ausrechnen lassen (siehe Kapitel 6.2).
Copy & Paste ist fehleranfällig und sollte nur bei weniger als zehn wiederholten Anwendungen von Funktionen verwendet werden. Alternativ können diese Funktionswiederholungen auf verschiedene Arten automatisiert werden.
12.2 Listenbasierte Berechnungen
Für dieses Kapitel muss das purrr
Package aus dem tidyverse
installiert und geladen werden.
Ein Beispiel zum wiederholten Anwenden einer Funktion mithilfe von map()
wurde bereits im vorherigen Kapitel eingeführt. Während dort die Funktion auf einen tibble
angewandt wurde (eine Sonderform der Liste), werden wir uns hier den klassischen Anwendungsfall von map()
im Kontext von Listen anschauen.
Als Beispiel wollen wir an dieser Stelle ein lineares Regressionsmodell zur Erklärung der Variation von Extraversion durch Geschlecht für jede Altersgruppe einzeln berechnen (siehe Kapitel 9.5.1). Dafür trennen wir den Datensatz zunächst mithilfe der Funktion split()
in eine Liste mit drei Elementen. Das erste Element enthält einen Datensatz mit den jüngsten Personen, das zweite die mittlere Altersklasse und das dritte Element die Ältesten.
Zum Erstellen der Regressionsmodelle kopieren wir den Befehl dreimalig. Dabei wird jedes Mal mithilfe der doppelten eckigen Klammern auf ein anderes Listenelement zugegriffen (siehe Kapitel 11.4).
model1 <- lm(Extraversion ~ Geschlecht, data = mod_ls[[1]])
model2 <- lm(Extraversion ~ Geschlecht, data = mod_ls[[2]])
model3 <- lm(Extraversion ~ Geschlecht, data = mod_ls[[3]])
Mit der Funktion map()
kann das redundante Kopieren der Funktion vermieden werden. Als einziges Argument wird das Regressionsmodell als anonyme Lambdafunktion übergeben (siehe Kapitel 6.4.4). Als Ergebnis erhalten wir eine Liste mit einem Modell pro Listenelement.
$Jung
Call:
lm(formula = Extraversion ~ Geschlecht, data = teildaten)
Coefficients:
(Intercept) Geschlechtm
3.067 0.055
$Mittel
Call:
lm(formula = Extraversion ~ Geschlecht, data = teildaten)
Coefficients:
(Intercept) Geschlechtm
3.07000 0.05632
$Weise
Call:
lm(formula = Extraversion ~ Geschlecht, data = teildaten)
Coefficients:
(Intercept) Geschlechtm
2.8333 0.1267
Mit einem weiteren Aufruf von map()
können wir für jedes der drei Modelle die Zusammenfassungen der Ergebnisse zeigen.
Alternativ könnte auch die tidy()
Funktion zum Ausgeben als tibble
(anstelle einer Liste) verwendet werden (siehe Kapitel 9.8). Da die gewünschte Ausgabe hier ein tibble
bzw. data.frame
(kurz: df) ist, müssen wir stattdessen die Funktion map_df()
benutzen.
# A tibble: 6 × 5
term estimate std.error statistic p.value
<chr> <dbl> <dbl> <dbl> <dbl>
1 (Intercept) 3.07 0.0376 81.6 4.49e-123
2 Geschlechtm 0.0550 0.0598 0.919 3.60e- 1
3 (Intercept) 3.07 0.0649 47.3 1.08e- 34
4 Geschlechtm 0.0563 0.0930 0.605 5.49e- 1
# ℹ 2 more rows
Wenn die Ausgabe nicht als Liste erfolgen soll, verändert sich der Funktionsname je nach gewünschter Datenstruktur und gewünschtem Datentyp in andere Varianten von map()
(z.B. map_chr()
für Character Vektoren oder map_dbl()
für numerische Vektoren). Alternativ sind Funktionen mit ähnlicher Funktionsweise direkt in R integriert, die allerdings mitunter umständlicher zu verwenden sind (z.B. lapply()
, apply()
, tapply()
und mapply()
). Innerhalb der hier vorgestellten Funktionen werden sogenannte Schleifen verwendet.
12.3 for-Schleifen
Eine wiederholte Anwendung von Funktionen für jeden Kontext unabhängig der Datenstruktur und des Datentyps wird durch for-Schleifen ermöglicht. Um das Konzept einzuführen, greifen wir an dieser Stelle auf dieselbe listenbasierte Berechnung mehrerer linearer Regressionmodelle des vorherigen Kapitels zurück. Zuerst wird also erneut der Datensatz als Liste aufgeteilt.
Mit Copy & Paste müsste das Modell dreimalig ausgeschrieben werden.
model1 <- lm(Extraversion ~ Geschlecht, data = mod_ls[[1]])
model2 <- lm(Extraversion ~ Geschlecht, data = mod_ls[[2]])
model3 <- lm(Extraversion ~ Geschlecht, data = mod_ls[[3]])
Den ersten Schritt beim Berechnen einer for-Schleife stellt das Kreieren einer leeren Ergebnisliste dar.
[[1]]
NULL
[[2]]
NULL
[[3]]
NULL
Falls das Ergebnis für andere Kontexte nicht als Liste, sondern bspw. als numerischer Vektor gespeichert werden soll, könnte man stattdessen vector("numeric", 3)
wählen. In der eigentlichen Schleife soll für jedes i
von 1
bis 3
das Modell aufgestellt und in erg_ls
an entsprechender Stelle gespeichert werden. Dabei greifen wir auf den jeweiligen Datensatz an der Stelle von Index i
aufgrund der Liste innerhalb einer doppelten eckigen Klammer zu.
[[1]]
Call:
lm(formula = Extraversion ~ Geschlecht, data = mod_ls[[i]])
Coefficients:
(Intercept) Geschlechtm
3.067 0.055
[[2]]
Call:
lm(formula = Extraversion ~ Geschlecht, data = mod_ls[[i]])
Coefficients:
(Intercept) Geschlechtm
3.07000 0.05632
[[3]]
Call:
lm(formula = Extraversion ~ Geschlecht, data = mod_ls[[i]])
Coefficients:
(Intercept) Geschlechtm
2.8333 0.1267
Eine sinnvolle Herangehensweise beim Erstellen von for-Schleifen ist es, den zu wiederholenden Funktionsaufruf zwei- bis dreimal zu kopieren, weil so die Automatisierung leichter fällt. In unserem Beispiel konnten wir so sehen, dass sich nur mod_ls[[1]]
zu mod_ls[[2]]
und mod_ls[[3]]
verändert.
Effiziente Schleifen zu schreiben ist nicht einfach. Daher sollte nach Möglichkeit auf die map()
Funktionen aus dem purrr
Package zurückgegriffen werden. Dies macht sich vor allem bei großen Datensätzen bemerkbar (siehe Kapitel 12.2).
12.4 Einnisten
Zum Bearbeiten dieses Kapitels muss das tidyr
Package aus dem tidyverse
geladen werden.
Durch die Funktion nest()
können innerhalb von Zellen eines tibbles
Datenstrukturen jeder Art verschachtelt werden. Dies wurde bereits in Kapitel 11.4 anhand eines einfachen Beispiels eingeführt. Dieses Konzept wenden wir beim big5_mod
Datensatz an und gruppieren dafür innerhalb von nest()
mithilfe des Arguments .by
.
# A tibble: 3 × 2
Gruppe data
<fct> <list>
1 Mittel <tibble [39 × 5]>
2 Jung <tibble [147 × 5]>
3 Weise <tibble [14 × 5]>
Als Ergebnis erhalten wir einen tibble
, welcher in der ersten Spalte die Altersgruppen abbildet. Daneben steht eine neue zweite Spalte namens data
, in der wiederum drei tibbles
mit den Dimensionen 39x5
, 147x5
und 14x5
gespeichert sind.
Ähnlich wie bei den listenbasierten Berechnungen in Kapitel 12.2 können wir auch auf diese verschachtelten tibbles
die map()
Funktionen anwenden. Der Unterschied besteht darin, dass wir den Befehl innerhalb von mutate()
ausführen müssen (siehe Kapitel 6.4). Schließlich soll mit den Inhalten einer Spalte eines tibbles
eine neue Spalte erstellt werden. Im zweiten Schritt nehmen wir die Modelle und geben diese in einem aufgeräumten Format mit der Funktion tidy()
aus (siehe Kapitel 9.8).
big5_mod |>
nest(.by = Gruppe) |>
mutate(
Modelle = map(data, \(teildaten) lm(Extraversion ~ Geschlecht, data = teildaten)),
Ergebnisse = map(Modelle, tidy)
)
# A tibble: 3 × 4
Gruppe data Modelle Ergebnisse
<fct> <list> <list> <list>
1 Mittel <tibble [39 × 5]> <lm> <tibble [2 × 5]>
2 Jung <tibble [147 × 5]> <lm> <tibble [2 × 5]>
3 Weise <tibble [14 × 5]> <lm> <tibble [2 × 5]>
Die neue Spalte namens Modelle
hat Einträge des Datentyps <lm>
(die linearen Modelle). Daneben sind die Ergebnisse von tidy()
verschachtelt (u.a. mit Teststatistiken und p-Werten). Damit wir an diese Ergebnisse herankommen, müssen wir diese abschließend mit der Funktion unnest()
aus der eingenisteten Struktur herausholen.
big5_mod |>
nest(.by = Gruppe) |>
mutate(
Modelle = map(data, \(teildaten) lm(Extraversion ~ Geschlecht, data = teildaten)),
Ergebnisse = map(Modelle, tidy)
) |>
unnest(Ergebnisse)
# A tibble: 6 × 8
Gruppe data Modelle term estimate std.error statistic p.value
<fct> <list> <list> <chr> <dbl> <dbl> <dbl> <dbl>
1 Mittel <tibble [39 × 5]> <lm> (Intercept) 3.07 0.0649 47.3 1.08e- 34
2 Mittel <tibble [39 × 5]> <lm> Geschlechtm 0.0563 0.0930 0.605 5.49e- 1
3 Jung <tibble [147 × 5]> <lm> (Intercept) 3.07 0.0376 81.6 4.49e-123
4 Jung <tibble [147 × 5]> <lm> Geschlechtm 0.0550 0.0598 0.919 3.60e- 1
# ℹ 2 more rows