3  Datenmanipulation

Zuverlässige Informationen sind unbedingt nötig für das Gelingen eines Unternehmens.

Christoph Kolumbus, italienischer Seefahrer

3.1 Import und Export von Daten

Daten werden in Unternehmen in vielen verschiedenen Formen erzeugt und abgelegt. Excel, Softwaretools wie SAP oder moderne Datenbanksysteme – sie alle speichern Daten auf unterschiedliche Art und Weise ab. In R lassen sich zahlreiche Datenformate einlesen und weiterverarbeiten.

3.1.1 Textdateien

Häufig erfolgt ein Datenaustausch über Dateien, welche die Daten als reinen Text enthalten. Nahezu jede Software bietet die Funktionalität, Daten in Textform zu exportieren. Daher ist dieser Weg des Datenaustausches sehr weit verbreitet, auch wenn er nicht der effizienteste ist. Als Data Scientist erhält man dennoch sehr häufig Daten für Analysen im Textformat. Das spezielle Format unterscheidet sich dann vor allem darin, welches Textzeichen zur Trennung der Spalten im Datensatz verwendet wird.

Die Standardfunktion zum Einlesen solcher Textdateien in R ist read.table. Die wichtigsten Argumente der Funktion sind in Tabelle 3.1 aufgelistet.

Tabelle 3.1: Wichtige Argumente der Funktion read.table
Parameter Beschreibung
file Pfad zur Datei, welche eingelesen werden soll (als character)
header Logischer Wert der angibt, ob die erste Zeile die Spaltenüberschriften enthält
sep Trennzeichen für Spalten
dec Dezimaltrennzeichen (z. B. im englischen ., im deutschen ,)
na.strings Character Vektor mit allen möglichen Zeichen, die für fehlende Werte verwendet werden

Im Ordner /home/share/datascience/data/ finden Sie eine Datei namens flights.csv. Diese können wir nun einlesen:

Tabelle 3.2
file <- "/home/share/datascience/data/flights.csv"
df_flights <- read.table(file = file, 
                         header = TRUE, 
                         sep = ",", 
                         dec = ".", 
                         na.strings = c(""))
WichtigAchtung

Wenn Sie einen sehr großen Datensatz wie die Flugdaten einlesen (dieser Datensatz hat 336.776 Zeilen!) wird dieser in einem data.frame Objekt abgelegt. Wollen Sie diesen Datensatz in der Konsole betrachten und tippen einfach den Namen des Data Frames ein, wird der gesamte Inhalt des Datensatzes in der Konsole angezeigt. Bei sehr großen Datenmengen ist das sehr aufwändig, langsam und kann die R Konsole zum Absturz bringen.

Daher sollten Sie zum Anzeigen eines Datensatzes immer die Funktion head oder tail verwenden, um die ersten bzw. letzten Zeilen angezeigt zu bekommen oder sie verwenden str, um einen Überblick über das Objekt zu erhalten (siehe Kapitel 3.2). Beim alternativen Datenformat tibble aus dem dplyr-Paket wird diese Problematik gelöst und standardmäßig immer nur ein Ausschnitt des Datensatzes dargestellt (siehe Kapitel 3.1.3).

Mit der Funktion str können wir uns die Struktur des Datensatzes ansehen

str(df_flights)
'data.frame':   100 obs. of  19 variables:
 $ year          : int  2013 2013 2013 2013 2013 2013 2013 2013 2013 2013 ...
 $ month         : int  1 1 1 1 1 1 1 1 1 1 ...
 $ day           : int  1 1 1 1 1 1 1 1 1 1 ...
 $ dep_time      : int  517 533 542 544 554 554 555 557 557 558 ...
 $ sched_dep_time: int  515 529 540 545 600 558 600 600 600 600 ...
 $ dep_delay     : int  2 4 2 -1 -6 -4 -5 -3 -3 -2 ...
 $ arr_time      : int  830 850 923 1004 812 740 913 709 838 753 ...
 $ sched_arr_time: int  819 830 850 1022 837 728 854 723 846 745 ...
 $ arr_delay     : int  11 20 33 -18 -25 12 19 -14 -8 8 ...
 $ carrier       : chr  "UA" "UA" "AA" "B6" ...
 $ flight        : int  1545 1714 1141 725 461 1696 507 5708 79 301 ...
 $ tailnum       : chr  "N14228" "N24211" "N619AA" "N804JB" ...
 $ origin        : chr  "EWR" "LGA" "JFK" "JFK" ...
 $ dest          : chr  "IAH" "IAH" "MIA" "BQN" ...
 $ air_time      : int  227 227 160 183 116 150 158 53 140 138 ...
 $ distance      : int  1400 1416 1089 1576 762 719 1065 229 944 733 ...
 $ hour          : int  5 5 5 5 6 5 6 6 6 6 ...
 $ minute        : int  15 29 40 45 0 58 0 0 0 0 ...
 $ time_hour     : chr  "2013-01-01 05:00:00" "2013-01-01 05:00:00" "2013-01-01 05:00:00" "2013-01-01 05:00:00" ...

Eine Übersicht über die in der Tabelle enthaltenen Daten finden Sie in Tabelle 3.3 - Tabelle 3.5, wobei jeweils nur die ersten zehn Zeilen (also Flüge) darstellt werden.

