4  Daten transformieren

4.1 Tidy data

Wenn in der Statistik/Data Science über Datensätze gesprochen wird, werden oft die Begriffe wide und long verwendet. Diese beziehen sich auf die Form eines Datensatzes: ein wide Datensatz hat mehr Spalten als ein entsprechender long Datensatz und umgekehrt hat ein long Datensatz mehr Zeilen als ein entsprechender wide Datensatz.

Genauer gesagt hat in einem long Datensatz jede Beobachtung eine eigene Zeile und jede Variable ist eine Spalte. In einem wide Datensatz können (insbesondere messwiederholte) Variablen über mehrere Spalten verteilt werden, und somit jede Zeile eine bestimmte Person repräsentieren.

Betrachten wir folgendes Beispiel: Wir haben zwei Beobachtungen (irgendeiner psychologischen Variable) für zwei Personen, zu drei verschiedenen Zeitpunkten. Nennen wir die Zeitpunkte t1, t2 und t3. Die folgende Darstellung des Datensatzes gilt als wide format:

# A tibble: 2 × 4
  Name        t1    t2    t3
  <fct>    <dbl> <dbl> <dbl>
1 Marshall     4     5     7
2 Skye         3     6     7

Hier steht jede Zeile für eine Person und die Beobachtungen sind auf die drei Spalten t1, t2 und t3 verteilt. Diese Spalten repräsentieren aber eigentlich die drei Stufen eines (messwiederholten) Faktors Zeit. Wir können genau die gleichen Daten in einem längeren Format darstellen, in dem jede Zeile einer Beobachtung entspricht und jede Spalte eine Variable darstellt. Insgesamt gibt es dann drei Variablen: Name, Zeit und Punkte, wobei Punkte hier als Platzhalter für die gemessenene (psychologische) Variable steht, deren Ausprägungen in dieser Spalte aufgeführt werden.

df |> 
  pivot_longer(!Name, names_to = "Zeit", values_to = "Punkte")
# A tibble: 6 × 3
  Name     Zeit  Punkte
  <fct>    <chr>  <dbl>
1 Marshall t1         4
2 Marshall t2         5
3 Marshall t3         7
4 Skye     t1         3
5 Skye     t2         6
6 Skye     t3         7

Wir haben jetzt nicht mehr für jede Person eine Zeile. Die Werte von Variablen, die sich nicht für jede Beobachtung (= jede Zeile) ändern, werden jetzt über die Zeilen hinweg wiederholt (gilt für Name und Zeit).

Ein Datensatz, der auf diese Weise organisiert ist, wird oft auch als tidy bezeichnet.

Wie wir später noch sehen werden, erfordern viele Arten von statistischen Analysen und insbesondere Grafik-Funktionen einen long Datensatz, und daher muss oft erstaunlich viel Zeit für die Organisation von Daten für die weitere Analyse aufgewendet werden (diese Art von Arbeit wird oft als “data wrangling” bezeichnet).

In diesem Kurs werden wir mit den tidyverse Packages zur Manipulation und Transformation von Daten arbeiten. Natürlich gibt es auch andere Möglichkeiten der Datenbearbeitung, aber unserer Ansicht nach bietet die tidyverse-Methode die konsistenteste Art der Arbeit mit Daten und reduziert daneben auch die kognitive Belastung der Benutzer.

Die Pakete, die für Datenmanipulationen verwendet werden, sind tidyr für die Transformation/Umformung von Datensätzen und dplyr für die Manipulation/Bearbeitung von Datensätzen und den dort enthaltenen Variablen. Für Faktoren benutzen wir zudem eine Funktion aus dem package forcats. Die Funktionen, die wir uns anschauen werden, sind:

Package Funktion Verwendung
tidyr pivot_longer() erhöht die Anzahl der Zeilen, verringert die Anzahl der Spalten
tidyr pivot_wider() verringert die Anzahl der Zeilen, erhöht die Anzahl der Spalten
tidyr drop_na() löscht alle Zeilen eines Datensatzes, die missing values (NA) enthalten
dplyr rename() zum Umbenennen von Variablen
dplyr select() wählt Variablen (Spalten) aus
dplyr relocate() verändert die Reihenfolge von Variablen (Spalten)
dplyr filter() wählt Beobachtungen (Zeilen) aus
dplyr arrange() sortiert einen Datensatz nach einer bestimmten Variablen
dplyr mutate() erstellt neue Variablen und ändert bereits vorhandene Variablen
dplyr case_when() zum Rekodieren von vorhandenen Variablen
forcats fct_recode() zum Rekodieren/Umbenennen von Faktorstufen
dplyr group_by() ermöglicht Operationen an Teilmengen der Daten
dplyr summarize() / summarize() fasst Daten zusammen

Mit diesen Funktionen können sehr komplexe Datenmanipulationen durchgeführt werden und trotzdem bleibt der R Code relativ übersichtlich. Neben den oben genannten enthält dplyr noch viele weitere Funktionen. Dazu gehören z.B. Funktionen, die es ermöglichen, verschiedene Datensätze miteinander zu verbinden (zu “mergen”).

4.2 Der Pipe Operator

Wir haben schon festgestellt, dass Code schnell unübersichtlich werden kann, wenn wir eine Sequenz von Operationen ausführen. Dies führt zu verschachtelten Funktionsaufrufen.

Beispiel: Wir haben einen numerischen Vektor von \(n = 10\) Messwerten (hier zu Übungszwecken mit rnorm() aus normalverteilten Zufallszahlen generiert) und wollen diese zuerst zentrieren, dann die Standardabweichung berechnen, und anschliessend noch auf zwei Nachkommastellen runden.

set.seed(1283)
stichprobe <- rnorm(10, 24, 5)
stichprobe
 [1] 24.74984 21.91726 23.98551 19.63019 23.96428 22.83092 18.86240 19.08125
 [9] 23.76589 21.88846

Die gewünschte Berechnung der gerundeten Standardabweichung der zentrierten Werte können wir als verschachtelte Funktionsaufrufe durchführen:

round(sd(scale(stichprobe,
               center = TRUE,
               scale = FALSE)),
      digits = 2)
[1] 2.19

Die Funktion scale(), sd() und round() werden nun der Reihe nach ausgeführt (von innen nach aussen), und zwar so, dass der Output einer Funktion der nächsten Funktion als Input übergeben wird.

Die Funktionen scale() und round() haben zusätzlich noch Argumente: center = TRUE, scale = FALSE, bzw. digits = 2. Dies ist zwar effizient, aber führt zu Code, der schwierig zu lesen ist.

Eine Alternative dazu wäre, die Zwischenschritte als Variablen zu speichern:

stichprobe_z <- scale(stichprobe, center = TRUE,
                      scale = FALSE)

sd_stichprobe_z <- sd(stichprobe_z)
sd_stichprobe_z_gerundet <- round(sd_stichprobe_z,
                                  digits = 2)

sd_stichprobe_z_gerundet
[1] 2.19

So steht jeder der Teilschritte in einer eigenen Zeile und wir verstehen den Code ohne Probleme. Diese Methode erfordert jedoch, dass wir zwei Variablen definieren, die wir eigentlich gar nicht brauchen.

Es gibt nun aber eine sehr elegante Methode, um Funktionen nacheinander aufzurufen, ohne diese Funktionen ineinander verschachtelt schreiben zu müssen: wir benützen dafür den pipe Operator. Er sieht so aus:

|>

und ist als Infix-Operator definiert. Das bedeutet, dass er zwischen zwei Objekten steht, ähnlich wie ein mathematischer Operator. Der Name pipe ist so zu verstehen, dass wir ein Objekt an eine Funktionen “weiterleiten” oder “übergeben”.

Dieser pipe Operator wird so oft verwendet, dass er schon eine eigene Tastenkombination hat: Cmd+Shift+M (MacOS) oder Ctrl+Shift+M (Windows und Linux).

Hinweis

Probieren Sie diese Tastenkombination aus, um den pipe Operator einzufügen.
Falls bei Ihnen statt |> das Symbol %>% erscheint, müssen Sie in den RStudio Optionen unter Code bei “Use native pipe operator, |> (requires R 4.1+)” ein Häkchen setzen (siehe Kapitel 1.1). %>% ist der ursprüngliche pipe Operator aus dem tidyverse-Package magrittr, der immer noch häufig verwendet wird. Die geringfügigen Funktionsunterschiede zwischen den beiden pipe Operatoren sind für unsere Zwecke vernachlässigbar.

Unser Beispiel von oben:

round(sd(scale(stichprobe,
               center = TRUE,
               scale = FALSE)),
      digits = 2)
[1] 2.19

wird mit dem |> Operator zu:

stichprobe |>
  scale(center = TRUE, scale = FALSE) |>
  sd() |>
  round(digits = 2)
[1] 2.19

Dieser Code ist so zu lesen:

  1. Wir beginnen mit dem Objekt stichprobe und übergeben es mit |> als Argument an die Funktion scale()
  2. Wir wenden scale(), mit den zusätzlichen Argumenten center = TRUE, scale = FALSE darauf an, und übergeben den Output als Argument an die Funktion sd()
  3. Wir wenden sd() an (ohne weitere Argumente) und reichen den Output als Argument weiter an round()
  4. round(), mit dem weiteren Argument digits = 2, wird ausgeführt. Da kein weiterer pipe folgt, wird der Output in die Konsole geschrieben.

Somit ist klar: wenn wir das Resultat weiterverwenden möchten, müssen wir es einer Variablen zuweisen:

sd_stichprobe_z_gerundet <- stichprobe |>
  scale(center = TRUE, scale = FALSE) |>
  sd() |>
  round(digits = 2)

sd_stichprobe_z_gerundet
[1] 2.19

Wir übergeben also mit |> ein Objekt an eine Funktion. Wenn wir nichts weiter spezifizieren, ist dieses Objekt das erste Argument der Funktion. Die grossen Vorteile sind:

  • Unser Code ist lesbarer.
  • Wir mussten keine unnötigen Variablen definieren.

Syntax des Pipe Operators

Der |> Operator wird im Allgemeinen wie folgt verwendet. Nehmen wir an f(), g() und h() seien Funktionen, dann gilt:

x |> f()

# ist äquivalent zu

f(x)

Wenn y ein weiteres Argument von f() ist, dann gilt:

x |> f(y)

# ist äquivalent zu

f(x, y)

Wenn wir der Reihe nach f(), g() und h() anwenden, dann gilt:

x |> f() |> g() |> h()

# oder

x |>
  f() |>
  g() |>
  h()

# ist äquivalent zu

h(g(f(x)))

Wir müssen das Objekt x nicht als erstes Argument weitergeben, sondern können es an einer beliebigen Stelle der Funktion verwenden, an die x weitergegeben wird. Dafür brauchen wir den Argument-Platzhalter _. Dabei muss der Name des Arguments, bei dem der Platzhalter eingesetzt wird, immer explizit angegeben werden:

x |> f(y, argument = _)

# ist äquivalent zu

f(y, argument = x)

Zum Beispiel könnten wir den oben in Kapitel 3.2 eingelesenen Datensatz zufriedenheit als letztes Argument data an die Funktion t.test() pipen, um das Zufriedenheits-Rating zwischen den beiden Messzeitpunkten zu vergleichen (t-Test für abhängige Stichproben):

load(file = "data/zufriedenheit.Rda")

zufriedenheit
# A tibble: 8 × 3
  Vpn   messzeitpunkt rating
  <chr> <chr>          <dbl>
1 VP_1  t1              64.3
2 VP_2  t1              65.1
3 VP_3  t1              54.2
4 VP_4  t1              54.5
5 VP_1  t2              67.2
6 VP_2  t2              75.6
7 VP_3  t2              84.6
8 VP_4  t2              73.9
zufriedenheit |> 
  t.test(rating ~ messzeitpunkt, paired = TRUE, data = _)

    Paired t-test

