Kapitel 6 Datenvorbereitung

Spalten oder Zeilen auswählen und umbenennen, Spalten hinzufügen und entfernen, Funktion erstellen und Spalteninhalte unter Bedingungen verändern, Spalten trennen oder das Zusammenfügen von Datensätzen sind nur ein kleiner Auszug aus den benötigten Werkzeugen für die Aufbereitung und Bereinigung der eigenen Daten. Bis der Datensatz fehlerfrei und bereit zur Auswertung ist, vergehen häufig mehrere Tage oder sogar Wochen des aktiven Bereinigens.

6.1 Einführung

Die Datenvorbereitung oder auch Datenaufbereitung ist in der Regel der mit Abstand aufwendigste und zeitintensivste Teil der Datenanalyse. Selten liegt nach der Datenerhebung bereits ein perfekt formatierter vor, den man statistisch auswerten kann. Mit den Funktionen des tidyverse ist die Datenvorbereitung unkompliziert und schneller zu bewerkstelligen.

Das tidyverse ist ein Sammelsurium an Packages, die die gleiche Philosophie teilen. Dabei steht die Abkürzung für tidy universe, also einem aufgeräumten Universum. Beim Laden des tidyverse werden neun Packages gemeinsam bereitgestellt, womit man sich im Prinzip nur das einzelne Aufrufen der neun Packages spart. Ausgeführt in R sieht das wie angeführt aus. Unter Conflicts werden Funktionen genannt, die denselben Namen wie andere R Funktionen haben und die von hier an überschrieben werden.

library(tidyverse)
-- Attaching core tidyverse packages ------------ tidyverse 2.0.0 --
✔ dplyr     1.1.2     ✔ readr     2.1.4
✔ forcats   1.0.0     ✔ stringr   1.5.0
✔ ggplot2   3.5.0     ✔ tibble    3.1.8
✔ lubridate 1.9.2     ✔ tidyr     1.3.0
✔ purrr     1.0.1 
-- Conflicts --------------------- tidyverse_conflicts() --
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag()    masks stats::lag()
ℹ Use the conflicted package to force all conflicts to become errors
  • ggplot2: Erstellen von Visualisierungen (siehe Kapitel 8).
  • tibble: Erweiterung des klassischen data.frames (siehe Kapitel 11.3).
  • tidyr: Wechsel zwischen langem und breitem Datenformat (siehe Kapitel 6.6).
  • readr: Einlesen von Dateien (siehe Kapitel 5).
  • purrr: Wiederholtes Anwenden von Funktionen (siehe Kapitel 12).
  • dplyr: Funktionen zur Datenvorbereitung.
  • stringr: Veränderung von Buchstaben und Wörtern (siehe Kapitel 6.9).
  • forcats: Manipulation von Faktoren (siehe Kapitel 6.10).
  • lubridate: Arbeiten mit Zeitdaten (siehe Kapitel 6.11).

Die in diesem Kapitel eingeführten Funktionen zur Datenaufbereitung sind in sich konsistent. Man muss das Prinzip also nur einmal verstehen, um sämtliche Funktionen anwenden zu können. Dabei sind diese durch eine ausdrucksstarke Namensgebung beinahe schon selbsterklärend. Schauen wir uns zunächst eine typische Aneinanderreihung von Befehlen an:

big5 |> 
  select(Geschlecht, Extraversion) |> 
  filter(Geschlecht == "m") |> 
  mutate(Extraversion_lg = log(Extraversion))
# A tibble: 82 × 3
  Geschlecht Extraversion Extraversion_lg
  <chr>             <dbl>           <dbl>
1 m                   3              1.10
2 m                   3.4            1.22
3 m                   3.3            1.19
4 m                   3.5            1.25
# ℹ 78 more rows

Gelesen würde es wie folgt:

Man nehme den Datensatz big5 UND DANN

  • wähle die Spalten Geschlecht und Extraversion UND DANN
  • wähle die Zeilen, in denen Geschlecht gleich “m” (für männlich) ist UND DANN
  • mutiere oder verändere die (neue) Spalte Extraversion_lg, wie in diesem Fall beschrieben, durch die logarithmierten Werte der Extraversion.

Die anderen Funktionen sind ähnlich intuitiv und nahe an der englischen Sprache benannt. Besonders ist an dieser Stelle die sogenannte Pipe (|>), welche für das Weiterreichen des modifizierten Datensatzes an die nächste Funktion zuständig ist. Der Name des Datensatzes muss nicht in jeder Funktion stehen, da dieser immer das erste Argument der hier behandelten Funktionen darstellt. In dem obigen Beispiel werden zuerst zwei der Spalten ausgewählt. Dann wird das Ergebnis dieses Befehls – also der Datensatz mit den zwei Spalten Geschlecht und Extraversion – im nächsten Schritt der Funktion filter() übergeben.

Das Verbindungssymbol |> wird als Pipe bezeichnet. Es kann mit dem Shortcut Strg + Shift + M (bzw. auf macOS mit Cmd + Shift + M) direkt eingefügt werden. Zuerst muss allerdings innerhalb von RStudio unter Tools/Global options .../Code der Haken bei Use native pipe operator, |> (requires R 4.1+) gesetzt werden. Das Verwenden der in R integrierten Pipe (|>) ist erst ab der R Version 4.1.0 möglich.

Die Verwendung der Pipe hat zwei große Vorteile:

  1. Wir müssen nicht jedes Ergebnis der verschiedenen Funktionen einzeln zwischenspeichern.
  2. Die Verschachtelung mehrerer Funktionen ineinander wird verringert.

Trotzdem müssen wir das Ergebnis dieser aneinandergeketteten Funktion irgendwann mit dem Zuweisungspfeil speichern.

daten <- big5 |> 
  select(Geschlecht, Extraversion) |> 
  filter(Geschlecht == "m") |> 
  mutate(Extraversion_lg = log(Extraversion))

Ohne Pipe müssten wir beim Anwenden jeder Funktion ein Zwischenergebnis abspeichern, was mühsam und fehleranfällig ist. Im Kontext der Datenvorbereitung müssen wir häufig so viele verschiedene Probleme bereinigen, dass zehn bis zwanzig verschiedene Befehle aneinandergekettet werden.

daten1 <- select(daten, Geschlecht, Extraversion) 
daten2 <- filter(daten1, Geschlecht == "m")  
daten3 <- mutate(daten2, Extraversion_lg = log(Extraversion))

Mit Verschachtelung ist eine Kombination mehrerer Funktion innerhalb einer Zeile gemeint. Die ersten beiden Zeilen des vorherigen Beispiels sind äquivalent zu:

filter(select(daten, Geschlecht, Extraversion), Geschlecht == "m")

Hier haben wir den Nachteil der erheblich schlechteren Lesbarkeit.

Beachte, dass sämtliche Änderungen, die du am Datensatz vollziehst, erst gespeichert werden, wenn du sie mit dem Zuweisungspfeil einer Variable zuweist (siehe Kapitel 4.2). Falls der gewählte Variablenname bereits vergeben ist (z.B. der bisherige Datensatzname), wird dieser überschrieben. Um das rückgängig zu machen, muss der Datensatz dann wieder neu eingelesen werden. Es empfiehlt sich bei einschneidenden Änderungen einen neuen Variablennamen zu wählen.

Ein zentrales Konzept ist die sogenannte Pipe (|>), die verschiedenste Funktionsaufrufe aneinanderbinden kann. Dabei wird der modifizierte Datensatz jeweils an die nächste Funktion weitergeben. In diesem Buch verwenden wir die Pipe (|>), welche seit der Version 4.1.0 direkt in R integriert ist. Älter ist die Pipe innerhalb des tidyverse, die mit %>% geschrieben wird. Die Funktionsweise der beiden Operatoren unterscheidet sich für die meisten NutzerInnen nicht nennenswert und langfristig wird die Base R Pipe (|>) weiter verbreitet sein.

6.2 Spalten auswählen, umbenennen und umsortieren

Für die schnelle Bereinigung schlecht formatierter Spaltennamen empfiehlt sich die Installation und das Laden des janitor Packages.

library(janitor)

Die Funktionen in diesem Kapitel beschäftigen sich mit der Auswahl, Umbenennung und Umordnung von Spalten. Wir haben die Funktion select() bereits im vorherigen Abschnitt kennengelernt. Es können beliebig viele Spalten ausgewählt werden. Dies ist vor allem nützlich, wenn der Datensatz groß ist und man übersichtlich nur die Spalten haben möchte, die zur Auswertung verwendet werden. Zur Auswahl einer Spalte muss nur der Name (ohne Anführungszeichen) der Funktion übergeben werden. Man kann auch direkt in der Funktion die Spalte umbenennen. Dabei muss auf der linken Seite des Gleichheitszeichens der neue Name stehen.

big5 |> 
  select(Extraversion, Neuro = Neurotizismus)
# A tibble: 200 × 2
  Extraversion Neuro
         <dbl> <dbl>
1          3     1.9
2          3.1   3.4
3          3.4   2.4
4          3.3   4.2
# ℹ 196 more rows

Zur Auswahl der Spalten von Extraversion bis O2 verwendet man einen Doppelpunkt.

big5 |> 
  select(Extraversion:O2)

Soll nur die Spalte Geschlecht entfernt und der Rest ausgegeben werden, erreicht man dies mit einem Minus vor dem Spaltennamen. Bei mehreren zu entfernenden Spalten müsste man diese zwischen Klammern einbetten (z.B. -(Extraversion:O2)).

big5 |> 
  select(-Geschlecht)

Darüber hinaus können wir sogenannte Helferfunktionen verwenden. Diese können nur in Kombination mit einer anderen Funktion verwendet werden. Ein nützliches Beispiel ist where(), womit beispielsweise alle numerischen Spalten ausgewählt werden können.

big5 |> 
  select(where(is.numeric))

Eine weitere nützliche Helferfunktion ist starts_with(). So könnte man in diesem Fall beispielsweise alle Fragen zum Persönlichkeitsfaktor Offenheit auswählen, da diese alle mit dem Buchstaben O beginnen.

big5 |> 
  select(starts_with("O"))

Mit der Helferfunktion ends_with() können auf dieselbe Art und Weise Spalten ausgewählt werden, die mit einem bestimmten Character enden (z.B. eine Spalte endend mit dem Buchstaben A), während die Helferfunktion contains() prüft, ob ein Character im Spaltennamen enthalten ist . Darüber hinaus gibt es noch all_of(), um alle zuvor ausgewählten Variablen (Spaltennamen als Character) auszuwählen.

Möchte man hingegen die Spalten nur umbenennen und dabei den gesamten Datensatz behalten, verwendet man anstelle von select() die Funktion rename(). Die Schreibweise der Argumente beim Umbenennen bleibt dabei im Vergleich zu select() gleich.

big5 |> 
  rename(Sex = Geschlecht)
# A tibble: 200 × 7
  Alter Sex   Extraversion Neurotizismus    O1    O2    O3
  <dbl> <chr>        <dbl>         <dbl> <dbl> <dbl> <dbl>
1    36 m              3             1.9     5     1     5
2    30 f              3.1           3.4     5     3     5
3    23 m              3.4           2.4     3     3     5
4    54 m              3.3           4.2     2     5     3
# ℹ 196 more rows

Während beide Funktionen Spalten umbenennen können, gibt select() nur die ausgewählten Spalten und rename() hingegen alle Spalten zurück.