Tabelle 3.3: Datensatz mit allen Flügen die in 2013 in NYC (d. h. JFK, LGA or EWR) abgeflogen sind, Spalten 1-6 (Quelle: RITA, Bureau of transportation statistics)
year month day dep_time sched_dep_time dep_delay
2013 1 1 517 515 2
2013 1 1 533 529 4
2013 1 1 542 540 2
2013 1 1 544 545 -1
2013 1 1 554 600 -6
2013 1 1 554 558 -4
2013 1 1 555 600 -5
2013 1 1 557 600 -3
2013 1 1 557 600 -3
2013 1 1 558 600 -2
Tabelle 3.4: Datensatz mit allen Flügen die in 2013 in NYC (d. h. JFK, LGA or EWR) abgeflogen sind, Spalten 7-12 (Quelle: RITA, Bureau of transportation statistics)
arr_time sched_arr_time arr_delay carrier flight tailnum
830 819 11 UA 1545 N14228
850 830 20 UA 1714 N24211
923 850 33 AA 1141 N619AA
1004 1022 -18 B6 725 N804JB
812 837 -25 DL 461 N668DN
740 728 12 UA 1696 N39463
913 854 19 B6 507 N516JB
709 723 -14 EV 5708 N829AS
838 846 -8 B6 79 N593JB
753 745 8 AA 301 N3ALAA
Tabelle 3.5: Datensatz mit allen Flügen die in 2013 in NYC (d. h. JFK, LGA or EWR) abgeflogen sind, Spalten 13-19 (Quelle: RITA, Bureau of transportation statistics)
origin dest air_time distance hour minute time_hour
EWR IAH 227 1400 5 15 2013-01-01 05:00:00
LGA IAH 227 1416 5 29 2013-01-01 05:00:00
JFK MIA 160 1089 5 40 2013-01-01 05:00:00
JFK BQN 183 1576 5 45 2013-01-01 05:00:00
LGA ATL 116 762 6 0 2013-01-01 06:00:00
EWR ORD 150 719 5 58 2013-01-01 05:00:00
EWR FLL 158 1065 6 0 2013-01-01 06:00:00
LGA IAD 53 229 6 0 2013-01-01 06:00:00
JFK MCO 140 944 6 0 2013-01-01 06:00:00
LGA ORD 138 733 6 0 2013-01-01 06:00:00

Da sich in der Praxis einige spezielle Formate für Textdateien durchgesetzt haben, gibt es in R entsprechende Funktionen, welche die read.table Funktion mit voreingestellten Parametern aufrufen (vgl. Tabelle 3.6).

Tabelle 3.6: Varianten der read.table Funktion mit Parameterdefinition für Dateiformate csv und tsv
Funktion Beschreibung sep dec header
read.csv Englisches Komma-getrenntes Format (csv) , . TRUE
read.csv2 Deutsches Komma-getrenntes Format (csv) ; , TRUE
read.delim Englisches Tabulator-getrenntes Format (tsv) \t . TRUE
read.delim2 Deutsches Tabulator-getrenntes (tsv) \t , TRUE

Möchten Sie einen Datensatz im Textformat einlesen (z. B. csv) und möchten zunächst feststellen, wie diese Datei aufgebaut ist (z. B. Spaltentrennzeichen, Dezimalzeichen, etc.), müssen Sie zunächst einen Blick in die Datei werfen. Ist diese ist aber so groß, dass Sie in sich nicht in einem herkömmlichen Editor oder Excel öffnen lässt, können Sie in R die Funktion readLines nutzen, um sich die ersten n Zeilen der Datei anzeigen zu lassen.

Für unseren Flugdatensatz prüfen wir also den Aufbau der Datei durch

readLines(file, n = 3)
[1] "\"year\",\"month\",\"day\",\"dep_time\",\"sched_dep_time\",\"dep_delay\",\"arr_time\",\"sched_arr_time\",\"arr_delay\",\"carrier\",\"flight\",\"tailnum\",\"origin\",\"dest\",\"air_time\",\"distance\",\"hour\",\"minute\",\"time_hour\""
[2] "2013,1,1,517,515,2,830,819,11,\"UA\",1545,\"N14228\",\"EWR\",\"IAH\",227,1400,5,15,2013-01-01 05:00:00"                                                                                                                                   
[3] "2013,1,1,533,529,4,850,830,20,\"UA\",1714,\"N24211\",\"LGA\",\"IAH\",227,1416,5,29,2013-01-01 05:00:00"                                                                                                                                   

Wir können hieraus erkennen, dass im Datensatz als Trennzeichen für die Spalten ein Komma verwendet wurde und außerdem die erste Zeile die Spaltenbezeichnungen enthält. Somit lässt sich das Einlesen der Datei flights.csv vereinfachen zu

df_fligths <- read.csv(file)
rm(df_flights)

3.1.2 Proprietäre Formate

Viele Softwareprodukte verwenden eigene, proprietäre Dateiformat zum Abspeichern von Daten. Für die wichtigsten gibt es Pakete, die das Einlesen solcher Daten ermöglichen.

Hier ein kurzer Überblick über die Möglichkeiten zum Einlesen solcher Dateien:

Excel

