7 – Datenaufbereitung

Statistische Datenanalyse mit R

Autor

Clemens Brunner

Veröffentlicht

17. November 2022

Daten umformen

Es gibt in der Praxis (mindestens) zwei verschiedene Möglichkeiten, ein und dieselben Daten in einer Tabelle darzustellen, und zwar entweder im Wide-Format oder im Long-Format. Zur Berechnung deskriptiver Statistiken eignet sich das Wide-Format besser, das Long-Format ist aber bei anderen Aufgabenstellungen notwendig. Deswegen ist es sinnvoll, wenn man weiß, wie man das eine Format in die andere Darstellung umwandeln kann.

Bei Daten im Wide-Format gibt es für jede Variable eine eigene Spalte. Daten im Long-Format haben nur eine Spalte, die alle Werte beinhaltet, und eine oder mehrere Spalte(n) mit Indikator-Variablen, welche den Kontext der Werte definieren. Die folgende Tabelle zeigt Beispieldaten im Wide-Format:

Person Age Weight Height
Bob 32 98 188
Al 24 61 176
Sue 64 87 174

Man sieht, dass es drei Wertespalten gibt (Age, Weight und Height), sowie eine Spalte, welche die Person identifiziert (Person). Dieselben Daten sehen im Long-Format so aus (man beachte, dass es nur eine einzige Werte-Spalte namens Value gibt):

Person Variable Value
Bob Age 32
Bob Weight 98
Bob Height 188
Al Age 24
Al Weight 61
Al Height 176
Sue Age 64
Sue Weight 87
Sue Height 174

In R kann man mit dem Paket tidyr zwischen den beiden Formaten hin- und herwechseln, d.h. wenn die Daten in einem Format vorliegen, kann man relativ einfach das andere Format produzieren. Die Daten aus dem Beispiel oben können wir zunächst einmal im Wide-Format erzeugen:

library(tibble)

(df = tibble(
    Person=c("Bob", "Al", "Sue"),
    Age=c(32, 24, 64),
    Weight=c(98, 61, 87),
    Height=c(188, 176, 174)
))
# A tibble: 3 × 4
  Person   Age Weight Height
  <chr>  <dbl>  <dbl>  <dbl>
1 Bob       32     98    188
2 Al        24     61    176
3 Sue       64     87    174

Das Paket tidyr aus dem Tidyverse beinhaltet die Funktionen pivot_longer() und pivot_wider(), welche Data Frames von wide nach long bzw. von long nach wide umwandeln können. Das Data Frame df (welches im Wide-Format vorliegt) können wir also wie folgt ins Long-Format konvertieren:

library(tidyr)

(df_long = pivot_longer(
    df,
    Age:Height,
    names_to="Variable",
    values_to="Value"
))
# A tibble: 9 × 3
  Person Variable Value
  <chr>  <chr>    <dbl>
1 Bob    Age         32
2 Bob    Weight      98
3 Bob    Height     188
4 Al     Age         24
5 Al     Weight      61
6 Al     Height     176
7 Sue    Age         64
8 Sue    Weight      87
9 Sue    Height     174

Hier übergibt man zuerst die Daten im Wide-Format (df), gefolgt von einer Auswahl der Spalten, die man ins Long-Format bringen möchte – in unserem Beispiel sind das die Spalten von Age bis Height. Für diese Spaltenauswahl kann man die Spaltennamen ohne Anführungszeichen schreiben, und auch der Doppelpunkt funktioniert wie bei Zahlensequenzen. Man könnte hier auch die Indizes der Spalten verwenden, also 2:4 statt Age:Height. Schließlich gibt man mit names_to den gewünschten Namen der Indikatorspalte an (im Beispiel soll diese also Variable heißen) und mit values_to den Namen der Werte-Spalte (Value).

Der umgekehrte Weg wird mit pivot_wider() beschritten; diese Funktion kann eine Spalte auf mehrere Spalten aufteilen, d.h. vom Long-Format ins Wide-Format konvertieren:

(df_wide = pivot_wider(
    df_long,
    id_cols=Person,
    names_from=Variable,
    values_from=Value
))
# A tibble: 3 × 4
  Person   Age Weight Height
  <chr>  <dbl>  <dbl>  <dbl>
1 Bob       32     98    188
2 Al        24     61    176
3 Sue       64     87    174

Hier gibt man zunächst die Daten im Long-Format an (df_long). Danach folgt mit id_cols der Spaltenname, welcher die einzelnen Fälle identifiziert (also die individuellen Personen in unserem Beispiel). Danach gibt man mit names_from die Spalte an, welche die Namen der Werte beinhaltet (also die Indikatorspalte Variable). Schließlich definiert man mit values_from die Spalte, welche die Werte beinhaltet (Value).

Diese beiden Funktionen können sehr viel komplexere Strukturen in die jeweiligen Formate konvertieren – dies ist alles in der Hilfe nachzulesen, inklusive vieler Beispiele, die die Verwendung demonstrieren.

Zeilen filtern mit subset()

Oft ist es wünschenswert, nur gewisse Zeilen aus einem vorhandenen Data Frame zu verwenden. Beispielsweise könnte es für eine Datenanalyse notwendig sein, männliche und weibliche Versuchspersonen getrennt auszuwerten. Dieses Merkmal ist im folgenden Beispiel in einer Spalte sex mit den Ausprägungen m und f vorhanden:

(df = tibble(
    name=c("Bob", "Al", "Sue", "John", "Mary", "Ann"),
    age=c(32, 24, 64, 44, 21, 75),
    weight=c(98, 61, 87, 82, 73, 66),
    height=c(188, 176, 174, 182, 181, 159),
    sex=factor(c("m", "m", "f", "m", "f", "f"))
))
# A tibble: 6 × 5
  name    age weight height sex  
  <chr> <dbl>  <dbl>  <dbl> <fct>
1 Bob      32     98    188 m    
2 Al       24     61    176 m    
3 Sue      64     87    174 f    
4 John     44     82    182 m    
5 Mary     21     73    181 f    
6 Ann      75     66    159 f    

Nun haben wir bereits gelernt, dass wir spezifische Zeilen mittels Indizieren herausfiltern können. Mit folgendem Befehl erhalten wir ein neues Data Frame mit allen weiblichen Versuchspersonen:

df[df$sex == "f",]
# A tibble: 3 × 5
  name    age weight height sex  
  <chr> <dbl>  <dbl>  <dbl> <fct>
1 Sue      64     87    174 f    
2 Mary     21     73    181 f    
3 Ann      75     66    159 f    

Diese Schreibweise ist für Data Frames aber relativ unübersichtlich, vor allem weil man das zugrundeliegende Data Frame df zwei Mal nennen muss. Glücklicherweise gibt es aber eine intuitivere Alternative durch die Funktion subset(). Diese Funktion kann, wie der Name bereits andeutet, Untermengen (Subsets) von bestehenden Vektoren bzw. Data Frames erzeugen. Im Falle eines Data Frames bedeutet das, dass man eine Untermenge der Zeilen und/oder Spalten auswählen kann.

Beginnen wir mit der Auswahl von Zeilen (man bezeichnet diese Operation auch als “filtern”). Hier übergibt man der Funktion subset() als erstes Argument das ursprüngliche Data Frame. Das zweite Argument (welches subset heißt, nicht zu verwechseln mit dem Funktionsnamen) bestimmt dann, welche Zeilen ausgewählt (gefiltert) werden sollen. Das obige Beispiel kann damit wie folgt angeschrieben werden:

subset(df, sex == "f")
# A tibble: 3 × 5
  name    age weight height sex  
  <chr> <dbl>  <dbl>  <dbl> <fct>
1 Sue      64     87    174 f    
2 Mary     21     73    181 f    
3 Ann      75     66    159 f    

Beachten Sie, dass man den Spaltennamen direkt anschreiben kann ohne df$ voranstellen zu müssen.

