Skip to Tutorial Content
Einstellungen für Barrierefreiheit

Start

Ziele dieses Tutorials

  • Hier werden die Grundlagen der Datentranformation mit dplyr und tidyr gelehrt, sowie die Visualisierung von Daten mit ggplot2.

  • Hier werden keine allgemeine Grundlagen der R-Programmierung gelehrt! Bitte schaue dir das R & RStudio Grundlagen Tutorial an.

Tidyverse

Das tidyverse ist eine Sammlung von open-source R-Paketen, die speziell für die Datenaufbereitung und -visualisierung entwickelt wurde. Alle Pakete basieren auf einer konsistenten Design-Philosophie, Grammatik und auch Datenstruktur, welche innerhalb des tidyverse-Universums einheitlich ist, aber z.T auch sehr stark von base-R abweicht. Zu den Paketen im tidyverse gehören:

  • dplyr (Datenmanipulation),

  • tidyr (Datenbereinigung),

  • ggplot2 (Datenvisualisierung),

  • readr (Datenimport),

  • stringr (Umgang mit Strings/Text),

  • forcats (Umgang mit kategorialen daten),

  • purrr (funktionales Programmieren) und

  • tibble (Alternative zu data.frames).

Warum ist das tidyverse sinnvoll als Ergänzung zu base R?

Benutzerfreundlichkeit und Lesbarkeit: Der tidyverse bietet eine intuitive Syntax, die mit Pipes (%>%) arbeitet. Dies ermöglicht eine klare und lesbare Darstellung von Datenmanipulationsprozessen, wodurch auch komplexe Analysen nachvollziehbar bleiben.

Konsistenz: Alle Pakete im tidyverse folgen gemeinsamen Designprinzipien und Funktionen, was den Lernaufwand reduziert und eine nahtlose Integration zwischen den Paketen ermöglicht.

Effizienz: Viele tidyverse-Funktionen sind im Vergleich zu base-R einheitlicher und damit auch leichter anzuwenden, wenn man sich innerhalb der Syntax gut auskennt.

Erweiterte Funktionalität: Das tidyverse bietet moderne Werkzeuge, die in base-R fehlen oder umständlicher umzusetzen sind, wie z. B. das einfache Pivotieren von Tabellen, die Gruppierung von Daten oder das Erstellen aussagekräftiger Visualisierungen.

Community und Dokumentation: Das tidyverse hat eine große Community, hervorragende Dokumentationen und Tutorials, die den Einstieg und die Weiterentwicklung erleichtern.

Kritische Punkte und Herausforderungen

Häufige Änderungen: Der tidyverse entwickelt sich kontinuierlich weiter, und Funktionen werden mitunter angepasst oder durch neue ersetzt. Dies kann dazu führen, dass älterer Code schnell veraltet und nachträglich angepasst werden muss. Viele Pakete enthalten daher einen Lifecycle-Status, der angibt, ob eine Funktion stabil ist oder zukünftig durch andere ersetzt wird.

Lernaufwand: Wer an die Arbeitsweise von base-R gewöhnt ist, muss sich zunächst an die andere Syntax und Denkweise des tidyverse gewöhnen.

Performance in spezifischen Fällen: Während der tidyverse in vielen Bereichen performant ist, kann es in sehr spezialisierten Anwendungsfällen oder bei extrem großen Datensätzen weniger effizient sein als optimierte Alternativen.

Abhängigkeiten: Das tidyverse umfasst eine Vielzahl von Paketen, was die Abhängigkeiten und den Overhead im Vergleich zu reinem base-R erhöht.

Insgesamt bietet das tidyverse eine kohärente, mächtige und weit verbreitete Ergänzung zu base-R.

dplyr– Grundlagen

Übersicht über die wichtigsten Funktionen

All diese Funktionen werden in diesem Kapitel näher erklärt:

  • select(): Auswahl von Spalten
  • filter(): Auswahl von Zeilen basierend auf Bedingungen
  • arrange(): Sortieren von Zeilen
  • rename(): Umbenennen von Spalten
  • mutate(): Hinzufügen oder Verändern von Spalten
  • summarize(): Aggregation von Daten, z. B. Berechnung von Mittelwerten
  • group_by(): Gruppieren von Daten für Aggregationen

Beispieldatensatz

Der Datensatz df wird nachfolgend im Kapitel genutzt und ist in den jeweiligen R-Umgebungen hinterlegt.

Zeige die Erstellung der Daten

set.seed(3)

df <- data.frame(
  id = 1:6,
  sex = c("M", "F", "M", "F", "M", "F"),
  age = c(25, 35, 45, 20, 30, 40),
  intelligence = c(110, 120, 130, 115, 125, 135),
  school_years = c(10, 7, 8, 12, 10, 8),
  income = sample(2000:5000, 6),
  student = c(FALSE, TRUE, FALSE,TRUE, TRUE, FALSE),
  x_1 = rnorm(6),
  x_2 = rnorm(6),
  x_3 = rnorm(6),
  x_4 = rnorm(6),
  life1 = rnorm(6),
  life2 = rnorm(6),
  life3 = rnorm(6),
  life4 = rnorm(6),
  x1_study = rnorm(6),
  x2_study = rnorm(6),
  x3_study = rnorm(6),
  x_study_na = rnorm(6)
)
df 

Piping mit %>%

Der Pipe-Operator (%>%) ist ein zentrales Werkzeug im tidyverse und hilft dabei, Datenmanipulationen in einer klaren und lesbaren Weise zu verketten. Er ermöglicht es, mehrere Funktionen nacheinander auf einen Datensatz anzuwenden, ohne dabei den Datensatz-Objekt in jedem Funktionsaufruf explizit wiederholen zu müssen. Der Shortcut für ein schnelles Erstellen des Operators ist Strg + Shift + M für Windows und Cmd + Shift + M für MacOS.

Betrachten wir folgendes Beispiel, in dem wir Daten mit vier Funktionen manipulieren. Was die einzelnen Funktionen machen, werden wir uns später im Detail anschauen.

# Option 1
# Zergliederung der Schritte und Speicherung der Zwischenschritte
df <- select(df, X, sex, age, intelligence)
df <- filter(df, age <= 40)
df <- arrange(df, sex)
df <- rename(df, subjects = X)

Die erste Optione ist zwar gut leserlich, aber sehr umständlich gecoded, da sie einige Wiederholungen enthält. So wird der einzelne Bearbeitungsschritt jedes Mal zwischen gespeichert und der Datensatz muss jedes Mal neu übergeben werden.

# Option 2
# Verschachtelung der Funktionen ineinander
df <- rename(
  arrange(
    filter(
      select(df, X, sex, age, intelligence), 
      age <= 40
    ), 
    sex
  ), 
  subjects = X
)

Die zweite Option ist deutlich effizienter geschrieben, jedoch schwer zum Lesen und Debuggen.

Der Pipe-Operator %>% hilft nun, genaus diese Verarbeitungs-Ketten besser darzustellen im Code. Anstatt den Datensatz in jeder Funktion explizit zu nennen, wird der Datensatz implizit an die nachfolgende Funktionen weitergegeben. So sieht der Code mit Pipe-Operator aus:

# Option 3
# empfohlene Variante
df %>% 
  # der Originaldatensatz wird nun weitergeleitet an die select Funktion
  select(id, sex, age, intelligence) %>%  
  # der veränderte Datensatz wird weitergeleitet nach filter()
  filter(age <= 40) %>%
  arrange(sex) %>%
  rename(subjects = id)

Die Funktionsweise bleibt gleich, aber der Code wird übersichtlicher und leichter lesbar. Die Pipe-Schreibweise verbindet die Übersichtlichkeit von Option 1 und die Effizienz von Option 2.

Vorteile des Pipe-Operators

  1. Lesbarkeit: Der Code zeigt die Verarbeitungsschritte klar und in einer logischen Reihenfolge.
  2. Weniger Wiederholungen: Der Datensatz-Objekt muss nicht bei jeder Funktion neu referenziert werden.
  3. Effizienz bei komplexen Prozessen: Die Verkettung macht besonders bei vielen Verarbeitungsschritten Sinn.

Hier ist ein Beispiel mit dem Beispieldatensatz:

# Datenmanipulation mit Pipe-Operator
df %>% 
  select(id, sex, age, intelligence) %>% 
  filter(age <= 40) %>% 
  arrange(sex) %>% 
  rename(subjects = id)

Der Pipe-Operator (%>%) ist ein mächtiges Werkzeug, das den Code übersichtlicher macht und oft näher am menschlichen Denken dran ist. Diese Art zu Programmieren ist so beliebt geworden, dass es inzwischen auch einen Pipe-Operator in base-R gibt, welcher so aussieht: |>. Die Operatoren sind sich in vielen Funktionalitäten ähnlich. Wir werden innerhalb dieses Tutorials wegen der Einfachheit immer den %>% Operator verwenden.

Übungen

Aufgabe: Schreibe den Code um und verwende hier den Pipe Operator %>%. Ändere ansonsten so wenig, wie möglich.

Otter IconTipp
df <- group_by(df, sex)
df <- select(df, id, sex, age, intelligence)
df <- filter(df, age <= 40)
df <- rename(df, subjects = id)
df 
df <- df %>%
  group_by(sex) %>% 
  select(id, sex, age, intelligence) %>% 
  filter(age <= 40) %>% 
  rename(subjects = id)
df

select()