data:  rating by messzeitpunkt
t = -2.6835, df = 3, p-value = 0.07483
alternative hypothesis: true mean difference is not equal to 0
95 percent confidence interval:
 -34.559601   2.939601
sample estimates:
mean difference 
         -15.81 

In den meisten Fällen ist jedoch das Objekt, welches übergeben wird, gleichzeitig auch das erste Argument der nächsten Funktion (vor allem für die tidyverse Funktionen), so dass wir diesen Platzhalter selten brauchen.

Wenn wir mit den Funktionen der tidyr und dplyr Packages arbeiten, werden wir diesen Operator sehr häufig benutzen. Ein weiterer Grund, sich damit anzufreunden, ist, dass dieser immer häufiger Verwendung findet und sehr viele Beispiele im Internet (z.B. auf Stackoverflow) den |> Operator benutzen.

4.3 Reshaping: tidyr

Um einen Datensatz zu transformieren (reshaping) brauchen wir zwei Funktionen: pivot_longer() und pivot_wider(), beide befinden sich im Package tidyr.

library(tidyr)

4.3.1 pivot_longer()

Wir benutzen pivot_longer(), wenn wir einen wide Datensatz zu einem long Datensatz konvertieren möchten. pivot_longer() wird also dazu verwendet, mehrere Spalten, die die Stufen eines Faktors repräsentieren, zu einer Spalte zusammenzufügen, welche den Faktor selbst repräsentiert. Die Werte in den ursprünglichen Variablen werden in einer Werte-Variable zusammengefasst.

Hinweis

pivot_longer() nimmt mehrere Spalten als Eingabe und erzeugt eine Variable/Spalte, deren Datenwerte aus den Spaltennamen bestehen (names_to). Ausserdem wird eine Variable/Spalte erstellt, die die in den ursprünglichen Spalten enthaltenen Datenwerte (oben als Punkte bezeichnet) beinhaltet (values_to).

Die Syntax von pivot_longer() sieht so aus:

pivot_longer(data, cols, names_to, values_to)

oder mit |>

data |>
  pivot_longer(cols, names_to, values_to)

Die Argumente haben die folgende Bedeutung:

data:       ein data frame
cols:       die Spalten, deren Werte in einer Spalte zusammengeführt werden sollen
names_to:   "Name" (string) der neuen Spalte, die aus den ausgewählten Spaltennamen 
erstellt wird
values_to   "Name" (string) der Spalte, die aus den Beobachtungen in den Zellen 
erstellt wird

Sehen wir uns noch einmal das oben verwendete Beispiel an. Wir haben einen wide Datensatz (mit Namen df) mit zwei Personen, die zu drei Zeitpunkten gemessen wurden.

df
# A tibble: 2 × 4
  Name        t1    t2    t3
  <fct>    <dbl> <dbl> <dbl>
1 Marshall     4     5     7
2 Skye         3     6     7

Wir nehmen an, dass t1, t2 und t3 keine wirklich getrennten Variablen sind, sondern dass sie als Stufen eines Zeitfaktors betrachtet werden können (z.B. als drei Zeitpunkte in einem Messwiederholungsdesign). Die Werte in den Spalten t1, t2 und t3 beziehen sich auf die gleiche Art von Messung (z.B. Punktzahl) und sollten daher tatsächlich durch eine Variable in einem long Datensatz repräsentiert werden. Das sind also die im Argument cols zu bestimmenden Variablen/Spalten.

Wir möchten, dass unser neuer Faktor den Namen Zeit erhält und die gemessene Variable Punkte genannt wird.

Die Name-Variable sollte ignoriert werden, d.h. sie sollte bei der Umstrukturierung nicht mit benutzt werden (also von der Funktion nicht als weiterer Messzeitpunkt betrachtet werden).

Hinweis

Um beim Subsetting eine Variable wegzulassen (und damit alle anderen auszuwählen), haben wir weiter oben den Minus-Operator benutzt (z.B. -Name). Wenn wir mit den tidyverse-Funktionen arbeiten, benutzen wir an der Stelle von - den logischen Operator ! (NOT). Um im Folgenden das cols-Argument zu definieren, benutzen wir daher !Name statt -Name (obwohl letzteres auch funktionieren würde, aber - zumindest für tidyverse-Funktionen - als veraltet gilt).

library(tidyr)
df_long <- df |>
  pivot_longer(!Name, names_to = "Zeit", values_to = "Punkte")
df_long
# A tibble: 6 × 3
  Name     Zeit  Punkte
  <fct>    <chr>  <dbl>
1 Marshall t1         4
2 Marshall t2         5
3 Marshall t3         7
4 Skye     t1         3
5 Skye     t2         6
6 Skye     t3         7

Zeit sollte im nächsten Schritt noch als Faktor definiert werden:

df_long$Zeit <- as_factor(df_long$Zeit)
df_long
# A tibble: 6 × 3
  Name     Zeit  Punkte
  <fct>    <fct>  <dbl>
1 Marshall t1         4
2 Marshall t2         5
3 Marshall t3         7
4 Skye     t1         3
5 Skye     t2         6
6 Skye     t3         7

Eine weitere Eigenschaft von pivot_longer() ist, dass die Werte derjenigen Variablen, die nicht Teil des Reshapings sind (in unserem Fall also einzig die Variable Name) wiederholt werden. Die Zeilen 1-3 beinhalten jetzt also Beobachtungen, die zu Marshall gehören und die Zeilen 4-6 Beobachtungen, die zu Skye gehören.

Der Aufruf der Funktion pivot_longer() hätte auch expliziter geschrieben werden können:

df |> 
  pivot_longer(cols = !Name, 
               names_to = "Zeit", 
               values_to = "Punkte")

Im cols-Argument haben wir !Name verwendet: so werden alle Spalten ausser Name für die wide-to-long-Transformation verwendet. Wir hätten stattdessen auch direkt die drei Zeit-Spalten auswählen können:

df |> 
  pivot_longer(cols = c(t1, t2, t3), 
               names_to = "Zeit", 
               values_to = "Punkte")
# A tibble: 6 × 3
  Name     Zeit  Punkte
  <fct>    <chr>  <dbl>
1 Marshall t1         4
2 Marshall t2         5
3 Marshall t3         7
4 Skye     t1         3
5 Skye     t2         6
6 Skye     t3         7

Wir werden weitere und allgemeinere Möglichkeiten für die Auswahl von Spalten/Variablen eines Datensatzes kennenlernen, wenn wir uns das dplyr Package ansehen.

Beispiel

Schauen wir uns ein weiteres Beispiel an, diesmal unter Verwendung des Therapie-Datensatzes:

library(haven)
Therapy <- read_sav("data/Therapy.sav")
Therapy$Vpnr <- as_factor(Therapy$Vpnr)
Therapy$Gruppe <- as_factor(Therapy$Gruppe)
Therapy
# A tibble: 100 × 4
   Vpnr  Gruppe         Pretest Posttest
   <fct> <fct>            <dbl>    <dbl>
 1 1     Kontrollgruppe    4.29     3.21
 2 2     Kontrollgruppe    6.18     5.99
 3 3     Kontrollgruppe    3.93     4.17
 4 4     Kontrollgruppe    5.06     4.76
 5 5     Kontrollgruppe    6.45     5.64
 6 6     Kontrollgruppe    4.49     4.67
 7 7     Kontrollgruppe    4.60     4.24
 8 8     Kontrollgruppe    4.46     3.34
 9 9     Kontrollgruppe    4.76     4.11
10 10    Kontrollgruppe    5.12     5.29
# ℹ 90 more rows

Die Struktur dieses Datensatzes ähnelt der des obigen Beispiels. Wir wollen die Spalten Pretest und Posttest kombinieren, da sie Stufen eines gemeinsamen Faktors Zeit sind. Die Daten in den Zellen repräsentieren also wiederholte Messungen. Vpnr und Gruppe sollten für das Pivotieren ignoriert werden, aber im Datensatz verbleiben.

Führen wir die wide-to-long-Transformation durch:

Therapy_long <- Therapy |>
  pivot_longer(c("Pretest", "Posttest"),
               names_to = "Zeit",
               values_to = "Punkte")

# Zeit sollte ein Faktor sein
Therapy_long$Zeit <- factor(Therapy_long$Zeit,
                            levels = c("Pretest", "Posttest"))
# Hier definieren wir explizit 'Pretest' als erste und 'Posttest' als zweite 
# Faktorstufe, weil es inhaltlich und für spätere Datenanalysen Sinn macht, den 
# zeitlich früher gelegenen Messzeitpunkt als erste Faktorstufe zu bestimmen.
# Ohne explizite Definition über das levels-Argument wäre hier 'Posttest' die
# erste Faktorstufe (weil alphabetisch vor 'Pretest').
Therapy_long
# A tibble: 200 × 4
   Vpnr  Gruppe         Zeit     Punkte
   <fct> <fct>          <fct>     <dbl>
 1 1     Kontrollgruppe Pretest    4.29
 2 1     Kontrollgruppe Posttest   3.21
 3 2     Kontrollgruppe Pretest    6.18
 4 2     Kontrollgruppe Posttest   5.99
 5 3     Kontrollgruppe Pretest    3.93
 6 3     Kontrollgruppe Posttest   4.17
 7 4     Kontrollgruppe Pretest    5.06
 8 4     Kontrollgruppe Posttest   4.76
 9 5     Kontrollgruppe Pretest    6.45
10 5     Kontrollgruppe Posttest   5.64
# ℹ 190 more rows

4.3.2 pivot_wider()

pivot_wider() ist das Gegenteil von pivot_longer(). Diese Funktion nimmt einen Faktor und eine Messvariable und “verteilt” die Werte der Messvariable über neue Spalten, welche die Stufen des Faktors repräsentieren. Dies bedeutet, dass wir pivot_wider() verwenden, wenn wir aus einem long Datensatz einen wide Datensatz machen wollen.

Die pivot_wider() Syntax sieht so aus:

pivot_wider(data, names_from, values_from)

# oder

data |>
  pivot_wider(names_from, values_from)

An unserem einfachen Beispiel illustriert bedeutet dies, dass wir neue Variablen für jede Stufe des Faktors Zeit erstellen wollen, uns zwar unter Verwendung der Werte der Variable Punkte.

df_long
# A tibble: 6 × 3
  Name     Zeit  Punkte
  <fct>    <fct>  <dbl>
1 Marshall t1         4
2 Marshall t2         5
3 Marshall t3         7
4 Skye     t1         3
5 Skye     t2         6
6 Skye     t3         7
df_wide <- df_long |>
  pivot_wider(names_from = Zeit, values_from = Punkte)
df_wide
# A tibble: 2 × 4
  Name        t1    t2    t3
  <fct>    <dbl> <dbl> <dbl>
1 Marshall     4     5     7
2 Skye         3     6     7

df_wide sieht genau so aus wie der ursprüngliche Datensatz df. Wir können noch prüfen, ob beide wirklich äquivalent sind:

all.equal(df, df_wide)   
[1] TRUE

Beispiel

Nun konvertieren wir den “Therapy”-Datensatz von long zurück zu wide:

Therapy_long
# A tibble: 200 × 4
   Vpnr  Gruppe         Zeit     Punkte
   <fct> <fct>          <fct>     <dbl>
 1 1     Kontrollgruppe Pretest    4.29
 2 1     Kontrollgruppe Posttest   3.21
 3 2     Kontrollgruppe Pretest    6.18
 4 2     Kontrollgruppe Posttest   5.99
 5 3     Kontrollgruppe Pretest    3.93
 6 3     Kontrollgruppe Posttest   4.17
 7 4     Kontrollgruppe Pretest    5.06
 8 4     Kontrollgruppe Posttest   4.76
 9 5     Kontrollgruppe Pretest    6.45
10 5     Kontrollgruppe Posttest   5.64
# ℹ 190 more rows
Therapy_wide <- Therapy_long |>
  pivot_wider(names_from = Zeit, values_from = Punkte)