Außerdem können Funktionen zur Umbenennung von Spalten verwendet werden. Dafür müssen wir rename_with() einfach nur die gewünschte Funktion (ohne Klammern) übergeben. In diesem Beispiel werden alle Buchstaben der Spaltennamen in Großbuchstaben umgewandelt. Alternativ könnten auch neue anonyme Funktion direkt innerhalb von rename_with() erstellt werden (siehe Kapitel 6.4.4).

big5 |> 
  rename_with(toupper)
# A tibble: 200 × 7
  ALTER GESCHLECHT EXTRAVERSION NEUROTIZISMUS    O1    O2    O3
  <dbl> <chr>             <dbl>         <dbl> <dbl> <dbl> <dbl>
1    36 m                   3             1.9     5     1     5
2    30 f                   3.1           3.4     5     3     5
3    23 m                   3.4           2.4     3     3     5
4    54 m                   3.3           4.2     2     5     3
# ℹ 196 more rows

Mit dem zusätzlichen Argument .cols könnte man die Umbenennung nur auf einen Teil der Spalten anwenden. Hierbei können wir ebenfalls die zuvor kennengelernten Helferfunktionen verwenden. Möchten wir bspw. nur jene Spaltennamen großschreiben, die mit einem A beginnen, könnten wir dies mit rename_with(.fn = toupper, .cols = starts_with("A")) erreichen.

Eine sehr praktische Möglichkeit zur Bereinigung aller im Datensatz enhaltenen Spaltennamen bietet die Funktion clean_names() aus dem janitor Package. In der wissenschaftlichen Praxis enthalten Spaltennamen häufig Leerzeichen, Klammern und Gleichheitszeichen, die sich nicht gut mit Programmiersprachen vertragen. Die Funktion clean_names() ersetzt dabei alle unerwünschten Zeichen mit einem Unterstrich (_). Wäre eine Spalte für das Geschlecht beispielsweise mit Geschlecht (1=m, 2=f) benannt, würden wir nach der Bereinigung stattdessen den Spaltennamen geschlecht_1_m_2_f erhalten. Auch wenn das möglicherweise nach wie vor nicht der Zielname für diese Spalte ist, können wir jedoch die Spalten jetzt ohne begleitende Apostrophs aufrufen.

big5 |>
  clean_names()

Bei sehr großen Datensätzen mit vielen Spalten ist die Funktion relocate() äußerst nützlich. Eine neue Spalte wird zum Beispiel immer ans Ende des Datensatzes angefügt. Um diese trotzdem direkt am Anfang betrachten zu können, übergeben wir den Spaltennamen unserer Funktion.

big5 |> 
  relocate(O1)
# A tibble: 200 × 7
     O1 Alter Geschlecht Extraversion Neurotizismus    O2    O3
  <dbl> <dbl> <chr>             <dbl>         <dbl> <dbl> <dbl>
1     5    36 m                   3             1.9     1     5
2     5    30 f                   3.1           3.4     3     5
3     3    23 m                   3.4           2.4     3     5
4     2    54 m                   3.3           4.2     5     3
# ℹ 196 more rows

Wenn die Spalte nicht zu Beginn, sondern nach einer bestimmten anderen Spalte eingeordnet werden soll, können wir dies mit dem .after Argument festlegen. Hier würde die Spalte O1 hinter der Spalte Alter ausgegeben werden.

big5 |> 
  relocate(O1, .after = Alter)

Auch hier können wir wieder Helferfunktionen wie where() verwenden. Man könnte beispielsweise alle numerischen Spalten hinter alle character Spalten einfügen.

big5 |> 
  relocate(where(is.numeric), .after = where(is.character))

Eine weitere nützliche Funktion bei sehr breiten Datensätzen mit vielen Spalten ist colnames(). So können wir auf einem Blick alle Spaltennamen ausgegeben bekommen.

colnames(big5)
[1] "Alter"         "Geschlecht"    "Extraversion"  "Neurotizismus" "O1"           
[6] "O2"            "O3"           

Von Zeilennamen (rownames()) sollte hingegen grundsätzlich Abstand genommen werden. Falls der Datensatz Zeilennamen enthält, die tatsächlich von Bedeutung sind, sollte man diese mit der Funktion rownames_to_column("Spaltenname") aus dem tibble Package in eine eigene Spalte befördern.

Übung 6.2. (Noch nicht enthalten) Starte die Übung mit uebung_starten(6.2).

6.3 Zeilen auswählen und umsortieren

Anders als im vorherigen Kapitel beschäftigen sich diese beiden Funktionen mit der Auswahl und Umordnung von Zeilen. Der Funktion filter() muss dabei ein logischer Ausdruck übergeben werden. Das Ergebnis der Abfrage muss also immer TRUE oder FALSE zurückgeben (siehe Kapitel 4.3). Zur Auswahl aller männlichen Probanden würde man Geschlecht == "m" schreiben. Beachte an dieser Stelle das doppelte Gleichheitszeichen.

big5 |> 
  filter(Geschlecht == "m")
# A tibble: 82 × 7
  Alter Geschlecht Extraversion Neurotizismus    O1    O2    O3
  <dbl> <chr>             <dbl>         <dbl> <dbl> <dbl> <dbl>
1    36 m                   3             1.9     5     1     5
2    23 m                   3.4           2.4     3     3     5
3    54 m                   3.3           4.2     2     5     3
4    32 m                   3.5           3.1     3     1     5
# ℹ 78 more rows

Um Zeilen neu anzuordnen, benutzt man arrange(). Wenn die Zeilen nach aufsteigendem Alter sortiert werden sollen, muss man lediglich den Spaltennamen übergeben.

big5 |> 
  arrange(Alter)

Für eine absteigende Anordnung muss der Spaltennamen innerhalb der Helferfunktion desc() geschrieben werden.

big5 |> 
  arrange(desc(Alter))
# A tibble: 200 × 7
  Alter Geschlecht Extraversion Neurotizismus    O1    O2    O3
  <dbl> <chr>             <dbl>         <dbl> <dbl> <dbl> <dbl>
1  1995 f                   2.5           3.7     4     1     4
2  1964 f                   3.2           2.3     5     1     5
3    60 f                   3             2.7     5     1     5
4    59 m                   2.7           2.3     5     1     5
# ℹ 196 more rows

So sehen wir hier beispielsweise zwei falsch eingetragene Alterswerte. Hier haben zwei Probandinnen nicht das Alter sondern das jeweilige Geburtsjahr in den Datensatz eingetragen. Das müsste vor einer Auswertung noch entsprechend korrigiert werden.

Übung 6.3. (Noch nicht enthalten) Starte die Übung mit uebung_starten(6.3).

6.4 Spalten hinzufügen und Spalteninhalte verändern

6.4.1 Einzelne Spalten

Die Funktion mutate() ist eine sehr vielseitig einsetzbare Funktion zum Verändern bestehender oder zum Hinzufügen neuer Spalten. Dabei wird der neue oder bereits bestehende Spaltenname auf die linke Seite des Gleichheitszeichens geschrieben. Auf der rechten Seite kann so ziemlich alles stehen, solange die Funktion eine Spalte zurückgibt, die genauso lang ist wie der Datensatz. Einen Mittelwert über eine ganze Spalte könnte mit mutate() also nicht berechnet werden, da dabei nur ein einziger Wert zurückgeben werden würde. Mit der Funktion log() logarithmieren wir hingegen die Extraversionsausprägungen für jede Person, sodass wir 200 Werte erhalten.

big5 |> 
  mutate(Extraversion_lg = log(Extraversion))

Standardmäßig wird die neue erstellte Spalte hinten als letzte Spalte zum Datensatz hinzugefügt. Möchte man die neue Spalte an einer anderen Position haben, können wir dies mit dem zusätzlichen Argument .after erreichen. So könnte man die neue Spalte namens Extraversion_lg bspw. direkt hinter der Spalte Extraversion einfügen. Dem .after Argument können ebenfalls die in Kapitel 6.2 eingeführten Helferfunktion wie starts_with() und ends_with() übergeben werden.

big5 |> 
  mutate(Extraversion_lg = log(Extraversion), .after = Extraversion)
# A tibble: 200 × 8
  Alter Geschlecht Extraversion Extraversion_lg Neurotizismus    O1    O2    O3
  <dbl> <chr>             <dbl>           <dbl>         <dbl> <dbl> <dbl> <dbl>
1    36 m                   3              1.10           1.9     5     1     5
2    30 f                   3.1            1.13           3.4     5     3     5
3    23 m                   3.4            1.22           2.4     3     3     5
4    54 m                   3.3            1.19           4.2     2     5     3
# ℹ 196 more rows

Möchten wir die bestehende Spalte Extraversion mit den logarithmierten Werten überschreiben, wählen wir auf der linken Seite des Gleichheitszeichens ebenfalls den Spaltennamen Extraversion. Es wird folglich keine neue Spalte hinzugefügt, sondern nur eine bestehende verändert.

big5 |> 
  mutate(Extraversion = log(Extraversion))
# A tibble: 200 × 7
  Alter Geschlecht Extraversion Neurotizismus    O1    O2    O3
  <dbl> <chr>             <dbl>         <dbl> <dbl> <dbl> <dbl>
1    36 m                  1.10           1.9     5     1     5
2    30 f                  1.13           3.4     5     3     5
3    23 m                  1.22           2.4     3     3     5
4    54 m                  1.19           4.2     2     5     3
# ℹ 196 more rows

Innerhalb eines mutate() Aufrufes können wir auch mehrere Spalten einzeln neu erstellen oder verändern. Die verschiedenen Spalten müssen dabei lediglich mit einem Komma voneinander getrennt werden. Hier logarithmieren wir exemplarisch die mittlere Ausprägung von Extraversion und Neurotizismus. Der Abstand der öffnenden Klammer oben und der schließenden Klammer unten ist aus funktioneller Sicht nicht relevant. Es ist allerdings im Sinne der Lesbarkeit bei Benutzung mehrerer Argumenten unter Umständen sinnvoller, die Argumente auf mehrere Zeilen zu verteilen. Um unsere Berechnung überprüfen zu können, bringen wir noch unsere neuen mit lg endenden Spalten an den Anfang des Datensatzes (siehe Kapitel 6.2). Dies ist vor allem beim reinen Überprüfen nützlich, da der relocate() Aufruf im Anschluss übersichtlich wieder gelöscht werden kann. Möchte man die Anordnung dauerhaft verändern, ist das zuvor erwähnte Argument .after zu bevorzugen.

big5 |> 
  mutate(
    Extraversion_lg = log(Extraversion),
    Neurotizismus_lg = log(Neurotizismus)
  ) |> 
  relocate(ends_with("lg"))
# A tibble: 200 × 9
  Extraversion_lg Neurotizismus_lg Alter Geschlecht Extraversion Neurotizismus    O1    O2
            <dbl>            <dbl> <dbl> <chr>             <dbl>         <dbl> <dbl> <dbl>
1            1.10            0.642    36 m                   3             1.9     5     1
2            1.13            1.22     30 f                   3.1           3.4     5     3
3            1.22            0.875    23 m                   3.4           2.4     3     3
4            1.19            1.44     54 m                   3.3           4.2     2     5
# ℹ 196 more rows
# ℹ 1 more variable: O3 <dbl>

Übung 6.4.1. (Noch nicht enthalten) Starte die Übung mit uebung_starten(6.4.1).