Zum Einlesen von Excel Dateien gibt es verschiedene Pakete:

  • XLConnect: Funktionen zum Einlesen, Schreiben und Formatieren von Excel Dateien
  • xlsx: Funktionen zum Einlesen, Schreiben und Formatieren von Excel Dateien
  • gdata: Funktion read.xls zum Einlesen von Excel Dateien
  • readxl (empfohlen): Funktionen zum Einlesen von Excel Dateien

SPSS, SAS, etc.

Für die Dateiformate der gängigen Statistik-Tools können folgende Pakete verwendet werden:

  • foreign: Funktionen zum Einlesen und Schreiben von Dateien der Softwaretools ‘Epi Info’, ‘Minitab’, ‘S’, ‘SAS’, ‘SPSS’, ‘Stata’, ‘Systat’, ‘Weka’
  • sas7bdat: Funktion zum Einlesen von SAS Dateien im Format sas7bdat

3.1.3 Das readr Paket aus dem tidyverse

Das Ziel von readr (vgl. Wickham, Hester, und Bryan (2024) bzw. Wickham (2023)) ist es, eine schnelle und benutzerfreundliche Möglichkeit zum Lesen von Daten in Tabellenform zu bieten. Das Paket bietet einige Vorteile gegenüber den Standardfunktionen aus R:

  • Schnelleres einlesen (bis zu 10mal schneller)
  • Automatisches Konvertieren von Datums- und Zeitformaten
  • Anzeigen einer Fortschrittsanzeige bei längeren Einlesedauern
  • Bessere Anpassung an lokale Spracheinstellungen

Die Verwendung ist dabei sehr einfach: für Standardformate wie csv und tsv gibt es passende Funktionen (read_csv, read_csv2, read_tsv), für allgemeine Textdateien auch eine allgemeine Variante (read_delim). Die Parameter sind ähnlich zu den Standardfunktionen (schauen Sie sich die Hilfe an: readr.tidyverse.org/).

Anders als bei den Standardfunktionen, wird die resultierende Tabelle allerdings nicht als data.frame Objekt abgelegt, sondern als tibble. Ein tibbleoder auch tbl_df ist eine moderne Neuinterpretation des data.frame, bei der beibehalten wurde, was sich als effektiv erwiesen hat, und aussortiert wurde, was nicht hilfreich ist.

Tibbles “faule” und “mürrische” Data Frames: sie tun weniger (d. h. sie ändern nicht automatisch Variablennamen oder -typen) und beschweren sich mehr (z. B. wenn eine Variable nicht existiert). Dies zwingt Sie, Probleme früher zu lösen, was in der Regel zu sauberem, aussagekräftigem Code führt. Tibbles haben auch eine verbesserte print()-Methode, die die Verwendung bei großen Datensätzen mit komplexen Objekten erleichtert. Mehr zu tibbles finden Sie unter r4ds.had.co.nz/tibbles.html.

Lesen wir nun unseren Flugdatensatz ein, nutzen wir also read_csv:

df_flights <- read_csv(file)
df_flights
# A tibble: 336,776 × 19
    year month   day dep_time sched_dep_time dep_delay arr_time sched_arr_time
   <dbl> <dbl> <dbl>    <dbl>          <dbl>     <dbl>    <dbl>          <dbl>
 1  2013     1     1      517            515         2      830            819
 2  2013     1     1      533            529         4      850            830
 3  2013     1     1      542            540         2      923            850
 4  2013     1     1      544            545        -1     1004           1022
 5  2013     1     1      554            600        -6      812            837
 6  2013     1     1      554            558        -4      740            728
 7  2013     1     1      555            600        -5      913            854
 8  2013     1     1      557            600        -3      709            723
 9  2013     1     1      557            600        -3      838            846
10  2013     1     1      558            600        -2      753            745
# ℹ 336,766 more rows
# ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <dbl>,
#   tailnum <chr>, origin <chr>, dest <chr>, air_time <dbl>, distance <dbl>,
#   hour <dbl>, minute <dbl>, time_hour <dttm>

An der Ausgabe können wir erkennen, dass der Datensatz nun als tibble Objekt hinterlegt ist. Das Einlesen des Datensatzes war sehr viel schneller, außerdem wurde nun die Spalte time_hour als Datentyp dttm erkannt und nicht als Text bzw. factor.

3.1.4 Export von Daten

Neben dem Import ist es ebenso wichtig, nach einer durchgeführten Analyse Ergebnisse wieder zu exportieren. Wir hatten schon gesehen, wie man R Objekte in dem speziellen Datenformat .RData für die spätere Verwendung in R abspeichern kann (vgl. Kapitel 2.1).

Datensätze (also Data Frames), die später mit anderer Software weiterverarbeitet werden sollen (z. B. Excel), lassen sich auch in anderen Formaten exportieren. Die einfachste Möglichkeit ist im Textformat (z. B. .csv) über die Funktion write.csv (Base R) oder write_csv (aus dem readr Paket). Die in diesem Kapitel vorgestellen Pakete zum Import von Daten aus Excel und anderen proprietären Formaten bieten in der Regel ebenfalls eine Möglichkeit zum Export an.

3.1.5 Import und Export von Daten mit Hilfe der Benutzeroberfläche von RStudio

Daten lassens ich neuerdings in RStudio auch über die Benutzeroberfläche importieren. Hierzu gibt es im Environment Reiter der Oberfläche einen Button mit dem Namen “Import Dataset” (siehe Abbildung 3.1).

