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.

library(tidyverse)

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.

table(big5_mod$Geschlecht)

  f   m 
118  82 
table(big5_mod$Gruppe)

  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.

big5_mod |>
  select(Geschlecht, Gruppe) |>
  map(table)
$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.

big5_mod |>
  select(Geschlecht, Gruppe) |>
  map(table) |>
  map(prop.table)
$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).

big5_mod |>
  select(where(is.character) | where(is.factor)) |>
  map(table)

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.

library(tidyverse)

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.

mod_ls <- split(big5_mod, ~ Gruppe)

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.

mod_ls |> 
  map(\(teildaten) lm(Extraversion ~ Geschlecht, data = teildaten)) 
$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.

mod_ls |> 
  map(\(teildaten) lm(Extraversion ~ Geschlecht, data = teildaten)) |>
  map(summary)

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.

mod_ls |> 
  map(\(teildaten) lm(Extraversion ~ Geschlecht, data = teildaten)) |> 
  map_df(tidy)
# 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.

mod_ls <- split(big5_mod, ~ Gruppe)

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.

erg_ls <- vector("list", 3)
erg_ls
[[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.

for(i in 1:3) {
  erg_ls[[i]] <- lm(Extraversion ~ Geschlecht, data = mod_ls[[i]])
}
erg_ls
[[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.

library(tidyverse)

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.

big5_mod |>  
  nest(.by = Gruppe)
# 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