Das Paket tidyverse

Die grundlegenden Funktionen für das Benutzen von R kennen Sie nun. Im Laufe des Seminars werden wir viele dieser Funktionen wiederholt benutzen, und damit üben. Diese Fuktionen sind implementiert im sog. baseR Paket des Programs. Wenn Sie jedoch in Zukunft einmal größere und komplexere Operationen vornehmen, dann gibt es deutlich effizientere und mächtigere Pakete. Eines davon ist das Paket tidyverse. Dieses hat während der vergangenen Jahre enorm an Popularität gewonnen und wird mehr und mehr genutzt. Wir wollen daher dieses Seminar auch dazu nutzen, Sie mit dem Paket tidyverse frühzeitig in Kontakt zu bringen. Natürlich können wir in dem begrenzten Rahmen lediglich einzelne grundlegende Funktionen beibringen - wir ermutigen Sie jedoch, selbstständig weiter nach Funktionen zu suchen und recherchieren. Nach jedem Kapitel werden wir daher weitere Quellen zur Verfügung stellen.

So ist es auch im Fall von tidyverse. Dieses Skript hier hat nicht den Anspruch auf Vollständigkeit, sondern gibt eine kurze Einführung in Befehle und Funktionen, welche wir in der jeweiligen Woche brauchen. Viele Erläuterungen und Hintergründe basieren auf diesem Online Skript.

Tibble

Das tibble ist im Prinzip eine Weiterentwickung des Data Frames in baseR - es handelt sich also um eine Tabelle. Allerdings gibt es Regeln, die dazu führen, dass die Daten in einem tibble sauberer und konsistenter abgespeichert werden. Insbesondere sind Spaltenname in einem tibble syntaktisch korrekt und folgen den Regeln für Variablennamen in R. Dies bedeutet, dass sie keine Leerzeichen enthalten und andere spezielle Zeichen vermieden werden. tibbles versuchen zudem, die Datenklasse (z. B. numerisch, character, etc.) jeder Spalte so gut wie möglich beizubehalten. Dies unterscheidet sich von herkömmlichen Datenrahmen, die manchmal automatisch Datentypen konvertieren. Daraus ergibt sich, dass tibbles ein einfacheres und konsistenteres subsetting (d.h., ausschneiden von Teilen) erlauben.

Zur besseren Orientierung laden wir einmal einen Datensatz ein. Mit der Funktion slice_head() können wir uns die oberen Zeilen ausgeben lassen.

library(tidyverse)
── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
✔ dplyr     1.1.4     ✔ readr     2.1.5
✔ forcats   1.0.0     ✔ stringr   1.5.1
✔ ggplot2   4.0.1     ✔ tibble    3.2.1
✔ lubridate 1.9.4     ✔ tidyr     1.3.1
✔ purrr     1.0.4     
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag()    masks stats::lag()
ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
df <- airquality
tib <- as_tibble(airquality)

slice_head(tib, n=5)
# A tibble: 5 × 6
  Ozone Solar.R  Wind  Temp Month   Day
  <int>   <int> <dbl> <int> <int> <int>
1    41     190   7.4    67     5     1
2    36     118   8      72     5     2
3    12     149  12.6    74     5     3
4    18     313  11.5    62     5     4
5    NA      NA  14.3    56     5     5

Wie Sie sehen, gibt es für unseren Anwendungen erst einmal keine großen Unterschiede - und während der ersten Wochen brauchen Sie sich keine großen Gedanken zu machen. Wenn Sie jedoch jetzt shcon tiefer eintauchen wollen, dann empfehle ich Ihnen dieses Online-Skript.

Der %>% Operator (pipe-Operator)

Was Sie jedoch häufiger sehen werden, ist der sog. Pipe-Operator %>%. Dieser erlaubt es Ihnen, mehrere (im Prinzip unbegrenzt viele) Operationen durchzuführen, ohne jedes Zwischenergebnis auf einer neuen variablen abspeichern zu müssen. Dies klingt erstmal nicht sehr spektakulär, aber es vereinfacht Ihren Code ganz erheblich. Wir werden immer mal wieder diesen Operator benutzen, in Woche I z.B. im Kontext von Selektionen.