Mit der Funktion select() können spezifische Spalten eines Dataframes ausgewählt werden. Die Funktionen bietet viele Möglichkeiten, die Spalten-Auswahl zu gestalten.

Die wichtigsten Funktionen und Operatoren sind:

  • : um einen Bereich von Spalten auszuwählen
  • ! um Spalten auszuschließen
  • & (und) sowie | (oder) um Bedingungen zu kombinieren
  • starts_with("") um Spalten auszuwählen, die mit einem bestimmten Präfix beginnen
  • ends_with("") um Spalten auszuwählen, die mit einem bestimmten Suffix enden
  • contains("") um Spalten auszuwählen, die einen bestimmten Text enthalten
  • everything() um alle Spalten auszuwählen (z. B. nach vorheriger Modifikation der Reihenfolge)
  • num_range("prefix", range) um nummerierte Spalten auszuwählen, z. B. x1, x2, …
  • matches("") um Spalten basierend auf regulären Ausdrücken “Regex” auszuwählen

Spalten auflisten

Das Komma , wir verwendet, um alle gewünschten Spaltennamen aufzulisten.
df %>% 
  select(sex, age, intelligence)

Spaltenbereich auswählen

Der Doppelpunkt : wird verwendet, um eine Spaltensequenz auszuwählen. Es bedeutet: “Wähle alle Spalten zwischen (einschließlich) sex und intelligence”. Die Reihenfolge basiert auf der tatsächlichen Anordnung der Spalten im Datensatz df.
df %>% 
  select(sex:intelligence)

Spalten ausschließen

Negiere die ausgewählten Spalten mit !. Es werden alle Spalten ausgewählt, außer den negierten Spalten.
df %>% 
  select(!sex:intelligence)

Spalten auswählen, welche einen bestimmten character String enthalten

Der gesuchte String kann Text, aber auch Zahlen oder Symbole enthalten.

df %>% 
  select(contains("x_"))
Spalten auswählen, welche mit einem bestimmten character String beginnen
df %>% 
  select(starts_with("life"))

Alles oder den übrigen Rest auswählen

# Wähle age an erster Stelle und füge dann den Rest aller Spalten dazu
df %>% 
  select(age, everything())

Nummerierte Spalten auswählen

Es gibt oft Spaltennamen, welche dem Muster folgen [Textname + aufsteigende Zahl]. Solch ein Muster kann auch leicht ausgewählt werden mit num_range.

df %>% 
  select(num_range("x_", 1:3))

Komplexe Mustererkennung

Es können auch nach sehr komplexen Mustern Spaltennamen ausgewählt werden. Dafür werden jedoch reguläre Ausdrücke, sogenannte Regex verwendet, was eine eigene Sprache darstellt. Weitere Informationen dazu gibt es hier.

Eine kleine Übersicht über wichtige Regex-Symbole:

Symbol Beschreibung Beispiel
. Beliebiges Zeichen (außer Zeilenumbruch) a.c erkennt abc, a1c, etc.
^ Start des Textes oder einer Zeile ^Hello erkennt Hello world.
$ Ende des Textes oder einer Zeile world$ erkennt Hello world.
* Null oder mehr Wiederholungen ab* erkennt a, ab, abb.
+ Eine oder mehr Wiederholungen ab+ erkennt ab, abb, etc.
? Null oder eine Wiederholung ab? erkennt a, ab.
{n} Genau n Wiederholungen a{3} erkennt aaa.
{n,} Mindestens n Wiederholungen a{2,} erkennt aa, aaa, …
{n,m} Zwischen n und m Wiederholungen a{2,4} erkennt aa, aaa.
[0-9] Ziffern zwischen 0 bis 9 a[0-9] erkennt a5, a0.
[a-z] nur kleine Buchstaben _[a-z] erkennt _a, _b.
[A-Z] nur große Buchstaben _[A-Z] erkennt _A, _B.
[A-Za-z] große und kleine Buchstaben _[A-Za-z] erkennt _A, _b.
[A-Za-zÄÖÜäöüß] alle Buchstaben mit Umlauten oder Sonderzeichen (wie ä, ö, ü, ß) ) _[A-Za-zÄÖÜäöüß] erkennt _A, .
# suche nach Spaltennamen, welche das _ Symbol und nachfolgend 
# eine oder mehrere Ziffern enthält
df %>% 
  select(matches("_[0-9]+"))

Übungen

Aufgabe: Wähle die Spalten “income”, “id” und “school_years” aus.

Otter IconTipp
df %>% select(income, id, school_years)

Aufgabe: *Wähle alle Spalten aus, die mit “_study” enden.*

Otter IconTipp
df %>% 
  select(ends_with("_study"))

Aufgabe: Wähle alle Spalten aus, welche eine Ziffer enthalten.

Otter IconTipp
df %>% select(matches(".*[0-9]+.*"))

filter()

Mit filter() kannst du Zeilen basierend auf einer oder mehreren Bedingungen auswählen. Diese Bedingungen können mit:

  • Vergleichsoperatoren (==, !=, <, >, <=, >=) definiert werden,

  • Logischen Operatoren (&für “und”, | für “oder”), und

  • Zeichenfolgen in Anführungszeichen für Textwerte.

Beispiele mit logischen Operatoren
# Wähle Zeilen aus, in denen die Variable sex
# den Inhalt F enthält
df %>% 
  filter(sex == "F")
Mehrere Bedingungen
# Das Geschlecht muss weiblich sein und das
# Alter muss über 40 Jahre sein
df %>% 
  filter(sex == "F" & age < 40)

Übungen

Aufgabe: Wähle alle Studenten aus.

Otter IconTipp
df %>% filter(student == TRUE)
Aufgabe: Wähle alle Studenten aus, die 30 Jahre oder älter sind.
df %>% filter(student == TRUE & age >= 30)

Aufgabe: Wähle Personen aus, die älter als 40 Jahre sind oder ein Einkommen über 4000€ haben.

Otter IconTipp
df %>% filter(age > 40 | income > 4000)

arrange()

Die Funktion arrange() verändert die Zeilen eines Datensatzes basierend auf einer oder mehreren Spalten. Standardmäßig erfolgt die Sortierung aufsteigend, aber mit der Funktion desc() kann auch absteigend sortiert werden.

# Die Zeilen werden zuerst sotiert nach dem
# Geschlecht und danach nach dem Alter
df %>% 
  arrange(sex, age)
# Das Alter wird nun aufsteigend sortiert
df %>% 
  arrange(desc(age))

Übungen

Aufgabe: Sortiere die Daten nach der Intelligenz.
df %>% arrange(intelligence)

Aufgabe: Sortiere die Daten nach dem Einkommen. Das höchste Einkommen soll in der ersten Zeile erscheinen.

Otter IconTipp
df %>% arrange(desc(income))

rename()

Die rename()-Funktion hilft dabei, Spalten eines Datensatzes umzubenennen. Sie unterstützt sowohl die Umbenennung einzelner Spalten, als auch mehrere Umbenennungen auf einmal.

Die Syntaxlogik ist: rename(neuer_name = alter_name)

df %>% 
  rename(person_id = id,
         experience_1 = x_1)

Übungen

Aufgabe: Benenne die Spalte “intelligence” in “iq_score” um.

Otter IconTipp
df %>% 
  rename(iq_score = intelligence)

mutate()

Die Funktion wird verwendet, um neue Spalten zu erstellen oder bestehende Spalten in einem Datensatz zu überschreiben. Sie ist vielseitig einsetzbar und erlaubt mathematische Berechnungen, logische Operationen sowie die Anwendung von Funktionen auf Spalten.

Besonderheiten von mutate():

  • Existierende Spalten überschreiben : Wenn die Spalte bereits existiert, wird sie mit den neuen Werten überschrieben.

  • Neue Spalten erstellen : Wenn die Spalte noch nicht existiert, wird sie neu erstellt.

  • Mathematische Operationen : Du kannst einfache mathematische Berechnungen wie Addition +, Subtraktion -, Multiplikation * und Division / verwenden.

  • Vektorisierung : Die Operationen werden auf jede Zeile des Datensatzes angewendet.

  • Funktionen verwenden : Du kannst R-Funktionen wie mean(), if_else(), n() und benutzerdefinierte Funktionen verwenden.

  • Position: Du kannst bestimmen, wo die neuen Splaten plaziert werden sollen mit den Funktionsargumenten .before = Spaltenname und .after = Spaltenname.

Neue Spalte erstellen

# Erstellt eine neue Spalte age_months,
# die das Alter in Monaten darstellt
df %>% 
  mutate(age_months = age * 12, .before = age)

Mehrere Spalten gleichzeitig erstellen oder berechnen

# Berechnet den Unterschied des Alters und des
# IQ im Vergleich zum Mittelwert:
df %>% 
  mutate(age_diff = age - mean(age),
         iq_diff = intelligence - mean(intelligence),
         .after = age)

Spalte mit logischer Bedingung erstellen

Erstellt eine Spalte age_general, die Menschen in die Kategorien “jung” (≤ 37 Jahre) und “alt” (> 37 Jahre) einteilt:
df %>% 
  mutate(age_general = if_else(age <= 37, "young", "old"),
         .after = age)

Existierende Spalte überschreiben

Achtung: wenn ein Spaltenname gewählt wird, der bereits im Datensatz existiert, wird der bestehende Inhalt überschrieben.

# das Einkommen wird um 10% erhöht
df %>% 
  mutate(income = income * 1.10,
         .after = age)

Übungen

Aufgabe: Erstelle eine neue Spalte namens “name” und setze “unbekannt” als Wert.