6.4.2 Mehrere Spalten

Wenn man eine Funktion auf mehrere Spalten anwenden möchte, kann man die Berechnung, wie im vorherigen Kapitel gezeigt, entweder für jede Spalte separat vornehmen oder mithilfe von across() (engl. für herüber) den Prozess abkürzen. Wir wollen schließlich eine Funktion über mehrere Spalten anwenden. Innerhalb von across() erfolgt die Auswahl der Spalten dabei genau wie in select() (siehe Kapitel 6.2). So kann bspw. der Doppelpunkt zur Auswahl eines mehrere Spalten umfassenden Bereiches oder c() zur Auswahl einzelner Spalten verwendet werden. Wichtig ist das Setzen der Klammern an der richtigen Stelle, da die Funktion log() innerhalb von across() aufgerufen wird.

big5 |> 
  mutate(across(
    .cols = Extraversion:Neurotizismus,
    .fns = log,
    .names = "{.col}_lg")
  )
# A tibble: 200 × 9
  Alter Geschlecht Extraversion Neurotizismus    O1    O2    O3 Extraversion_lg
  <dbl> <chr>             <dbl>         <dbl> <dbl> <dbl> <dbl>           <dbl>
1    36 m                   3             1.9     5     1     5            1.10
2    30 f                   3.1           3.4     5     3     5            1.13
3    23 m                   3.4           2.4     3     3     5            1.22
4    54 m                   3.3           4.2     2     5     3            1.19
# ℹ 196 more rows
# ℹ 1 more variable: Neurotizismus_lg <dbl>

Das .names Argument verhindert dabei das Überschreiben der Spalten. Innerhalb der geschweiften Klammern wird hier der Spaltenname für jede in .cols gewählten Spalte nacheinander übergeben und die Endung _lg angehängt. Würden wir den Namen der neu zu erstellenden Spalten nicht explizit mit .names definieren, würden die ursprünglichen Spalten Extraversion und Neurotizismus mit den logarithmierten Werten überschrieben werden. Die eigentlichen Werte wären also nach Abspeichern dieses Zwischenergebnisses nicht mehr im Datensatz enthalten, sondern nur noch die Spalten mit den logarithmierten Werten.

Die auf mehrere Spalten anzuwendende Funktion muss innerhalb von across() übergeben werden. Falls ein Fehler auftritt, ist dieser in der Regel auf falsche Positionierung der Klammern zurückzuführen.

Ein weiterer Unterschied besteht in der manuellen Auswahl einzelner Spalten. Hier müssen wir die einzelnen Spalten im Gegensatz zur Anwendung bei select() in diesem Fall innerhalb von c() übergeben. An dieser Stelle würden die eigentlichen Werte der Spalten Extraversion und Neurotizismus mit den logarithmierten Werten überschrieben werden, da das zuvor beschriebene .names Argument nicht definiert ist.

big5 |> 
  mutate(across(
    .cols = c(Extraversion, Neurotizismus),
    .fns = log)
  )

Auch andere bereits in Kapitel 6.2 besprochene Helferfunktionen wie starts_with(), ends_with(), contains() oder where() können zur Variablensauswahl für das .cols Argument verwendet werden.

big5 |> 
  mutate(across(
    .cols = where(is.numeric),
    .fns = log)
  )

Übung 6.4.2. (Noch nicht enthalten) Starte die Übung mit uebung_starten(6.4.2).

6.4.3 Änderungen unter Bedingungen

Häufig möchte man Funktionen nicht auf alle Zeilen innerhalb der ausgewählten Spalten gleichermaßen anwenden. Wenn wir bspw. eine bestehende Spalte auf bestimmte Art und Weise nur dann verändern wollen, wenn eine bestimmte Bedingung zutrifft, erreichen wir dies mit der Funktion if_else(). Wir haben in Kapitel 6.3 gesehen, dass zwei Probandinnen ihr Geburtsjahr anstelle des Alters in Jahren angegeben haben. Wenn unsere Bedingung (condition) zutrifft, also das Alter in Jahren größer als 120 ist, soll das Jahr der Erhebung (2020) Minus das Alter gerechnet werden. Ansonsten (else bzw. false) wird nur das unveränderte Alter zurückgegeben. Anschließend überprüfen wir noch unsere Berechnung, indem wir das Alter wieder absteigend anordnen.

big5 |> 
  mutate(Alter = if_else(
    condition = Alter > 120, 
    true = 2020 - Alter, 
    false = Alter)
  ) |> 
  arrange(desc(Alter))
# A tibble: 200 × 7
  Alter Geschlecht Extraversion Neurotizismus    O1    O2    O3
  <dbl> <chr>             <dbl>         <dbl> <dbl> <dbl> <dbl>
1    60 f                   3             2.7     5     1     5
2    59 m                   2.7           2.3     5     1     5
3    59 m                   2.8           1.6     4     2     5
4    58 m                   2.3           2.9     5     1     4
# ℹ 196 more rows

Auf dieselbe Art und Weise könnte man Variablen vor dem Abspeichern umkodieren. Schließlich kann sich spätestens nach 2 Jahren niemand mehr erinnern, ob die 1 bei der Kodierung für Geschlecht nun für männlich oder weiblich stand. Deswegen ist es besser, diese direkt in m oder f abzuspeichern. Wenn das Geschlecht gleich 1 ist (also männlich), schreibe in die Spalte ein m und ansonsten ein f.

big5 |> 
  mutate(Geschlecht = if_else(
    condition = Geschlecht == 1, 
    true = "m", 
    false = "f")
  ) 

Bei mehr als zwei Bedingungen können wir anstelle von if_else() die Funktion case_when() verwenden. Auf der linken Seite der Tilde (~) ist dabei immer die Bedingung angegeben, während auf der rechten Seite steht, was bei zutreffender Bedingung in die Spalte geschrieben werden soll. Allen Werten, auf die keine der explizit genannten Bedingungen zutrifft, wird NA (Akronym für Not Available, engl. für nicht vorhanden) zugewiesen. Dies kann vermieden werden, indem man am Ende das Argument .default als Bedingung hinzufügt. Achtung, hier wird ein Gleichheitszeichen verwendet, da es sich um ein gewöhnliches Argument handelt.

Somit werden in diesem Beispiel alle ProbandInnen, die bisher keiner Bedingung zugeordnet sind, in die ältesten Altersgruppe kategorisiert. Am Ende ordnen wir unsere neu erstellte Spalte zum Kontrollieren unserer Berechnung wie gewohnt nach vorne.

big5 |> 
  mutate(Gruppe = case_when(
    Alter <= 25 ~ "Jungspund",
    Alter > 25 & Alter <= 45  ~ "Mittel",
    between(Alter, 46, 65) ~ "Erfahren",
    .default = "Weise")
  ) |>
  relocate(Gruppe)
# A tibble: 200 × 8
  Gruppe    Alter Geschlecht Extraversion Neurotizismus    O1    O2    O3
  <chr>     <dbl> <chr>             <dbl>         <dbl> <dbl> <dbl> <dbl>
1 Mittel       36 m                   3             1.9     5     1     5
2 Mittel       30 f                   3.1           3.4     5     3     5
3 Jungspund    23 m                   3.4           2.4     3     3     5
4 Erfahren     54 m                   3.3           4.2     2     5     3
# ℹ 196 more rows

Die Helferfunktion between() ist eine übersichtliche Alternative zum kombinierten logischen Begriff eine Zeile darüber. Wichtig ist an dieser Stelle, dass der Datentyp auf der linken Seite der Tilde immer logisch sein muss. Der Ausdruck der linken Seite muss also entweder TRUE oder FALSE zurückgeben. Auf der rechten Seite der Tilde muss über alle Bedingungen hinweg immer der gleiche Datentyp sein. Wenn wir also wie hier den Datentyp Character haben, muss bei allen diesen Zuweisungen auf der rechten Seite der Datentyp übereinstimmen.

Ein weiterer praktischer Anwendungsfall ist die Umkodierung (auch Invertierung) von Antwortoptionen bei der Befragung mit Fragebögen. Angenommen, wir messen auf einer Skala von 1 (trifft gar nicht zu) bis 5 (trifft vollkommen zu) die Ausprägung der Offenheit für neue Erfahrungen. Um Verzerrungen zu vermeiden, sind in einem derartigen Fragebogen immer einige Items verneint gestellt. Normalerweise würde beispielsweise gefragt, ob man gerne neue Sportarten ausprobiert. Würden wir allerdings fragen, ob man ungerne neue Sportarten ausprobiert, trifft unsere Skala natürlich nicht mehr zu. Hier wäre 5 trifft gar nicht zu und 1 trifft vollkommen zu. Stellen wir uns vor, dies würde für die erste Frage zur Offenheit O1 zutreffen. Zum Vergleich erstellen wir eine neue Spalte namens O1_neu, welche die umkodierten Werte enthält. Um die Ausgabe zu überprüfen lassen wir die originale und neue Spalte mithilfe von relocate() zu Beginn ausgeben (siehe Kapitel 6.2).

big5 |> 
  mutate(O1_neu = case_when(
    O1 == 1 ~ 5,
    O1 == 2 ~ 4,
    O1 == 3 ~ 3,
    O1 == 4 ~ 2,
    O1 == 5 ~ 1)
  ) |> 
  relocate(O1, O1_neu)
# A tibble: 200 × 8
     O1 O1_neu Alter Geschlecht Extraversion Neurotizismus    O2    O3
  <dbl>  <dbl> <dbl> <chr>             <dbl>         <dbl> <dbl> <dbl>
1     5      1    36 m                   3             1.9     1     5
2     5      1    30 f                   3.1           3.4     3     5
3     3      3    23 m                   3.4           2.4     3     5
4     2      4    54 m                   3.3           4.2     5     3
# ℹ 196 more rows

Immer wenn die Spalte O1 den Werte 1 hat, wird eine 5 daraus gemacht und immer wenn eine 2 angekreuzt wurde, diese mit einer 4 ersetzt. Die 3 können wir so belassen und die 4 und 5 wandeln wir auf die gleiche Art und Weise um. Beachte auch hier, dass wir auf der linken Seite immer eine logische Abfrage und rechts denselben Datentyp (hier Double) vorliegen haben. Auch hier können wir die nützlichen Helferfunktionen contains(), starts_with(), ends_with() und where() auf der linken Seite der Tilde verwenden.

Eine verkürzte Schreibweise für Szenarien des Umkodierens, wie zuvor gezeigt, bietet die Funktion case_match(). So könnte der vorherige Befehl wie folgt abgekürzt werden.

big5 |> 
  mutate(O1_neu = case_match(
    O1,
    1 ~ 5,
    2 ~ 4,
    3 ~ 3,
    4 ~ 2,
    5 ~ 1)
  )

Auch Spalten vom Typ Character, wie z.B. die Bezeichnung von Gruppen, können wir so verändern. Soll das Geschlecht bspw. nicht als "f" und "m" sondern als weiblich und maennlich gespeichert werden, müssen wir wie auch bei case_when() die alte Bedingung auf die linke Seite der Tilde und das neue Ergebnis auf die rechte Seite schreiben.

big5 |> 
  mutate(Geschlecht = case_match(
    Geschlecht, 
    "f" ~ "weiblich", 
    "m" ~ "maennlich")
  )
# A tibble: 200 × 7
  Alter Geschlecht Extraversion Neurotizismus    O1    O2    O3
  <dbl> <chr>             <dbl>         <dbl> <dbl> <dbl> <dbl>