Im Grundsatz ist die Funktionsweise des %>% Operators wie folgt: auf das Objekt links des Operators wird die Operation rechts des Operators angewandt. Dieses Prozedur kann beliebig oft wiederholt werden, sodass am Ende z.B. eine Operation lauten kann: ergebnis <- data %>% Operation01 %>% Operation02 %>% Operation03 Dabei wird Operation01 auf Data Frame (zw. den tibble) angewandt. Auf das Ergebnis der Operation wird dann Operation02 angewandt, usw. weiter unten werden wir uns ein paar Beispiele anschauen, wodurch dies dann klarer wird.

Als shortcut zu Schreiben des %>%-Operators in R können Sie die Tastenkombination Strg+Umschalt+M verwenden.

Gängige Funktionen im tidyverse

Die Anzahl der möglichen Operationen im Paket tidyverse sind sehr umfangreich, und im Rahmen dieses Kurses (und möglicherweise weiteren Kursen während Ihres Studiums) werden wir nicht alle benötigen. Es gibt jedoch eine Auswahl von ca. 10-15 Funktionen, die man immer wieder benutzen wird. Keine Angst: Sie müssen diese nicht alle auswendig können oder gar aus dem *ff** beherrschen. Jedes Mal, wenn wir eine Funktion anwenden, werden wir sie detailliert erläutern.

Die am häufigsten Funktionen sind:

  • filter() - Auswahl von Zeilen, basierend auf einem Kriterium (oder mehreren Kriterien) der Spalten \(\rightarrow\) siehe Beispiel weiter unten.
  • select() - Auswählen von Spalten
  • rename() - Umbennen von Spaltenname
  • arrange() - Umsortieren von Spalten
  • mutate() - Erstellen von neuen Variablen
  • group_by() - Gruppieren von Variablen
  • summarize() - Zusammenfassen von Variablen (d.h., Spalten) innerhalb eines Datensatzes
  • left_join() - spaltenweises Zusammenfügen von mehreren Datensätzen
  • tally() - liefert die Gesamtsumme der Werte der angegebenen Spalte(n) oder die Anzahl der Zeilen im Tibble
  • count() - liefert die Anzahl der eindeutigen Werte der angegebenen Spalten
  • add_count() - count()-Werte als neue Spalte hinzufügen
  • glimpse() - einen Überblick über die im Datensatz enthaltenen Daten zu erhalten
  • pull() - das Ergebnis einer vorangegangenen Operation als Einzelwert oder Vektor erhalten
  • pivot_longer() - Umwandlung vom wide ins long Format
  • pivot_wider() - Umwandlung vom long ins wide Format
  • rbind() - Zusammenfügen mehrerer Datensätze zeilenweise (untereinander)
  • na.rm = TRUE - Argument in vielen Funktionen zum Umgang mit fehlenden Werten
  • drop = TRUE - Argument beim Extrahieren einzelner Werte aus tibbles

Das klingt erstmal ziemlich komplex und überwältigend? Das stimmt - und ich kann mich nur wiederholen, dass wir nicht von Ihnen erwarten, dass Sie die Funktionen können am Ende des Kurses. Dennoch hilft es m.E. wenn man frühzeitig von solchen Funktionen gehört hat.

Am einfachsten ist es, wenn Sie die Fnktionen “einfach mal anwenden” - so gehe ich zumindest immer an neue Sachen, und bisher hat es geholfen. Sie können z.B. die Daten aus der Übungsaufgabe nehmen und dann Stück für Stück die ein oder andere Funktion ausprobieren. Wir haben gelernt, die Daten wie folgt einzuladen:

garten <- c(283.3, 285.6, 286.1, 286.7, 287.0, 288.2, 284.4, 284.6, 283.2, 284.1, 284.1, 282.2, 283.5, 285.2, 287.7, 288.9, 290.5, 287.8, 284.2, 281.5, 284.6, 287.6, 287.4, 288.2, 286.7, 285.1, 287.4, 289.4, 289.8, 288.4, 286.3)
dach <- c(283.5, 285.6, 286.1, 286.8, 287.1, 288.4, 284.9, 284.8, 283.6, 284.4, 284.6, 282.9, 283.8, 285.6, 287.8, 289.3, 290.8, 288.1, 284.4, 281.6, 284.5, 287.8, 287.5, 288.4, 286.9, 285.5, 287.7, 289.7, 290.1, 289.2, 287.0)