Otter IconTipp
df %>% 
  mutate(name = "unbekannt", .after = id)

Aufgabe: Überschreibe die Werte von “income”, sodass R den Wert als fehlend (“not available”) begreift.

Otter IconTipp
df %>% 
  mutate(income = NA)

Aufgabe: Erstelle eine neue Spalte namens “mean_income” und berechne dort den Durchschnitt des Einkommens aller Personen.

Otter IconTipp
df %>% 
  mutate(mean_income = mean(income), .after = id)

summarize()

Die Funktion summarize() (auch summarise()geschrieben) ermöglicht es, Datensätze durch Aggregation von Werten zu kondensieren. Dabei werden neue Spalten erstellt, die zusammenfassende Statistiken (wie mean(), sum(), max(), etc.) enthalten. Das Ergebnis ist ein neuer Datensatz mit weniger Zeilen als der Originaldatensatz.

Die Syntaxlogik ist: summarize(neue_spalte = Funktion(spaltenname))

df %>%
  summarize(
    total_age = sum(age),
    average_age = mean(age),
    max_age = max(age)
  )

In diesem Beispiel:

  • Berechnet total_age die Summe der Werte in der Spalte “age.”

  • Ermittelt average_age den Durchschnittswert der Spalte “age.”

  • Findet max_age den höchsten Wert in der Spalte “age.”

Jede neue Spalte im Ergebnis enthält den Output der entsprechenden Funktion. Da alle Werte zu einer einzigen Zusammenfassung aggregiert werden, reduziert sich die Anzahl der Zeilen deutlich.

Übungen

Aufgabe: Aggregiere die Tabelle: Erstelle dabei eine neue Spalte namens “mean_income” und berechne dort den Durchschnitt des Einkommens aller Personen.

Otter IconTipp
df %>%
  summarize(
    mean_income = mean(income)
  )
Aufgabe: Aggregiere die Tabelle: Erstelle dabei eine neue Spalte namens “mean_income” und berechne dort den Durchschnitt des Einkommens aller Personen. Berechne auch den Median der Schuljahre und benenne die Spalte “median_school”.
df %>%
  summarize(
    mean_income = mean(income),
    median_school = median(school_years)
  )

group_by()

Mit der Funktion group_by() kannst du deinen Datensatz in verschiedene Gruppen aufteilen, um dann für jede Gruppe getrennte Operationen auszuführen. Dies ist besonders nützlich, wenn du gruppenspezifische Statistiken oder Filter anwenden möchtest. Zum Beenden einer Gruppierung innerhalb einer Pipe kannst du ungroup() verwenden.

Die Syntaxlogik ist: group_by(gruppe1) oder auch group_by(gruppe1, gruppe2)

Kurze Übersicht:

  • group_by(): Teilt den Datensatz in Gruppen auf.

  • ungroup(): Hebt die Gruppierung wieder auf, sodass nachfolgende Operationen wieder auf den gesamten Datensatz angewendet werden.

Diese Funktionen lassen sich hervorragend mit folgenden Funktionen kombinieren:

  • summarize(): Erzeugt zusammenfassende Statistiken für jede Gruppe.

  • filter(): Filtert Zeilen innerhalb jeder Gruppe basierend auf gruppenspezifischen Bedingungen.

  • mutate(): Fügt gruppenbezogene Berechnungen als neue Spalten hinzu.

Beispiel 1: Zusammenfassen nach Geschlecht

Im folgenden Beispiel werden Daten nach der Variable “sex” gruppiert und für jede Gruppe werden der Gesamtwert, der Durchschnitt und der Maximalwert des Alters berechnet:

df %>%
  group_by(sex) %>%
  summarize(
    total_age = sum(age),
    average_age = mean(age),
    max_age = max(age)
  )

In diesem Fall wird für jede Geschlecht eine eigene Zusammenfassung erstellt.

Beispiel 2: Filter() und mutate() in Gruppen

In diesem Beispiel wird der Datensatz nach “sex” gruppiert, dann werden nur Zeilen beibehalten, bei denen das Alter über dem jeweiligen Gruppenmittelwert liegt. Anschließend wird eine neue Spalte “age_diff” erstellt, die den Unterschied zwischen dem individuellen Alter und dem Gruppenmittelwert angibt:

df %>%
  group_by(sex) %>%
  filter(age > mean(age)) %>%
  mutate(age_diff = age - mean(age), .after = age) %>%
  ungroup()

In diesem Beispiel:

  • group_by(sex): Teilt den Datensatz in Gruppen basierend auf dem Geschlecht.

  • filter(age > mean(age)): Behalte nur die Zeilen, in denen das Alter größer als der Durchschnitt der jeweiligen Gruppe (hier das Geschlecht) ist.

  • mutate(age_diff = age - mean(age)): Fügt eine neue Spalte hinzu, die die Differenz zwischen dem individuellen Alter und dem Gruppenmittelwert berechnet.

  • ungroup(): Hebt die Gruppierung auf, damit nachfolgende Operationen wieder auf den gesamten Datensatz angewendet werden können.

Übungen

Aufgabe: Berechne in Abhängigkeit des Geschlechts das durchschnittliche Einkommen (aggregiere die Daten). Benenne die neue Spalte “mean_income”.

Otter IconTipp
df %>% 
  group_by(sex) %>% 
  summarize(mean_income = mean(income))

Aufgabe: Berechne in Abhängigkeit des Geschlechts das durchschnittliche Einkommen (aggregiere die Daten). Benenne die neue Spalte “mean_income”. Berechne danach noch das durchschnittliche Alter pro Geschlecht (speichere das Ergebnis in “mean_age”) ab. Löse danach die Gruppenstruktur auf.

Otter IconTipp
df %>% 
  group_by(sex) %>% 
  summarize(mean_income = mean(income),
            mean_age = mean(age)) %>% 
  ungroup()

dplyr – Daten zusammenführen

Kapitel

join Funktionen
bind Funktionen


Wir stellen hier zwei verschiedene Varianten vor, Daten zusammenzuführen.

Die join-Funktionen sind dazu geeignet Datensätze anhand von verknüpften IDs miteinander zu verbinden. bin_col ermöglicht es auch Spalten zusammen zu “kleben” - hier findet jedoch keine Zuordnung oder logische Prüfung der Zeilen statt.

join Funktionen

In vielen Datenanalysen hast du es mit mehreren Datensätzen zu tun, die zusammengehören, aber unterschiedliche Informationen enthalten. Mit den join-Funktionen kann man die Daten zueinander zuordnen und dabei zusammenführen.

In unserem Beispiel gibt es zwei Tabellen:

  • students: Enthält alle immatrikulierten Studenten, ihren Namen und die Anzahl der bereits absolvierten Semester.

  • enrollments: Enthält die Kursanmeldungen. Hierzu gehören neben der Kurs-ID und dem Kursnamen auch die Studenten ID und der Name, wie er in der Anmeldung erscheint.

Dabei kann es vorkommen, dass:

  • Ein Student in beiden Tabellen vorkommt (z. B. Max oder Sophie).

  • Ein Student nur in “students” vorhanden ist (z. B. Lukas oder Mia, falls keine Anmeldung erfolgt).

  • Anmeldungen von Studenten existieren, die nicht im aktuellen “students”-Datensatz aufgeführt sind (z. B. student_id 5).

Mit den join-Funktionen in dplyr kann man die Informationen der Datensätze zusammenführen.

Hier ist eine grafische Übersicht über die einzelnen Funktionen und was sie bedeuten:

Zeigt in Venndiagrammen die Teilmengen der join Funktionen.

Datensatz “students”

Datensatz “enrollments”

Anmerkung: In diesem Beispiel ist nur die Studierenden-ID eine eindeutige und fehlerfreie Art, die Personen (Zeilen) eindeutig zuzuordnen. Der Datensatz “enrollments” enthält z.T. unvollständige Angaben zum Namen, deswegen wird nur die Studierenden-ID verwendet.

full_join()

Mit full_join() werden beide Datensätze anhand des gemeinsamen ID vollständig student_id kombiniert, ohne eine Person (oder Zeile) zu verlieren. Es handelt sich somit um Studierende, welche entweder offiziell als immatrikuliert gelistet sind und/oder sich aktuell zu Prüfungen angemeldet haben (es handelt sich um eine logische ODER Verknüpfung).

full_join(students, enrollments,
           # mit welcher ID werden die Daten zugeordnet
           by = "student_id",
           # wie werden doppelte Spaltennamen benannt
           suffix = c("_stud", "_enroll"))

Die Informationen werden sofern möglich zusammengeführt, wenn Werte fehlen werden NA Werte vergeben.

inner_join()

Mit inner_join() werden beide Datensätze anhand des gemeinsamen ID student_id kombiniert. Es werden nur die Zeilen (Studierende) zurückgegeben, bei denen die ID in beiden Datensätzen vorhanden ist. Es handelt sich somit um Studierende, welche offiziell als immatrikuliert gelistet sind und sich aktuell zu Prüfungen angemeldet haben (es handelt sich um eine logische UND Verknüpfung).

inner_join(students, enrollments,
           # mit welcher ID werden die Daten zugeordnet
           by = "student_id",
           # wie werden doppelte Spaltennamen benannt
           suffix = c("_stud", "_enroll"))

Die ID 2 (Sophie) wird zwei Mal angezeigt, da sie sich zu zwei Klausuren angemeldet hat.

left_join()