1    36 maennlich           3             1.9     5     1     5
2    30 weiblich            3.1           3.4     5     3     5
3    23 maennlich           3.4           2.4     3     3     5
4    54 maennlich           3.3           4.2     2     5     3
# ℹ 196 more rows

Mit dieser Funktion können auch verschiedene Schreibweisen beim Ausfüllen von Fragebögen korrigiert werden. In diesem Beispiel gab es in der Spalte Kontakt vier Arten das Wort täglich zu schreiben und zwei für wöchentlich.

daten |> 
  mutate(Kontakt = case_match(
    Kontakt,
    c("taeglich", "Taeglich", "täglich", "Täglich") ~ "Taeglich",
    c("wöchentlich", "Wöchentlich") ~ "Woechentlich")
  )

Nach gleichem Schema könnte man auch eine neue Spalte für Medikament A erstellen, die eine 1 bei Medikamenteneinnahme enthält und sonst eine 0. In der Spalte Medikamente wären bspw. bei manchen PatientInnen der Wirkstoff und bei manchen der Markenname des Medikaments enthalten. Auch hier wird mithilfe des .default Arguments der Wert festgelegt, welcher bei nicht-zutreffen der Bedingung in die Spalte geschrieben wird. Das Beispiel kann um beliebig viele Bedingungen erweitert werden.

daten |> 
  mutate(MedikamentA = case_match(
    Medikamente, 
    c("A_Wirkstoff", "A_Markenname") ~ 1, 
    .default = 0)
  )

Übung 6.4.3. (Noch nicht enthalten) Starte die Übung mit uebung_starten(6.4.3).

6.4.4 Eigene Funktionen erstellen

Obwohl in R selbst oder in zusätzlichen Packages bereits eine Vielzahl von Funktionen enthalten sind, braucht man doch immer wieder eigene Funktionen für spezifische Anwendungsfälle. Dies versuchen wir anhand einer neuen Funktion zu illustrieren, die den Logarithmus einer der Funktion übergebenen Zahl mit zwei summiert. Diese Funktion soll den Namen new_log() haben. Eine Funktion wird mit function() erstellt. Innerhalb der runden Klammern können wir mit einem Komma getrennt beliebig viele Argumente festlegen. An dieser Stelle nehmen wir nur x. Der Name dieses Arguments ist grundsätzlich egal, solange er wie hier in dem Beispiel sowohl innerhalb von function() als auch in log() miteinander übereinstimmt. Die eigentliche Berechnung findet innerhalb der geschweiften Klammern statt. Es ist wichtig, dass wir einmal vor Benutzung diese Funktion durch Ausführen (strg + enter) lokal als Variable speichern.

new_log <- function(x) {
  log(x) + 2
}

Eigene Funktionen müssen genau wie Packages nach Neustart von R immer wieder neu geladen werden. Dies erreicht man durch einmaliges Ausführen des obigen Befehls zu Beginn der Auswertung. Es gibt in dieser Hinsicht also keinen Unterschied zum Speichern gewöhnlicher Variablen.

Eine Funktion mit zwei Argumenten, wenn wir beispielsweise zusätzlich noch die zu addierende Zahl innerhalb der Funktion anpassen möchten, könnte wie folgt aussehen.

new_log <- function(x, zahl = 2) {
    log(x) + zahl
}

Durch das zweite Argument könnte man mit new_log(c(2, 4, 1), zahl = 5) den Logarithmus der drei Zahlen jeweils mit 5 (anstelle von 2) addieren. Da innerhalb der Funktion bereits ein Standardwert (hier 2) angegeben ist, ist die explizite Angabe des Arguments zahl optional. So könnte man zum Logarithmieren und addieren mit 2 einfach new_log(c(2, 4, 1)) verwenden.

Einmal erstellt und abgespeichert, können wir die eigene Funktion, wie in Kapitel 6.4.1 bereits gelernt, direkt innerhalb von mutate() anwenden.

big5 |> 
  mutate(across(Extraversion:Neurotizismus, new_log))
# A tibble: 200 × 7
  Alter Geschlecht Extraversion Neurotizismus    O1    O2    O3
  <dbl> <chr>             <dbl>         <dbl> <dbl> <dbl> <dbl>
1    36 m                  3.10          2.64     5     1     5
2    30 f                  3.13          3.22     5     3     5
3    23 m                  3.22          2.88     3     3     5
4    54 m                  3.19          3.44     2     5     3
# ℹ 196 more rows

Eine Kurzschreibweise zum Definieren eigener Funktionen sind sogenannte Lambda Funktionen. Dies sind anonyme Funktionen, die keinen Funktionsnamen erhalten und daher auch nur einmalig bei Verwendung aufgerufen werden können. Dabei sind zwei Sachen hervorzuheben: Auf der einen Seite muss man immer einen führenden Backslash gefolgt von runden Klammern (\()) hinzufügen. Innerhalb der Klammern wird der Name des Arguments (hier innerhalb von log()) übergeben. Im vorherigen Beispiel haben wir die Werte mit x und die zu addierende Zahl mit zahl bezeichnet. Bei anonymen Funktionen können wir entweder ebenfalls x oder einen beliebigen anderen Namen (z.B. wert) verwenden. Die folgenden zwei Funktionsaufrufe sind äquivalent.

big5 |> 
  mutate(across(Extraversion:Neurotizismus, \(x) log(x) + 2))

big5 |> 
  mutate(across(Extraversion:Neurotizismus, \(wert) log(wert) + 2))

Als zweites Beispiel verwenden wir die z-Transformation bzw. Standardisierung einer Variable. Die dafür in R integrierte Funktion namens scale() gibt noch zusätzliche Informationen wieder, weswegen der Funktionsaufruf innerhalb von as.numeric() (Umwandlung in einen rein numerischen Datentypen) stehen sollte. Da wir die Spalten Extraversion und Neurotizismus nicht überschreiben, sondern zwei neue Spalten erstellen wollen, wird zusätzlich das .names Argument verwendet. Die neuen Spalten mit den standardisierten Werten würden so die Endung z erhalten.

big5 |> 
  mutate(across(
    .cols = Extraversion:Neurotizismus, 
    .fns = \(wert) as.numeric(scale(wert)),
    .names = "{.col}_z")
  )

Innerhalb des tidyverse kann eine alternative, nicht mehr empfohlene Schreibweise mit einer führenden Tilde verwendet werden. Dabei muss das Argument der Funktion immer mit .x benannt werden.

big5 |> 
  mutate(across(
    .cols = Extraversion:Neurotizismus, 
    .fns = ~ as.numeric(scale(.x)),
    .names = "{.col}_z")
  )

Anonyme Funktionen sind eine praktische Möglichkeit, schnell eigene wenig komplexe Funktionen zu erstellen, die man nur an einer Stelle benötigt. So spart man sich das eigenständige Erstellen einer neuen Funktion. Für komplexere Anwendungen ist jedoch das Erstellen einer eigenen Funktion mit function() {} der übersichtlichere und damit empfohlene Weg.

Seit der R Version 4.1.0 sind anonyme Funktionen mit der \() Syntax ohne zusätzliche Packages direkt in R integriert. Für welche der beiden Optionen man sich letzten Endes entscheidet, hängt von der persönlichen Präferenz ab.

Mithilfe der Lambda Funktionen könnten wir auf einen Schlag anders als zuvor in Kapitel 6.4.3 nicht nur eine Spalte, sondern so viele wie wir wollen, umkodieren. Wir erinnern uns, eine Spalte könnten wir mithilfe von case_when() umkodieren.

big5 |> 
  mutate(O1_neu = case_when(
    O1 == 1 ~ 5,
    O1 == 2 ~ 4,
    O1 == 3 ~ 3,
    O1 == 4 ~ 2,
    O1 == 5 ~ 1)
  )

Möchten wir in einem Zug die Spalten O1, O2 und O3 in die richtige Reihenfolge bringen, können wir across() mit case_when() kombinieren. Durch die Lambda Funktion ändert sich der Spaltenname zum Backslash mit der Bezeichnung des Arguments (hier Auspraegung) in Klammern. Dieses Argument wird dann für alle ausgewählten Spalten umkodiert.

big5 |>  
  mutate(across(c(O1, O2, O3), \(Auspraegung) case_when(
    Auspraegung == 1 ~ 5,
    Auspraegung == 2 ~ 4,
    Auspraegung == 3 ~ 3,
    Auspraegung == 4 ~ 2,
    Auspraegung == 5 ~ 1)
  ))
# A tibble: 200 × 7
  Alter Geschlecht Extraversion Neurotizismus    O1    O2    O3
  <dbl> <chr>             <dbl>         <dbl> <dbl> <dbl> <dbl>
1    36 m                   3             1.9     1     5     1
2    30 f                   3.1           3.4     1     3     1
3    23 m                   3.4           2.4     3     3     1
4    54 m                   3.3           4.2     4     1     3
# ℹ 196 more rows

Alternativ könnten die Variablen innerhalb von across() in diesem Fall auch mit O1:O3 oder starts_with("O") ausgewählt werden. Beachte an dieser Stelle auch, dass die schließende Klammer von across() hinter dem vollständigen Funktionsaufruf von case_when() platziert wird.

Übung 6.4.4. (Noch nicht enthalten) Starte die Übung mit uebung_starten(6.4.4).

6.4.5 Zeilenweise berechnen

Für Berechnungen pro Beobachtung muss die Funktion rowwise() mit c_across() kombiniert werden. Durch die separate Berechnung für jede Zeile über mehrere Spalten können bspw. Mittelwerte für jede Person in einem bestimmten Merkmal berechnet werden.

Um das Konzept zu illustrieren, soll der Mittelwert pro Person für den Persönlichkeitsfaktor Offenheit berechnet werden. Dieser ergibt sich aus drei einzelnen Fragen zur Offenheit (O1, O2, O3). Zuerst müssen wir die Funktion rowwise() aufrufen, um R das zeilenweise Berechnen zu signalisieren. Innerhalb von mutate() müssen unsere drei Fragen zur Offenheit nun der Funktion c_across() übergeben werden. Beachte das Präfix c_ an dieser Stelle. Nach der Berechnung muss die zeilenweise Betrachtung des Datensatzes noch mit ungroup() aufgehoben werden.

Zur Kontrolle holen wir uns die neu erstellte Spalte namens Offenheit wieder an den Anfang des Datensatzes. Auf diesen Aufruf von relocate() kann natürlich beim Abspeichern verzichtet werden.

big5 |> 
  rowwise() |> 
  mutate(Offenheit = mean(c_across(O1:O3))) |>
  ungroup() |>
  relocate(Offenheit)
# A tibble: 200 × 8
  Offenheit Alter Geschlecht Extraversion Neurotizismus    O1    O2    O3
      <dbl> <dbl> <chr>             <dbl>         <dbl> <dbl> <dbl> <dbl>
1      3.67    36 m                   3             1.9     5     1     5
2      4.33    30 f                   3.1           3.4     5     3     5
3      3.67    23 m                   3.4           2.4     3     3     5
4      3.33    54 m                   3.3           4.2     2     5     3
# ℹ 196 more rows

Hinter die Spaltenauswahl mithilfe von c_across() können wir mit einem Komma getrennt wie gewohnt weitere Argumente der jeweiligen Funktion übergeben. Hier sei exemplarisch die Entfernung fehlender Werte mit na.rm = TRUE illustriert.