in einem ersten Schritt wandeln fassen wir die beiden Vektoren zu einem Data Frame zusammen und konvertieren die Daten von °F in °C. Zusätzlich berechnen wir eine Spalte datum, welche die Tage von 1.10.2023 bis zum 31.10.2023 als Datum formatiert enthält.

df <- data.frame(garten, dach)
df <- df - 273.15

df$datum <- seq(as_date("2023-10-01"), as_date("2023-10-31"), "1 day")

slice_head(df, n=10)
   garten  dach      datum
1   10.15 10.35 2023-10-01
2   12.45 12.45 2023-10-02
3   12.95 12.95 2023-10-03
4   13.55 13.65 2023-10-04
5   13.85 13.95 2023-10-05
6   15.05 15.25 2023-10-06
7   11.25 11.75 2023-10-07
8   11.45 11.65 2023-10-08
9   10.05 10.45 2023-10-09
10  10.95 11.25 2023-10-10

Die Funktion filter()

Die erste Funktion des Pakets tidyverse, die wir nutzen ist die Funktion filter. Diese Funktion wird verwendet, wenn man einen Data Frame (d.h., eine Tabelle) verkleinern will basierend auf einem Kriterium in einer Spalte. Zum Beispiel könnten wir sagen, dass wir aus der obigen Tabelle alle Zeilen rauswerfen wollen, bei denen die Temperatur an der Dachstation kleiner als 15°C ist. Wir kontrollieren unsere Abfrage, indem wir vor und nach der Operation die Anzahl der Zeilen ermitteln:

# Anzahl der Zeilen VOR der filter()-Operation
nrow(df)
[1] 31
df_sub <- df %>% filter(dach > 15)
nrow(df_sub)
[1] 7

Was haben wir gemacht? Wir haben unseren Data Frame df genommen, und die Funktion filter() darauf angewandt. Als “Bezug” zwischen Data Frame und filter() haben wir den %>%-Operator genommen. Das ergebnis haben wir auf eine neue Variable df_sub geschrieben. Experimentieren Sie ein wenig herum, und verändern Sie den filter().

Sie können auch komplexere Filter anwenden. Zum Beispiel könnten wir fragen: An welchem Tag im Oktober ist die Tempratur im Garten am höchsten?. Übersetzt heißt dass, dass wir (a) die Maximaltemperatur der Gartenstation ermitteln müssen, und dann (b) einen Filter anwenden, welcher diesen Maximalwert abfragt. Punkt (a) haben wir schon im vorherigen Kapitel kennengelernt.

# Ermitteln des Maximalwerts
maxGarten <- max(df$garten)
# Anwendung des Filters
df %>% filter(garten == maxGarten)
  garten  dach      datum
1  17.35 17.65 2023-10-17

Und schon haben wir die entsprechende Information. Probieren Sie es aus: wie sähe das Ergebnis aus für die Minimumtemperatur der Dachstation? Die Abfrage ist sehr ähnlich.

Zusätzliche wichtige Funktionen

Die Funktion rbind()

Mit rbind() können mehrere Datensätze zeilenweise zusammengefügt werden. Dies ist nützlich, wenn Sie Daten aus verschiedenen Quellen kombinieren möchten:

# Beispiel: Zwei separate Datensätze
df_teil1 <- df[1:10, ]
df_teil2 <- df[11:20, ]

# Zusammenfügen
df_kombiniert <- rbind(df_teil1, df_teil2)
nrow(df_kombiniert)
[1] 20

Die Funktion pull()

pull() extrahiert eine einzelne Spalte als Vektor, was für weitere Berechnungen hilfreich ist:

# Extrahieren der Garten-Temperaturen als Vektor
garten_werte <- df %>% pull(garten)
class(garten_werte)
[1] "numeric"
length(garten_werte)
[1] 31

Das Argument drop = TRUE

Das Argument drop = TRUE wird beim Extrahieren einzelner Werte aus tibbles verwendet. Es bestimmt, ob die Struktur des tibbles beibehalten wird oder ob ein einfacherer Datentyp (wie ein Vektor) zurückgegeben wird:

# Erstellen eines tibbles zur Demonstration
tib <- as_tibble(df)

# Mit drop = FALSE (Standard bei tibbles) - behält tibble-Struktur
ergebnis_tibble <- tib[1:3, "garten", drop = FALSE]
class(ergebnis_tibble)
[1] "tbl_df"     "tbl"        "data.frame"
print(ergebnis_tibble)
# A tibble: 3 × 1
  garten
   <dbl>
1   10.2
2   12.5
3   13.0
# Mit drop = TRUE - gibt einen Vektor zurück
ergebnis_vektor <- tib[1:3, "garten", drop = TRUE]
class(ergebnis_vektor)
[1] "numeric"
print(ergebnis_vektor)
[1] 10.15 12.45 12.95
# Vergleich: Bei data.frames ist drop = TRUE der Standard
df_ergebnis <- df[1:3, "garten"]  # Automatisch ein Vektor
class(df_ergebnis)
[1] "numeric"

Die Funktionen group_by() und summarise()

Diese Funktionen werden häufig zusammen verwendet, um Daten nach Gruppen zu analysieren:

# Beispiel: Gruppierung und Zusammenfassung
df_long <- df %>% 
  pivot_longer(cols = c(garten, dach), 
               names_to = "station", 
               values_to = "temperatur")

# Gruppierung nach Station und Berechnung von Statistiken
zusammenfassung <- df_long %>%
  group_by(station) %>%
  summarise(
    mittelwert = mean(temperatur, na.rm = TRUE),
    standardabweichung = sd(temperatur, na.rm = TRUE),
    anzahl = n()
  )

print(zusammenfassung)
# A tibble: 2 × 4
  station mittelwert standardabweichung anzahl
  <chr>        <dbl>              <dbl>  <int>
1 dach          13.3               2.30     31
2 garten        13.0               2.30     31

Umgang mit NA-Werten

In den meisten statistischen Funktionen müssen Sie explizit angeben, wie mit fehlenden Werten (NA) umgegangen werden soll:

# Beispiel mit NA-Werten
beispiel_daten <- c(10, 15, NA, 20, 25, NA, 30)

# Ohne na.rm=TRUE gibt mean() NA zurück
mean(beispiel_daten)
[1] NA
# Mit na.rm=TRUE werden NA-Werte ignoriert
mean(beispiel_daten, na.rm = TRUE)
[1] 20

Wechseln zwischen long und wide Format

Bisher haben wir die Tabellen ein für uns intuitives Format gehabt: die Werte einzelner Variablen (in unserem Fall dach und garten) sind in individuellen Spalten untergebracht. Obwohl das für die Übersicht der Daten gut ist, ist es bei Datenzusammenfassungen oder beim Erstellen von Gruppengrafiken (siehe nächstes Kapitel zum Erstellen von Grafiken) die Tabelle ein wenig anders anzuordnen. Man spricht hierbei auch vom sogenannten long-Format. Im “long”-Format ist jede Beobachtung in einer eigenen Zeile, und es gibt Spalten, welche die Werte der Beobachtungen und Metadaten beschreiben. Typischerweise gibt es eine Spalte, die die Art der Messung oder Variable angibt, eine Spalte, die die Werte dieser Variable enthält, und gegebenenfalls andere Spalten, die zusätzliche Informationen enthalten. Das Paket tidverse gibt uns Werkzeuge an die Hand, mit denen dies einfach möglich ist. Wir wenden die Funktion pivot_longer() einmal an, um den Unterschied festzustellen:

df_l <- df %>% pivot_longer(cols = c("garten", "dach"), names_to="station")

data.frame(df_l)
        datum station value
