2 Statistische Programmierung mit R
── 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 3.5.1 ✔ tibble 3.2.1
✔ lubridate 1.9.3 ✔ tidyr 1.3.1
✔ purrr 1.0.2
── 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
Attaching package: 'kableExtra'
The following object is masked from 'package:dplyr':
group_rows
Warning: package 'modelsummary' was built under R version 4.3.3
`modelsummary` 2.0.0 now uses `tinytable` as its default table-drawing
backend. Learn more at: https://vincentarelbundock.github.io/tinytable/
Revert to `kableExtra` for one session:
options(modelsummary_factory_default = 'kableExtra')
options(modelsummary_factory_latex = 'kableExtra')
options(modelsummary_factory_html = 'kableExtra')
Silence this message forever:
config_modelsummary(startup_message = FALSE)
Warning: package 'gt' was built under R version 4.3.3
Dieses Kapitel ist nicht als umfassende Einführung in R konzipiert, sondern behandelt Kernfunktionen aus der Paketsammlung tidyverse. Wenngleich die Inhalte deutlich über ein Hallo-Welt-Beispiel1 hinausgehen, betrachten wir hier grundlegende Funktionen für Datenmanipulation und Visualisierung unter dem Paradigma Funktionale Programmierung, mit einem starken Fokus auf Pipelines und Transformationen von Datenstrukturen. Diese Grundlagen sind Voraussetzung für das Verständnis fortgeschrittener Code-Bausteine in späteren Kapiteln. Falls Sie bereits über Grundkenntnisse im Umgang mit dem tidyverse verfügen, können Sie dieses Kapitel überspringen. Sollten Sie nicht oder nur teilweise mit den hier gezeigten Befehlen vertraut sein oder keinerlei Erfahrung mit R haben, empfiehlt sich vorab eine Erarbeitung bzw. Wiederholung dieser Inhalte. Nachstehende Ressourcen finden wir über dieses Kapitel hinaus hilfreich:
1 https://de.wikipedia.org/wiki/Hallo-Welt-Programm
-
Feedbackgestützte interaktive Übungsaufgaben bei DataCamp2, bspw.
-
Open-Source-Literatur wie
- der umfangreiche Leitfaden von Ellis und Mayer (2023)
- R for Data Science
- Hands-On Programming with R
Advanced R vertieft wichtige Programmierkonzepte und bietet einen umfassenden Einblick in die Funktionsweise von R. Das Buch richtet sich an erfahrenere R-Nutzer, kann jedoch auch für (Quer)Einsteiger*innen ein hilfreiches Nachschlagewerk sein.
2 Der überwiegende Teil des angebotenen Katalogs (exklusive Einführung in R!) ist kostenpflichtig.
2.1 tidyverse
tidyverse ist eine Sammlung von R-Paketen, die für Datenmanipulation, -analyse und -visualisierung entwickelt wurden, und einen konsistenten, benutzerfreundlichen und funktionalen Programmierstil fördern. Im Fokus der bereitgestellten Funktionen sind saubere, strukturierte und reproduzierbare Data-Science-Workflows.
Wir laden zunächst die Paketsammlung tidyverse
in R. Für die Reproduktion mit dem R GUI oder mit RStudio muss das Paket vorab mit install.packages()
installiert werden. In den interaktiven R-Konsolen in diesem Kapitel (und im Rest des Buchs) sind die benötigten R-Pakete bereits installiert und geladen, sofern nicht anders beschrieben.
Für das Verständnis von Code-Chunks ist es hilfreich, Zwischenergebnisse explizit zu evaluieren und in der Konsole auszugeben. Hierfür umschließen wir häufig Code-Zeilen mit runden Klammern. Der nächste Chunk illustriert dies für die Variable x
.
2.1.1 Lange, weite und “tidy” Datenformate
Wir betrachten den in Tabelle 2.1 dargestellten Datensatz Klausurergebnisse.
Name | Mikro | Makro | Mathe |
---|---|---|---|
Tim | NA | 1.3 | 3 |
Lena | 1 | 3 | NA |
Ricarda | 2 | 1.7 | 1.3 |
Simon | 2.3 | 3.3 | NA |
Der Datensatz ist noch nicht in der R-Arbeitsumgebung verfügbar. Mit der Funktion tribble()
können wir Tabelle 2.1 händisch als R-Objekt der Klasse tibble
definieren
klausurergebnisse
enthält die Klausurnoten der vier Studierenden (Beobachtungen) spaltenweise pro Modul, d.h. die Spaltennamen Mikro
, Makro
und Mathe
sind Ausprägungen der Variable Modul. Der Datensatz liegt also nicht im sog. Tidy-Format vor.
Tidy-Format: Jede Spalte ist eine Variable, jede Reihe ist eine Beobachtung und jede Zelle enthält einen einen Wert. Datensätze im Tidy-Format sind häufig lang: Die Zeilendimension ist größer als die Spaltendimension.
Das Tidy-Format ist hilfreich für statistische Analysen mit tidyverse
-Funktionen wie bspw. ggplot()
. Wir nutzen die Funktion tidyr::pivot_longer()
, um klausurergebnisse
ein (langes) Tidy-Format zu transformieren.
Beachte, dass die Spalte Name
die Zugehörigkeit der Ausprägungen (Note
) jeder Variable (Modul
) zu einer Beobachtung identifiziert. Mit dieser Information können wir den langen Datensatz wieder in das ursprüngliche (weite) Format zurückführen. Wir nutzen hierzu tidyr::pivot_wider()
.
Wenn die Zuweisung von Zwischenergebnissen in Variablen nicht benötigt wird, kann eine Verkettung von Funktionsaufrufen die Verständlichkeit des Codes verbessern. Hierzu wird der Pipe-Operator %>%
genutzt. Wir wiederholen die Transformationen mit den tidyr::pivot_*
-Funktion bei Verwendung von %>%
.
Ein Beispiel für den Nachteil des weiten Formats im Umgang mit tidyverse
-Paketen ist die Funktion tidyr::drop_na()
. Diese entfernt sämtliche Zeilen eines Datensatzes, die NA
-Einträge (d.h. fehlende Werte) aufweisen. Beachte, dass diese Operation im ursprünglichen weiten Format zum Entfernen ganzer Beobachtungen aus wide
führt.
Im Tidy-Format long
hingegen bleiben die übrigen Informationen betroffener Beobachtungen erhalten.
2.1.2 Pinguine und Pipes
In diesem Abschnitt zeigen wir die Verwendung häufig verwendeter dplyr
-Funktionen (s.g. Verben) für die Transformation von Datensätzen: mutate()
, select()
, filter()
,summarise()
und arrange()
.
Für die Illustration verwenden wir den Datensatz penguins
aus dem R-Paket palmerpenguins
. Dieser Datensatz wurde im Zeitraum 2007 bis 2009 von Dr. Kristen Gorman im Rahmen des Palmer Station Long Term Ecological Research Program zusammengetragen und enthält Größenmessungen für drei Pinguinarten, die auf den Inseln des Palmer-Archipels in der Antarktis beobachtet wurden.
# Paket 'palmerpenguins' installieren
# install.packages("palmerpenguins")
# Paket 'palmerpenguins' laden
library(palmerpenguins)
Mit data()
wird der Datensatz in der Arbeitsumgebung verfügbar gemacht. Wir nutzen glimpse()
, um einen Überblick zu erhalten.
2.1.2.1 dplyr::mutate()
Mit mutate()
können bestehende Variablen überschrieben oder neue Variablen als Funktion bestehender Variablen definiert werden. mutate()
operiert in der Spaltendimension des Datensatz.
Wir definieren eine neue Variable body_mass_kg
als Transformation body_mass_g/1000
.
Mit across()
kann die dieselbe Operation auf mehrere Variablen angewendet werden.
Im nachstehenden Beispiel ändern wir den Typ (type
) der Variablen species
, island
, sex
und year
zu character
.
transmute()
ist eine Variante von mutate()
, die lediglich die transformierten Variablen beibehält.
2.1.2.2 dplyr::select()
Mit select()
werden Variablen aus dem Datensatz ausgewählt. Dies geschieht entweder über den Variablennamen…
… oder über eine Indexmenge.3
3 Hilfreich: dplyr::pull()
selektiert eine Variable und wandelt diese in einen Vektor um.
Variablen können anhand eines Muster im Namen selektiert werden. Die Selektion von ends_with("mm")
bezieht nur Variablen mit Endung mm
im Namen ein:
Mit where()
können wir Variablen aufgrund bestimmter Eigenschaften ihrer Ausprägungen selektieren.
2.1.2.3 dplyr::filter()
Das Verb filter()
filtert den Datensatz in der Zeilendimension. So können Beobachtungen ausgewält werden, deren Merkmalsausprägungen bestimmte Kriterien erfüllen. Hierzu muss filter()
ein logischer (logical
) Ausdruck übergeben werden. Häufig erfolgt dies über Vergleichsoperatoren.
Oft ist es praktisch, mehrere Kriterien zu kombinieren.
Analog: komma-getrennte Kriterien werden intern über den Und-Operator (&
) verknüpft.
Ähnlich wie bei select()
verwenden wir häufig nützliche Funktionen, welche die Interpretation des Codes erleichtern. dplyr::between()
erlaubt filtern innerhalb eines Intervalls.
Mit diesen Verben sind wir bereits in der Lage, den Datensatz gemäß folgender Vorschrift zu bereinigen:
- Entfernen der Maßeinheiten aus den Variablennamen
- Entfernen von Pinguinen mit fehlenden Werten (
NA
) - Entfernen von Pinguinen mit einem Gewicht oberhalb des 95%-Stichprobenquantils
Durch die Verkettung mit %>%
können wir sämtliche Schritte für die Bereinigung ohne das Abspeichern von Zwischenergebnissen durchführen.
2.1.2.4 dplyr::summarise()
Das Verb summarise()
fasst Variablen über Beobachtungen hinweg zusammen. Der nachstehende Code-Chunk erzeugt eine Tabelle mit Stichprobenmittelwert und -standardabweichung von flipper_length_mm
.4 Um zu vermeiden, dass die Auswertung aufgrund fehlender Werte (NA
) in flipper_length_mm
scheitert, lassen wir NA
s mit na.rm = TRUE
bei der Berechnung unberücksichtigt (wir verwenden weiterhin den unbereinigten Datensatz penguins
).
4 dplyr::summarise()
darf nicht mit base::summary()
verwechselt werden!
Varianten von summarise()
können über mehrere Variablen angewendet werden. Wir verwenden across()
und where()
, um lediglich numerische Variablen mit den in der liste definierten Funktionen zusammenzufassen. Beachte, dass \(x) mean(x)
eine anonyme Funktion definiert.
2.1.2.5 dplyr::arrange()
Mit arrange()
können Datensätze in Abhängigkeit der beobachteten Ausprägungen von Variablen sortiert werden.
Die Funktion dplyr::desc()
kehrt die Reihenfolge zu einer absteigenden Sortierung um.
Komplexe Sortier-Muster werden durch Übergabe von Variablennamen in der gewünschten Reihenfolge erreicht.
2.1.2.6 Operationen mit gruppierten Datensätzen
Für manche Transformationen ist eine Gruppierung der Daten hilfreich. Wir illustrieren nachfolgend die unterschiedlichen Verhaltensweisen ausgewählter Verben durch Vergleiche von gruppierten und nicht-gruppierten Anwendungen.
species
hat drei Ausprägungen. Entsprechend ist penguins_grouped
nun in drei Gruppen eingeteilt.
Bei gruppierten Datensätzen fasst summarise()
die Variablen pro Guppe zusammen.
mutate()
definiert bzw. transformiert für jede Gruppe separat. Im dies zu veranschaulichen, ziehen wir eine Zufallsstichprobe von 10 Pinguinen aus der Datensatz.
Für den ungruppierten Datensatz berechnet mutate()
das Stichprobenmittel von bill_length_mm
über alle zehn Datenpunkte und weist diesen Wert jeweils in der Variable mean
zu.
Bei gruppierten Daten berechnet mutate()
das Stichprobenmittel pro Gruppe und weist die Mittelwerte entsprechend zu.
2.1.3 Eine explorative Analyse mit ggplot2
Der bereinigte Datensatz penguins_cleaned
eignet sich gut für eine graphische Auswertung mit dem R-Paket ggplot2
, welches Bestandteil des tidyverse
ist. Nachfolgend untersuchen wir Zusammenhänge zwischen den Körpermaßen der Pinguine.
Wir erstellen zunächst einen einfachen Punkteplot des Gewichts (body_mass
) und der Schnabeltiefe (bill_depth
).
Die Grafik zeigt einen positiven Zusammenhang zwischen dem Gewicht und der Schnabeltiefe. Als nächstes passen wir den Code so an, dass die Datenpunkte entsprechend der Art (species
) eingefärbt sind.
Offenbar gibt es deutliche Unterschiede in der (gemeinsamen) Verteilung von Gewicht und Schnabeltiefe zwischen den verschiedenen Arten.
Um den Zusammenhang zwischen Gewicht und Schnabeltiefe zu untersuchen, schätzen wir lineare Regressionen \[body\_mass = \beta_0 + \beta_1 bill\_depth + u\] separat für jede der drei Pinguinarten mit der KQ-Methode. Anschließend zeichnen wir die geschätzten Regressionsgeraden ein.
Die Schätzungen bekräftigen die Vermutung, dass der lineare Zusammenhang zwischen Gewicht und Schnabeltiefe sich nicht zwischen den verschiedenen Pinguinarten unterscheidet: Pinguine der Art Gentoo sind im Mittel schwerer als Pinguine der übrigen Arten, haben jedoch eine geringere Schnabeltiefe.
Der nachfolgende Code fügt der Grafik eine Regressionslinie über alle Arten hinzu. Wir setzen hierbei das Argument inherit_aes = FALSE
und legen damit fest, dass die Regression für body_mass
und bill_depth
ohne Differenzierung per species
durchgeführt wird.
Offenbar ist die vorherige Analyse per Spezies sinnvoller: Die Regression über alle Arten suggeriert einen negativen Zusammenhang zwischen Gewicht und Schnabeltiefe.
Facetting mit facet_wrap()
erlaubt eine Untersuchung des Zusammenhangs je Insel (island
), auf der die Messung erfolgt ist.
Wir sehen, dass es hinsichtlich des Zusammenhangs von Gewicht und Schnabeltiefe keine wesentlichen Diskrepanzen zwischen den drei Inseln gibt. Darüber hinaus lässt sich anhand der Facetten leicht erkennen, wie die drei Arten über die Inseln verteilt sind.