Abbildung 3.1: Import von Daten mit Hilfe der Benutzeroberfläche von RStudio

Für den Import hat man die Auswahl zwischen verschiedenen Formaten. Für Textdateien bietet sich die Option mit Nutzung von readr an. Wählt man hier eine zu importierende Datei aus, öffnet sich ein Fenster mit einem ersten Überblick über die Daten und der Möglichkeit, die verschiedenen Einstellungen für den Import vorzunehmen (z. B. Festlegen des Trennzeichens, Festlegen des Datentyps je Spalte etc.; vgl. Abbildung 3.2).

Abbildung 3.2: Import von Daten mit Hilfe der Benutzeroberfläche und readr

Bestätigt man in diesem Fenster den Import, so wird der entsprechende Code in die Konsole kopiert und ausgeführt. Man hat dann die Möglichkeit, den Code für spätere Ausführungen in ein eigenes Skript zu übernehmen.

VorsichtÜbung

Einlesen von Daten

Die Datei /home/share/datascience/data/zaehlstelle_neutor_2017.csv enthält die stündlichen Ergebnisse der Verkehrszählung für Fahrräder an der Zählstelle Neutor in Münster für 2017 (Sie müssen dabei xx123456 durch Ihre FH Kennung ersetzen).

  1. Betrachten Sie mit Hilfe der readLines Funktion die ersten Zeilen der Datei, um zu ermitteln, um welches Format es sich handelt.
  2. Importieren Sie nun die Datei und verwenden Sie dabei nicht die Benutzeroberfläche von RStudio.
  3. Legen Sie die ersten 10 Zeilen des Datensatzes in einem neuen Data Frame ab und exportieren Sie diesen in Ihr Arbeitsverzeichnis.

3.2 Schnelle Einblicke in Datensätze

Haben Sie nun einen Datensatz eingelesen, möchten Sie typischerweise zunächst prüfen, ob alle Daten korrekt importiert wurden. Hierzu gibt es einige nützliche Funktionen, mit denen Sie sich schnell einen Überblick verschaffen können.

  • str: fasst die Struktur des Datensatzes zusammen, gibt also Variablen/Spalten mit Datentyp zurück und einige Beobachtungen
  • names: gibt die Variablennamen/Spaltenüberschriften zurück
  • dim, nrow, ncol: gibt die Dimensionen des Datensatzes zurück (Zeilen und Spalten, Zeilen, Spalten)
  • head: zeigt Ihnen die ersten n Zeilen des Datensatzes an (Standardeinstellung ist n=6)
  • tail: zeigt Ihnen die letzten n Zeilen des Datensatzes an (Standardeinstellung ist n=6)
  • summary: gibt je Spalte eine Zusammenfassung zurück (für metrische Variablen Minimum, Maximum, Median, arithmetisches Mittel sowie 1. und 3. Quantil, für factor Variablen die absolute Häufigkeit der 6 häufigsten Faktorlevel)
VorsichtÜbung

Datenübersicht

  1. Ermitteln Sie die Datentypen der Spalten des eingelesenen Datensatzes der Verkehrszählstelle Neutor.
  2. Wie viele Zeilen und wie viele Spalten hat der Datensatz?
  3. Betrachten Sie die summary des Datensatzes. Was fällt Ihnen auf?

3.3 Data Wrangling

Mit Data Wrangling (engl. Gerangel, Hickhack) bezeichnen wir hier den Prozess der Datenaufbereitung. In diesem Prozess werden verschiedene Verarbeitungsschritte durchgeführt, um einen Datensatz in ein Format zu bringen, das für eine bestimmte Analyse benötigt wird. Hierzu können viele Standardfunktionen in R verwendet werden; Sie haben beispielsweise schon Möglichkeiten kennengelernt, Variablen oder Beobachtungen auszuwählen (per Subsetting mit Hilfe von [ , ] oder $, siehe Kapitel 2.2.2). Neben diesen Standardfunktionen wurde das dplyr Paket (Wickham u. a. 2023) speziell dazu entwickelt, die Datenmanipulation zu vereinfachen.

3.3.1 Tidy Data

Als Ausgangsbasis für die Verarbeitungsschritte wird im Folgenden ein “sauberer” (engl. tidy) Datensatz erwartet. Ein solcher Datensatz zeichnet sich dadurch aus, dass jede Zeile eine Beobachtung enthält, jede Spalte eine Variable und in den Zellen die Beobachtungen stecken (vgl. Abbildung 3.3).

Abbildung 3.3: Ein sauberer Datensatz: Variablen in den Spalten, Beobachtungen in den Zeilen, Werte in den Zellen (aus Wickham und Grolemund (2017))

Dies ist allerdings nicht immer der Fall, wie das folgende Beispiel zeigt.

Tabelle 3.7 gibt die Tuberkulose Fälle und die Bevölkerung in den Jahren 1999 und 2000 in Afghanistan, Brasilien und China an. Die Spalte count enthält dabei nicht eine eindeutige Variable, sie enthält sowohl die Bevölkerungszahlen als auch die Anzahl der Tuberkulosefälle.