1  2023-10-01  garten 10.15
2  2023-10-01    dach 10.35
3  2023-10-02  garten 12.45
4  2023-10-02    dach 12.45
5  2023-10-03  garten 12.95
6  2023-10-03    dach 12.95
7  2023-10-04  garten 13.55
8  2023-10-04    dach 13.65
9  2023-10-05  garten 13.85
10 2023-10-05    dach 13.95
11 2023-10-06  garten 15.05
12 2023-10-06    dach 15.25
13 2023-10-07  garten 11.25
14 2023-10-07    dach 11.75
15 2023-10-08  garten 11.45
16 2023-10-08    dach 11.65
17 2023-10-09  garten 10.05
18 2023-10-09    dach 10.45
19 2023-10-10  garten 10.95
20 2023-10-10    dach 11.25
21 2023-10-11  garten 10.95
22 2023-10-11    dach 11.45
23 2023-10-12  garten  9.05
24 2023-10-12    dach  9.75
25 2023-10-13  garten 10.35
26 2023-10-13    dach 10.65
27 2023-10-14  garten 12.05
28 2023-10-14    dach 12.45
29 2023-10-15  garten 14.55
30 2023-10-15    dach 14.65
31 2023-10-16  garten 15.75
32 2023-10-16    dach 16.15
33 2023-10-17  garten 17.35
34 2023-10-17    dach 17.65
35 2023-10-18  garten 14.65
36 2023-10-18    dach 14.95
37 2023-10-19  garten 11.05
38 2023-10-19    dach 11.25
39 2023-10-20  garten  8.35
40 2023-10-20    dach  8.45
41 2023-10-21  garten 11.45
42 2023-10-21    dach 11.35
43 2023-10-22  garten 14.45
44 2023-10-22    dach 14.65
45 2023-10-23  garten 14.25
46 2023-10-23    dach 14.35
47 2023-10-24  garten 15.05
48 2023-10-24    dach 15.25
49 2023-10-25  garten 13.55
50 2023-10-25    dach 13.75
51 2023-10-26  garten 11.95
52 2023-10-26    dach 12.35
53 2023-10-27  garten 14.25
54 2023-10-27    dach 14.55
55 2023-10-28  garten 16.25
56 2023-10-28    dach 16.55
57 2023-10-29  garten 16.65
58 2023-10-29    dach 16.95
59 2023-10-30  garten 15.25
60 2023-10-30    dach 16.05
61 2023-10-31  garten 13.15
62 2023-10-31    dach 13.85

Schauen Sie sich die Tabelle genau an: nun sind die Spalten für dach und garten untereinander angeordnet, sortiert nach der Variable datum. Definiert haben wir dies durch das Funktionsargument cols=c(), in welchem wir die Variablen angegeben haben, die wir untereinander angeben wollen; mit dem Argument names_to= haben wir der neuen Spalte einen einprägsamen Namen gegeben.

Die Tabelle wird dadurch länger und möglicherweise weniger intuitiv zum lesen; aber die Funktionalität erhöht sich massiv. Wir lernen in ein paar Wochen, wie wir einfache Berechnungen nun durchführen können - zudem können wir auomatisiert mehrere Grafiken mit wenigen Code-Zeilen erstellen (mehr dazu im nächsten Kapitel).

Natürlich können wir die Tabelle auch wieder in das *wide+-Format zurückwandeln:

df_w <- pivot_wider(df_l, names_from = station, values_from = value)
as.data.frame(df_w)
        datum garten  dach
1  2023-10-01  10.15 10.35
2  2023-10-02  12.45 12.45
3  2023-10-03  12.95 12.95
4  2023-10-04  13.55 13.65
5  2023-10-05  13.85 13.95
6  2023-10-06  15.05 15.25
7  2023-10-07  11.25 11.75
8  2023-10-08  11.45 11.65
9  2023-10-09  10.05 10.45
10 2023-10-10  10.95 11.25
11 2023-10-11  10.95 11.45
12 2023-10-12   9.05  9.75
13 2023-10-13  10.35 10.65
14 2023-10-14  12.05 12.45
15 2023-10-15  14.55 14.65
16 2023-10-16  15.75 16.15
17 2023-10-17  17.35 17.65
18 2023-10-18  14.65 14.95
19 2023-10-19  11.05 11.25
20 2023-10-20   8.35  8.45
21 2023-10-21  11.45 11.35
22 2023-10-22  14.45 14.65
23 2023-10-23  14.25 14.35
24 2023-10-24  15.05 15.25
25 2023-10-25  13.55 13.75
26 2023-10-26  11.95 12.35
27 2023-10-27  14.25 14.55
28 2023-10-28  16.25 16.55
29 2023-10-29  16.65 16.95
30 2023-10-30  15.25 16.05
31 2023-10-31  13.15 13.85