Therapy_wide
# A tibble: 100 × 4
   Vpnr  Gruppe         Pretest Posttest
   <fct> <fct>            <dbl>    <dbl>
 1 1     Kontrollgruppe    4.29     3.21
 2 2     Kontrollgruppe    6.18     5.99
 3 3     Kontrollgruppe    3.93     4.17
 4 4     Kontrollgruppe    5.06     4.76
 5 5     Kontrollgruppe    6.45     5.64
 6 6     Kontrollgruppe    4.49     4.67
 7 7     Kontrollgruppe    4.60     4.24
 8 8     Kontrollgruppe    4.46     3.34
 9 9     Kontrollgruppe    4.76     4.11
10 10    Kontrollgruppe    5.12     5.29
# ℹ 90 more rows

4.3.3 Fehlende Werte auschliessen: drop_na()

Eine ganz wichtige Funktion im tidyr Package ist drop_na(). Damit können wir alle Zeilen löschen, welche fehlende Werte haben.

Als Illustration dient dieses Beispiel:

df_1 <- tibble(var1 = factor(c("a", NA, "b", "b")), var2 = c(1, NA, 2, 3), var3 = c(NA, 21, 24, NA))
df_1
# A tibble: 4 × 3
  var1   var2  var3
  <fct> <dbl> <dbl>
1 a         1    NA
2 <NA>     NA    21
3 b         2    24
4 b         3    NA
df_1 |> 
  drop_na() 
# A tibble: 1 × 3
  var1   var2  var3
  <fct> <dbl> <dbl>
1 b         2    24

Wir haben einen Faktor (var1) und zwei numerische Variablen (var2 und var3). Für viele statistische Analysen benötigen wir vollständige Datensätze, d.h. Datensätze ohne fehlende Werte/NAs. In diesem Fall bleibt von den vier Beobachtungen nur noch eine übrig, da nur Zeile 3 des Datensatzes kein einziges NA aufweist.

Angenommen, die für eine bestimmte Analyse interessierenden Variablen sind var1 und var2, während var3 (könnte z.B. die Altersvariable sein), keine Rolle spielt.

Dann wäre es sinnvoll, zunächst einen Subdatensatz zu bilden, der nur die ersten beiden Variablen enthält, und erst dann drop_na() auszuführen:

df_1a <- df_1[1:2]

df_1a |> 
  drop_na() 
# A tibble: 3 × 2
  var1   var2
  <fct> <dbl>
1 a         1
2 b         2
3 b         3

So verbleiben drei Beobachtungen für die nachfolgende Datenanalyse (alle ausser die zweite Zeile/Person aus df_1, die sowohl in var1 als auch in var2 ein NA aufweist).

Diese Funktion ist sehr nützlich, sollte aber wie im Beispiel gezeigt vorsichtig verwendet werden, denn sonst löscht man ggf. Zeilen, die NAs in Variablen besitzen, die für die vorliegende Fragestellung nicht relevant sind.

4.4 Data Wrangling: dplyr

Nun sind wir in der Lage, Datensätze von wide zu long und umgekehrt zu transformieren, aber damit ist unsere Arbeit noch nicht getan. Die meisten Datensätze müssen bearbeitet werden, bevor sie analysiert werden können: Wir müssen z.B. Fälle und/oder Variablen auswählen, Werte recodieren, Variablen umbenennen, nach bestimmten Variablen sortieren, neue Variablen bilden, Datensätze nach Gruppierungsvariablen aufteilen oder Variablen zusammenfassen. Im Englischen wird diese Art von Arbeit an und mit Daten oft als data wrangling bezeichnet.

Das dplyr Package stellt Funktionen für alle diese Aufgaben zur Verfügung (und noch viele mehr, wir betrachten hier nur eine Auswahl). dplyr besteht sozusagen aus Verben (Funktionen) für all diese Operationen, und diese Funktionen können - je nach Bedarf - auf sehr elegante Weise zusammengesetzt werden.

Hinweis

Wir werden in dieser Vorlesung nur einen kleinen Teil der Funktionalität von dplyr kennenlernen. Wer mehr wissen will, kann dies in den Help Pages nachschauen: help(package = "dplyr").

Für den Rest dieses Kapitels arbeiten wir mit long Datensätzen. Falls ein Datensatz wide ist, sollte er zu long konvertiert werden.

Wir laden zuerst das dplyr Package:

library(dplyr)

Wir sehen uns nun der Reihe nach die verschiedenen Funktionen und deren Verwendung an. Wir verwenden immer den |> Operator. Der Input Dataframe ist dabei immer als erstes Argument der Funktion zu verstehen. Für die Beispiele verwenden wir die Datensätze df_long.Rda, Therapy_long.Rda, Therapy_wide.Rda sowie alkohol_aggression.csv und beispieldaten.sav.

Während die ersten drei bereits weiter oben geladen und verwendet wurden, lesen wir die letzten beiden beim jeweiligen Beispiel ein.

Bei allen unten stehenden Beispielen gilt: wenn wir das Resultat weiterverwenden möchten, müssen wir den Output einer neuen Variablen zuweisen.

4.4.1 Variablen umbennen mit rename()

Variablen können mit rename() umbenannt werden. Die nicht umbenannten Variablen verbleiben im Datensatz.

Syntax:

df |> rename(neuer_name = alter_name)

Beispiel

# "Vpnr" umbennen in "ID"
Therapy_long |>
  rename(ID = Vpnr)
# A tibble: 200 × 4
   ID    Gruppe         Zeit     Punkte
   <fct> <fct>          <fct>     <dbl>
 1 1     Kontrollgruppe Pretest    4.29
 2 1     Kontrollgruppe Posttest   3.21
 3 2     Kontrollgruppe Pretest    6.18
 4 2     Kontrollgruppe Posttest   5.99
 5 3     Kontrollgruppe Pretest    3.93
 6 3     Kontrollgruppe Posttest   4.17
 7 4     Kontrollgruppe Pretest    5.06
 8 4     Kontrollgruppe Posttest   4.76
 9 5     Kontrollgruppe Pretest    6.45
10 5     Kontrollgruppe Posttest   5.64
# ℹ 190 more rows

4.4.2 Variablen auswählen mit select()

Mit der Funktion select() wählen wir Variablen aus einem Datensatz aus.

Syntax:

# df steht für data frame
select(df, variable1, variable2, !variable3)

# oder

df |> select(variable1, variable2, !variable3)

select() wählt hier aus dem Dataframe df die Variablen variable1 und variable2 aus, variable3 wird weggelassen.

Beispiele

# nur ID
df_long |> select(Name)
# A tibble: 6 × 1
  Name    
  <fct>   
1 Marshall
2 Marshall
3 Marshall
4 Skye    
5 Skye    
6 Skye    
# Bedingung und Score
df_long |> select(Zeit, Punkte)
# A tibble: 6 × 2
  Zeit  Punkte
  <fct>  <dbl>
1 t1         4
2 t2         5
3 t3         7
4 t1         3
5 t2         6
6 t3         7
# oder

df_long |> select(!Name)
# A tibble: 6 × 2
  Zeit  Punkte
  <fct>  <dbl>
1 t1         4
2 t2         5
3 t3         7
4 t1         3
5 t2         6
6 t3         7
Therapy_long |>
  select(Vpnr, Gruppe, Punkte)
# A tibble: 200 × 3
   Vpnr  Gruppe         Punkte
   <fct> <fct>           <dbl>
 1 1     Kontrollgruppe   4.29
 2 1     Kontrollgruppe   3.21
 3 2     Kontrollgruppe   6.18
 4 2     Kontrollgruppe   5.99
 5 3     Kontrollgruppe   3.93
 6 3     Kontrollgruppe   4.17
 7 4     Kontrollgruppe   5.06
 8 4     Kontrollgruppe   4.76
 9 5     Kontrollgruppe   6.45
10 5     Kontrollgruppe   5.64
# ℹ 190 more rows
# wählt alle Variablen von Gruppe bis Punkte aus 
# (also auch die Variablen dazwischen)
Therapy_long |>
  select(Gruppe:Punkte)
# A tibble: 200 × 3
   Gruppe         Zeit     Punkte
   <fct>          <fct>     <dbl>
 1 Kontrollgruppe Pretest    4.29
 2 Kontrollgruppe Posttest   3.21
 3 Kontrollgruppe Pretest    6.18
 4 Kontrollgruppe Posttest   5.99
 5 Kontrollgruppe Pretest    3.93
 6 Kontrollgruppe Posttest   4.17
 7 Kontrollgruppe Pretest    5.06
 8 Kontrollgruppe Posttest   4.76
 9 Kontrollgruppe Pretest    6.45
10 Kontrollgruppe Posttest   5.64
# ℹ 190 more rows
# Für die gleichzeitige Auswahl und Umbenennung 
# einer Variable verwendet man =. Der neue Name 
# steht auf der linken Seite des =, die alte 
# Variable auf der rechten Seite:
Therapy_long |> 
  select(ID = Vpnr)
# A tibble: 200 × 1
   ID   
   <fct>
 1 1    
 2 1    
 3 2    
 4 2    
 5 3    
 6 3    
 7 4    
 8 4    
 9 5    
10 5    
# ℹ 190 more rows

Hilfsfunktionen für select()

Für die Auswahl von Variablen gibt es eine Reihe von sogenannten “helper functions”:

# einschliessen
Therapy_long |> select(starts_with("Gr"))
# A tibble: 200 × 1
   Gruppe        
   <fct>         
 1 Kontrollgruppe
 2 Kontrollgruppe
 3 Kontrollgruppe
 4 Kontrollgruppe
 5 Kontrollgruppe
 6 Kontrollgruppe
 7 Kontrollgruppe
 8 Kontrollgruppe
 9 Kontrollgruppe
10 Kontrollgruppe
# ℹ 190 more rows
Therapy_long |> select(ends_with("te"))
# A tibble: 200 × 1
   Punkte
    <dbl>
 1   4.29
 2   3.21
 3   6.18
 4   5.99
 5   3.93
 6   4.17
 7   5.06
 8   4.76
 9   6.45
10   5.64
# ℹ 190 more rows
Therapy_long |> select(contains("u"))
# A tibble: 200 × 2
   Gruppe         Punkte
   <fct>           <dbl>
 1 Kontrollgruppe   4.29
 2 Kontrollgruppe   3.21
 3 Kontrollgruppe   6.18
 4 Kontrollgruppe   5.99
 5 Kontrollgruppe   3.93
 6 Kontrollgruppe   4.17
 7 Kontrollgruppe   5.06
 8 Kontrollgruppe   4.76
 9 Kontrollgruppe   6.45
10 Kontrollgruppe   5.64
# ℹ 190 more rows
vars <- c("Gruppe", "Punkte")
# einschliessendes ODER mit one_of()
Therapy_long |> select(one_of(vars))
# A tibble: 200 × 2
   Gruppe         Punkte
   <fct>           <dbl>
 1 Kontrollgruppe   4.29
 2 Kontrollgruppe   3.21
 3 Kontrollgruppe   6.18
 4 Kontrollgruppe   5.99
 5 Kontrollgruppe   3.93
 6 Kontrollgruppe   4.17
 7 Kontrollgruppe   5.06
 8 Kontrollgruppe   4.76
 9 Kontrollgruppe   6.45
10 Kontrollgruppe   5.64
# ℹ 190 more rows
# ausschliessen
Therapy_long |> select(!starts_with("Gr"))
# A tibble: 200 × 3
   Vpnr  Zeit     Punkte
   <fct> <fct>     <dbl>
 1 1     Pretest    4.29
 2 1     Posttest   3.21
 3 2     Pretest    6.18
 4 2     Posttest   5.99
 5 3     Pretest    3.93
 6 3     Posttest   4.17
 7 4     Pretest    5.06
 8 4     Posttest   4.76
 9 5     Pretest    6.45