Damit kann man auch komplexere Filter-Operationen durchführen, indem man einen entsprechend komplexen Vergleich für das subset-Argument übergibt, z.B. durch Verknüpfen mehrerer Vergleiche:

subset(df, age > 40 & weight <= 73)
# A tibble: 1 × 5
  name    age weight height sex  
  <chr> <dbl>  <dbl>  <dbl> <fct>
1 Ann      75     66    159 f    

Hier kann man also alle möglichen Vergleiche bzw. Verknüpfungsoperatoren (& und, | oder, xor() exklusives oder) verwenden.

Spalten selektieren mit subset()

Manchmal ist es auch gewünscht, nur spezifische Spalten aus einem Data Frame weiterzuverwenden. Klassisch kann man dies wieder über Indizieren lösen, z.B. wenn man im Beispiel nur die Spalten name, age und sex benötigt:

df[, c("name", "age", "sex")]
# A tibble: 6 × 3
  name    age sex  
  <chr> <dbl> <fct>
1 Bob      32 m    
2 Al       24 m    
3 Sue      64 f    
4 John     44 m    
5 Mary     21 f    
6 Ann      75 f    

Auch hier kann man alternativ die subset()-Funktion verwenden, und zwar unter Verwendung des dritten Arguments (namens select):

subset(df, select=c(name, age, sex))
# A tibble: 6 × 3
  name    age sex  
  <chr> <dbl> <fct>
1 Bob      32 m    
2 Al       24 m    
3 Sue      64 f    
4 John     44 m    
5 Mary     21 f    
6 Ann      75 f    

Beachten Sie, dass auch hier die Spaltennamen ohne Anführungszeichen angegeben werden können. Es ist sogar möglich, einen Bereich mit einem : wie folgt anzuschreiben (so wie wir es bereits bei pivot_longer() bzw. pivot_wider() gesehen haben):

subset(df, select=name:weight)
# A tibble: 6 × 3
  name    age weight
  <chr> <dbl>  <dbl>
1 Bob      32     98
2 Al       24     61
3 Sue      64     87
4 John     44     82
5 Mary     21     73
6 Ann      75     66

Hier werden also alle Spalten von name bis weight selektiert.

Selbstverständlich kann man beide Operationen (also Zeilenauswahl und Spaltenauswahl) auch gleichzeitig durchführen:

subset(df, subset=age > 30, select=c(name, age, sex))
# A tibble: 4 × 3
  name    age sex  
  <chr> <dbl> <fct>
1 Bob      32 m    
2 Sue      64 f    
3 John     44 m    
4 Ann      75 f    

Spalten transformieren mit transform()

Eine weitere wichtige Aufgabe in der Datenanalyse ist es, neue Spalten zu einem bestehenden Data Frame hinzuzufügen. Wenn die Werte dieser neuen Spalten dabei auf vorhandenen Spalten basieren, spricht man von einer Transformation. Sehen wir uns dazu das in R eingebaute Data Frame airquality an:

head(airquality)
  Ozone Solar.R Wind Temp Month Day
1    41     190  7.4   67     5   1
2    36     118  8.0   72     5   2
3    12     149 12.6   74     5   3
4    18     313 11.5   62     5   4
5    NA      NA 14.3   56     5   5
6    28      NA 14.9   66     5   6

Die Temperaturen in der Temp-Spalte sind in Fahrenheit angegeben. Wir können nun eine neue Spalte namens celsius hinzufügen, indem wir die vorhandene Temp-Spalte wie folgt transformieren:

aq = transform(airquality, celsius=(Temp - 32) * (5/9))
head(aq)
  Ozone Solar.R Wind Temp Month Day  celsius
1    41     190  7.4   67     5   1 19.44444
2    36     118  8.0   72     5   2 22.22222
3    12     149 12.6   74     5   3 23.33333
4    18     313 11.5   62     5   4 16.66667
5    NA      NA 14.3   56     5   5 13.33333
6    28      NA 14.9   66     5   6 18.88889