Tabelle 3.7: Beispiel für einen unsauberen Datensatz: Tuberkulosefälle pro Jahr und Land (Quelle: World Health Organization Global Tuberculosis Report)
country year type count
Afghanistan 1999 cases 745
Afghanistan 1999 population 19987071
Afghanistan 2000 cases 2666
Afghanistan 2000 population 20595360
Brazil 1999 cases 37737
Brazil 1999 population 172006362
Brazil 2000 cases 80488
Brazil 2000 population 174504898
China 1999 cases 212258
China 1999 population 1272915272
China 2000 cases 213766
China 2000 population 1280428583

Eine saubere Variante desselben Datensatzes findet sich in Tabelle 3.8. Hier sind Variablen und Beobachtungen sauber in Spalten und Zeilen getrennt.

Tabelle 3.8: Beispiel für einen sauberen Datensatz: Tuberkulosefälle pro Jahr und Land (Quelle: World Health Organization Global Tuberculosis Report)
country year cases population
Afghanistan 1999 745 19987071
Afghanistan 2000 2666 20595360
Brazil 1999 37737 172006362
Brazil 2000 80488 174504898
China 1999 212258 1272915272
China 2000 213766 1280428583
TippTipp

Im tidyverse gibt es auch einige Werkzeuge, um unsaubere Datensätze in saubere Datensätze zu transformieren. Besonders nützlich ist dafür das Paket tidyr. Eine gute Einführung finden Sie unter tidyr.tidyverse.org/.

3.3.2 Operationen mit Daten

Im Folgenden werden die essentiellen Operationen für Datensätze erläutert. Diese werden im Datenaufbereitungsprozess in der Regel kombiniert, um das gewünschte Ergebnis zu erreichen.

WichtigLeitentscheidung

In diesem Kapitel verwenden wir dplyr als Standardsyntax für Datenmanipulation. Base-R-Subsetting, merge und andere Standardfunktionen bleiben als Hintergrundwissen wichtig, werden hier aber nur noch als kompakte Referenz mitgeführt.

Tabelle 3.9: Kurzübersicht: Base R und dplyr bei zentralen Wrangling-Operationen
Aufgabe Base R dplyr Kommentar
Beobachtungen auswählen df[df$arr_delay > 120, ] filter(df, arr_delay > 120) weniger Zeilen, gleiche Variablen
Variablen auswählen df[c("year", "month")] select(df, year, month) weniger Spalten, gleiche Zeilen
Neue Variablen erzeugen df$delay_total <- ... mutate(df, delay_total = ...) gleiche Zeilenanzahl
Daten aggregieren mean(df$arr_delay, na.rm = TRUE) summarise(df, avg_delay = mean(arr_delay, na.rm = TRUE)) Kennzahlen statt Rohdaten
Tabellen verknüpfen merge(x, y, by = "carrier", all.x = TRUE) left_join(x, y, by = "carrier") Schlüssel bewusst wählen

Beobachtungen auswählen

Im Wrangling-Alltag ist filter(...) der Standardweg, um Beobachtungen auszuwählen. Die Funktion behält genau die Zeilen, für die die angegebenen Bedingungen TRUE ergeben. Das Resultat ist wieder ein Datensatz mit denselben Variablen, aber weniger Zeilen (vgl. Abbildung 3.4).

Abbildung 3.4: Visuelle Darstellung der Auswahl von Beobachtungen (Quelle: RStudio Data Wrangling Cheat Sheet)
df_2hdelay <- df_flights %>%
  filter(arr_delay > 120)

df_stipro <- df_flights %>%
  slice_sample(prop = 0.5)

Neben filter(...) sind im aktuellen dplyr-Workflow insbesondere distinct(...), slice_sample(...), slice_head(...) und slice_max(...) nützlich. Base R kann dieselbe Auswahl natürlich weiterhin über logische Indizes oder Zeilennummern ausdrücken; für die Lehrlogik dieses Kapitels arbeiten wir aber mit filter(...).

Variablen auswählen

Möchte man bestimmte Variablen auswählen, erfolgt dies in einem sauberen Datensatz über die Spalten. Man reduziert also die Anzahl der Spalten des Datensatzes (siehe Abbildung 3.5). Dafür verwenden wir hier select(...).

Abbildung 3.5: Visuelle Darstellung der Auswahl von Variablen (Quelle: RStudio Data Wrangling Cheat Sheet)
df_flights %>%
  select(year, month, day, origin, dest, arr_delay)

Hilfsfunktionen wie starts_with(...), ends_with(...), contains(...) oder everything() helfen später bei größeren Tabellen. Base R kann dieselbe Auswahl z. B. mit df_flights[c("year", "month")] ausdrücken; in diesem Kapitel ist select(...) aber der Hauptweg.

Variablen erzeugen

Wenn wir aus bestehenden Variablen neue berechnen wollen, erzeugen wir also eine neue Spalte im Datensatz (vgl. Abbildung 3.6). In dplyr ist mutate(...) dafür der zentrale Befehl.

Abbildung 3.6: Visuelle Darstellung der Erzeugung neuer Variablen(Quelle: RStudio Data Wrangling Cheat Sheet)

mutate(...) berechnet neue Werte pro Zeile und hängt sie an den Datensatz an. Die Anzahl der Zeilen bleibt also gleich. Häufig werden dabei sogenannte Window-Funktionen verwendet, die auf ganze Spalten blicken, aber wieder ein Ergebnis pro Zeile zurückgeben (vgl Abbildung 3.7).