big5 |> 
  rowwise() |> 
  mutate(Offenheit = mean(c_across(O1:O3), na.rm = TRUE)) |>
  ungroup()

Alternativ könnte man auf diese Weise mit sum() auch die Summe über bestimmte Spalten berechnen. Grundsätzlich können wir so jede Funktion aufrufen, die einen Wert pro Beobachtung zurückgibt (z.B. einen Mittelwert oder einen Median). Falls diese zusammenfassenden Berechnungen nicht pro Beobachtung sondern pro Gruppe oder über alle Beobachtungen ausgegeben werden soll, muss stattdessen summarise() verwendet werden (siehe Kapitel 7).

Übung 6.4.5. (Noch nicht enthalten) Starte die Übung mit uebung_starten(6.4.5).

6.5 Umgang mit fehlenden Werten und Duplikaten

Zur besseren Illustration der verschiedenen Möglichkeiten verwenden wir an dieser Stelle einen kleinen selbst erstellten Datensatz namens df.

df <- tibble(
  Alter = c(34, NA, 45, 999),
  Geschlecht = c(NA, "m", "f", ""),
  Extraversion = c(4, 3, 999, 2)
)
df
# A tibble: 4 × 3
  Alter Geschlecht Extraversion
  <dbl> <chr>             <dbl>
1    34 <NA>                  4
2    NA "m"                   3
3    45 "f"                 999
4   999 ""                    2

Enthalten sind zum einen fehlende Werte als NA und zum anderen als 999 kodiert. NA ist dabei ein besonderer Datentyp, der für Not Available (engl. für nicht verfügbar) steht. Die Kodierung als 999 ist typisch für Nutzer des alternativen Statistikprogramms SPSS, da dort kein dedizierter Datentyp für fehlende Werte existiert. Wir sind also daran interessiert, diese 999 oder andere nicht passende Werte in NAs sowie umgekehrt NAs in bestimmte Zahlen umzuwandeln.

Einen ersten Überblick über die Anzahl der fehlenden Werte in allen Spalten erhalten wir mit colSums() und is.na(). Erstere Funktion bildet die Summe pro Spalte und letztere fragt ab, ob der Wert fehlend ist.

colSums(is.na(df))
       Alter   Geschlecht Extraversion 
           1            1            0 

Hier sehen wir richtiger Weise, dass ein Wert in der Altersspalte und zwei Werte in der Geschlechtsspalte fehlen. Möchten wir sehen, auf welche Beobachtungen das genau zutrifft, können wir das in Kapitel 6.3 kennengelernte filter() verwenden. Zum Beispiel könnte man so sehen, wer keine Geschlechtsangabe gemacht hat.

df |> 
  filter(is.na(Geschlecht))
# A tibble: 1 × 3
  Alter Geschlecht Extraversion
  <dbl> <chr>             <dbl>
1    34 <NA>                  4

Zum Umwandeln von Werten in NA können wir die Funktion na_if() aus dem dplyr Package verwenden. Wenn bspw. in der Spalte Alter die Zahl 999 vorkommt, soll stattdessen NA geschrieben werden. Das ganze müssen wir natürlich innerhalb von mutate() verwenden (siehe Kapitel 6.4).

df |> 
  mutate(Alter = na_if(Alter, 999))
# A tibble: 4 × 3
  Alter Geschlecht Extraversion
  <dbl> <chr>             <dbl>
1    34 <NA>                  4
2    NA "m"                   3
3    45 "f"                 999
4    NA ""                    2

Dasselbe können wir natürlich mit across() auch gleich auf mehrere Spalten anwenden (siehe Kapitel 6.4.2). Hierbei müssen jedoch die Datentypen gleich sein.

df |> 
  mutate(across(c(Alter, Extraversion), \(x) na_if(x, 999)))
# A tibble: 4 × 3
  Alter Geschlecht Extraversion
  <dbl> <chr>             <dbl>
1    34 <NA>                  4
2    NA "m"                   3
3    45 "f"                  NA
4    NA ""                    2

So könnte man auch ein NA in jene Zellen schreiben, die einen leeren Character beinhalten.

df |> 
  mutate(across(where(is.character), \(x) na_if(x, "")))
# A tibble: 4 × 3
  Alter Geschlecht Extraversion
  <dbl> <chr>             <dbl>
1    34 <NA>                  4
2    NA m                     3
3    45 f                   999
4   999 <NA>                  2

Umgekehrt zur Umwandlung von NAs z.B. in die Zahl 999, können wir replace_na() aus selbigen Package benutzen. Wenn in der Spalte Alter ein NA steht, soll dieses mit 999 ersetzt werden.

df |> 
  mutate(Alter = replace_na(Alter, 999))
# A tibble: 4 × 3
  Alter Geschlecht Extraversion
  <dbl> <chr>             <dbl>
1    34 <NA>                  4
2   999 "m"                   3
3    45 "f"                 999
4   999 ""                    2

Man könnte NAs auch in Abhängigkeit einer Bedingung mithilfe von if_else() oder case_when() zuweisen. So würde bspw. bei allen Personen mit einer Altersangabe über 120 ein fehlender Wert mit NA eingetragen werden.

df |>
  mutate(Alter = if_else(condition = Alter > 120, true = NA, false = Alter))
# A tibble: 4 × 3
  Alter Geschlecht Extraversion
  <dbl> <chr>             <dbl>
1    34 <NA>                  4
2    NA "m"                   3
3    45 "f"                 999
4    NA ""                    2

Die meisten Statistikfunktionen haben im Regelfall ein Argument namens na.rm (Akronym für not available remove), welches die fehlenden Werte der entsprechenden Spalte direkt entfernt. Genauer gesagt verwenden diese Funktionen an dieser Stelle die sogenannten Pairwise complete observations. Im letzten Kapitel haben wir die Anwendung bereits im Kontext von zeilenweisen Mittelwertsberechnungen kennengelernt.

big5 |> 
  rowwise() |> 
  mutate(Offenheit = mean(c_across(O1:O3), na.rm = TRUE)) |>
  ungroup()

Eine weitere Möglichkeit ist das Entfernen von Zeilen, die fehlende Werte enthalten. Dies erreichen wir mit drop_na() aus dem dplyr Package. Allerdings entfernt diese Funktion die gesamte Zeile von allen Beobachtungen, in denen auch nur ein NA vorkommt. Wenn du also zwei Spalten auswerten möchtest und in einer dritten für die Auswertung irrelevanten Spalte ist ein fehlender Wert, würde die entsprechende Zeile trotzdem entfernt werden. Hier ist also Vorsicht geboten, um keine Informationen zu verlieren.

df |> 
  drop_na()
# A tibble: 2 × 3
  Alter Geschlecht Extraversion
  <dbl> <chr>             <dbl>
1    45 "f"                 999
2   999 ""                    2

Alternativ können wir mithilfe von filter() und der logischen Abfrage is.na() das Entfernen von NAs auch auf eine bestimmte Spalte begrenzen (hier Geschlecht). Der Unterschied zum Aufruf zuvor ist das Ausrufezeichen vor is.na(). Es sollen schließlich jene Zeilen ausgegeben werden, die keinen fehlenden Wert in der Spalte Geschlecht haben.

df |> 
  filter(!is.na(Geschlecht))
# A tibble: 3 × 3
  Alter Geschlecht Extraversion
  <dbl> <chr>             <dbl>
1    NA "m"                   3
2    45 "f"                 999
3   999 ""                    2

Exemplarische sei eine doppelte Zeile in den Datensatz df hinzugefügt.

df <- tibble(
  Alter = c(34, NA, 45, 45),
  Geschlecht = c(NA, "m", "f", "f"),
  Extraversion = c(4, 3, 999, 999)
)
df
# A tibble: 4 × 3
  Alter Geschlecht Extraversion
  <dbl> <chr>             <dbl>
1    34 <NA>                  4
2    NA m                     3
3    45 f                   999
4    45 f                   999

Mit distinct() können derartige Duplikate entfernt werden.

df |> 
  distinct()
# A tibble: 3 × 3
  Alter Geschlecht Extraversion
  <dbl> <chr>             <dbl>
1    34 <NA>                  4
2    NA m                     3
3    45 f                   999

Der Funktion können auch Spaltennamen übergeben werden. Dies ist nützlich, wenn zwei Beobachtungen von derselben Person vorhanden sind, man aber bspw. nur die Erstdiagnose behalten möchte. Für dieses Beispiel würde mit arrange() erst die Reihenfolge so verändert, dass die Erstdiagnose bei doppelten Personeneinträgen an erster Stelle steht und anschließend nur die erste Zeile behalten wird.

daten |> 
  arrange(Name, Vorname, Erstdiagnose) |> 
  distinct(Name, Vorname, .keep_all = TRUE)

Das Argument .keep_all sorgt dafür, dass auch alle nicht explizit genannten Spaltennamen behalten werden. Ohne dieses Argument würde der Datensatz nur mit den Spalten Name und Vorname zurückgegeben werden. Falls du stattdessen Duplikate nur innerhalb einer Spalte überprüfen möchtest, kannst du die Funktion duplicated() innerhalb von filter() verwenden (siehe Kapitel 6.3). Beachte, dass bei der Ausgabe immer nur die doppelten Einträge angezeigt werden.

daten |> 
  filter(duplicated(Name))

6.6 Breites und langes Datenformat

Grundsätzlich unterscheidet man ein sogenanntes breites Datenformat von einem langen Datenformat. In der Literatur werden äquivalent dazu auch die Begriffe flach respektive tief verwendet. Im breiten Datensatz ist jede Spalte eine Variable, jede Zeile eine Beobachtung und jede Zelle ein Wert. Für die meisten Anwendungsfälle ist das breite Format erwünscht. In Abbildung 6.1 ist ein einfaches Beispiel für einen breiten Datensatz mit drei Personen und zwei Variablen illustriert.

Breites Datenformat mit drei Personen und drei Variablen.

Abbildung 6.1: Breites Datenformat mit drei Personen und drei Variablen.

In der klinischen Praxis besteht die Notwendigkeit für das lange Datenformat am häufigsten im Kontext von Messwiederholungen. Schließlich werden dort pro Person eine oder mehrere Variablen mehrfach erhoben, was wir bei der statistischen Auswertung sowie bei der Erstellung von Visualisierung berücksichtigen müssen. Die Messungen können dabei zu mehreren Zeitpunkten oder in Bezug auf mehrere Merkmale stattgefunden haben. In Abbildung 6.2 ist der zuvor gezeigte Datensatz in ein langes Format umgewandelt.

Langes Datenformat mit Persönlichkeitsfaktor als Innersubjektfaktor.

Abbildung 6.2: Langes Datenformat mit Persönlichkeitsfaktor als Innersubjektfaktor.

Im tidyr Package sind zwei Funktionen für genau diese Umwandlungen enthalten. Mit pivot_longer() (engl. für Drehpunkt länger) können wir ein breites Datenformat in ein langes verändern. Die Funktion pivot_wider() fungiert umgekehrt für die Transformation vom langen ins breite Datenformat. Erstere Funktion findet deutlich häufiger Anwendung, da die Daten häufig initial im breiten Format vorhanden sind.

