2  Statistische Programmierung mit R

Hinweis

Für eine optimale technische Stabilität empfehlen wir, dieses Online-Lehrbuch auf einem Notebook oder Desktop-Computer zu nutzen. Die interaktiven Komponente, insbesondere die R-Konsole (WebR), sind rechenintensiv und funktionieren auf mobilen Geräten nur eingeschränkt.

── 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

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.

# Paket tidyverse installieren
# install.packages("tidyverse")

# Paket 'tidyverse' laden
library(tidyverse)

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
Tabelle 2.1: Datensatz Klausurergebnisse

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

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:

  1. Entfernen der Maßeinheiten aus den Variablennamen
  2. Entfernen von Pinguinen mit fehlenden Werten (NA)
  3. 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 NAs 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.