10 5     Posttest   5.64
# ℹ 190 more rows
Therapy_long |> select(!ends_with("te"))
# A tibble: 200 × 3
   Vpnr  Gruppe         Zeit    
   <fct> <fct>          <fct>   
 1 1     Kontrollgruppe Pretest 
 2 1     Kontrollgruppe Posttest
 3 2     Kontrollgruppe Pretest 
 4 2     Kontrollgruppe Posttest
 5 3     Kontrollgruppe Pretest 
 6 3     Kontrollgruppe Posttest
 7 4     Kontrollgruppe Pretest 
 8 4     Kontrollgruppe Posttest
 9 5     Kontrollgruppe Pretest 
10 5     Kontrollgruppe Posttest
# ℹ 190 more rows
Therapy_long |> select(!contains("u"))
# A tibble: 200 × 2
   Vpnr  Zeit    
   <fct> <fct>   
 1 1     Pretest 
 2 1     Posttest
 3 2     Pretest 
 4 2     Posttest
 5 3     Pretest 
 6 3     Posttest
 7 4     Pretest 
 8 4     Posttest
 9 5     Pretest 
10 5     Posttest
# ℹ 190 more rows
vars <- c("Gruppe", "Punkte")
Therapy_long |> select(!one_of(vars))
# A tibble: 200 × 2
   Vpnr  Zeit    
   <fct> <fct>   
 1 1     Pretest 
 2 1     Posttest
 3 2     Pretest 
 4 2     Posttest
 5 3     Pretest 
 6 3     Posttest
 7 4     Pretest 
 8 4     Posttest
 9 5     Pretest 
10 5     Posttest
# ℹ 190 more rows
# Datensatz 'beispieldaten.sav' importieren und alle Items von swk1 bis swk12 auswählen
beispieldaten <- read_sav("data/beispieldaten.sav")
beispieldaten |> 
  select(num_range("swk", 1:12))
# A tibble: 286 × 12
    swk1  swk2  swk3  swk4  swk5  swk6  swk7  swk8  swk9 swk10 swk11 swk12
   <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
 1     2     5     6     4     7     5     5     6     6     4     4     5
 2     4     6     4     4     6     5     5     6     5     4     4     4
 3     5     4     5     6     5     7     7     6     4     5     3     3
 4     3     6     6     5     7     7     6     6     7     7     3     4
 5     5     6     6     5     7     6     5     5     7     6     4     4
 6     3     5     5     4     5     7     5     6     6     5     3     5
 7     4     3     5     4     6     7     1     6     5     7     3     6
 8     5     4     7     7     7     7    NA     7     7     6     4     3
 9     6     6     5     5     7     6     5     6     6     6     6     6
10     5     3     4     5     5     5     5     4     5     5     5     6
# ℹ 276 more rows

Damit können Variablen anhand von Suchkriterien ausgewählt oder ausgeschlossen werden. Wir werden in späteren Kapiteln weitere Beispiele dafür sehen.

# alle numerischen Variablen auswählen
Therapy_wide |> 
  select(where(is.numeric))
# A tibble: 100 × 2
   Pretest Posttest
     <dbl>    <dbl>
 1    4.29     3.21
 2    6.18     5.99
 3    3.93     4.17
 4    5.06     4.76
 5    6.45     5.64
 6    4.49     4.67
 7    4.60     4.24
 8    4.46     3.34
 9    4.76     4.11
10    5.12     5.29
# ℹ 90 more rows
Hinweis

Die Funktion where() ist eine besondere Hilfsfunktion, weil in ihr wie hier mit is.numeric() eine weitere Funktion definiert wird. Diese Funktion prüft, ob die Variablen im Datensatz eine bestimmte Bedingung erfüllen (hier: ob sie vom Typ numeric sind). Die Bedingungsfunktion muss in diesem Fall ohne Funktionsklammern in die Funktion where() eingefügt werden (also als where(is.numeric), nicht als where(is.numeric())).

Weitere Bedingungsfunktionen sind z.B. is.factor(), is.character() oder is.labelled(). Die Kombination von where() mit einer Bedingungsfunktion wird auch noch weiter unten in Verbindung mit mutate() eine wichtige Rolle spielen

4.4.3 Reihenfolge der Variablen verändern mit relocate()

Mit der Funktion relocate() verändern wir die Reihenfolge der Variablen. Per default werden die Variablen an den Anfang des Datensatzes verschoben. Mit den Argumenten .after und .before kann spezifiziert werden, wohin die Variablen verschoben werden sollen.

Beispiele

# Pretest und Posttest an den Anfang verschieben
Therapy_wide |>
  relocate(Pretest, Posttest)
# A tibble: 100 × 4
   Pretest Posttest Vpnr  Gruppe        
     <dbl>    <dbl> <fct> <fct>         
 1    4.29     3.21 1     Kontrollgruppe
 2    6.18     5.99 2     Kontrollgruppe
 3    3.93     4.17 3     Kontrollgruppe
 4    5.06     4.76 4     Kontrollgruppe
 5    6.45     5.64 5     Kontrollgruppe
 6    4.49     4.67 6     Kontrollgruppe
 7    4.60     4.24 7     Kontrollgruppe
 8    4.46     3.34 8     Kontrollgruppe
 9    4.76     4.11 9     Kontrollgruppe
10    5.12     5.29 10    Kontrollgruppe
# ℹ 90 more rows
# Gruppe vor Posttest verschieben
Therapy_wide |>
  relocate(Gruppe, .before = Posttest)
# A tibble: 100 × 4
   Vpnr  Pretest Gruppe         Posttest
   <fct>   <dbl> <fct>             <dbl>
 1 1        4.29 Kontrollgruppe     3.21
 2 2        6.18 Kontrollgruppe     5.99
 3 3        3.93 Kontrollgruppe     4.17
 4 4        5.06 Kontrollgruppe     4.76
 5 5        6.45 Kontrollgruppe     5.64
 6 6        4.49 Kontrollgruppe     4.67
 7 7        4.60 Kontrollgruppe     4.24
 8 8        4.46 Kontrollgruppe     3.34
 9 9        4.76 Kontrollgruppe     4.11
10 10       5.12 Kontrollgruppe     5.29
# ℹ 90 more rows
# Vpnr nach Gruppe verschieben
Therapy_wide |>
  relocate(Vpnr, .after = Gruppe)
# A tibble: 100 × 4
   Gruppe         Vpnr  Pretest Posttest
   <fct>          <fct>   <dbl>    <dbl>
 1 Kontrollgruppe 1        4.29     3.21
 2 Kontrollgruppe 2        6.18     5.99
 3 Kontrollgruppe 3        3.93     4.17
 4 Kontrollgruppe 4        5.06     4.76
 5 Kontrollgruppe 5        6.45     5.64
 6 Kontrollgruppe 6        4.49     4.67
 7 Kontrollgruppe 7        4.60     4.24
 8 Kontrollgruppe 8        4.46     3.34
 9 Kontrollgruppe 9        4.76     4.11
10 Kontrollgruppe 10       5.12     5.29
# ℹ 90 more rows

4.4.4 Beobachtungen (Fälle) auswählen mit filter()

Beobachtungen oder Fälle (Zeilen) werden mit filter() ausgewählt, d.h. wir können damit Fälle auswählen, welche gewisse Bedingungen erfüllen.

Es können auch mehrere Bedingungen mit logischen Operatoren verknüpft werden:

Syntax:

df |> filter(variable1 < WERT1 & variable2 == WERT2)

Beispiele

# nur Kontrollgruppe auswählen
Therapy_long |>
  filter(Gruppe == "Kontrollgruppe")
# A tibble: 100 × 4
   Vpnr  Gruppe         Zeit     Punkte
   <fct> <fct>          <fct>     <dbl>
 1 1     Kontrollgruppe Pretest    4.29
 2 1     Kontrollgruppe Posttest   3.21
 3 2     Kontrollgruppe Pretest    6.18
 4 2     Kontrollgruppe Posttest   5.99
 5 3     Kontrollgruppe Pretest    3.93
 6 3     Kontrollgruppe Posttest   4.17
 7 4     Kontrollgruppe Pretest    5.06
 8 4     Kontrollgruppe Posttest   4.76
 9 5     Kontrollgruppe Pretest    6.45
10 5     Kontrollgruppe Posttest   5.64
# ℹ 90 more rows
# nur Posttest
Therapy_long |>
  filter(Zeit == "Posttest")
# A tibble: 100 × 4
   Vpnr  Gruppe         Zeit     Punkte
   <fct> <fct>          <fct>     <dbl>
 1 1     Kontrollgruppe Posttest   3.21
 2 2     Kontrollgruppe Posttest   5.99
 3 3     Kontrollgruppe Posttest   4.17
 4 4     Kontrollgruppe Posttest   4.76
 5 5     Kontrollgruppe Posttest   5.64
 6 6     Kontrollgruppe Posttest   4.67
 7 7     Kontrollgruppe Posttest   4.24
 8 8     Kontrollgruppe Posttest   3.34
 9 9     Kontrollgruppe Posttest   4.11
10 10    Kontrollgruppe Posttest   5.29
# ℹ 90 more rows
# nur Pretest
Therapy_long |>
  filter(Zeit != "Posttest")
# A tibble: 100 × 4
   Vpnr  Gruppe         Zeit    Punkte
   <fct> <fct>          <fct>    <dbl>
 1 1     Kontrollgruppe Pretest   4.29
 2 2     Kontrollgruppe Pretest   6.18
 3 3     Kontrollgruppe Pretest   3.93
 4 4     Kontrollgruppe Pretest   5.06
 5 5     Kontrollgruppe Pretest   6.45
 6 6     Kontrollgruppe Pretest   4.49
 7 7     Kontrollgruppe Pretest   4.60
 8 8     Kontrollgruppe Pretest   4.46
 9 9     Kontrollgruppe Pretest   4.76
10 10    Kontrollgruppe Pretest   5.12
# ℹ 90 more rows
# nur Punkte >= 5
Therapy_long |>
  filter(Punkte >= 5)
# A tibble: 66 × 4
   Vpnr  Gruppe         Zeit     Punkte
   <fct> <fct>          <fct>     <dbl>
 1 2     Kontrollgruppe Pretest    6.18
 2 2     Kontrollgruppe Posttest   5.99
 3 4     Kontrollgruppe Pretest    5.06
 4 5     Kontrollgruppe Pretest    6.45
 5 5     Kontrollgruppe Posttest   5.64
 6 10    Kontrollgruppe Pretest    5.12
 7 10    Kontrollgruppe Posttest   5.29
 8 11    Kontrollgruppe Pretest    6.04
 9 13    Kontrollgruppe Posttest   5.16
10 19    Kontrollgruppe Pretest    5.20
# ℹ 56 more rows
# nur Punkte zwischen 3 und 5
Therapy_long |>
  filter(Punkte >= 3 & Punkte <= 5)
# A tibble: 130 × 4
   Vpnr  Gruppe         Zeit     Punkte
   <fct> <fct>          <fct>     <dbl>
 1 1     Kontrollgruppe Pretest    4.29
 2 1     Kontrollgruppe Posttest   3.21
 3 3     Kontrollgruppe Pretest    3.93
 4 3     Kontrollgruppe Posttest   4.17
 5 4     Kontrollgruppe Posttest   4.76
 6 6     Kontrollgruppe Pretest    4.49
 7 6     Kontrollgruppe Posttest   4.67
 8 7     Kontrollgruppe Pretest    4.60
 9 7     Kontrollgruppe Posttest   4.24
10 8     Kontrollgruppe Pretest    4.46
# ℹ 120 more rows
# nur Vpn 3 und 5
Therapy_long |>
  filter(Vpnr == 3 | Vpnr == 5)
# A tibble: 4 × 4
  Vpnr  Gruppe         Zeit     Punkte
  <fct> <fct>          <fct>     <dbl>