Für das Umformatieren ins lange Datenformat ist es essentiell, einen eindeutigen Personenidentifikator im breiten Datensatz zu haben (z.B. eine Kombination aus Vor- und Nachname mit dem Geburtsjahr). Ansonsten könnten doppelte Zeilen vorkommen, die der Integrität des Datensatzes schaden.. Hier entscheiden wir uns einfach für die Zeilennummer, die wir mit der Funktion row_number() in die Spalte VPN (Akronym für Versuchspersonennummer) schreiben.

wide_big5 <- big5 |> 
  mutate(VPN = row_number()) |> 
  select(VPN, Geschlecht, Extraversion, Neurotizismus) 
wide_big5
# A tibble: 200 × 4
    VPN Geschlecht Extraversion Neurotizismus
  <int> <chr>             <dbl>         <dbl>
1     1 m                   3             1.9
2     2 f                   3.1           3.4
3     3 m                   3.4           2.4
4     4 m                   3.3           4.2
# ℹ 196 more rows

Nun müssen wir in der Funktion pivot_longer() nur noch die gewünschten Spalten auswählen. Im obigen Beispiel wären das Extraversion und Neurotizismus. Beachte, dass genau wie bei across() auch hier die Spalten bei einzelner Auswahl der Funktion innerhalb von c() übergeben werden müssen. Es werden zwei neue Spalten erstellt, die erst noch benannt werden müssen. Wie man diese benennt, ist einem selbst überlassen. Der Name für die Spalte mit den Werten wird mit dem Argument values_to und die Spalte mit den Spaltennamen mit names_to festgelegt. Hier entscheiden wir uns für die neuen Spaltennamen "Auspraegung" und "Faktor". Die Namen müssen hier unbedingt in Anführungszeichen geschrieben werden, da die Spalten noch nicht existieren. Das Ergebnis speichern wir an dieser Stelle als long_big5 ab.

long_big5 <- wide_big5 |> 
  pivot_longer(
    cols = c(Extraversion, Neurotizismus),
    values_to = "Auspraegung",
    names_to = "Faktor"
  )
long_big5
# A tibble: 400 × 4
    VPN Geschlecht Faktor        Auspraegung
  <int> <chr>      <chr>               <dbl>
1     1 m          Extraversion          3  
2     1 m          Neurotizismus         1.9
3     2 f          Extraversion          3.1
4     2 f          Neurotizismus         3.4
# ℹ 396 more rows

Zur Auswahl der Spalten können dieselben Helferfunktionen verwendet werden, die in Kapitel 6.2 beschrieben sind (z.B. starts_with(), ends_with() oder everything()). Umgekehrt können wir mithilfe von pivot_wider() den Datensatz long_big5 wieder ins breite Datenformat bringen. Dafür müssen wir hier nur festlegen, aus welcher Spalte die Werte (values_from) und woher die Spaltennamen (names_from) kommen sollen. Hier benötigen wir keine Anführungszeichen, da die Spalten bereits in unserem Datensatz enthalten sind.

long_big5 |> 
  pivot_wider(
    values_from = Auspraegung,
    names_from = Faktor
  )
# A tibble: 200 × 4
    VPN Geschlecht Extraversion Neurotizismus
  <int> <chr>             <dbl>         <dbl>
1     1 m                   3             1.9
2     2 f                   3.1           3.4
3     3 m                   3.4           2.4
4     4 m                   3.3           4.2
# ℹ 196 more rows

Als grobe Daumenregel kann man sich merken, dass man nicht vorhandene Spalten mit Anführungszeichen übergeben muss. Auf bereits im Datensatz vorhandene Spalten kann man hingegen im Regelfall ohne Anführungszeichen zugreifen.

Übung 6.6. (Noch nicht enthalten) Starte die Übung mit uebung_starten(6.6).

6.7 Spalten trennen

Mit den Funktionen namens separate_wider_*() aus dem tidyr Package können Spalten getrennt werden. Dabei unterscheiden wir drei Szenarien, für die jeweils eine eigene Funktion existiert. Die Informationen innerhalb einer Spalte sind getrennt durch

  • einen Trenner (z.B. Unterstrich, Komma, Punkt): separate_wider_delim(),
  • eine genaue Position: separate_wider_position(),
  • einen Regex (siehe Kapitel 6.9): separate_wider_regex().

Falls die Spalten anstelle des weiten Datenformates in ein langes gebracht werden sollen, existieren äquivalent dazu die Funktionen separate_longer_delim(), separate_longer_position() und separate_longer_regex().

Exemplarisch sei hier der im remp Package enthaltene Datensatz big5_zeit geladen.

big5_zeit
# A tibble: 5 × 5
    VPN Extrav_T1 Extrav_T2 NeurotFA NeurotFB
  <int>     <dbl>     <dbl>    <dbl>    <dbl>
1     1       3.2       3.3      2.8      3.2
2     2       1.7       1.5      4.1      3.2
3     3       2.8       2.7      3.2      2.8
4     4       4.7       4.2      1.7      2.4
# ℹ 1 more row

Um das hier bestehende Problem klarer zu machen, wandeln wir diesen erst einmal in ein langes Datenformat um (siehe Kapitel 6.6).

zeit1 <- big5_zeit |> 
  select(-NeurotFA, -NeurotFB) |> 
  pivot_longer(
    cols = Extrav_T1:Extrav_T2,
    names_to = "Faktor",
    values_to = "Auspraegung"
  ) 
zeit1
# A tibble: 10 × 3
    VPN Faktor    Auspraegung
  <int> <chr>           <dbl>
1     1 Extrav_T1         3.2
2     1 Extrav_T2         3.3
3     2 Extrav_T1         1.7
4     2 Extrav_T2         1.5
# ℹ 6 more rows

Die Spalte Faktor enthält hier zwei Informationen: den Persönlichkeitsfaktor (Extrav) und den entsprechenden Messzeitpunkt (T1, T2). Es muss mit dem names Argument festgelegt werden, wie die neuen Spalten mit den getrennten Informationen heißen sollen. Die beiden Informationen sind durch einen Unterstrich (_) getrennt, weswegen wir dem delim Argument innerhalb der Funktion separate_wider_delim() diesen Unterstrich als Character übergeben. Würde die Spalte Faktor mehr als zwei Informationen getrennt durch mehrere Unterstriche enthalten, müssten wir dem Argument names entsprechend drei Spaltennamen übergeben.

zeit1 |> 
  separate_wider_delim(
    cols = Faktor,
    names = c("Faktor", "Zeitpunkt"),
    delim = "_"
  )
# A tibble: 10 × 4
    VPN Faktor Zeitpunkt Auspraegung
  <int> <chr>  <chr>           <dbl>
1     1 Extrav T1                3.2
2     1 Extrav T2                3.3
3     2 Extrav T1                1.7
4     2 Extrav T2                1.5
# ℹ 6 more rows

Falls die Informationen innerhalb einer Spalten nicht mit einem Unterstrich, sondern durch unterschiedliche Wortlängen, getrennt sind, verwendet man stattdessen die Funktion separate_wider_position(). Hierbei muss dem widths Argument innerhalb von c() die Anzahl der Buchstaben der ersten und zweiten Information übergeben werden. Der Persönlichkeitsfaktor Neurot hat sechs Buchstaben und die Information über den Messzeitpunkt zwei.

big5_zeit |> 
  select(-Extrav_T1, -Extrav_T2) |> 
  pivot_longer(
    cols = NeurotFA:NeurotFB,
    names_to = "Faktor",
    values_to = "Auspraegung"
  ) |> 
  separate_wider_position(
    cols = Faktor,
    widths = c(Faktor = 6, Zeitpunkt = 2)
  )
# A tibble: 10 × 4
    VPN Faktor Zeitpunkt Auspraegung
  <int> <chr>  <chr>           <dbl>
1     1 Neurot FA                2.8
2     1 Neurot FB                3.2
3     2 Neurot FA                4.1
4     2 Neurot FB                3.2
# ℹ 6 more rows

Übung 6.7. (Noch nicht enthalten) Starte die Übung mit uebung_starten(6.7).

6.8 Datensätze zusammenführen

In der Praxis hat man häufig nicht nur einen Datensatz vorliegen, der alle notwendigen Informationen enthält. Stattdessen sind oft die eigentlichen Daten der Studie in einem Datensatz, die demographischen Daten wie Geschlecht und Alter in einem zweiten und Laborwerte wiederum in einem anderen Datensatz. Andere mögliche Szenarien sind mehrere Untersucher oder verschiedenen Messzeitpunkte. Welcher Grund auch immer für separate Datensätze verantwortlich ist, vor der Auswertung müssen diese zusammengeführt werden.

Um dieses Prinzip zu illustrieren, haben wir zwei Datensätze mit Informationen über den Puls, Vorhandensein einer Blasenentleerungsstörung und die Anzahl an Infektionen innerhalb von zwei Jahren. Diese wurden von zwei Untersuchern erhoben, weswegen diese in dem Datensatz df_oben und df_unten vorliegt.

df_oben
# A tibble: 3 × 4
  ID        Puls Blasenstoerung Infekt_2j
  <chr>    <dbl>          <dbl>     <dbl>
1 AX161095    83              0         1
2 NM020683   108              1         7
3 IO240576    60              0         2
df_unten
# A tibble: 3 × 4
  ID        Puls Blasenstoerung Infekt_2j
  <chr>    <dbl>          <dbl>     <dbl>
1 EW180265    53              1         5
2 CB280682    92              0         0
3 JH051199    65              0         1

Der Datensatz demogr enthält darüber hinaus demographische Daten in Form des biologischen Geschlechts und Alters.

demogr
# A tibble: 4 × 3
  ID       Sex   Alter
  <chr>    <chr> <dbl>
1 AX161095 m        28
2 NM020683 f        47
3 IO240576 f        40
4 JH051199 m        24

Solange in beiden Datensätzen dieselben Spalten vorhanden sind, könnte man beide zeilenweise zusammenfügen. In Kapitel 5.1 haben wir bereits gesehen, dass mehrere Datensätze mit gleichem Format mithilfe des zusätzlichen Arguments rbind (row bind) verbunden werden können.

import_list(dateien, rbind = TRUE)

Mit der Funktion bind_rows() kann diese Operation auch mit bereits in R geladenen Datensätzen durchgeführt werden. Die einzigen Argumente sind dabei die Namen der Datensätze.

bind_rows(df_oben, df_unten)
# A tibble: 6 × 4
  ID        Puls Blasenstoerung Infekt_2j
  <chr>    <dbl>          <dbl>     <dbl>
1 AX161095    83              0         1
2 NM020683   108              1         7
3 IO240576    60              0         2
4 EW180265    53              1         5
5 CB280682    92              0         0
6 JH051199    65              0         1

Problematisch ist bei dieser Funktion, dass nicht überprüft wird, ob Beobachtungen mehrfach vorkommen. Mit einem sogenannten Join (engl. für aneinanderfügen) können zwei Datensätze kontrolliert zusammengefügt werden. Die Funktion full_join() kombiniert alle Informationen aus beiden Datensätzen.

full_join(df_oben, df_unten)
Joining with `by = join_by(ID, Puls, Blasenstoerung, Infekt_2j)`
# A tibble: 6 × 4
  ID        Puls Blasenstoerung Infekt_2j
  <chr>    <dbl>          <dbl>     <dbl>
1 AX161095    83              0         1
2 NM020683   108              1         7
3 IO240576    60              0         2
4 EW180265    53              1         5
5 CB280682    92              0         0
6 JH051199    65              0         1