Abbildung 3.7: Visuelle Darstellung von Windowfunktionen (Quelle: RStudio Data Wrangling Cheat Sheet)

Solche Funktionen können z. B. die Summe aus mehreren Spalten oder der Rang einer Variable sein. So lässt sich also die Verspätung, die sich während eines Fluges unabhängig vom (möglicherweise) verspäteten Start angehäuft hat, berechnen als

mutate(df_flights, delay_on_flight = arr_delay - dep_delay)

Ein weiteres Beispiel für eine Window-Funktion ist ein Rang:

df_flights %>%
  mutate(delay_rank = dense_rank(arr_delay))

Die Base-R-Kurzreferenz dafür ist eine direkte Zuweisung wie df_flights$delay_on_flight <- .... Für saubere Wrangling-Pipelines ist mutate(...) in der Regel lesbarer und besser kombinierbar.

Daten aggregieren

Bei der Aggregation von Daten wird die Information aus mehreren oder sogar allen Zeilen in einem Wert zusammengefasst. Hierzu wird entsprechend eine sogenannte Summary- bzw. Aggregationsfunktion verwendet (vgl. Abbildung 3.8).

Abbildung 3.8: Visuelle Darstellung von Aggregationsfunktionen (Quelle: RStudio Data Wrangling Cheat Sheet)

Eine Aggregation reduziert damit die Anzahl der Zeilen des Datensatzes. Bei einer Zusammenfassung über alle Zeilen hat der resultierende Datensatz nur noch eine Zeile (vgl. Abbildung 3.9).

Abbildung 3.9: Visuelle Darstellung der Aggregation von Daten (Quelle: RStudio Data Wrangling Cheat Sheet)

Im dplyr-Workflow ist summarise(...) die Standardfunktion für solche Verdichtungen:

df_flights %>%
  summarise(
    max_delay = max(arr_delay, na.rm = TRUE),
    avg_delay = mean(arr_delay, na.rm = TRUE)
  )

Eine konkrete Fragestellung lässt sich dann direkt in einer Pipeline formulieren:

df_flights %>% 
  filter(origin == "JFK", month == 1, arr_delay >= 0) %>%  
  summarise(avg_delay = mean(arr_delay, na.rm = TRUE))
# A tibble: 1 × 1
  avg_delay
      <dbl>
1      31.0

Als Base-R-Referenz entspräche dies typischerweise einer Funktion wie mean(...) auf einem logisch gefilterten Vektor. Im Kapitel verwenden wir dafür jedoch konsistent summarise(...).

TippTipp

In dplyr lassen sich mehrere aufeinanderfolgende Bearbeitungsschritte für denselben Datensatz mit Hilfe von sogenannten Pipes verknüpfen. Das hilft bei der Lesbarkeit und Nachvollziehbarkeit der Berechnungsschritte enorm.

In diesem Kapitel verwenden wir dafür die dplyr-Pipe %>%. Das Objekt auf der linken Seite der Pipe wird als erstes Argument der Funktion auf der rechten Seite verwendet. Die Pipe leitet also den Input in die Funktion weiter, wie eine Leitung (engl. pipe).

In einfachen Fällen ist das analog zur Base-R-Pipe |> aus Kapitel 2.4, z. B. x %>% sqrt() und x |> sqrt().

Somit ist also x %>% f(y) dasselbe wie f(x,y). Besonders hilfreich wird dies bei einer Aneinanderreihung von Operationen, die ohne Pipes schnell sehr verschachtelt und schwer verständlich wird: f(g(h(x))) ist das gleiche wie x %>% h() %>% g() %>% f().

Variablen gruppieren

Sehr häufig möchte man die Berechnung von neuen Variablen oder die Aggregation nicht über den gesamten Datensatz durchführen, sondern gruppenweise für jeweils zusammengehörende Teile des Datensatzes (z. B. je Kundensegment, Land oder Business Unit). Dafür kombinieren wir group_by(...) mit summarise(...) oder mutate(...).

Abbildung 3.10: Visuelle Darstellung der Aggregation von Daten nach Gruppen (Quelle: RStudio Data Wrangling Cheat Sheet)

Die durchschnittliche Verspätung je Flughafen in NYC im Januar 2013 ergibt sich damit sehr kompakt:

df_flights %>% 
  filter(month == 1, arr_delay >= 0) %>%  
  group_by(origin) %>% 
  summarise(avg_delay = mean(arr_delay, na.rm = TRUE))
# A tibble: 3 × 2
  origin avg_delay
  <chr>      <dbl>
1 EWR         38.2
2 JFK         31.0
3 LGA         27.2

Base R kennt für gruppierte Berechnungen u. a. tapply, by oder aggregate. Für eine einheitliche Arbeitslogik verwenden wir hier aber group_by(...) plus Folgeverb.

So können wir beispielsweise schnell herausfinden, wie viele Flugzeuge pro Tag mehr als einen Start in New York hatten:

df_flights %>% 
  count(year, month, day, tailnum) %>% 
  filter(n > 1 & !is.na(tailnum)) %>% 
  arrange(desc(n))