left_join() liefert alle Zeilen aus dem linken Datensatz (hier: “students”) und ergänzt, falls vorhanden, die zugehörigen Informationen aus dem rechten Datensatz (hier:“enrollments”). Falls ein Student keine Kursanmeldung hat (keinen Eintrag in “enrollments” existiert), erscheinen in den entsprechenden Spalten NA.

right_join(students, enrollments, by = "student_id", suffix = c("_stud", "_enroll"))

Alle immatrikulierten Studenten werden angezeigt mit der Information der angemeldeten Klausuren. Da Lukas und Mia keine Anmeldung in “enrollments” haben, werden die fehlenden Informationen durch NA definiert. Studenten, die in mehreren Kursen angemeldet sind (wie Sophie), erscheinen mehrfach. Beatrice wird nicht angezeigt, da sie nicht offiziell immatrikuliert ist.

semi_join()

Diese Funktionen behält alle Zeilen (Personen) des ersten Datensatzes (“students”), welche sich auch im zweiten Datensatz befinden (“enrollments”) und es fügt zusätzliche Spalten des zweiten Datensatzes nicht hinzu.

Vorteile von semi_join()

  • Datenfilterung: Du erhältst einen klaren Überblick darüber, welche Einträge in beiden Datensätzen vorhanden sind, ohne die Daten durch zusätzliche Spalten zu erweitern.

  • Einfachheit: Da keine zusätzlichen Informationen aus dem rechten Datensatz übernommen werden, bleibt die Struktur des linken Datensatzes erhalten.

  • Effizienz: Perfekt geeignet, wenn du nur prüfen möchtest, ob ein Eintrag in einem zweiten Datensatz vorhanden ist.

semi_join(students, enrollments, by = "student_id")

Es werden alle immatrikulierten Studierenden angezeigt, welche sich auch zu einem Kurs angemeldet haben. Die Informationen des Kurses werden nicht zusätzlich mit angezeigt.

anti_join()

Gibt die Zeilen aus dem “enrollments”-Datensatz zurück, für die kein passender Eintrag im “students”-Datensatz gefunden wird.

anti_join(enrollments, students, by = "student_id", suffix = c("_stud", "_enroll"))

Hier handelt es sich um alle Personen (Zeilen), die sich zu einer Klausur angemeldet haben, obwohl sie nicht offiziell immatrikuliert sind.

Übungen

In unserem Beispiel gibt es zwei Datensätze:

  • df_id: Die Übersicht über alle registrierten Proband*innen

  • df_med: Die erhobenen medizinischen Daten von fast allen registrierten Proband*innen

In echte Studien kann es passieren, dass Probanden obwohl sie bereits für die Studie registriert sind nicht an der Studie teilnehmen und somit keine Daten vorliegen.

Datensatz “df_id”

Datensatz “df_med”

Aufgabe: Fülle den Datensatz “df_id” auf mit allen Infos aus “df_med”.

Otter IconTipp
left_join(df_id, df_med, by = "proband_id")
Aufgabe: Zu welche Personen haben wir keine medizinischen Daten erhoben?
anti_join(df_id, df_med, by = "proband_id")
Aufgabe: Für die Analyse, erstelle einen Datensatz, welche alle vorhandenen Infos aus beiden Datensätzen enthält. Es sollen jedoch nur Personen erscheinen, für die auch medizinische Daten vorliegen.
# Lösung 1 ----
inner_join(df_id, df_med, by = "proband_id")

# Lösung 2 ----
# In diesem Beispiel kann man auch das machen
right_join(df_id, df_med, by = "proband_id")
Aufgabe: Wir möchten den Datensatz “df_id” reduzieren, sodass nur Personen enthalten sind, bei denen auch medizinische Daten vorliegen.
semi_join(df_id, df_med, by = "proband_id")

bind Funktionen

Die bind-Funktionen ermöglichen es unkompliziert Datensätze miteinander zu verbinden. Die Daten können anhand der Spalten oder der Zeilen zusammengefügt werden.

Zeilen zusammenfügen mit bind_rows()

Mit bind_rows() kannst du mehrere Datensätze vertikal (also zeilenweise) zusammenführen. Dies ist nützlich, wenn du beispielsweise Daten aus verschiedenen Quellen hast, welche dieselbe Struktur haben (ähnliche oder identische Spalten) beinhalten, und diese in einen einzigen Datensatz integrieren möchtest.

# Datensatz 1
df1 <- data.frame(
  id = c(1, 2),
  name = c("Alice", "Bob"),
  hobby = c( "sport", "baking")
)
df1
# Datensatz 2
df2 <- data.frame(
  id = c(3, 4),
  name = c("Claudia", "Dieter"),
  allergy = c(TRUE, FALSE)
)
df2
# Zusammenführen der Zeilen
df <- bind_rows(df1, df2)
df

Alle Zeilen aus df1 und df2 werden untereinander angeordnet. Alle Spalten werden übernommen. Fehlende Werte werden mit NA aufgefüllt.

Spalten zusammenfügen mit bind_cols()

Mit bind_cols() kannst du Data Frames spaltenweise (also horizontal) zusammenführen. Dabei werden die Datensätze nebeneinander angeordnet. Wichtig ist, dass die Datensätze dieselbe Anzahl an Zeilen haben, da sonst die Zusammenführung nicht eindeutig erfolgt. Im besten Fall gibt es auch keine Überlappung der Spaltennamen.

# Datensatz 1
df1 <- data.frame(
  id = c(3, 4),
  name = c("Claudia", "Dieter")
)
df1
# Datensatz 2
df2 <- data.frame(
  hobby = c( "sport", "baking"),
  allergy = c(TRUE, FALSE)
)
df2
# Zusammenführen der Spalten
df <- bind_cols(df1, df2)
df

Übungen

Aufgabe: Füge die zwei Datensätze zusammen mit einer bind-Funktion. Gebe den Datensatz aus.
Otter IconTipp
# Datensatz 1
df_A <- data.frame(
  id = c("x1", "x2"),
  name = c("Alice", "Bob"),
  hobby = c( "sport", "baking")
)
# Datensatz 2
df_B <- data.frame(
  id = c("x3", "x4"),
  name = c("Claudia", "Dieter"),
  hobby = c( "dancing", "singing")
)
# Schreibe ab hier deinen Code:
# Datensatz 1
df_A <- data.frame(
  id = c("x1", "x2"),
  name = c("Alice", "Bob"),
  hobby = c( "sport", "baking")
)
# Datensatz 2
df_B <- data.frame(
  id = c("x3", "x4"),
  name = c("Claudia", "Dieter"),
  hobby = c( "dancing", "singing")
)
# Schreibe ab hier deinen Code:
df <- bind_rows(df_A, df_B)
df
Aufgabe: Füge die zwei Datensätze zusammen mit einer bind-Funktion. Gebe den Datensatz aus.
Otter IconTipp
# Datensatz 1
df_A <- data.frame(
  sport_types = c("tennis", "dancing", "football"),
  hobby_types = c( "sport", "cooking", "art")
)
# Datensatz 2
df_B <- data.frame(
  animals = c("tiger", "bird", "snake"),
  colors = c( "red", "green", "yellow")
)
# Schreibe ab hier deinen Code:
# Datensatz 1
df_A <- data.frame(
  sport_types = c("tennis", "dancing", "football"),
  hobby_types = c( "sport", "cooking", "art")
)
# Datensatz 2
df_B <- data.frame(
  animals = c("tiger", "bird", "snake"),
  colors = c( "red", "green", "yellow")
)
# Schreibe ab hier deinen Code:
df <- bind_cols(df_A, df_B)
df

tidyr – Pivoting

pivot_longer(): Umwandeln von breiten in lange Datensätze

In diesem Tutorial lernst du, wie du einen Datensatz zum Thema Säugetiere im Wide-Format mithilfe der Funktion pivot_longer() aus dem tidyr-Paket in das Long-Format überführst. Im Wide-Format werden verschiedene Messgrößen in separaten Spalten dargestellt, während im long-Format jede Beobachtung in einer eigenen Zeile erscheint. Das long-Format ist häufig vorteilhaft für Visualisierungen (insbesondere auch ggplot2).

Einfaches Beispiel

Zunächst erstellen wir einen Datensatz, in dem für jedes Tier das Gewicht in den Jahren 2010, 2020 und 2030 festgehalten wird.

Zeige die Erstellung der Daten

# Beispieldatensatz im Wide-Format erstellen
mammals <- data.frame(
  Animal = c("Elephant", "Lion", "Rhinoceros"),
  Weight_2010 = c(4800, 180, 2200),
  Weight_2020 = c(5000, 190, 2300),
  Weight_2030 = c(5200, 195, 2400),
  Weight_2040 = c(4900, 185, 2220)
)

Hier können wir uns den Datensatz anschauen:

Datensatz “mammals”

Umwandeln in das Long-Format mit pivot_longer()

Wir transformieren nun den Datensatz, sodass die Spalten mit den Gewichtsangaben in zwei Spalten umgewandelt werden:

  • Year: Enthält das Jahr (z. B. “2010”, “2020”, “2030”).

  • Weight_kg: Enthält das Gewicht des Tieres in Kilogramm.

mammals_long <- mammals %>%
  pivot_longer(
    cols = starts_with("Weight_"),
    names_to = "Year",
    values_to = "Weight_kg"
  ) %>%
  # Entferne den Präfix "Weight_" aus der Year-Spalte
  mutate(Year = sub("Weight_", "", Year))

mammals_long