Da in diesem Beispiel alle Spalten aus beiden Datensätzen integriert werden sollen, muss zusätzlich das by Argument nicht spezifiziert werden. Es wird eine Benachrichtigung ausgegeben, dass nach den Spalten ID, Puls, Blasenstoerung und Infekt_2j zusammengefügt wurde. Vorsicht ist geboten, wenn es Überschneidung in den Datensätzen gibt (z.B. die gleiche Person in beiden Datensätzen aber mit unterschiedlichen Werten in einer gleichnamigen Spalte). Daher sollte bspw. ein zweiter Messzeitpunkt direkt innerhalb des Spaltennamens entsprechend gekennzeichnet werden (z.B. Puls_T1 und Puls_T2).

Sollen in den linken Datensatz (1. Argument, hier df_oben) die Informationen eines zweiten Datensatzes (2. Argument, hier demogr) in Abhängigkeit der Übereinstimmung einer dritten Spalte (by Argument, hier ID) eingefügt werden, verwenden wir left_join().

left_join(df_oben, demogr, by = "ID")
# A tibble: 3 × 6
  ID        Puls Blasenstoerung Infekt_2j Sex   Alter
  <chr>    <dbl>          <dbl>     <dbl> <chr> <dbl>
1 AX161095    83              0         1 m        28
2 NM020683   108              1         7 f        47
3 IO240576    60              0         2 f        40

Obwohl in demogr die demographischen Informationen von vier Personen enthalten sind, werden an dieser Stelle nur jene drei an df_oben angehängt, deren ID mit der in df_oben übereinstimmt. Wenn der linke Datensatz in den rechten integriert werden soll, kann man äquivalent dazu right_join() benutzen.

Wenn hingegen alle IDs vorhanden sind, allerdings einige demographische Informationen fehlen, werden diese mit NA (not available, engl. für nicht vorhanden) angegeben. Um das zu illustrieren, werden erst df_oben und df_unten zusammengefügt.

df_alle <- full_join(df_oben, df_unten)
Joining with `by = join_by(ID, Puls, Blasenstoerung, Infekt_2j)`

Anschließend joinen wir wie zuvor eingeführt in Abhängigkeit der ID.

left_join(df_alle, demogr, by = "ID") 
# A tibble: 6 × 6
  ID        Puls Blasenstoerung Infekt_2j Sex   Alter
  <chr>    <dbl>          <dbl>     <dbl> <chr> <dbl>
1 AX161095    83              0         1 m        28
2 NM020683   108              1         7 f        47
3 IO240576    60              0         2 f        40
4 EW180265    53              1         5 <NA>     NA
5 CB280682    92              0         0 <NA>     NA
6 JH051199    65              0         1 m        24

Wenn wir nur diejenigen Werte integrieren möchten, die in beiden Datensätzen enthalten sind, verwenden wir inner_join(). Abschließend gibt es noch zwei Funktionen, die nicht direkt zusammenführen, sondern nur eine Bedingung prüfen und davon abhängig den ersten (linken) Datensatz zurückgeben. Die Funktion semi_join() gibt nur jene Werte aus dem ersten Datensatz zurück, welche im ersten (linken) und zweiten (rechten) vorkommen. Die Funktion anti_join() hingegen gibt nur die Werte aus dem ersten (linken) Datensatz zurück, die nicht im zweiten (rechten) Datensatz enthalten sind.

Das einfache Zusammenfügen von mehreren Zeilen mit bind_rows() ist risikoreich und sollte genau überprüft werden. Kontrollierter ist das Kombinieren mithilfe der Joins. Diese fügen Datensätze nur dann zusammen, wenn es Übereinstimmungen in weiteren Spalten gibt.

Übung 6.8. (Noch nicht enthalten) Starte die Übung mit uebung_starten(6.8).

6.9 Buchstaben und Wörter bearbeiten

Oft muss man entweder die Spaltennamen oder die Inhalte verschiedener Spalten, die Characters enthalten, in irgendeiner Form anpassen. In diesem Kapitel schauen wir uns an, wie man mit Funktionen aus stringr Character ersetzt (str_replace()), extrahiert (str_extract()) und entdeckt (str_detect()). Das Präfix str steht dabei für String – einem anderen Wort für Character.

Ein häufiges Ärgernis im Kontext von Programmiersprachen sind Umlaute, da verschiedene Zeichenkodierungen diese intern unter Umständen anders übersetzen, was auf anderen Betriebssystemen zu Kauderwelsch führen kann. Schauen wir uns im Folgenden an, wie man Umlaute ersetzt. Es ist folgender Satz in der Variable char gespeichert.

char <- "Österreich hat 28610 schräge Berge"

Möchte man nun das eine ä mit ae ersetzen, verwendet man str_replace().

str_replace(string = char, pattern = "ä", replacement = "ae")
[1] "Österreich hat 28610 schraege Berge"

In dem Satz ist allerdings nicht nur ein ä sondern auch ein ö enthalten. Für mehr als eine Anpassung verwendet man str_replace_all(). Die Syntax ist hierbei etwas anders als bisher kennengelernt, da in diesem Fall die alte Bezeichnung auf der linken Seite der jeweiligen Gleichung steht. Alle Änderungen müssen innerhalb von c() übergeben werden.

str_replace_all(string = char, c("ä" = "ae", "Ö" = "Oe"))
[1] "Oesterreich hat 28610 schraege Berge"

Beachte, dass ein Umlaut großgeschrieben ist und die Funktionen case sensitive sind. Das bedeutet, dass wir mit den Befehlen oben nur den kleinen Buchstaben ä und den großen Buchstaben Ö ersetzen.

Aber wie geht man vor, wenn Spaltennamen Umlaute enthalten? Um dem auf den Grund zu gehen, erstellen wir uns einen neuen Datensatz, der die Preise für drei verschiedene Sägen in Österreich enthält.

umlaut <- tibble(
  Säge = c("Häxler", "Sünde3000", "Lölf4"),
  Österreich = c(10.45, 4.60, 9.70)
)
umlaut
# A tibble: 3 × 2
  Säge      Österreich
  <chr>          <dbl>
1 Häxler          10.4
2 Sünde3000        4.6
3 Lölf4            9.7

Die Namen enthalten jeweils einen Umlaut. Möchten wir alle Spaltennamen von Umlauten befreien, könnten wir dies mithilfe von rename_with() und str_replace_all() erreichen. An dieser Stelle verwenden wir eine im Kapitel 6.4.4 bereits eingeführte Lambda Funktion, um die Funktion auf alle Spalten anzuwenden.

umlaut |> 
  rename_with(\(x) str_replace_all(
    string = x, 
    c("ä" = "ae", "ö" = "oe", "ü" = "ue", "Ä" = "Ae", "Ö" = "Oe", "Ü" = "Ue")
  ))
# A tibble: 3 × 2
  Saege     Oesterreich
  <chr>           <dbl>
1 Häxler           10.4
2 Sünde3000         4.6
3 Lölf4             9.7

Zum Ändern von Umlauten innerhalb von Spalten muss str_replace_all(), wie in Kapitel 6.4.2 kennengelernt, innerhalb von mutate() in Kombination von across() verwendet werden. So könnte man alle Spalten, die vom Datentyp Character sind, von Umlauten befreien.

umlaut |> 
  mutate(across(where(is.character), \(x) str_replace_all(
    string = x, 
    c("ä" = "ae", "ö" = "oe", "ü" = "ue", "Ä" = "Ae", "Ö" = "Oe", "Ü" = "Ue")
  )))
# A tibble: 3 × 2
  Säge       Österreich
  <chr>           <dbl>
1 Haexler          10.4
2 Suende3000        4.6
3 Loelf4            9.7

Für das Extrahieren von Buchstaben oder Zahlen können wir str_extract() verwenden. Wir nehmen wieder unseren Beispielsatz von oben, der als char gespeichert ist. Es ist möglich, die Zahl mithilfe eines sogenannten Regex zu erkennen (Akronym für Regular Expression). Regex sind grundsätzlich sehr komplex selbst zu schreiben. In der Praxis muss man in der Regel nur online nach dem gewünschten Regex suchen, ohne die genaue Syntax zu verstehen.

Um eine Zahl mit mehr als einer Ziffer herauszuholen, könnte man nach dem Regex "\d+" suchen. In R muss noch ein zusätzlicher Backslash verwendet werden, wodurch wir den Ausdruck "\\d+" erhalten. Das d steht für digit (engl. für Ziffer) und das Plus für eine oder mehrere Ziffern.

str_extract(char, "\\d+")
[1] "28610"

Angenommen, die Spalten eines Datensatzes mit den Antworten eines Fragebogens starten mit "Q" folgend von Nummer, Namen und der genauen Beschreibung. Ein Beispiel hierfür wäre die 12. Frage zur Risikowahrnehmung mit Antwortschema in Klammern "Q12_Risikowahrnehmung (0 = "Trifft nicht zu")". Für die Auswertung möchten wir aufgrund der Leerzeichen und der redundanten Information den hinteren in Klammern geschriebenen Teil löschen. In anderen Worten soll der gesamte Spaltenname bis zum ersten Leerzeichen extrahiert werden. Ein möglicher Regex dafür wäre ([^\\s]+). Beachte auch in diesem Fall, dass bei jedem Backslash für einen Regex aus dem Internet innerhalb von R ein zweiter Backslash hinzugefügt werden muss.

daten |> 
  rename_with(
    .cols = starts_with("Q")
    .fn = \(x) str_extract(x, "([^\\s]+)"), 
  )

Die Funktion str_detect() entdeckt bestimmte Buchstaben, Wörter oder ganze Regex. Dabei gibt die Funktion einen logischen Wert zurück, wenn das Gesuchte gefunden (TRUE) oder nicht gefunden wurde (FALSE). Das ist daher praktisch, da man diese Funktion für logische Bedingungen innerhalb von if_else(), case_when() oder filter() verwenden kann.

str_detect(char, "\\d+")
[1] TRUE

So ermöglicht str_detect() beispielsweise genauere Abfragen innerhalb von filter() (siehe Kapitel 6.3). Alle Käufer der Säge namens Häxler auszugeben, benötigt nur die Funktion filter().

umlaut |> 
  filter(Säge == "Häxler")
# A tibble: 1 × 2
  Säge   Österreich
  <chr>       <dbl>
1 Häxler       10.4

Möchte man hingegen alle gekauften Sägen mit dem Buchstaben ä auswählen, könnte man stattdessen str_detect() verwenden. Dies ist das zeilen-bezogene Äquivalent zu der bereits kennengelernten Helferfunktion contains(), welche auf der Auswahl von Spalten fokussiert ist (siehe Kapitel 6.2).

umlaut |> 
  filter(str_detect(Säge, "ä"))
# A tibble: 1 × 2
  Säge   Österreich
  <chr>       <dbl>
1 Häxler       10.4

Anstelle eines Buchstabens könnten wir auch nach ganzen Wörtern suchen. Wenn beispielsweise Weitere Erkrankungen in einer einzigen Spalte erhoben wurden, wäre mit dem Aufruf filter(str_detect(Weitere_Erkrankungen, "Hypertonie")) die Auswahl aller Personen mit Bluthochdruck möglich, obwohl noch diverse andere Informationen in der Spalte enthalten sein könnten.

In der Praxis müssen die Funktionen aus dem stringr Package in der Regel in Kombination mit mutate() oder rename_with() verwendet werden. Einen weiteren Anwendungsfall stellt der Umbruch langer Achsenbeschriftung durch str_wrap() bei der Erstellung von Visualisierungen dar.