1 3     Kontrollgruppe Pretest    3.93
2 3     Kontrollgruppe Posttest   4.17
3 5     Kontrollgruppe Pretest    6.45
4 5     Kontrollgruppe Posttest   5.64
# Alternative dazu (mit dem %in% Operator)
Therapy_long |>
  filter(Vpnr %in% c(3, 5))
# A tibble: 4 × 4
  Vpnr  Gruppe         Zeit     Punkte
  <fct> <fct>          <fct>     <dbl>
1 3     Kontrollgruppe Pretest    3.93
2 3     Kontrollgruppe Posttest   4.17
3 5     Kontrollgruppe Pretest    6.45
4 5     Kontrollgruppe Posttest   5.64

4.4.5 Beobachtungen (Fälle) sortieren mit arrange()

Mit der arrange() Funktion können wir Beobachtungen sortieren, entweder in aufsteigender oder in absteigender Reihenfolge.

# aufsteigend
Therapy_long |>
  arrange(Vpnr)
# A tibble: 200 × 4
   Vpnr  Gruppe         Zeit     Punkte
   <fct> <fct>          <fct>     <dbl>
 1 1     Kontrollgruppe Pretest    4.29
 2 1     Kontrollgruppe Posttest   3.21
 3 2     Kontrollgruppe Pretest    6.18
 4 2     Kontrollgruppe Posttest   5.99
 5 3     Kontrollgruppe Pretest    3.93
 6 3     Kontrollgruppe Posttest   4.17
 7 4     Kontrollgruppe Pretest    5.06
 8 4     Kontrollgruppe Posttest   4.76
 9 5     Kontrollgruppe Pretest    6.45
10 5     Kontrollgruppe Posttest   5.64
# ℹ 190 more rows
# absteigend
Therapy_long |>
  arrange(desc(Vpnr))
# A tibble: 200 × 4
   Vpnr  Gruppe         Zeit     Punkte
   <fct> <fct>          <fct>     <dbl>
 1 100   Therapiegruppe Pretest    4.77
 2 100   Therapiegruppe Posttest   4.50
 3 99    Therapiegruppe Pretest    4.66
 4 99    Therapiegruppe Posttest   3.80
 5 98    Therapiegruppe Pretest    4.36
 6 98    Therapiegruppe Posttest   3.91
 7 97    Therapiegruppe Pretest    4.53
 8 97    Therapiegruppe Posttest   4.36
 9 96    Therapiegruppe Pretest    5.15
10 96    Therapiegruppe Posttest   5.05
# ℹ 190 more rows

4.4.6 Neue Variablen erstellen mit mutate()

Neue Variablen können mit mutate() aus schon bestehenden Variablen gebildet werden.

Syntax:

df |> mutate(neue_variable_1 = FORMEL_1,
             neue_variable_2 = FORMEL_2)

Beispiele

In Fragebogenstudien kommt es häufig vor, dass man für jede Person aus mehreren Variablen/Items einen Mittelwert berechnen will, z.B. um einige der Selbstwirksamkeitsitems aus dem Beispieldatensatz zu einer Skala zusammenzufassen:

# Neue Variable mit dem Mittelwert der Items swk4 bis swk7 erstellen
beispieldaten |>
  select(num_range("swk", 4:7)) |>
  mutate(swk_mean = (swk4 + swk5 + swk6 + swk7)/4)
# A tibble: 286 × 5
    swk4  swk5  swk6  swk7 swk_mean
   <dbl> <dbl> <dbl> <dbl>    <dbl>
 1     4     7     5     5     5.25
 2     4     6     5     5     5   
 3     6     5     7     7     6.25
 4     5     7     7     6     6.25
 5     5     7     6     5     5.75
 6     4     5     7     5     5.25
 7     4     6     7     1     4.5 
 8     7     7     7    NA    NA   
 9     5     7     6     5     5.75
10     5     5     5     5     5   
# ℹ 276 more rows

An diesem Beispiel wird ersichtlich, dass die Berechnung des Mittelwerts über eine Formel nicht funktioniert, wenn NAs (fehlende Werte) vorhanden sind. Da in Zeile 8 kein Wert für swk7 vorhanden ist, kann in dieser Zeile kein Mittelwert berechnet werden.

Die Verwendung von mean() mit dem Argument na.rm = TRUE ist auch nicht möglich, da mean() dann den Mittelwert über alle Personen UND alle Variablen berechnet (und nicht pro Person den Mittelwert über die Variablen).

beispieldaten |>
  select(num_range("swk", 4:7)) |>
  mutate(swk_mean = mean(c(swk4, swk5, swk6, swk7), na.rm = TRUE))
# A tibble: 286 × 5
    swk4  swk5  swk6  swk7 swk_mean
   <dbl> <dbl> <dbl> <dbl>    <dbl>
 1     4     7     5     5     5.58
 2     4     6     5     5     5.58
 3     6     5     7     7     5.58
 4     5     7     7     6     5.58
 5     5     7     6     5     5.58
 6     4     5     7     5     5.58
 7     4     6     7     1     5.58
 8     7     7     7    NA     5.58
 9     5     7     6     5     5.58
10     5     5     5     5     5.58
# ℹ 276 more rows

Die Funktion rowwise() aus dem dplyr-Package kann uns hier weiterhelfen.

beispieldaten |>
  select(num_range("swk", 4:7)) |>
  rowwise() |>
  mutate(swk_mean = mean(c(swk4, swk5, swk6, swk7), na.rm = TRUE))
# A tibble: 286 × 5
# Rowwise: 
    swk4  swk5  swk6  swk7 swk_mean
   <dbl> <dbl> <dbl> <dbl>    <dbl>
 1     4     7     5     5     5.25
 2     4     6     5     5     5   
 3     6     5     7     7     6.25
 4     5     7     7     6     6.25
 5     5     7     6     5     5.75
 6     4     5     7     5     5.25
 7     4     6     7     1     4.5 
 8     7     7     7    NA     7   
 9     5     7     6     5     5.75
10     5     5     5     5     5   
# ℹ 276 more rows

Die Verwendung von rowwise() führt dazu, dass mean(na.rm = TRUE) den Mittelwert für jede Zeile berechnet. In Zeile 8 wurde nun also der Mittelwert über die verbliebenen 3 Items berechnet.

Mit mutate() können wir auch eine bestehende Variable umberechnen. In folgendem Beispiel rechnen wir die Punkte (Skala von 0 bis 7) in Prozentwerte um.

Therapy_long |>
  mutate(Punkte_p = round(Punkte/7 * 100, digits = 1))
# A tibble: 200 × 5
   Vpnr  Gruppe         Zeit     Punkte Punkte_p
   <fct> <fct>          <fct>     <dbl>    <dbl>
 1 1     Kontrollgruppe Pretest    4.29     61.2
 2 1     Kontrollgruppe Posttest   3.21     45.8
 3 2     Kontrollgruppe Pretest    6.18     88.2
 4 2     Kontrollgruppe Posttest   5.99     85.5
 5 3     Kontrollgruppe Pretest    3.93     56.2
 6 3     Kontrollgruppe Posttest   4.17     59.6
 7 4     Kontrollgruppe Pretest    5.06     72.3
 8 4     Kontrollgruppe Posttest   4.76     68.1
 9 5     Kontrollgruppe Pretest    6.45     92.2
10 5     Kontrollgruppe Posttest   5.64     80.6
# ℹ 190 more rows

Mit dem Argument .keep = c("all", "used", "unused", "none") kann kontrolliert werden, welche Variablen im Output erscheinen. .keep = "used" führt z.B. dazu, dass neben der neu gebildeten Variable nur diejenigen Variablen, die für die Erstellung der neuen Variable verwendet wurden, im Output erscheinen.

Hinweis

Im obigen Beispiel haben wir zunächst mit select() nur die Variablen ausgewählt, aus denen wir anschliessend die neue Variable gebildet haben. Das mutate()-Argument .keep = "used" erfüllt dieselbe Funktion und macht den Code kürzer.

Therapy_long |>
  mutate(Punkte_p = round(Punkte/7 * 100, digits = 1), 
         .keep = "used")
# A tibble: 200 × 2
   Punkte Punkte_p
    <dbl>    <dbl>
 1   4.29     61.2
 2   3.21     45.8
 3   6.18     88.2
 4   5.99     85.5
 5   3.93     56.2
 6   4.17     59.6
 7   5.06     72.3
 8   4.76     68.1
 9   6.45     92.2
10   5.64     80.6
# ℹ 190 more rows

.keep = "none" führt dazu, dass nur die neue Variable im Output erscheint.

Therapy_long |>
  mutate(Punkte_p = round(Punkte/7 * 100, digits = 1), 
         .keep = "none")
# A tibble: 200 × 1
   Punkte_p
      <dbl>
 1     61.2
 2     45.8
 3     88.2
 4     85.5
 5     56.2
 6     59.6
 7     72.3
 8     68.1
 9     92.2
10     80.6
# ℹ 190 more rows

across(): Mehrere Variablen auswählen und verändern

Möchten wir Transformationen auf mehrere Variablen gleichzeitig anwenden, können wir mutate() mit across() verwenden.

Syntax:

across(.cols, .fns, .names)

Das erste Argument .cols wählt die Variablen aus, mit denen gearbeitet wird. Das zweite Argument .fns bestimmt eine oder mehrere Funktionen, die auf die ausgewählten Variablen angewendet wird/werden. Mit dem dritten Argument .names kann spezifiziert werden, wie die Variablen im Output benannt werden sollen.

Als Beispiel wollen wir die Punkte im Therapiedatensatz (diesmal in Therapy_wide) in Prozentwerte umrechnen. Die Punkte befinden sich in den Spalten Pretest and Posttest. Diese sind die einzigen numerischen Variablen im Datensatz, daher können wir zur Auswahl dieser Variablen die Bedingung where(is.numeric) benutzen, die alle numerischen Variablen eines Data Frames auswählt.

Oben haben wir der in mutate() zu bildenden Variable einfach einen Namen gegeben und anschliessend die Formel bzw. Funktion angegeben, mit der die neue Variable erstellt werden soll.

Bei der Bildung/Transformation von mehr als einer Variablen muss zur Definition der Formel/Funktion eine anonymous function erstellt werden, d.h. es muss explizit eine Funktion definiert werden, die auf die ausgewählten Variablen angewendet wird.

Hinweis

Das Erstellen eigener Funktionen in R ist über diese Anwendung hinaus nicht Gegenstand dieses Kurses. Sie sehen aber hier schon, dass es nicht allzu schwierig ist, eigene Funktionen in R zu definieren.

Eine anonymous function startet mit function(x) gefolgt von der Definition der Funktion (in diesem Fall eine Kombination von round() und der Formel zur Umrechnung in Prozentwerte), wobei x (das Argument der zu bildenden Funktion) für die jeweils einzufügende Variable steht:

Therapy_wide |> 
  mutate(across(.cols = where(is.numeric), 
                .fns = function(x) round(x / 7 * 100, digits = 1)))
# A tibble: 100 × 4
   Vpnr  Gruppe         Pretest Posttest
   <fct> <fct>            <dbl>    <dbl>
 1 1     Kontrollgruppe    61.2     45.8
 2 2     Kontrollgruppe    88.2     85.5
 3 3     Kontrollgruppe    56.2     59.6
 4 4     Kontrollgruppe    72.3     68.1
 5 5     Kontrollgruppe    92.2     80.6
 6 6     Kontrollgruppe    64.1     66.7
 7 7     Kontrollgruppe    65.7     60.5
 8 8     Kontrollgruppe    63.7     47.8
 9 9     Kontrollgruppe    68       58.7
10 10    Kontrollgruppe    73.1     75.6
# ℹ 90 more rows

Hier werden die Variablen Pretest und Posttest direkt verändert/überschrieben. Mit dem .names-Argument können wir dafür sorgen, dass die transformierten Variablen unter einem neuen Namen dem Datensatz hinzugefügt werden (und die Ausgangsvariablen unverändert im Datensatz verbleiben).