# A tibble: 64,682 × 5
    year month   day tailnum     n
   <dbl> <dbl> <dbl> <chr>   <int>
 1  2013     1    16 N15572      5
 2  2013     1    28 N26549      5
 3  2013     2     2 N329JB      5
 4  2013     2     5 N546MQ      5
 5  2013     2     8 N351JB      5
 6  2013     2    10 N197JB      5
 7  2013     2    11 N713MQ      5
 8  2013     2    11 N840MQ      5
 9  2013     2    15 N732US      5
10  2013     2    18 N296JB      5
# ℹ 64,672 more rows
Abbildung 3.11: Die United Express Maschine vom Typ Embraer ERJ-145LR mit Tail Number ‘N15572’ mit 5 Starts am 16.1.2013 von New York (Quelle: jetphotos.com)

Auch Window-Funktionen können gruppenweise angewendet werden. Die Anzahl der Zeilen des Datensatzes bleibt wieder konstant, aber die Berechnung der neuen Variable erfolgt separat für die verschiedenen Gruppen (vgl. Abbildung 3.12).

Abbildung 3.12: Visuelle Darstellung der Erzeugung neuer Variablen nach Gruppen (Quelle: RStudio Data Wrangling Cheat Sheet)

Möchten wir beispielsweise eine neue Variable berechnen, indem der Verspätung jedes Fluges ein Rang zugewiesen wird, allerdings separat für jeden Flughafen, nutzen wir wieder dplyr und erhalten mit folgendem Statement die Flugnummern der Flüge mit der größten Verspätung je Flughafen:

df_flights %>%
  filter(month == 1, arr_delay >= 0) %>%
  group_by(origin) %>% 
  mutate(delay_rank_per_origin = dense_rank(arr_delay), 
         flight_number = paste0(carrier, flight)) %>% 
  select(flight_number, arr_delay, 
         delay_rank_per_origin) %>% 
  arrange(desc(arr_delay))
# A tibble: 11,655 × 4
# Groups:   origin [3]
   origin flight_number arr_delay delay_rank_per_origin
   <chr>  <chr>             <dbl>                 <int>
 1 JFK    HA51               1272                   215
 2 EWR    MQ3695             1109                   265
 3 JFK    MQ3944              851                   214
 4 JFK    DL269               612                   213
 5 EWR    B6517               497                   264
 6 LGA    DL2119              486                   189
 7 EWR    EV4321              456                   263
 8 LGA    UA544               394                   188
 9 JFK    9E4019              370                   212
10 JFK    AA179               368                   211
# ℹ 11,645 more rows

Datensätze kombinieren

Häufig muss ein Data Scientist sich Daten aus vielen verschiedenen Quellen zusammensuchen und zu einem verwendbaren Datensatz zusammenführen. Haben zwei Datensätze exakt die gleiche Form, lassen sich Zeilen mit rbind(...) anhängen; enthalten sie bei identischer Reihenfolge unterschiedliche Spalten, kann cbind(...) sinnvoll sein. In der Praxis ist die Zusammenführung meist komplexer und läuft über fachliche Schlüsselvariablen.

Wenn alle einzelnen Datenquellen flache Tabellen sind, lässt sich dies besonders gut in relationalen Datenbanken mit Hilfe von SQL oder in R mit Join-Funktionen ausdrücken. Die folgenden Typen von Joins betrachten wir im Weiteren (vgl. auch Abbildung 3.13):

  • (Inner) Join: Gibt alle Beobachtungen zurück, die in beiden Datensätzen auftauchen
  • Left (Outer) Join: Gibt alle Beobachtungen des linken Datensatzes und die dazugehörigen Beobachtungen aus dem rechten Datensatz zurück
  • Right (Outer) Join: Gibt alle Beobachtungen des rechten Datensatzes und die dazugehörigen Beobachtungen aus dem linken Datensatz zurück
  • Full (Outer) Join: Gibt alle Beobachtungen zurück, die in einem der beiden Datensätze auftauchen
Abbildung 3.13: Visuelle Darstellung der Zusammenführung von Datensätzen (Quelle: Wickham und Grolemund (2017), vgl. r4ds.had.co.nz)

Als Beispiel für die Verknüpfung von mehreren Tabellen betrachten wir im Folgenden zusätzliche Tabellen zu unseren Flugdaten der New Yorker Flughäfen aus dem Jahr 2013. Im Paket nycflights13 befinden sich neben den Daten der Flüge (flights) vier weitere Tabellen:

  • airlines: gibt die Namen der Fluggesellschaft für das entsprechende Kürzel an
  • airports: gibt detaillierte Informationen zum Flughafen für das entsprechende Kürzel an
  • planes: gibt detaillierte Informationen zum Flugzeug für die entsprechende Tail Number an
  • weather: enthält für jede Stunde und an jedem New Yorker Flughafen die entsprechenden Wetterdaten

Die Zusammenhänge zwischen den einzelnen Tabellen lassen sich wie in Abbildung 3.14 darstellen.

Abbildung 3.14: Visuelle Darstellung der Beziehungen zwischen den Tabellen im nycflights13 Datensatz (Quelle: Wickham und Grolemund (2017), vgl. r4ds.had.co.nz)

Als Base-R-Referenz kann ein Left Join mit merge(x, y, by = "carrier", all.x = TRUE) ausgedrückt werden. Für saubere und gut lesbare Datenpipelines verwenden wir in diesem Kapitel jedoch die Join-Funktionen aus dplyr.