Übung 6.9. (Noch nicht enthalten) Starte die Übung mit uebung_starten(6.9).

6.10 Faktoren verändern

Falls du nicht mehr im Kopf hast, was genau Faktoren sind, schaue dir noch einmal Kapitel 4.3.2 an. Faktoren sind vor allem zur automatischen Erstellung von Dummy Variablen für Regressionsmodelle und das Umbenennen oder Ändern der Reihenfolge beim Erstellen von Visualisierungen nützlich. Dummy Variablen stellen binäre (0, 1) Variablen dar, welche für nominale Merkmale mit mehr als zwei Ausprägungen im Kontext von Regressionsmodellen erstellt werden müssen. Es wird so eine Referenzkategorie festlegt, mit der die anderen Kategorien verglichen werden können.

Im Folgenden schauen wir uns Beispiele an, wie man mit Funktionen aus dem forcats Package Faktoren umbenennen (fct_recode()) und deren Reihenfolge ändern (fct_relevel(), fct_reorder()) kann. Dafür verwenden wir die Spalte Gruppe aus dem big5_mod Datensatz mit den Faktorstufen (oder Level) Jung, Mittel und Weise.

big5_mod |> 
  relocate(Gruppe)
# A tibble: 200 × 6
  Gruppe Alter Geschlecht Extraversion Neurotizismus    ID
  <fct>  <dbl> <chr>             <dbl>         <dbl> <int>
1 Mittel    36 m                   3             1.9     1
2 Jung      30 f                   3.1           3.4     2
3 Jung      23 m                   3.4           2.4     3
4 Weise     54 m                   3.3           4.2     4
# ℹ 196 more rows

Um die Veränderungen der Faktorstufen besser darstellen zu können, ziehen wir uns die Spalte Gruppe aus dem Datensatz heraus (siehe Kapitel 4.5). Zuvor müssen wir die Spalte allerdings noch zum Datentyp Faktor umwandeln. Dafür übergibt man der Funktion factor() den Spaltennamen sowie die Faktorstufen (levels).

big5_mod <- big5_mod |> 
  mutate(Gruppe = factor(Gruppe, levels = c("Jung", "Mittel", "Weise")))

faktoren <- big5_mod$Gruppe

Alternativ könnte auch as.factor(Gruppe) aufgerufen werden, allerdings werden so nur bestehende Stufen zu Faktoren umgewandelt. Wenn bspw. eine Erkrankung in der eigenen Stichprobe nie vorkommt, würde das später nicht angezeigt werden. Mit dem expliziten Festlegen aller grundsätzlich möglichen Faktorstufen mithilfe des levels Arguments innerhalb von factor() würde bei weiterer Auswertung eine Häufigkeit von 0 für die fehlende Kategorie ausgegeben werden.

Zum Anzeigen der Faktorstufen verwenden wir die Funktion levels().

faktoren |> 
  levels()
[1] "Jung"   "Mittel" "Weise" 

Dass die Reihenfolge schon der Altersreihenfolge entspricht, liegt nur daran, dass wir die Faktorstufen oben genau spezifiziert haben. Ansonsten können durchaus unerwartete Reihenfolgen der Faktorstufen auftreten. Es lohnt sich also in jedem Fall ein Blick in die Faktorstufen zu werfen, bevor man sie verwendet. Möchte man einzelne Faktoren umbenennen, verwendet man fct_recode().

faktoren |> 
  fct_recode(Alt = "Weise") |> 
  levels()
[1] "Jung"   "Mittel" "Alt"   

Mit der Funktion fct_relevel() kann ein Faktor als erste Stufe definiert werden.

faktoren |> 
  fct_relevel("Mittel") |> 
  levels()
[1] "Mittel" "Jung"   "Weise" 

Zum Ändern der gesamten Reihenfolge kann man beliebig viele weitere Faktorstufen der Funktion übergeben.

faktoren |> 
  fct_relevel("Weise", "Mittel", "Jung") |> 
  levels()
[1] "Weise"  "Mittel" "Jung"  

In der Praxis werden sämtlich Funktion aus dem forcats Package meistens innerhalb von mutate() verwendet. So könnte man die Referenzgruppe, also die erste Faktorstufe, für die ältestes Altersgruppe festlegen.

big5_mod |> 
  mutate(Gruppe = fct_relevel(Gruppe, "Weise"))

Wenn die Reihenfolge der Faktoren in absteigender (.desc = TRUE) oder aufsteigender (.desc = FALSE) Reihenfolge z.B. in Abhängigkeit des Mittelwertes einer anderen Spalte (wie dem Ausmaß an Extraversion) sortiert werden soll, verwendet man fct_reorder(). Dabei kann man mit dem Argument .fun die gewünschte Funktion zur Auswertung der zweiten Variable (hier Extraversion) festlegen.

extraversion <- big5_mod$Extraversion
faktoren |> 
  fct_reorder(extraversion, .fun = mean, .desc = TRUE) |> 
  levels()
[1] "Mittel" "Jung"   "Weise" 

In unserem Beispiel ist die Ausprägung der Extraversion in der mittleren Altersklasse am höchsten gefolgt von der jüngsten und der ältesten.

Zur Verwendung direkt am Datensatz wird auch hier der Befehl innerhalb von mutate() aufgerufen (siehe Kapitel 6.4). Die erste Spalte ist der umzugruppierende Faktor und die zweite (hier Alter) jene, nach der gereiht werden soll.

big5_mod |> 
  mutate(Gruppe = fct_reorder(Gruppe, Alter, .fun = mean, .desc = FALSE))
# A tibble: 200 × 6
  Alter Geschlecht Extraversion Neurotizismus Gruppe    ID
  <dbl> <chr>             <dbl>         <dbl> <fct>  <int>
1    36 m                   3             1.9 Mittel     1
2    30 f                   3.1           3.4 Jung       2
3    23 m                   3.4           2.4 Jung       3
4    54 m                   3.3           4.2 Weise      4
# ℹ 196 more rows

Faktoren sollten erst unmittelbar vor Verwendung erstellt und verändert werden. Es kann im Umgang von Faktoren zu seltsamen Fehlermeldungen kommen, da diese innerhalb von R als Integer (Zahl) und nicht als Character (Buchstabenfolge) behandelt werden.

Übung 6.10. (Noch nicht enthalten) Starte die Übung mit uebung_starten(6.10).

6.11 Mit Zeitdaten arbeiten

Für das Arbeiten mit Zeitdaten muss das lubridate Package installiert und geladen sein.

library(lubridate)

In Kapitel 4.3.3 wurden bereits die Datentypen POSIXct, Date und Difftime vorgestellt. Dabei ist POSIXct ein selten erwünschter Datentyp. Wir können mit mutate() in Kombination mit across() aus Kapitel 6.4.2 alle Spalten vom Datentyp POSIXct in den Datentyp Date umwandeln.

Im Datensatz videostream aus dem remp Package liegt die Spalte Watchdate im falschem Format vor. Die Abkürzung <dttm> steht dabei für den Datentyp POSIXct.

videostream
# A tibble: 1,455 × 4
  Titel               Staffel    Folge                           Watchdate          
  <chr>               <chr>      <chr>                           <dttm>             
1 The Big Bang Theory Staffel 10 Das kuenstliche Koffein-Problem 2018-02-17 00:00:00
2 New Girl            Staffel 2  Der Tag danach                  2017-11-14 00:00:00
3 Archer              Staffel 4  Fugue and Riffs                 2017-09-01 00:00:00
4 The Big Bang Theory Staffel 4  31 Liebhaber, aufgerundet       2017-12-06 00:00:00
# ℹ 1,451 more rows

Innerhalb von mutate() können wir den Datentyp der Spalte nun verändern (siehe Kapitel 6.4.1).

videostream |> 
  mutate(Watchdate = as.Date(Watchdate))
# A tibble: 1,455 × 4
  Titel               Staffel    Folge                           Watchdate 
  <chr>               <chr>      <chr>                           <date>    
1 The Big Bang Theory Staffel 10 Das kuenstliche Koffein-Problem 2018-02-17
2 New Girl            Staffel 2  Der Tag danach                  2017-11-14
3 Archer              Staffel 4  Fugue and Riffs                 2017-09-01
4 The Big Bang Theory Staffel 4  31 Liebhaber, aufgerundet       2017-12-06
# ℹ 1,451 more rows

In größeren Datensätzen möchten wir in der Regel alle falsch formatierten Spalten ändern, welches wir mithilfe von across() erreichen (siehe Kapitel 6.4.2).

videostream |> 
  mutate(across(where(is.POSIXct), as.Date))

Die Funktion as.Date() stößt an ihre Grenzen, wenn das Datum nicht im Jahr-Monat-Tag Format ist (z.B. "2002-02-15"). Daher gibt es im lubridate Package neben ymd() (year month date) für denselben Anwendungsfall wie as.Date() zusätzlich die Funktion dmy() (day month year). Möchte man nur den Tag, Monat oder das Jahr separat aus dem Datum extrahieren, können wir dies mit day(), month() und year() erreichen.

videostream |> 
  mutate(
    Watchdate = ymd(Watchdate),
    Jahre = 2023 - year(Watchdate)
  ) 

Wenn das genaue Datum bekannt ist, können die Daten auch direkt voneinander subtrahiert werden. Dabei wird die Differenz in Tagen angegeben. Möchte man die Zeitdifferenz in Jahren angegeben haben (z.B. beim Alter), muss man durch die Funktion dyears() teilen. Als Argument wird die Anzahl an zu teilenden Jahren übergeben. Äquivalent dazu können wir auch durch Monate (dmonths()) und Tage (ddays()) teilen. So können wir auch auch die Überlebenszeit (OS_Zeit) bei bekanntem Datum der Erstdiagnose berechnen.

daten |> 
  mutate(
    Alter_Diagnose = (ymd(Erstdiagnose) - ymd(Geburtsdatum)) / dyears(1),
    OS_zeit = ymd(OS_datum) - ymd(Erstdiagnose),
  ) 

6.12 Binäre Antwortmatrix erstellen

Im Kontext von Fragebögen ist man häufig an richtiger oder falscher Beantwortung der ProbandInnen interessiert. Mit data_binary() aus dem remp Package kann man den Datensatz in die gewünschte binäre Antwortmatrix umwandeln. Für jede Frage pro Person wird also zurückgegeben, ob das Item richtig (1) oder falsch (0) beantwortet wurde.

Exemplarisch nehmen wir die ersten drei Spalten zur Offenheit für neue Erfahrungen aus dem big5 Datensatz.

df2 <- big5 |> 
  select(O1:O3)
df2
# A tibble: 200 × 3
     O1    O2    O3
  <dbl> <dbl> <dbl>
1     5     1     5
2     5     3     5
3     3     3     5
4     2     5     3
# ℹ 196 more rows

Stell dir vor, bei Frage 1 ist die Antwort 3 richtig, bei Frage 2 die Antwort 2 und bei Item 3 die Antwort 4. Dann würden diese richtigen Antworten dem answers Argument kombiniert übergeben werden.

df2 |> 
  data_binary(answers = c(4, 1, 5))
# A tibble: 200 × 3
     O1    O2    O3
  <dbl> <dbl> <dbl>
1     0     1     1
2     0     0     1
3     0     0     1
4     0     0     0
# ℹ 196 more rows