Therapy_wide |> 
  mutate(across(.cols = where(is.numeric), 
                .fns = function(x) round(x / 7 * 100, digits = 1),
                .names = "{.col}_percent"))
# A tibble: 100 × 6
   Vpnr  Gruppe         Pretest Posttest Pretest_percent Posttest_percent
   <fct> <fct>            <dbl>    <dbl>           <dbl>            <dbl>
 1 1     Kontrollgruppe    4.29     3.21            61.2             45.8
 2 2     Kontrollgruppe    6.18     5.99            88.2             85.5
 3 3     Kontrollgruppe    3.93     4.17            56.2             59.6
 4 4     Kontrollgruppe    5.06     4.76            72.3             68.1
 5 5     Kontrollgruppe    6.45     5.64            92.2             80.6
 6 6     Kontrollgruppe    4.49     4.67            64.1             66.7
 7 7     Kontrollgruppe    4.60     4.24            65.7             60.5
 8 8     Kontrollgruppe    4.46     3.34            63.7             47.8
 9 9     Kontrollgruppe    4.76     4.11            68               58.7
10 10    Kontrollgruppe    5.12     5.29            73.1             75.6
# ℹ 90 more rows

In einer Kurzvariante zur Definition einer anonymous function wird das Wort function durch das Zeichen \ (Backslash) ersetzt. Wir benutzen im Folgenden diese Variante.

Ausserdem können wir die Variablen auch mithilfe eines Vektors von Variablennamen (hier c(Pretest, Posttest)) auswählen, ohne eine Bedingung wie where(is.numeric) zu verwenden.

Zuletzt müssen die ersten beiden Argumente von across() nicht unbedingt benannt werden (wenn sie in der richtigen Reihenfolge verwendet werden), was den Code noch etwas verkürzt:

Therapy_wide |> 
  mutate(across(c(Pretest, Posttest), \(x) round(x / 7 * 100, digits = 1), 
                .names = "{.col}_percent"))
# A tibble: 100 × 6
   Vpnr  Gruppe         Pretest Posttest Pretest_percent Posttest_percent
   <fct> <fct>            <dbl>    <dbl>           <dbl>            <dbl>
 1 1     Kontrollgruppe    4.29     3.21            61.2             45.8
 2 2     Kontrollgruppe    6.18     5.99            88.2             85.5
 3 3     Kontrollgruppe    3.93     4.17            56.2             59.6
 4 4     Kontrollgruppe    5.06     4.76            72.3             68.1
 5 5     Kontrollgruppe    6.45     5.64            92.2             80.6
 6 6     Kontrollgruppe    4.49     4.67            64.1             66.7
 7 7     Kontrollgruppe    4.60     4.24            65.7             60.5
 8 8     Kontrollgruppe    4.46     3.34            63.7             47.8
 9 9     Kontrollgruppe    4.76     4.11            68               58.7
10 10    Kontrollgruppe    5.12     5.29            73.1             75.6
# ℹ 90 more rows

Falls die Funktion, die wir auf mehrere Variablen/Spalten anwenden möchten, kein weiteres Argument hat, kann Sie auch ohne Definition einer anonymous function direkt verwendet werden, dann allerdings ohne Funktionsklammern. Z.B. können wir alle als labelled double eingelesenen Gruppierungsvariablen im Beispieldatensatz mit der Funktion haven::as_factor() zu Faktoren konvertieren und gleichzeitig die eingelesenen Labels zu Faktorstufen machen (vgl. Kap. 3.2).

Für diesen Zweck wurde im Package haven extra eine Funktion namens is.labelled() definiert, mit der wir (zusammen mit across(where())) die als labelled double eingelesenen Variablen identifizieren und auswählen können:

beispieldaten |> 
  mutate(across(where(is.labelled), haven::as_factor), 
         .keep = "used")