Beachten Sie, dass wir in der Transformation vorhandene Spalten wieder direkt benennen können (im Beispiel Temp), ohne Anführungszeichen oder airquality$ verwenden zu müssen. Außerdem wird immer ein neues Data Frame erzeugt; Sie könnten dieses aber dem ursprünglichen Namen zuweisen wenn Sie möchten (also airquality in obigem Beispiel statt aq).

Wichtig

Das Ergebnis der Funktion transform() ist ein Data Frame, auch wenn Sie ein Tibble als Ausgangsdaten verwenden. Falls das Ergebnis aber auch ein Tibble sein soll, muss man den Rückgabewert von transform() mit as_tibble() (aus dem Paket tibble) explizit umwandeln.

Der Pipe-Operator |>

Seit R 4.1 gibt es einen sogenannten Pipe-Operator. Dieser wird als |> angeschrieben. Das Prinzip dahinter ist so einfach wie genial. Obwohl damit keinerlei neue Funktionalität hinzugefügt wird (d.h. man kann alle Operationen auch ohne den Pipe-Operator umsetzen), werden viele Operationen einfacher bzw. intuitiver.

Grundsätzlich kann man mit dem Pipe-Operator einen Funktionsaufruf f(x) als x |> f() anschreiben. Das ist eine reine syntaktische Alternative, d.h. beide Varianten tun genau das gleiche. Beispiel:

x = 1:10
mean(x)  # klassisch
[1] 5.5
x |> mean()  # Pipe
[1] 5.5

Das macht in diesem Beispiel natürlich wenig Sinn, denn hier ist mean(x) wesentlich kürzer und einfacher lesbar. Interessant wird es aber dann, wenn man das Ergebnis eines Funktionsaufrufes direkt als Argument für einen weiteren Funktionsaufruf verwenden will. Klassisch würde man dies so anschreiben:

g(f(x))

Hier ist es schon schwieriger zu sehen, in welcher Reihenfolge die Berechnung eigentlich durchgeführt wird: zuerst wird f(x) berechnet, und dessen Ergebnis wird als Argument für die Funktion g() übergeben. Mit dem Pipe-Operator würde dieselbe Operation wie folgt aussehen:

x |> f() |> g()

Hier ist sofort klar, in welcher Reihenfolge die Berechnung durchgeführt wird: x wird zuerst der Funktion f() übergeben, und das Ergebnis davon wird der Funktion g() übergeben.

Das folgende Beispiel berechnet zuerst den Mittelwert von einem Vektor x und gleich danach den Logarithmus dieses Mittelwerts:

log(mean(x))
[1] 1.704748
x |> mean() |> log()
[1] 1.704748

Die Variante mit dem Pipe-Operator ist meistens intuitiver. Noch übersichtlicher wird es, wenn jeder Schritt in der Pipeline in eine eigene Zeile geschrieben wird (das ist in R ja bei allen Befehlen möglich):

x |>
    mean() |>
    log()
[1] 1.704748

Data Wrangling

Kombinieren wir nun alles, was wir in diesem Kapitel gelernt haben (nämlich subset(), transform() und |>), dann erhalten wir ein Werkzeug, welches das Umformen eines Data Frames (das sogenannte Data Wrangling) sehr intuitiv ermöglicht. Sehen wir uns das anhand des airquality-Datensatzes an.

Nehmen wir an, wir möchten die Temperaturen (in °C) im Monat Juli untersuchen. Folgende Pipeline könnten wir dazu verwenden:

library(tibble)

airquality |>
    transform(celsius=(Temp - 32) * (5/9)) |>
    subset(Month == 7) |>
    subset(select=-c(Month, Day)) |>
    as_tibble()