Erklärungen:

  • pivot_longer(): Diese Funktion “schmilzt” die Spalten, die mit “Weight_” beginnen, in zwei neue Spalten um.

  • cols = starts_with("Weight_"): Wählt alle Spalten aus, die nun umgewandelt werden sollen.

  • names_to = "Year": Speichert die ursprünglichen Spaltennamen (z. B. “Weight_2010”) in der neuen Spalte Year.

  • values_to = "Weight_kg": Legt den Namen der neuen Spalte fest, in der die Gewichtswerte abgelegt werden.

  • mutate(Year = sub("Weight_", "", Year)): Entfernt den Text “Weight_” aus den Werten in der Spalte Year, sodass nur noch das Jahr übrig bleibt.

Das Ergebnis ist ein Long-Format-Datensatz, in dem jede Zeile eine Gewichtsmessung eines Tieres in einem bestimmten Jahr darstellt. Der Datensatz ist nun länger geworden (hat mehr Zeilen) und schmaler (hat wneiger Spalten).

Komplexeres Beispiel

Jetzt schauen wir uns noch einen komplexeren Datensatz an bei dem es zwei verschiedene inhaltliche Spalten gibt (die Größe und das Gewicht).

Die Komplexität des Pivoting hängt maßgeblich von der Konsistenz der Spaltennamen ab. Es wird dringend empfohlen, eine einheitliche Benennung zu verwenden und klare Trennzeichen wie den Unterstrich _ einzusetzen. Falls die Spaltennamen inkonsistent sind, sollte dies vor dem Pivoting idealerweise mit Hilfe von dplyr::rename() bereinigt werden.

Zeige die Erstellung der Daten

mammals_2 <- data.frame(
  Animal = c("Elephant", "Lion", "Rhinoceros"),
  Region = c("Africa", "Africa", "Asia"),
  Weight_2010 = c(4800, 180, 2200),
  Height_2010 = c(300, 120, 170),
  Weight_2020 = c(5000, 190, 2300),
  Height_2020 = c(320, 130, 180),
  Weight_2030 = c(5200, 195, 2400),
  Height_2030 = c(340, 135, 190)
)

Datensatz “mammals_2”

In diesem Beispiel enthält der Datensatz Informationen zu Gewicht und Höhe von Säugetieren in den Jahren 2010, 2020 und 2030 sowie Angaben zu Tierart und Region.

Wir fassen nun alle Spalten, außer Animal und Region, in zwei neue Spalten zusammen:

  • Measurement: Enthält den Typ der Messgröße, z. B. “Weight” oder “Height”.

  • Year: Enthält das Jahr, das ursprünglich Teil des Spaltennamens war (z. B. “2030”).

Die eigentlichen Werte aus den ursprünglichen Spalten werden in der neuen Spalte “Value” abgelegt. Dadurch wird aus einer Vielzahl von Spalten (wie etwa “Height_2030”, “Weight_2010” usw.) ein einheitlicher Datensatz, in dem jede Zeile eine einzelne Messung eines Tieres darstellt.

mammals_long <- mammals_2 %>%
  pivot_longer(
    cols = -c(Animal, Region),
    names_to = c("Measurement", "Year"),
    names_sep = "_",
    values_to = "Value"
  )

mammals_long

Erklärung:

  • pivot_longer(): Diese Funktion wandelt mehrere Spalten in zwei Spalten um.

  • cols = -c(Animal, Region): Alle Spalten außer Animal und Region werden transformiert.

  • names_to = c("Measurement", "Year"): Die ursprünglichen Spaltennamen werden in zwei Teile aufgespalten – den Messwert-Typ und das Jahr.

  • names_sep = "_": Der Unterstrich (“_“) dient als Trennzeichen für die Spaltennamen und deren Separierung.

  • values_to = "Value": Die Messwerte werden in der neuen Spalte Value gespeichert.

Übungen

In diesem Beispiel hast du einen Datensatz über Früchte, der die Anzahl verkaufter Früchte in verschiedenen Monaten enthält.

Zeige die Erstellung der Daten

fruits_wide <- data.frame(
  fruit = c("appel", "banana", "orange"),
  sales_jan = c(120, 150, 100),
  sales_feb = c(140, 170, 110),
  sales_mar = c(130, 160, 105)
)

Aufgabe: Verwandle den Datensatz “fruits_wide” ins Long-Format, sodass du eine Spalte Monat erhältst (mit den Werten “Jan”, “Feb”, “Mar”) und eine Spalte Verkauf für die Verkaufszahlen.

Otter IconTipp
fruits_long <- fruits_wide %>%
  pivot_longer(
    cols = starts_with("sales_"),
    names_to = "month",
    values_to = "sales"
  ) %>%
  mutate(month = sub("sales_", "", month))
fruits_long

pivot_wider(): Umwandeln von langen in breite Datensätze

In diesem Tutorial lernst du, wie du einen Datensatz zum Thema Pflanzenwachstum im Long-Format mithilfe der Funktion pivot_wider() aus dem tidyr-Paket in das Wide-Format überführst. Im Long-Format erscheinen Beobachtungen in einzelnen Zeilen, während das Wide-Format verschiedene Messgrößen oder Beobachtungszeitpunkte in separaten Spalten darstellt. Dieses Format kann vorteilhaft für bestimmte Datenanalysen und Tabellenübersichten sein.

Einfaches Beispiel

Zunächst schauen wir uns den Datensatz plants_long an, der für verschiedene Pflanzenarten Höhenmessungen in verschiedenen Jahren enthält:

Zeige die Erstellung der Daten

plants_long <- data.frame(
  Species = rep(c("Oak", "Pine", "Birch"), each = 3),
  Year = rep(c("2010", "2020", "2030"), times = 3),
  Height_cm = c(150, 180, 210, 200, 250, 290, 170, 210, 240)
)

Datensatz “plants_long”

Umwandeln in das Wide-Format mit pivot_wider()

Wir transformieren nun den Datensatz, sodass jedes Jahr eine eigene Spalte bildet:

  • 2010, 2020, 2030: Enthalten die gemessenen Pflanzenhöhen je Jahr.
plants_wide <- plants_long %>%
  pivot_wider(
    names_from = Year,
    names_prefix = "height_cm_",
    values_from = Height_cm
  )

plants_wide

Erklärungen:

  • pivot_wider(): Wandelt die Werte aus der Spalte Year in separate Spaltennamen um.
  • names_from = Year: Definiert, welche Spalte als neue Spaltenüberschrift verwendet wird.
  • names_prefix = "height_cm_": Die Spaltennamen werden noch mit dem Prefix “height_cm_” ausgestattet.
  • values_from = Height_cm: Definiert, welche Werte in den neuen Spalten erscheinen.

Der Datensatz ist nun im wide-Format: der Datensatz ist breiter geworden (mehr Spalten) und weniger lang (weniger Zeilen).

Komplexeres Beispiel

Wir betrachten nun einen Datensatz, der neben der Höhe auch die Blattgröße verschiedener Pflanzen in unterschiedlichen Jahren enthält:

Zeige die Erstellung der Daten

plants_long_2 <- data.frame(
  Species = rep(c("Oak", "Pine", "Birch"), each = 6),
  Year = rep(c("2010", "2020", "2030"), each = 2, times = 3),
  Measurement = rep(c("Height_cm", "Leaf_cm"), times = 9),
  Value = c(150, 10, 180, 12, 210, 14, 200, 8, 250, 9, 290, 11, 170, 7, 210, 8, 240, 9)
)

Datensatz “plants_long_2”

Dieser Datensatz enthält zwei Messgrößen pro Pflanze und Jahr: Höhe (Height_cm) und Blattgröße (Leaf_cm).

Jetzt wandeln wir diesen komplexeren Datensatz um, sodass jede Kombination aus Jahr und Messgröße eine eigene Spalte bildet:

plants_wide_2 <- plants_long_2 %>%
  pivot_wider(
    names_from = c(Measurement, Year),
    values_from = Value
  )

plants_wide_2

Erklärung:

  • names_from = c(Measurement, Year): Hier werden die neuen Spalten aus der Kombination von Messgrößen (Measurement) und Jahren (Year) gebildet.
  • values_from = Value: Die Werte der neuen Spalten stammen aus der ursprünglichen Spalte “Value”.

Übungen

In diesem Beispiel erhältst du einen Datensatz über Niederschlagsmengen, gemessen in zwei Städten (Berlin und Hamburg) über drei Monate.

Zeige die Erstellung der Daten

rainfall_long <- data.frame(
  city = rep(c("Berlin", "Hamburg"), each = 3),
  month = rep(c("january", "february", "march"), times = 2),
  rainfall_mm = c(35, 30, 40, 50, 45, 55)
)

Aufgabe: Wandle den Datensatz “rainfall_long” ins Wide-Format um, sodass jeder Monat eine eigene Spalte bildet, welche die Niederschlagswerte enthält.

Otter IconTipp
rainfall_wide <- rainfall_long %>%
  pivot_wider(
    names_from = month,
    values_from = rainfall_mm
  )
rainfall_wide

Datenaufbereitung – Übungen

Datensatz

Wir haben hier einen Datensatz, der Informationen über Studierende sowie ihre Testergebnisse aus verschiedenen Semestern enthält:

Hier können wir uns den Datensatz anschauen, den wir bearbeiten möchten:

students_data

Datenaufbereitung dplyr

Aufgabe: Erstelle eine dplyr Pipe und überschreibe den Datensatz:

1. Wähle nur die ID, den Namen, den Kurs und die Testergebnisse aller Semester aus (aber keine Anwesenheitswerte).

2. Benenne die Spalte “stud_id” in “student_id” um.

4. Überschreibe die Inhalte in “student_id”: die Spalte sollen den Datentyp “numeric” haben.

5. Danach filtere alle Studierenden heraus, die im Fach “Physics” sind.

Zeige dir den Datensatz am Ende an.

Otter IconTipp
students_data <- students_data %>%
  select(stud_id, name, course, starts_with("test_")) %>% 
  rename(student_id = stud_id) %>% 
  mutate(student_id) %>% 
  filter(course != "Physics")
students_data

Pivoting

Hier können wir uns den Datensatz anschauen, den wir bearbeiten möchten:

students_data

Aufgabe: Verwandle den Datensatz “students_data” so, dass eine Spalte “Semester” entsteht (mit Werten “Sem1”, “Sem2”, “Sem3”) und eine Spalte “Test_Score”, in der die Testergebnisse gespeichert sind.

Otter IconTipp
students_data <- students_data %>%
  pivot_longer(
    cols = starts_with("test_"),
    names_to = c(".value", "Semester"),
    names_sep = "_"
  )
students_data

ggplot2 – Plot erstellen

Hinweis: Die Übungen zu dem Kapitel sind im Kapitel Visualisierungen – Übungen

Kapitel

Ideen & Literatur
full_join
Data Mapping
Plot erstellen

ggplot2 ist ein sehr mächtiges Werkzeug zur Visualisierung von Daten.

Eine Visualisierung, also ein Bild, wird nachfolgend “Plot” genannt in Anlehnung an die englische Bezeichnung. Die nachfolgende Struktur ist wichtig um einen Plot zu programmieren mit ggplot2

Schritte zur Erstellung eines Plots:

  1. die Daten müssen definiert werden

  2. Spalten im Datensatz müssen zu grafischen Elementen zugeordnet werden (x-Achse, y-Achse, Farben, Größen, …)

  3. der Diagrammtyp muss festgelegt werden (Histogramm, Liniendiagramm, Balkendiagramm, Streudiagramm, …)

  4. das Diagramm kann individuell angepasst werden (Themen/Designs, zusätzliche Linien, Größen, Farben, Beschriftungen, …)

Ideen & Literatur

Weiterführende Literatur: ggplot2 Buch

Plot Galerie zur Inspiration:

Kunst Galerie Vorschau

Kunst Galerie von Danielle Navarro

Kunst Galerie Vorschau

Beispieldatensatz

Zeige die Erstellung der Daten

set.seed(3)

N = 140

df <- sprtt::draw_sample_normal(2, f = 0.25, max_n = N/2, sd = c(0.8, 1.2), sample_ratio = c(1,1))
df <- df %>% 
  mutate(
    group = factor(x, levels = c(1,2), labels = c("treatment", "placebo")),
    sex = sample(c("female", "male", "unknown"), size = N, replace = TRUE, prob = c(0.50, 0.40, 0.10)),
    age = sample(33:67, size = N, replace = TRUE),
    life_satisfaction = round(10 + y*-0.9 + rnorm(N, sd = 3)),
    depression = round(20 + life_satisfaction*-0.6 + rnorm(N, sd = 3.2)),
    intelligence = round(rnorm(N, 100, 10)),
    finances = cumsum(rnorm(N, 0.1, 1)) + 100
  ) %>% 
  select(-x)

Hier können wir uns den Datensatz anschauen, den wir visualisieren möchten: Der Datensatz befindet sich bereits im long-Format und das ist besonders hilfreich für ggplot2.

Fall der Datensatz in einem anderen Format vorliegt, müssten wir zunächst den Datensatz umstrukturieren. Siehe dazu das Otter Kapitel tidyr - Pivoting.

df

Piping mit +

Man kann die einzelnen Ebenen eines Plots auch pipen - es wird hier jedoch das + Symbol genutzt und nicht wie wir es von dplyr kennen %>%.

df %>% # die Daten werden mit der dplyr Pipe übergeben
  filter() %>% 
  ggplot() + # aber hier wird in die + Pipe gewechselt
  geom_point() +
  theme_bw()

In diesem Beispiel werden die Daten zunächst mit dplyr gepiped und noch gefiltert. Nach dem die ggplot() Funktion aufgerufen wurde wird zu + gewechselt. Danach wird jede Ebene desd Plots mit + hinzugefügt.

Data Mapping

Der erste Schritt ist es die Daten zu definieren und auf die Bildachsen (x-Achse, y-Achse) zu “mappen” (zuzuweisen). Der Plot bleibt damit erst mal noch leer - wir legen hier aber bereits sehr wichtige Grundlagen des Plots fest.

Leere Plots erstellen

Hier ist ein Beispiel bei dem ein leerer Plot erstellt wird und die Daten zu den Achsen zugeordnet werden. Alle Funktionsargumente werden hier ausgeschrieben. Dies ist eine sehr ausführliche Schreibweise. Es hilft für das Verständnis es sich so zu merken.
ggplot(
  data = df, # die Daten werden als Argument übergeben
  mapping = aes(x = group, y = life_satisfaction)
)
Meist sieht man eine verkürzte Schreibweise:
df %>% # die Daten werden mit dplyr übergeben
  ggplot(
    aes(x = group, y = life_satisfaction)
    )

Oft wird mit dplyr davor der Datensatz noch angepasst und direkt an ggplot2 übergeben.

Hier werden z.B. nur die Daten der Frauen visualisiert:
df %>% 
  filter(sex == "female") %>%
  ggplot(
    aes(x = group, y = life_satisfaction)
    )
Wir können nun langsam damit beginnen den Plot nach unseren Wünschen anzupassen. Hier wurde noch eine weitere Ebene mit + gepiped in der das Design (Theme genannt) angepasst wird.
ggplot(
  data = df,
  mapping = aes(x = group, y = life_satisfaction)
) +
theme_bw(base_size = 20)

aesthetics

Wir haben bereits in den oberen Beispielen gesehen, dass die Funktion aes() genutzt wird, um die x- und y-Achsen zu definieren. aes() ist die Kurzschreibweise für aesthetics(). Hier wird definiert, welche Spalten im Datensatz zu welchen Elementen im Plot zugeordnet werden sollen.

ggplot(
  df,
  aes(x = group,        # x-Achse
      y = intelligence, # y-Achse
      colour = age,     # Umrandungsfarbe
      shape = age,      # Form (Kreis, Viereck, ...)
      size = age,       # Größe der Formen
      fill = age,       # Füllfarbe
      alpha = age,      # Transparenz
      group = age,      # Gruppenzuordnung
      linetype = age,   # Linienart
      linewidth = age   # Liniendicke
      )
)

x- und oder y-Achse sind Pflichtargumente, um ein Plot erstellen zu können. Damit können die Daten von 1 bis 2 Variablen (Spalten im Datensatz) innerhalb eines Plots visualisiert werden. Die anderen Argumente wie color oder shape ermöglichen es noch komplexere Zusammenhänge mit weiteren Variablen darzustellen.

Hinweis: Man kann visuell auch mehrere aes-Argumente nutzen für dieselbe Variable. So werden z.B. color und shape oft gleichzeitig einer Variable zugeordnet. Dies hat den Vorteil, dass man auch als schwarz-weiß-Grafik die Gruppen unterscheiden kann.

df %>% 
  ggplot(
    aes(x = group,
        y = life_satisfaction,
        colour = sex,
        shape = sex)
    )

Im nächsten Schritt wählen wir die Art des Plots aus - damit können wir dann auch endlich unsere Daten sehen!

Plot erstellen

Die Art des Plots wird bestimmt durch die geom_*-Funktion also z. B. geom_point.

Hier ist eine Übersicht über die verschiedenen

Geom-Funktion Diagrammtyp Beschreibung (Deutsch)
geom_point() Punktdiagramm (Scatterplot) Zeigt einzelne Datenpunkte – ideal für Zusammenhänge/Verteilungen.
geom_line() Liniendiagramm Verbindet Datenpunkte mit Linien – gut für zeitliche Entwicklungen.
geom_smooth() Trendlinie Fügt eine geglättete Linie (z. B. LOESS oder linear) hinzu.
geom_bar() Balkendiagramm (zähle) Erzeugt Balken basierend auf Häufigkeiten.
geom_col() Balkendiagramm (direkt) Wie geom_bar(), aber mit vorgegebenen y-Werten.
geom_histogram() Histogramm Zeigt die Verteilung numerischer Daten (automatisch gebinnt).
geom_boxplot() Boxplot Visualisiert Median, Quartile, Ausreißer.
geom_violin() Violinplot Kombination aus Boxplot & Dichtekurve.
geom_density() Dichtekurve Schätzung der Wahrscheinlichkeitsdichte einer Verteilung.
geom_area() Flächendiagramm Wie Liniendiagramm, aber mit Fläche unter der Linie gefüllt.
geom_ribbon() Banddiagramm Zeigt Unsicherheitsbereiche (z. B. Konfidenzintervalle).
geom_tile() Heatmap Farbige Kacheln – gut für z. B. Korrelationsmatrizen.
geom_hex() Hexbin-Diagramm Alternative zu Scatterplot mit großen Datenmengen.
geom_jitter() Streudiagramm mit Jitter Punktdiagramm mit zufälligem Versatz gegen Überlagerung.
geom_text() Textbeschriftungen Fügt Textlabels an Datenpunkten hinzu.
geom_label() Beschriftungen mit Rahmen Wie geom_text(), aber mit Hintergrundrahmen.
geom_segment() Liniensegmente Zeichnet Linien zwischen zwei Koordinaten.
geom_abline() Gerade (Achsenabschnitt + Steigung) Fügt einfache Gerade (z. B. Regressionslinie) hinzu.
geom_vline() Vertikale Linie Fügt eine vertikale Linie bei einem festen x-Wert ein.
geom_hline() Horizontale Linie Fügt eine horizontale Linie bei einem festen y-Wert ein.
geom_path() Pfad Wie geom_line(), aber beachtet die Reihenfolge der Punkte.
geom_polygon() Polygon Zeichnet geschlossene Formen – z. B. Karten.
geom_map() Kartendarstellung Für geografische Karten mit Regionen.
geom_sf() Simple Features (Karten) Für räumliche Daten im sf-Format.