# A tibble: 286 × 6
   westost geschlecht bildung_vater          bildung_mutter bildung_vater_binaer
   <fct>   <fct>      <fct>                  <fct>          <fct>               
 1 West    weiblich   Fachhochschulabschlus… Fachhochschul… hoch                
 2 West    männlich   Fachabitur, Abitur     Realschulabsc… hoch                
 3 West    weiblich   Realschulabschluss (m… Hauptschulabs… niedrig             
 4 West    weiblich   Realschulabschluss (m… Fachhochschul… niedrig             
 5 West    weiblich   Fachhochschulabschlus… Realschulabsc… hoch                
 6 West    männlich   Realschulabschluss (m… Realschulabsc… niedrig             
 7 West    männlich   Realschulabschluss (m… Realschulabsc… niedrig             
 8 West    weiblich   Fachhochschulabschlus… Fachhochschul… hoch                
 9 West    männlich   Realschulabschluss (m… Realschulabsc… niedrig             
10 West    männlich   Fachabitur, Abitur     Realschulabsc… hoch                
# ℹ 276 more rows
# ℹ 1 more variable: bildung_mutter_binaer <fct>

4.4.7 Variablen rekodieren mit case_when()

Manchmal möchte man Variablen nach bestimmten Bedingungen umkodieren, die neue Variable soll also bestimmte Werte aufweisen, je nachdem welche Eigenschaften die Werte der alten Variable haben.

Syntax:

case_when(bedingung_1 ~ "neuer_wert_1",
          bedingung_2 ~ "neuer_wert_2")

Da man hier Variablen nach bestimmten Bedingungen verändert bzw. neu bildet, wird die Funktion innerhalb von mutate() verwendet, es handelt sich also um eine Art Hilfsfunktion für mutate().

Syntax mit mutate():

mutate(neue_variable = case_when(alte_variable_bedingung_1 ~ "neuer_wert_1",
                                 alte_variable_bedingung_2 ~ "neuer_wert_2")

Beispiel

Wir wollen das alter der Jugendlichen im Beispieldatensatz so rekodieren, dass diejenigen, die älter als der Mittelwert sind, den Wert “älter” bekommen, und diejenigen, die jünger als der Mittelwert sind, den Wert “jünger” bekommen.

beispieldaten |> 
  mutate(alter_dichotom = case_when(alter > mean(alter) ~ "älter",
                                    alter < mean(alter) ~ "jünger"),
         .keep = "used")
# A tibble: 286 × 2
   alter alter_dichotom
   <dbl> <chr>         
 1    13 jünger        
 2    14 jünger        
 3    14 jünger        
 4    14 jünger        
 5    14 jünger        
 6    14 jünger        
 7    14 jünger        
 8    15 älter         
 9    15 älter         
10    14 jünger        
# ℹ 276 more rows

Diese Variable können wir z.B. anschliessend nutzen, um herauszufinden, wie die Altersverteilung in dieser Hinsicht nach Geschlecht aussieht. Dazu müssen wir die Daten aber zuerst wieder zuweisen (hier zu dat_table):

dat_table <- beispieldaten |> 
  select(alter, geschlecht) |> 
  drop_na() |> 
  mutate(geschlecht = haven::as_factor(geschlecht),
         alter_dichotom = case_when(alter > mean(alter) ~ "älter",
                                    alter < mean(alter) ~ "jünger"))

table(dat_table$geschlecht, dat_table$alter_dichotom) 
          
           älter jünger
  männlich    93     59
  weiblich    70     64
# als Anteile pro Geschlecht (Zeilen der Kreuztabelle)
table(dat_table$geschlecht, dat_table$alter_dichotom) |> 
  proportions(1) |> 
  round(2)
          
           älter jünger
  männlich  0.61   0.39
  weiblich  0.52   0.48

4.4.8 Faktorstufen rekodieren mit fct_recode()

Um die Stufen eines Faktors umzukodieren, verwenden wir fct_recode() aus dem forcats-Package.

Syntax:

fct_recode(variable,
           neuer_wert_1 = "alter_wert_1",
           neuer_wert_2 = "alter_wert_2")

Auch diese Funktion wird normalerweise innerhalb von mutate() verwendet.

Beispiel

# Kontrollgruppe in "control" und die Therapiegruppe in "treatment" umbenennen
Therapy_long |>
  mutate(Gruppe = fct_recode(Gruppe,
                             control = "Kontrollgruppe",
                             treatment = "Therapiegruppe"))
# A tibble: 200 × 4
   Vpnr  Gruppe  Zeit     Punkte
   <fct> <fct>   <fct>     <dbl>
 1 1     control Pretest    4.29
 2 1     control Posttest   3.21
 3 2     control Pretest    6.18
 4 2     control Posttest   5.99
 5 3     control Pretest    3.93
 6 3     control Posttest   4.17
 7 4     control Pretest    5.06
 8 4     control Posttest   4.76
 9 5     control Pretest    6.45
10 5     control Posttest   5.64
# ℹ 190 more rows

Hier wird mit mutate() keine neue Variable gebildet, sondern es wird eine Variable verändert, indem die durch fct_recode() veränderte Variable Gruppe der ursprünglichen Variable wiederzugewiesen wird.

Wir können Faktorstufen nicht nur umbenennen, sondern sie auch “zusammenlegen”, um einen Faktor mit weniger Faktorstufen zu erhalten. Das macht man manchmal, wenn manche Kategorien nur sehr geringe Häufigkeiten haben. Z.B. können wir die Altersvariable zunächst als Faktor definieren (es gibt dort nur ganzzahlige Altersangaben):

# Alter als Faktorvariable definieren
beispieldaten <- beispieldaten |>
  mutate(alter_f = factor(alter))

table(beispieldaten$alter_f)

 13  14  15  16  17 
  6 117 121  40   2 

Es gibt nur sehr wenige 13-Jährige sowie 17-Jährige im Datensatz. Man könnte daher eine dreistufige Altersvariable bilden, bei der diese Alterskategorien mit der jeweils benachbarten zusammengelegt werden:

beispieldaten <- beispieldaten |>
  mutate(alter_dreistufig = fct_recode(alter_f,
                             alt = "17",
                             alt = "16",
                             mittel = "15",
                             jung = "14",
                             jung = "13"))

table(beispieldaten$alter_dreistufig)

  jung mittel    alt 
   123    121     42 

4.4.9 Daten gruppieren mit group_by()

Oft wollen wir bestimmte Operationen nicht auf den ganzen Datensatz anwenden, sondern nur auf Subgruppen, welche durch Faktorstufen definiert sind. Dafür gibt es die Funktion group_by() - diese teilt den Datensatz anhand einer Gruppierungsvariable, wendet eine Funktion auf jeden Teil an, und setzt den Datensatz danach wieder zusammen (split-apply-combine). group_by() wird deshalb meistens in Kombination mit anderen Funktionen verwendet.

Hinweis

Wollen wir die Gruppierung nach einer abgeschlossenen gruppierten Operation wieder rückgängig machen (um weitere ungruppierte Operationen durchzuführen), so verwenden wir ungroup(). Falls man das Ergebnis einem Objekt zuweist, ist es ratsam, am Schluss der Operation immer ungroup() zu verwenden, um zu verhindern, dass die Gruppierungsinformation in dem Objekt mit abgespeichert wird.

Syntax:

df <- group_by(gruppierung_1, gruppierung_2, gruppierung_3)

Beispiele

Lassen Sie uns einen neuen Datensatz importieren. In diesem Beispiel wurden 12 Probanden vier Versuchsbedingungen zugeordnet, und als abhängige Variable wurde das Ausmass des aggressiven Verhaltens gemessen. Nehmen wir nun an, dass wir dem Datensatz eine gruppenzentrierte Aggressionsvariable hinzufügen wollen (die Gruppen sind die Bedingungen). Solche gruppenzentrierten Variablen (jede Person hat als Wert die Abweichung vom Gruppenmittelwert der Gruppe, der sie zugehört) werden für bestimmte statistische Verfahren benötigt, die wir in den nächsten Semestern kennenlernen werden.

library(readr)
alk_aggr <- read_csv("data/alkohol-aggression.csv", show_col_types = FALSE)
alk_aggr$alkoholbedingung <- factor(alk_aggr$alkoholbedingung)

alk_aggr
# A tibble: 12 × 2
   aggressivitaet alkoholbedingung
            <dbl> <fct>           
 1             64 kein_alkohol    
 2             58 kein_alkohol    
 3             64 kein_alkohol    
 4             74 placebo         
 5             79 placebo         
 6             72 placebo         
 7             71 anti_placebo    
 8             69 anti_placebo    
 9             67 anti_placebo    
10             69 alkohol         
11             73 alkohol         
12             74 alkohol         

Wir können group_by() verwenden, um den Datensatz in vier separate Teile zu teilen, berechnen dann den Gruppenmittelwert für jeden einzelnen, und verwenden diesen gleich, um die gruppenzentrierte Variable zu berechnen. Anschliessend werden die Teile wieder “zusammengesetzt” (d.h. es ist wieder ein Datensatz). Da wir das ganze wieder zuweisen, benutzen wir am Ende ungroup().

alk_aggr <- alk_aggr |>
  group_by(alkoholbedingung) |>
  mutate(group_mean = mean(aggressivitaet),
         aggr_c = aggressivitaet - group_mean) |> 
  ungroup()

alk_aggr
# A tibble: 12 × 4
   aggressivitaet alkoholbedingung group_mean aggr_c
            <dbl> <fct>                 <dbl>  <dbl>
 1             64 kein_alkohol             62      2
 2             58 kein_alkohol             62     -4
 3             64 kein_alkohol             62      2
 4             74 placebo                  75     -1
 5             79 placebo                  75      4
 6             72 placebo                  75     -3
 7             71 anti_placebo             69      2
 8             69 anti_placebo             69      0
 9             67 anti_placebo             69     -2
10             69 alkohol                  72     -3
11             73 alkohol                  72      1
12             74 alkohol                  72      2

4.4.10 Variablen zusammenfassen mit summarize()

Mit summarize() oder summarize() können wir Variablen zusammenfassen und deskriptive Kennzahlen berechnen. summarize() wird oft zusammen mit group_by() verwendet.

Hinweis

Im Gegensatz zu mutate() gibt summarize() nicht einen Wert für jede Beobachtung als Output, sondern einen Wert (z.B. eine deskriptive Statistik) für jede durch group_by() definierte Gruppe.

Syntax:

df |> summarize(kennzahl = FUNKTION(variable))

FUNKTION ist ein Platzhalter für jede Funktion, die zum Zusammenfassen von Daten verwendet werden kann, z.B. mean() oder sd().

Als Beispiel mit dem Therapy_long Datensatz berechnen wir die Mittelwerte und die Standardabweichungen der Variable Punkte getrennt für alle Gruppen und Messzeitpunkte:

# Gruppenmittelwerte pro Messzeitpunkt
Therapy_long |>
  group_by(Gruppe, Zeit) |>
  summarize(Mittelwert = mean(Punkte),
            Standardabweichung = sd(Punkte))
`summarise()` has grouped output by 'Gruppe'. You can override using the
`.groups` argument.
# A tibble: 4 × 4
# Groups:   Gruppe [2]
  Gruppe         Zeit     Mittelwert Standardabweichung
  <fct>          <fct>         <dbl>              <dbl>
1 Kontrollgruppe Pretest        5.06              0.908
2 Kontrollgruppe Posttest       4.65              0.834
3 Therapiegruppe Pretest        4.82              0.691
4 Therapiegruppe Posttest       4.23              0.752

Statt wie oben summarize() nur auf eine Variable anzuwenden, können wir diese Funktion mit across() auch auf mehrere Variablen anwenden. Z.B. möchten wir die Mittelwerte der Items swk4 bis swk7 berechnen. Dazu wird die Funktion mean() wieder ohne Funktionsklammern benutzt, da sie innerhalb von across() ohne weiteres Argument verwendet wird:

beispieldaten |>
  summarize(across(swk4:swk7, mean))
# A tibble: 1 × 4
   swk4  swk5  swk6  swk7
  <dbl> <dbl> <dbl> <dbl>
1    NA    NA    NA    NA

Es scheint NAs zu geben, daher funktioniert mean() nicht ohne das Argument na.rm = TRUE. Damit wir dieses Argument aber innerhalb von across() aufrufen können, müssen wir wieder eine anonymous function definieren:

beispieldaten |>
  summarize(across(swk4:swk7, \(x) mean(x, na.rm = TRUE)))
# A tibble: 1 × 4
   swk4  swk5  swk6  swk7
  <dbl> <dbl> <dbl> <dbl>
1  5.01  6.17  6.16  4.97

Oder wir löschen die NAs zuerst:

beispieldaten |>
  select(swk4:swk7) |> 
  drop_na() |> 
  summarize(across(everything(), mean))
# A tibble: 1 × 4
   swk4  swk5  swk6  swk7
  <dbl> <dbl> <dbl> <dbl>
1  5.01  6.17  6.17  4.96

Warum unterscheiden sich die Ergebnisse minimal? - Weil oben die NAs pro Variable entfernt wurden, während hier alle Personen gelöscht wurden, die einen fehlenden Wert auf irgendeiner der vier Variablen haben.

Es ist auch möglich, mehrere Funktionen gleichzeitig auf mehrere Variablen anzuwenden. Dann muss unter .fns eine list() von anonymous functions erstellt werden. Z.B. können wir zusätzlich zum Mittelwert noch die Standardabweichung berechnen:

beispieldaten |>
  summarize(across(.cols = swk4:swk7, 
                   .fns = list(
                     mean = \(x) mean(x, na.rm = TRUE),
                     sd = \(x) sd(x, na.rm = TRUE)),
                   .names = NULL))
# A tibble: 1 × 8
  swk4_mean swk4_sd swk5_mean swk5_sd swk6_mean swk6_sd swk7_mean swk7_sd
      <dbl>   <dbl>     <dbl>   <dbl>     <dbl>   <dbl>     <dbl>   <dbl>
1      5.01    1.14      6.17    1.01      6.16   0.918      4.97    1.50

Ohne Argumentwert (.names = NULL) wird die Benennung der Output-Variablen nach dem Default Variablenname_Funktionsname gemacht. Übersetzt heisst das: .names = "{.col}_{.fn}"

Wenn wir es andersherum haben wollen, können wir das .names-Argument entsprechend ändern:

beispieldaten |>
  summarize(across(.cols = swk4:swk7, 
                   .fns = list(
                     mean = \(x) mean(x, na.rm = TRUE),
                     sd = \(x) sd(x, na.rm = TRUE)),
                   .names = "{.fn}_{.col}"))
# A tibble: 1 × 8
  mean_swk4 sd_swk4 mean_swk5 sd_swk5 mean_swk6 sd_swk6 mean_swk7 sd_swk7
      <dbl>   <dbl>     <dbl>   <dbl>     <dbl>   <dbl>     <dbl>   <dbl>
1      5.01    1.14      6.17    1.01      6.16   0.918      4.97    1.50

Um den Output bei der Anwendung mehrerer Funktionen übersichtlicher zu machen, kann man im Anschluss pivot_longer() mit fortgeschrittenen Argumenten verwenden, die wir oben nicht betrachtet haben. Um das noch besser zu veranschaulichen, berechnen wir jetzt zusätzlich zu Mittelwert und Standardabweichung auch noch die Anzahl der fehlenden Werte mit sum(is.na(x)):

beispieldaten |>
  summarize(across(.cols = swk4:swk7, 
                   .fns = list(
                     mean = \(x) mean(x, na.rm = TRUE),
                     sd = \(x) sd(x, na.rm = TRUE),
                     nmiss = \(x) sum(is.na(x))),
                   .names = "{.fn}_{.col}"
                   )
            ) |> 
  pivot_longer(everything(),
               names_to = c("source", ".value"),
               names_sep = "_")
# A tibble: 3 × 5
  source  swk4  swk5  swk6  swk7
  <chr>  <dbl> <dbl> <dbl> <dbl>
1 mean    5.01  6.17 6.16   4.97
2 sd      1.14  1.01 0.918  1.50
3 nmiss   1     2    1      2   

4.5 Übungsaufgaben

Jugendliche in West-/Ostdeutschland

library(haven)
beispieldaten <- read_sav("data/beispieldaten.sav")
beispieldaten$westost <- haven::as_factor(beispieldaten$westost)
beispieldaten$geschlecht <- haven::as_factor(beispieldaten$geschlecht)

head(beispieldaten)
# A tibble: 6 × 98
     ID westost geschlecht alter  swk1  swk2  swk3  swk4  swk5  swk6  swk7  swk8
  <dbl> <fct>   <fct>      <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
1     1 West    weiblich      13     2     5     6     4     7     5     5     6
2     2 West    männlich      14     4     6     4     4     6     5     5     6
3    10 West    weiblich      14     5     4     5     6     5     7     7     6
4    11 West    weiblich      14     3     6     6     5     7     7     6     6
5    12 West    weiblich      14     5     6     6     5     7     6     5     5
6    14 West    männlich      14     3     5     5     4     5     7     5     6
# ℹ 86 more variables: swk9 <dbl>, swk10 <dbl>, swk11 <dbl>, swk12 <dbl>,
#   swk13 <dbl>, swk14 <dbl>, swk15 <dbl>, swk16 <dbl>, swk17 <dbl>,
#   swk18 <dbl>, swk19 <dbl>, swk20 <dbl>, swk21 <dbl>, swk22 <dbl>,
#   swk23 <dbl>, swk24 <dbl>, swk25 <dbl>, swk26 <dbl>, swk27 <dbl>,
#   swk28 <dbl>, swk29 <dbl>, swk30 <dbl>, swk31 <dbl>, swk32 <dbl>,
#   swk33 <dbl>, swk34 <dbl>, swk35 <dbl>, swk36 <dbl>, unteltern1 <dbl>,
#   unteltern2 <dbl>, unteltern3 <dbl>, unteltern4 <dbl>, unteltern5 <dbl>, …

Durchschnittsalter

Berechnen Sie das mittlere Alter und die Standardabweichung für beide Geschlechter.

beispieldaten |>
  group_by(geschlecht) |>
  summarize(alter_mw = round(mean(alter), 1),
            alter_sd = round(sd(alter), 1))
# A tibble: 2 × 3
  geschlecht alter_mw alter_sd
  <fct>         <dbl>    <dbl>
1 männlich       14.8      0.7
2 weiblich       14.6      0.8

Unterstützung durch Freunde und Eltern

Wählen Sie die Variablen unt_freunde (Unterstützung durch Freunde) und unt_eltern (Unterstützung durch Eltern) aus, und machen sie daraus einen long Datensatz. Beide Variablen wurden mit denselben Items gemessen, sie unterscheiden sich lediglich durch die Quelle der Unterstützung (Freunde oder Eltern). Insofern können wir diese Unterstützungsquelle als messwiederholten Faktor auffassen.

Schritt 1:

# Variablen auswählen
unterstuetzung <- beispieldaten |>
  select(ID, westost, unt_freunde, unt_eltern)

Schritt 2:

# gibt es fehlende Werte?
unterstuetzung[!complete.cases(unterstuetzung), ]
# A tibble: 2 × 4
     ID westost unt_freunde unt_eltern
  <dbl> <fct>         <dbl>      <dbl>
1    16 West          NA          5.17
2   332 Ost            4.83      NA   
# fehlende Werte ausschliessen
unterstuetzung <- unterstuetzung |> 
  drop_na()

Schritt 3:

# von wide zu long: wir nennen den Messwiederholungsfaktor "quelle" und die 
# Variable mit den Werten "unterstuetzung"
library(tidyr)

unterstuetzung <- unterstuetzung |>
  pivot_longer(!c(ID, westost), names_to = "quelle",
               values_to = "unterstuetzung")

Schritt 4:

# quelle zu Faktor konvertieren
unterstuetzung <- unterstuetzung |> 
  mutate(quelle = factor(quelle))

Schritt 5 (Fortgeschritten):

# Stufen umbennen ("unt_" entfernen)
library(stringr)
unterstuetzung <- unterstuetzung |>
  mutate(quelle = str_replace(quelle, ".*_", ""))

Schritt 6:

# Data Frame anschauen
unterstuetzung
# A tibble: 568 × 4
      ID westost quelle  unterstuetzung
   <dbl> <fct>   <chr>            <dbl>
 1     1 West    freunde           6.67
 2     1 West    eltern            6.67
 3     2 West    freunde           4.83
 4     2 West    eltern            5.17
 5    10 West    freunde           4   
 6    10 West    eltern            6.83
 7    11 West    freunde           7   
 8    11 West    eltern            7   
 9    12 West    freunde           7   
10    12 West    eltern            6.67
# ℹ 558 more rows

Und nun alles einem Schritt:

unterstuetzung <- beispieldaten |>
  select(ID, westost, unt_freunde, unt_eltern) |>
  # fehlende Werte ausschliessen
  drop_na() |>
  # von wide zu long
  pivot_longer(!c(ID, westost), names_to = "quelle",
               values_to = "unterstuetzung") |> 
  # Variable quelle zu Faktor konvertieren und 
  # davor Faktorstufen bearbeiten
  mutate(quelle = factor(str_replace(quelle, ".*_", "")))

unterstuetzung
# A tibble: 568 × 4
      ID westost quelle  unterstuetzung
   <dbl> <fct>   <fct>            <dbl>
 1     1 West    freunde           6.67
 2     1 West    eltern            6.67
 3     2 West    freunde           4.83
 4     2 West    eltern            5.17
 5    10 West    freunde           4   
 6    10 West    eltern            6.83
 7    11 West    freunde           7   
 8    11 West    eltern            7   
 9    12 West    freunde           7   
10    12 West    eltern            6.67
# ℹ 558 more rows

Zufriedenheit

Wir wollen nun die Zufriedenheit mit verschiedenen Lebensbereichen untersuchen. Wir brauchen dazu nicht den ganzen Datensatz. Die relevanten Variablen beginnen alle mit leben_, und sollen ausgewählt werden. Die Variable leben_gesamt ist aber schon eine Zusammenfassung der Zufriedenheit mit allen Bereichen, diese wollen wir nicht berücksichtigen.

Dies bedeutet, dass wir alle Variablen beginnend mit leben_ auswählen möchten, ausser leben_gesamt. Eine Möglichkeit wäre, die Variablen mit starts_with("leben_") auszuwählen, und dann leben_gesamt individuell auszuschliessen (das funktioniert hier nur mit -leben_gesamt, nicht mit !leben_gesamt). Zusätzlich wollen wir die IDs der Versuchspersonen behalten.

zufriedenheit_wide <- beispieldaten |>
  select(ID, starts_with("leben_"), -leben_gesamt)

zufriedenheit_wide
# A tibble: 286 × 5
      ID leben_selbst leben_familie leben_schule leben_freunde
   <dbl>        <dbl>         <dbl>        <dbl>         <dbl>
 1     1          5.5          7            4.33           6.5
 2     2          4.5          4.67         4.67           5  
 3    10          6.5          6.33         3.33           5  
 4    11          6            7            4              6.5
 5    12          6            5.67         5.33           6.5
 6    14          5            5.33         5              6  
 7    15          6            5.67         4              5  
 8    16          6            5.67         6.33           5  
 9    17          6            6            5              6  
10    18          5.5          4.67         4.33           5  
# ℹ 276 more rows

Weitere Möglichkeiten, dasselbe zu tun, sind:

beispieldaten |> 
  select(ID, leben_selbst, leben_fam, leben_schule, leben_freunde)
variablen <- c("leben_selbst", "leben_fam", 
               "leben_schule", "leben_freunde")
beispieldaten |>
  select(ID, one_of(variablen))

Die ausgewählten Zufriedenheitsvariablen können nun alle als Stufen eines messwiederholten Faktors aufgefasst werden. Der erstellte Datensatz ist aber noch im wide Format. Nun konvertieren wir diesen ins long Format.

zufriedenheit_long <- zufriedenheit_wide |> 
  pivot_longer(!ID, names_to = "lebensbereich",
               values_to = "zufriedenheit")

Nun müssen wir uns an dieser Stelle überlegen, was mit fehlenden Werten geschehen soll. Wollen wir alle Versuchspersonen ausschliessen, welche auf mindestens einem Lebensbereich einen fehlenden Wert haben, oder wollen wir zulassen, dass Vpn NAs haben, und dies später berücksichtigen?

Zuerst suchen wir Zeilen (Vpn) mit fehlenden Werten.

# Zeilen mit complete.cases() auswählen
# Die Verneinung ist !complete.cases()

# mit head(20) werden nur die ersten 20 in der Konsole angezeigt:
!complete.cases(zufriedenheit_wide) |> 
  head(20)
 [1] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
[13] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE

Dies hat einen logical vector als Output zur Folge. Damit können wir die Zeilen indizieren.

zufriedenheit_wide[!complete.cases(zufriedenheit_wide), ]
# A tibble: 2 × 5
     ID leben_selbst leben_familie leben_schule leben_freunde
  <dbl>        <dbl>         <dbl>        <dbl>         <dbl>
1   175           NA            NA        NA               NA
2   206            5            NA         5.33             5

Wir sehen, dass zwei Vpn fehlende Werte haben und entscheiden uns, diese auszuschliessen.

zufriedenheit_long <- zufriedenheit_wide |> 
  drop_na() |> 
  pivot_longer(!ID, names_to = "lebensbereich",
               values_to = "zufriedenheit")

Die Variable lebensbereich sollte nun noch zu einem Faktor konvertiert werden.

zufriedenheit_long <- zufriedenheit_long |> 
  mutate(lebensbereich = factor(lebensbereich))
zufriedenheit_long
# A tibble: 1,136 × 3
      ID lebensbereich zufriedenheit
   <dbl> <fct>                 <dbl>
 1     1 leben_selbst           5.5 
 2     1 leben_familie          7   
 3     1 leben_schule           4.33
 4     1 leben_freunde          6.5 
 5     2 leben_selbst           4.5 
 6     2 leben_familie          4.67
 7     2 leben_schule           4.67
 8     2 leben_freunde          5   
 9    10 leben_selbst           6.5 
10    10 leben_familie          6.33
# ℹ 1,126 more rows
Vertiefung

Wir können die Faktorstufen noch “schöner” machen, indem wir das Präfix leben_ entfernen.

library(stringr)
zufriedenheit_long <- zufriedenheit_long |> 
  mutate(lebensbereich = factor(str_replace(lebensbereich, ".*_", "")))

levels(zufriedenheit_long$lebensbereich)
[1] "familie" "freunde" "schule"  "selbst" 

Wir sortieren nun noch nach ID, und zwar in aufsteigender Reihenfolge.

zufriedenheit_long <- zufriedenheit_long |> 
  arrange(ID)

zufriedenheit_long
# A tibble: 1,136 × 3
      ID lebensbereich zufriedenheit
   <dbl> <fct>                 <dbl>
 1     1 selbst                 5.5 
 2     1 familie                7   
 3     1 schule                 4.33
 4     1 freunde                6.5 
 5     2 selbst                 4.5 
 6     2 familie                4.67
 7     2 schule                 4.67
 8     2 freunde                5   
 9    10 selbst                 6.5 
10    10 familie                6.33
# ℹ 1,126 more rows

Auch hier hätten wir alle Teilschritte zusammen ausführen können.

library(stringr) # für die Funktion str_replace()

zufriedenheit_long <- zufriedenheit_wide |> 
  drop_na() |> 
  pivot_longer(!ID, names_to = "lebensbereich",
               values_to = "zufriedenheit") |> 
  mutate(lebensbereich = factor(str_replace(lebensbereich, ".*_", ""))) |>
  arrange(ID)

Stress

Verschiedene Befunde in der Literatur zeigen, dass Mädchen im Jugendalter stärker unter depressiven Verstimmungen leiden als Jungen. Diese Hypothese können wir mit einem t-Test überprüfen, aber bevor wir das tun, können wir uns die mittlere psychische Symptomatik, nach Geschlecht aufgeteilt, deskriptiv anschauen. Berechnen Sie Mittelwert und Standardabweichung der Variablen stress_psychisch für beide Geschlechter getrennt, und runden Sie diese auf drei Nachkommastellen.

Hinweis: Damit wir überhaupt drei Nachkommastellen angezeigt bekommen können, müssen wir die Befehlsfolge immer mit as.data.frame() abschliessen. Der Grund dafür ist, dass der tibble, den die tidyverse-Funktionen erstellen, eine default Printmethode hat, die in manchen Fällen (z.B. in dieser Online-Version des Skripts) automatisch auf zwei Dezimalstellen rundet.

stress <- beispieldaten |> 
  select(ID, geschlecht, stress_psychisch) |> 
  as.data.frame()

head(stress)
  ID geschlecht stress_psychisch
1  1   weiblich         1.666667
2  2   männlich         3.500000
3 10   weiblich         3.666667
4 11   weiblich         1.500000
5 12   weiblich         2.500000
6 14   männlich         1.000000

Zuerst noch ohne Runden:

stress |> 
  group_by(geschlecht) |> 
  summarize(mean = mean(stress_psychisch),
            sd = sd(stress_psychisch)) |> 
  as.data.frame()
  geschlecht     mean       sd
1   männlich       NA       NA
2   weiblich 3.099502 1.120575

Vergessen Sie nicht, dass es möglicherweise fehlende Werte hat. Es gibt zwei Möglichkeiten, damit umzugehen:

  1. Wir entfernen alle Vpn mit fehlenden Werten.
  2. Wir belassen die Zeilen mit fehlenden Werten im Datensatz und benützen das na.rm Argument der Funktionen mean() und sd().

Jetzt runden wir die mit summarize() erstellten Statistiken, indem mit die gebildeten Variablen mean und sd mit mutate() verändern:

stress |> 
  drop_na() |> 
  group_by(geschlecht) |> 
  summarize(mean = mean(stress_psychisch),
            sd = sd(stress_psychisch)) |> 
  mutate(mean = round(mean, 3),
         sd = round(sd, 3)) |> 
  as.data.frame()
  geschlecht mean    sd
1   männlich 2.89 1.254
2   weiblich 3.10 1.121

Wir können die round() Funktion aber auch innerhalb von summarize() benutzen, und das sogar in Kombination mit der Pipe! Ausserdem jetzt mit dem Argument na.rm = TRUE für mean()und sd() zur Entfernung von fehlenden Werten.

stress |> 
  group_by(geschlecht) |> 
  summarize(mean = mean(stress_psychisch, na.rm = TRUE) |> round(3),
            sd = sd(stress_psychisch, na.rm = TRUE) |> round(3)) |> 
  as.data.frame()
  geschlecht mean    sd
1   männlich 2.89 1.254
2   weiblich 3.10 1.121
Fun fact

Dass die beiden Geschlechts-Mittelwerte jetzt nur zwei Kommastellen aufweisen, ist ein Zufall: wenn nämlich die Rundung zu einer Nullstelle als letzter Stelle führt, dann schneidet R diese 0 ab, weil sie in gewisser Weise überflüssig ist. Der Mittelwert der Mädchen hat sogar noch eine Nullstelle an der zweiten Nachkommastelle, diese wird aber nicht abgeschnitten, da der Mittelwert der Jungen in der Zeile darüber zwei Nachkommastellen benötigt… Probieren Sie es einmal mit vier Nachkommastellen, dann sehen Sie es selbst!

Und jetzt zur Veranschaulichung noch ein Boxplot. Die Erstellung eines solchen Plots mit dem Package ggplot2 ist Thema des nächsten Kapitels:

library(ggplot2)
stress |> 
  drop_na() |> 
  ggplot(aes(x = geschlecht, y = stress_psychisch, fill = geschlecht)) +
  geom_boxplot()