# A tibble: 31 × 5
   Ozone Solar.R  Wind  Temp celsius
   <int>   <int> <dbl> <int>   <dbl>
 1   135     269   4.1    84    28.9
 2    49     248   9.2    85    29.4
 3    32     236   9.2    81    27.2
 4    NA     101  10.9    84    28.9
 5    64     175   4.6    83    28.3
 6    40     314  10.9    83    28.3
 7    77     276   5.1    88    31.1
 8    97     267   6.3    92    33.3
 9    97     272   5.7    92    33.3
10    85     175   7.4    89    31.7
# … with 21 more rows
Hinweis

Im vorigen Beispiel wird mit as_tibble() am Ende alles in ein Tibble konvertiert. Ansonsten hätte man ein Data Frame, weil wir die Funktion transform() verwendet haben.

Um mit diesem Data Frame weiterzuarbeiten sollte man das Ergebnis einer (neuen) Variablen zuweisen, z.B.:

aq = airquality |>
    transform(celsius=(Temp - 32) * (5/9)) |>
    subset(Month == 7) |>
    subset(select=-c(Month, Day))

mean(aq$celsius)  # mittlere Temperatur (in °C) im Juli
[1] 28.83513

Abschließend ist noch anzumerken, dass das Tidyverse wesentlich mehr Funktionen bietet, die mit dem Pipe-Operator angewendet werden können. Insbesonders gruppierte zusammenfassende Statistiken (z.B. die mittleren Temperaturen für alle Monate) werden dadurch ähnlich intuitiv möglich wie in den hier gezeigten Beispielen. Mit Base-R ist dies zwar auch möglich, aber nicht in dieser einheitlichen Pipeline-Form.

Übungen

Übung 1

Installieren und aktivieren Sie das Paket tidyr. Darin enthalten ist der Datensatz table2. Erzeugen Sie daraus ein neues Data Frame, welches aus den Spalten type und count die Werte in zwei Spalten cases und population enthält.

Übung 2

Im Paket tidyr ist auch ein Datensatz table4a enthalten. Fassen Sie die beiden Spalten 1999 und 2000 zu einer Wertespalte namens count und einer Indikatorspalte namens year zusammen.

Hinweis

Die beiden Spaltennamen muss man mit Backticks (`) umschließen, da Namen die mit Ziffern starten und R diese sonst als Zahlen interpretieren würde, d.h. `1999` bzw. `2000`.

Übung 3

Erstellen Sie aus dem in R vorhandenen Data Frame mtcars ein neues Data Frame namens mtcars1, welches nur aus jenen Zeilen besteht in denen die Spalte mpg Werte größer als 25 aufweist. Aus wie vielen Zeilen bzw. Spalten bestehen mtcars bzw. mtcars1? Verwenden Sie dafür die Funktion subset()!

Übung 4

Installieren und aktivieren Sie das Paket nycflights13. Wir verwenden den Datensatz flights aus diesem Paket. Lesen Sie sich zuerst den Hilfetext zu flights durch. Führen Sie danach folgende Aufgaben durch (unter Verwendung des Pipe-Operators und den Funktionen subset() bzw. transform()):

  • Erstellen Sie einen neuen Datensatz, welcher alle Flüge am 1.1.2013 enthält. Außerdem sollen nur die Spalten year, month, day, dep_time, arr_time und tailnum vorhanden sein. Wie viele Flüge sind das?
  • Erzeugen Sie zwei neue Spalten hours (die Flugzeit in Stunden; die Spalte air_time beinhaltet die Flugzeit in Minuten) und speed (die Fluggeschwindigkeit in km/h; die Spalte distance beinhaltet den zurückgelegten Weg in Meilen, hier wäre also eine zusätzliche Spalte km hilfreich). Geben Sie das neue Data Frame nur mit den Spalten month, day, carrier, tailnum und speed aus!
  • Erstellen Sie einen neuen Datensatz, welcher alle Frühflüge beinhaltet (Flüge, die vor 6:00 gestartet sind).
  • Erstellen Sie einen neuen Datensatz, welcher nur jene Flüge beinhaltet, die schneller als geplant unterwegs waren (wo also die Verspätung bei der Ankunft kleiner war als beim Abflug).