Entscheidungshilfe für Plottypen

Anzahl Variablen Art der Variable(n) Empfohlene Plots
1 kontinuierlich Histogramm, Dichtekurve (geom_histogram, geom_density)
1 kategorisch (diskret) Balkendiagramm (geom_bar)
2 kontinuierlich & kontinuierlich Punktdiagramm, Liniendiagramm (geom_point, geom_line)
2 kategorisch & kontinuierlich Boxplot, Violinplot, Jitter (geom_boxplot, geom_violin, geom_jitter)
2 kategorisch & kategorisch Gruppierte Balken, Mosaikplots
>2 Kombinationen Zusätzliche Variablen über Farbe, Form, Facets

geom_point()

Ein Punktdiagramm (Scatterplot) stellt zwei kontinuierliche Variablen gegenüber. Es eignet sich gut zur Darstellung von Zusammenhängen oder Korrelationen.

ggplot(df, aes(x = life_satisfaction, y = depression)) + 
  geom_point()

Wichtige Argumente:

  • alpha für Transparenz

  • color, size, shape zur Darstellung von Zusatzinformationen

ggplot(df,
       aes(x = life_satisfaction,
           y = depression,
           colour = sex,
           shape = sex)) + 
  geom_point()

geom_line()

Ein Liniendiagramm verbindet Datenpunkte entsprechend ihrer Reihenfolge. Besonders nützlich für Zeitreihen oder Trends über eine kontinuierliche Achse.

ggplot(df, aes(x = 1:nrow(df), y = finances)) + 
  geom_line() 

Wichtige Argumente:

  • color, linetype, size
ggplot(df,
       aes(x = 1:nrow(df),
           y = finances,
           colour = sex)) + 
  geom_line() 

Weitere Linien

Es gibt verschiedene Möglichkeiten Linien zu einem Plot hinzuzufügen:

  • geom_abline: diagonale Linien mit einer Steigung (slope) und einem y-Achsenabschnitt (intercept)

  • geom_hline: horizontale Linien mit einem y-Achsenabschnitt

  • geom_vline: vertikale Linien mit einem x-Achsenabschnitt

ggplot(df, aes(x = life_satisfaction, y = depression)) + 
  geom_point() + 
  geom_abline(intercept = 0, slope = 1, color = "red")
ggplot(df, aes(x = life_satisfaction, y = depression)) + 
  geom_point() + 
  geom_hline(yintercept = mean(df$depression), color = "blue")
ggplot(df, aes(x = life_satisfaction, y = depression)) + 
  geom_point() + 
  geom_vline(xintercept = mean(df$depression), color = "green")

geom_smooth()

Zeichnet eine geglättete Linie zur Darstellung von Trends. Standardmäßig wird eine LOESS-Kurve oder lineares Modell gewählt

ggplot(df, aes(x = 1:nrow(df), y = finances)) + 
  geom_smooth() 
`geom_smooth()` using method = 'loess' and formula = 'y ~ x'

Wichtige Argumente:

  • method = "lm" wählt die smoothing-Funktion aus, hier z.B. lineares Modell

  • formula = "y ~ x" Formel, welche das Modell definiert

  • se = TRUE zum Aktivieren/Deaktivieren des Konfidenzbandes

ggplot(df,aes(x = 1:nrow(df), y = finances)) + 
  geom_smooth(method = "lm",
              formula = "y ~ x",
              se = TRUE) 

geom_bar()

Ein Balkendiagramm zählt Häufigkeiten einer kategorialen Variable.

ggplot(df, aes(x = sex)) + 
  geom_bar()  

Wichtige Argumente:

  • fill, color zur Gestaltung

  • position = "dodge" für gruppierte Balken

ggplot(df, aes(x = sex, fill = group)) + 
  geom_bar()  
ggplot(df, aes(x = sex, fill = group)) + 
  geom_bar(position = "dodge")  

geom_col()

Im Gegensatz zu geom_bar() verwendet geom_col() vorgegebene y-Werte (z. B. Mittelwerte). Ideal für gruppierte oder aggregierte Daten.

# Daten werden aufbereitet
df %>% 
  group_by(group) %>% 
  summarise(mean_dep = mean(depression)) %>% 
  # Plot
  ggplot(aes(x = group, y = mean_dep)) + 
  geom_col()

Wichtige Argumente:

  • fill, width, position zur Gestaltung
# Daten werden aufbereitet
df %>% 
  group_by(group) %>% 
  summarise(mean_dep = mean(depression)) %>% 
  # Plot
  ggplot(aes(x = group, y = mean_dep, fill = group)) + 
  geom_col(width = 0.5)

geom_histogram()

Ein Histogramm visualisiert die Verteilung einer kontinuierlichen Variable durch Bins.

ggplot(df, aes(x = depression)) + 
  geom_histogram()
`stat_bin()` using `bins = 30`. Pick better value with `binwidth`.

Wichtige Argumente:

  • bins oder binwidth zum Steuern der Auflösung

  • fill, color zur Gestaltung

ggplot(df, aes(x = depression, group = sex, fill = sex)) + 
  geom_histogram(bins = 20)

geom_boxplot()

Boxplots zeigen Median, Quartile, und Ausreißer – sehr nützlich zum Vergleich von Gruppen.

ggplot(df, aes(x = group, y = depression)) + 
  geom_boxplot() 

Wichtige Argumente:

  • notch = TRUE für Konfidenzintervall des Medians

  • outlier.shape, outlier.colour

ggplot(df, aes(x = group, y = depression, fill = sex)) + 
  geom_boxplot(notch = TRUE) 
Notch went outside hinges
ℹ Do you want `notch = FALSE`?
Notch went outside hinges
ℹ Do you want `notch = FALSE`?

geom_violin()

Eine Kombination aus Boxplot und Dichtekurve – stellt die Verteilung je Gruppe anschaulich dar.

ggplot(df, aes(x = group, y = depression)) + 
  geom_violin()

Wichtige Argumente:

  • draw_quantiles zum Einzeichnen bestimmter Quantile

  • scale = "count" zeigt die Fläche der Violins in Abhängigkeit der Beobachtungen

ggplot(df, aes(x = group, y = depression, fill = sex)) + 
  geom_violin(scale = "count",
              draw_quantiles = c(0.25, 0.5, 0.75))

geom_density()

Zeigt die geschätzte Dichteverteilung einer kontinuierlichen Variable – alternative zum Histogramm.

Wichtige Argumente:

  • adjust beeinflusst die Glättung

  • alpha für Transparenz bei Gruppenvergleichen

ggplot(df, aes(x = depression, fill = group)) + 
  geom_density(alpha = 0.5) 

geom_area()

Stellt eine kumulative Fläche unter einer Linie dar – ideal für Verläufe mit Gesamtmengen.

# Daten werden aufbereitet
df %>%
  count(life_satisfaction) %>% 
  arrange(life_satisfaction) %>% 
# Plot
  ggplot(aes(x = life_satisfaction, y = n)) + 
  geom_area()

geom_ribbon()

Zeigt Konfidenz- oder Streubereiche um eine Linie. Benötigt untere (ymin) und obere (ymax) Grenze.

# Daten werden aufbereitet
df %>%
  group_by(life_satisfaction) %>%
  summarise(m = mean(depression), sd = sd(depression)) %>% 
  mutate(ymin = m - sd, ymax = m + sd) %>% 
# Plot
  ggplot(aes(x = life_satisfaction, y = m)) + 
  geom_ribbon(aes(ymin = ymin, ymax = ymax), alpha = 0.3) + 
  geom_line()

geom_jitter()

Vermeidet Überlagerung diskreter Punkte durch zufälliges Versetzen.

Wichtige Argumente:

  • width, height zur Steuerung der Streuung
ggplot(df, aes(x = group, y = depression)) + 
  geom_jitter(width = 0.1)

ggplot2 – Plot anpassen

Hinweis: Die Übungen zu dem Kapitel sind im Kapitel Visualisierungen – Übungen

Beschriftungen

Sehr häufig möchte man Beschriftungen hinzufügen.

mit labs() kann man die üblichen Textbereiche beshriften: Titel, Achsen-Beschriftungen und weiteres.

Manchmal möchte man im Plot selbst Beschriftungen hinzufügen und deren Position selbst bestimmen - dazu eignet sich geom_text(). Eine Umrandung des Textes kann man einfach mit geom_label() umsetzen.

labs()

df %>%
  ggplot(
    aes(x = group,
        y = depression,
        fill = sex)
    ) +
  geom_boxplot() +
  labs(
  title = "Title of the plot",
  subtitle = "Subtitle of the plot",
  x = "x axis",
  y = "y axis",
  tag = "tag",
  caption = "This is the caption",
  alt = "plot description" # für Screenreader
) 