Möchten wir beispielsweise die Namen der Airlines wieder an die Tabelle der Flugdaten spielen, erfolgt dies mit

flights_airlines <- flights %>% left_join(airlines, by = "carrier")
head(flights_airlines)
# A tibble: 6 × 20
   year month   day dep_time sched_dep_time dep_delay arr_time sched_arr_time
  <int> <int> <int>    <int>          <int>     <dbl>    <int>          <int>
1  2013     1     1      517            515         2      830            819
2  2013     1     1      533            529         4      850            830
3  2013     1     1      542            540         2      923            850
4  2013     1     1      544            545        -1     1004           1022
5  2013     1     1      554            600        -6      812            837
6  2013     1     1      554            558        -4      740            728
# ℹ 12 more variables: arr_delay <dbl>, carrier <chr>, flight <int>,
#   tailnum <chr>, origin <chr>, dest <chr>, air_time <dbl>, distance <dbl>,
#   hour <dbl>, minute <dbl>, time_hour <dttm>, name <chr>

Ein typischer Workflow ist, nach dem Join sofort die fachlich relevanten Variablen auszuwählen und das Ergebnis kritisch zu prüfen:

ewr_delays <- flights %>% 
  filter(origin == "EWR", dep_delay > 15) %>% 
  group_by(carrier) %>% 
  summarise(
    n_delayed = n(),
    mean_arr_delay = mean(arr_delay, na.rm = TRUE)
  ) %>% 
  left_join(airlines, by = "carrier") %>% 
  arrange(desc(n_delayed))

ewr_delays
# A tibble: 12 × 4
   carrier n_delayed mean_arr_delay name                    
   <chr>       <int>          <dbl> <chr>                   
 1 EV          12978           68.5 ExpressJet Airlines Inc.
 2 UA          10050           49.1 United Air Lines Inc.   
 3 WN           1660           58.9 Southwest Airlines Co.  
 4 B6           1362           74.7 JetBlue Airways         
 5 DL            753           78.2 Delta Air Lines Inc.    
 6 AA            576           66.1 American Airlines Inc.  
 7 MQ            565           74.8 Envoy Air               
 8 US            482           61.4 US Airways Inc.         
 9 VX            253           74.7 Virgin America          
10 9E            168           69.4 Endeavor Air Inc.       
11 AS             94           50.8 Alaska Airlines Inc.    
12 OO              1          157   SkyWest Airlines Inc.   

Mehrere Joins lassen sich analog verketten. Dabei sollte nach jedem Schritt geprüft werden, ob Schlüssel, Zeilenzahl und Granularität des Ergebnisses noch zur fachlichen Frage passen.

VorsichtÜbung

Data Wrangling

Bearbeiten Sie die folgenden Aufgaben vorzugsweise mit dplyr.

  1. Ermitteln Sie mit Hilfe des Datensatzes der Verkehrszählstelle am Neutor, an welchem Wochentag durchschnittlich die meisten Radfahrer die Zählstelle passieren.
  2. Zu welchen Uhrzeiten gibt es den meisten Radverkehr stadteinwärts bzw. stadtauswärts? Nehmen Sie hierfür die Funktion strptime zu Hilfe.
  3. Welche Airline hatte 2013 den größten Anteil an Flügen (in Prozent) mit einem verspäteten Abflug vom Flughafen Newark (EWR) von mehr als 15 Minuten? Wie lautet der Name der Airline?
  4. Welcher Tag hatte die höchsten Windgeschwindigkeiten in 2013 (je Flughafen) und wie unterscheidet sich die durchschnittliche Verspätung des Abflugs im Vergleich mit allen übrigen Tagen?

3.4 Relationale Datenbanken

Anstatt Daten aus lokalen Dateien einzulesen, kann man sich mit Hilfe von R auch direkt mit Datenbanken (lokal oder remote) verbinden, um dort Queries auszuführen und Daten zu importieren. Hierzu können die Schnittstellen für Datenbanken und die Treiber der verschiedenen Systeme verwendet werden (z. B. ODBC Treiber für PostgreSQL, MySQL, SQLServer, Oracle, etc.).

Zur Verbindung mit Datenbanken benötigt man das Backend-Paket DBI installieren. Das DBI-Paket bietet eine gemeinsame Schnittstelle, die ermöglicht, mit vielen verschiedenen Datenbanken mit dem gleichen Code zu arbeiten. Sie müssen ein bestimmtes Backend für die Datenbank installieren, mit der Sie sich verbinden möchten.

Die am meisten verbreiteten Backends sind:

  • RMySQL für Verbindungen zu MySQL und MariaDB
  • RPostgreSQL für Verbindungen zu Postgres und Redshift
  • RSQLite enthält eine SQLite Datenbank
  • bigrquery für Verbindungen zu Google’s BigQuery
  • odbc stellt eine Verbindung zu vielen kommerziellen Datenbanksystemen über das “Open Database Connectivity Protocol” her

Das dbplyr Paket bietet die Möglichkeit, die Data Wrangling Syntax wie die Operatoren %>% und weitere Funktionen des Pakets dplyr für Operationen in der Datenbank zu verwenden (vgl. https://db.rstudio.com/dplyr/).