geom_text()

Fügt numerische oder kategoriale Labels an Positionen hinzu – z. B. zur Beschriftung von Mittelwerten oder Punkten.

Wichtige Argumente:

  • vjust, hjust für die Platzierung

  • check_overlap = TRUE vermeidet Überschneidungen

# Daten werden aufbereitet
df_avg <- df %>% group_by(group) %>% summarise(mean_dep = mean(depression))

# Plot
ggplot(df, aes(x = group, y = depression)) + 
  geom_boxplot() +
  geom_text(
    data = df_avg, # anderer Datensatz wird ausgewählt
    # die Position wird in Abhängigkeit des
    # Mittelwertes gesetzt
    aes(x = group,
        y = mean_dep + 1,
        label = round(mean_dep, 1)),
    color = "red")

geom_label()

Ist wie geom_text(), aber mit Rahmen um den Text. Dies ist nützlich für deutlichere Hervorhebung.

Wichtige Argumente:

  • label.size, label.r (Eckenradius)

  • fill, color zur Gestaltung

# Daten werden aufbereitet
df_medians <- df %>% group_by(group) %>% summarise(median_dep = median(depression))

# Plot
ggplot(df, aes(x = group, y = depression)) + 
  geom_violin(fill = "lightblue") +
  geom_label(data = df_medians,
             aes(x = group,
                 y = median_dep,
                 label = paste0("Median: ", median_dep)))

Themes (Designs)

Das Standarddesign von ggplot2 wird nicht unbedingt als schön wahrgenommen.😉 Zum Glück geht es ganz schnell ein neues Design auszuwählen.

Hier ist eine Übersicht über die vorgefertigten Themes.

Themes

Wenn dir diese Themes nicht ganz gefallen gefallen, nutze theme(), um Details zu ändern.

In diesem Beispiel kannst du gerne mal verschiedene Themes ausprobieren! Ändere einfach den Code und führe es aus. Als erstes Beispiel ist das Design theme_bw() ausgewählt.

ggplot(df, aes(x = group, y = depression)) + 
  geom_boxplot() +
  theme_bw() # hier wird das Theme ausgewählt

Schriftgröße

Die Schriftgröße (base_size) ist ein Argument innerhalb der Theme-Funktionen. Damit kann man schnell alle Schriften vergrößern oder verkleinern.

ggplot(df, aes(x = group, y = depression)) + 
  geom_violin(fill = "lightblue") +
  # Beschriftungen
  labs(x = "Behandlung",
  y = "Depression",
  title = "Wirksamkeit der Behandlung") +
  # Anpassung des Designs und der Schriftgröße
  theme_bw(base_size = 20)

Farbskalen

Wenn das Argumnet aes(fill = variable) genutzt wird, werden automatisch Farben vergeben. Diese Farben gefallen auch nicht jedem und können ausgetauscht werden mit anderen Farbskalen.

Es gibt sehr viele Farbpakete in R, wir stellen hier eins der bekanntesten vor: viridis

Das viridis R Paket enthält diese Skalen.

Abhängig davon, ob deine Variable dem fill oder dem color Argument zugeordnet ist oder ob deine Variable kontinuierlich oder diskret ist, musst du eine andere Funktion wählen.

Achtung: der letzte Buchstabe des Funktionsnamens bestimmt, ob es sich um d eine diskrete oder um c eine kontinuierliche Funktion handelt:

color:

  • diskret: scale_color_viridis_d()
  • kontinuierlich: scale_color_viridis_c()

fill:

  • diskret: scale_fill_viridis_d()

Hier ist ein Beispiel mit den Standardeinstellungen:

ggplot(df, aes(x = group, y = depression, fill = sex)) + 
  geom_boxplot() +
  scale_fill_viridis_d()

Die dunkle Farbe ist doch sehr dunkel, wir können nun durch weitere Argumente bestimmen, wo die Farbskala beginnen und enden soll.

Hier wird der Beginn der Skala mehr in die Mitte verschoben, damit wird die Farbe ein wenig heller.

ggplot(df, aes(x = group, y = depression, fill = sex)) + 
  geom_boxplot() +
  scale_fill_viridis_d(
    option = "viridis",
    begin = 0.3, # 0 ist der Standard
    end = 1      # 1 ist der Standard
  )

Probiere hier gerne andere Werte für end und begin aus und schaue, wie sich die Farben verändern!

Hier haben wir noch eine andere Skala inferno:

ggplot(df, aes(x = group, y = depression, fill = sex)) + 
  geom_boxplot() +
  scale_fill_viridis_d(
    option = "inferno",
    begin = 0.5, # 0 ist der Standard
    end = 1      # 1 ist der Standard
  )

Farben

Hier ist eine Übersicht über alle Standardfarben, die es in R gibt!

Damit kann man auch eigene Farbskalen festlegen! Nutze dafür die Funktionen scale_color_manual oder scale_fill_manual.

ggplot(df, aes(x = group,
               y = depression,
               fill = sex)) + # wir definieren fill
  geom_boxplot() +
  # wir müssen fill auswählen
  scale_fill_manual(values = c("female" = "firebrick", "male" = "gold", "unknown" = "darkcyan"))

Plot speichern

Mit ggsave() können wir ganz einfach unsere Plots speichern. Die Funktion speichert immer das zuletzt erstellte Plot.

Wichtig ist den Pfad anzugeben, wo das Bild gespeichert werden soll - dabei muss auch der Dateiname vollständig angegeben werden.

# Erstellung des Plots
ggplot(df, aes(x = group, y = depression, fill = sex)) + 
  geom_boxplot()
# Speicherung
ggsave(
  "images/plot_example.png",
  width = 35,
  height = 30,
  units = "cm",
  dpi = 300 # Auflösung
)

Praktisch ist auch, dass man ganz leicht die Größe und die Auflösung des Plots bestimmen kann.

Visualisierungen – Übungen

Datensatz

Hier können wir uns den Datensatz anschauen, den wir visualisieren möchten:

df

Boxplots

Wir möchten die Gesundheit der Tiere (health_value) darstellen.

Aufgabe 1: Versuche den angezeigten Plot so ähnlich wie möglich nachzubauen.

Otter IconTipp
ggplot(df, aes(y = health_value)) + 
  geom_boxplot()


Aufgabe 2: Versuche den angezeigten Plot so ähnlich wie möglich nachzubauen.

Otter IconTipp
ggplot(df, aes(y = health_value)) + 
  geom_boxplot() +
  labs(title = "Heath of the animals",
       y = "health score") +
  theme_classic(base_size = 14)


Aufgabe 3: Versuche den angezeigten Plot so ähnlich wie möglich nachzubauen.

Otter IconTipp
ggplot(df, aes(x = animals, y = health_value, fill = animals)) + 
  geom_boxplot() +
  labs(title = "Heath of the animals",
       y = "health score") +
  scale_fill_viridis_d(
    option = "viridis",
    begin = 0.3
  ) +
  theme_classic(base_size = 14)

Balkendiagramme

Wie sieht die Verteilung der Trainings aus? Dabei interessiert uns auch die Geschlechterverteilung.

Aufgabe 1: Versuche den angezeigten Plot so ähnlich wie möglich nachzubauen.

Otter IconTipp
df %>% 
  mutate(training = as.factor(training)) %>% 
  ggplot(aes(x = training)) + 
    geom_bar() +
    theme_light(base_size = 12)


Wir möchten den Plot noch ein wenig schöner machen und auch noch das Geschlecht als Variable hinzunehmen.

Aufgabe 2: Versuche den angezeigten Plot so ähnlich wie möglich nachzubauen.

Otter IconTipp
df %>% 
  mutate(training = as.factor(training)) %>% 
  ggplot(aes(x = training, fill = sex)) + 
    geom_bar() +
    labs(title = "Distribution of the training") +
    scale_fill_viridis_d(
      option = "inferno",
      begin = 0.2
    ) +
    theme_light(base_size = 12)

Punkte & Linien

Wir möchten nun sehen, wie die Gesundheit (health_value) mit dem Blutwert (blood_value) zusammenhängt. Dabei soll auch direkt das Geschlecht noch berücksichtigt werden.

Aufgabe 1: Versuche den angezeigten Plot so ähnlich wie möglich nachzubauen.

Otter IconTipp
ggplot(df, aes(x = health_value, y = blood_value, color = sex)) + 
  geom_point() +
  labs(title = "Distribution of the training") +
  scale_color_viridis_d(
    option = "inferno",
    begin = 0.2,
    end = 0.8
  ) +
  theme_classic(base_size = 12)

In einem nächsten Schritt möchten wir noch lineare Modelle fitten und als Linien für die Geschlechter getrennt hinzufügen. Damit es übersichtlicher wird, entfernen wir alle Datenpunkte, bei denen das Geschlecht unbekannt ist.

Aufgabe 2: Versuche den angezeigten Plot so ähnlich wie möglich nachzubauen.

Otter IconTipp
df %>% 
  filter(sex != "unknown") %>% 
ggplot(aes(x = health_value, y = blood_value, color = sex)) + 
  geom_point() +
  geom_smooth(method = "lm",
              formula = "y ~ x",
              se = TRUE) +
  labs(title = "Distribution of the training") +
  scale_color_viridis_d(
    option = "inferno",
    begin = 0.4,
    end = 0.8
  ) +
  theme_classic(base_size = 12)

dplyr & ggplot2