data > opinion

Tom Alby

02 Datenreinigung und -transformation mit dem Tidyverse

2021-05-08


Die wichtigsten Funktionen des tidyverse

Das tidyverse ist eine Erweiterung zu R, die die Transformation und Bereinigung von Daten sowie explorative Datenanalysen stark vereinfacht. Das tidyverse stammt von Hadley Wickham, dem Chief Data Scientist von RStudio. Scherzhafterweise wird das tidyverse von Hadleys Anhängern auch manchmal als Hadleyverse bezeichnet.

DasTidyverse enthält mehrere Libraries:

Im Kurs werden nicht alle Pakete behandelt. Es ist möglich, dass auch nur einzelne Libraries installiert werden, mit install.packages(tidyverse) werden alle Libraries auf einmal installiert.

#install.packages("tidyverse")
library(tidyverse)
## ── Attaching packages ─────────────────────────────────────── tidyverse 1.3.0 ──
## ✓ ggplot2 3.3.3     ✓ purrr   0.3.4
## ✓ tibble  3.1.0     ✓ dplyr   1.0.5
## ✓ tidyr   1.1.3     ✓ stringr 1.4.0
## ✓ readr   1.4.0     ✓ forcats 0.5.1
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## x dplyr::filter() masks stats::filter()
## x dplyr::lag()    masks stats::lag()

dplyr

Im Kurs werden wir uns vor allem mit dplyr beschäftigen (das wird Deutsch “dieplaier” ausgesprochen). Eine Einführung bietet der Autor in dem folgenden Video:

Für diejenigen, die nicht das ganze Video sehen wollen, hier die Kernaussagen. dplyr verwendet 5 Haupt-Verben, mit denen grundlegende Befehle ausgeführt werden.

Ein wichtiger Punkt im Tidyverse ist die so genannte Pipe. Unter UNIX sind Pipes ein beliebtes Mittel, um die Ausgabe aus einer Funktion als Input für eine andere Funktion zu nutzen, z.B.

grep ‘2018’ bericht.csv | wc -l

Hier wird in der Datei bericht.csv nach allen Zeilen gesucht, in denen der String 2018 vorkommt. Die Ausgabe wären dann genau diese Zeilen. Mit dem Operator | wird diese Ausgabe als Eingabe in den nächsten Befehl geleitet. wc -l (steht für word count mit dem Parameter lines, so dass Zeilen und nicht Wörter gezählt werden) zählt die Zeilen.

Pipes sind ein sehr effizientes Mittel unter UNIX, und genau diesen Mechanismus verwendet das tidyverse, wobei hier der Fokus auf der Lesbarkeit liegt.

In dplyr wird anstatt des “|” das Pipe-Symbol %>% verwendet (früher %.% wie oben im Video zu sehen).

# Der gesamte Datensatz mtcars wird an die nächste Zeile übergeben
mtcars %>% 
  # Gruppiere nach der Anzahl der Zylinder und übergib diesen Datensatz an die nächste Zeile
  group_by(cyl) %>% 
  # berechne das Mean und den Median der Meilen pro Gallon für jede Gruppe
  summarize(durchschnittsverbrauch = mean(mpg), medianverbrauch = median(mpg)) 
## # A tibble: 3 x 3
##     cyl durchschnittsverbrauch medianverbrauch
##   <dbl>                  <dbl>           <dbl>
## 1     4                   26.7            26  
## 2     6                   19.7            19.7
## 3     8                   15.1            15.2

Transformation und Data Cleaning am Beispiel des Book Crossing-Datensatzes

Ein Beispiel-Datensatz zur Demonstration ist der Book-Crossing-Datensatz. In dem Datensatz users ist das Alter zum Beispiel nicht bei jedem Nutzer vorhanden, und in diesem Beispiel hat R die Spalte mit dem Datentyp Character importiert:

users <- read_delim("BX-CSV-Dump/BX-Users.csv", 
    ";", escape_double = FALSE, trim_ws = TRUE)
str(users)
## spec_tbl_df[,3] [278,858 × 3] (S3: spec_tbl_df/tbl_df/tbl/data.frame)
##  $ User-ID : num [1:278858] 1 2 3 4 5 6 7 8 9 10 ...
##  $ Location: chr [1:278858] "nyc, new york, usa" "stockton, california, usa" "moscow, yukon territory, russia" "porto, v.n.gaia, portugal" ...
##  $ Age     : chr [1:278858] "NULL" "18" "NULL" "17" ...
##  - attr(*, "spec")=
##   .. cols(
##   ..   `User-ID` = col_double(),
##   ..   Location = col_character(),
##   ..   Age = col_character()
##   .. )

Diese Daten sollten zunächst einmal in eine Zahl konvertiert werden:

users %>%
  mutate(Age = as.numeric(Age)) %>%
  head()
## Warning in mask$eval_all_mutate(quo): NAs introduced by coercion
## # A tibble: 6 x 3
##   `User-ID` Location                             Age
##       <dbl> <chr>                              <dbl>
## 1         1 nyc, new york, usa                    NA
## 2         2 stockton, california, usa             18
## 3         3 moscow, yukon territory, russia       NA
## 4         4 porto, v.n.gaia, portugal             17
## 5         5 farnborough, hants, united kingdom    NA
## 6         6 santa monica, california, usa         61

Der Befehl head() wird dafür verwendet, dass nur die ersten Zeilen angezeigt werden (ansonsten würde diese Seite zu lange brauchen um zu laden). Die Warnhinweise kommen daher, dass NULL nicht zu einer Zahl konvertiert werden kann und daher anstatt dessen NA genutzt wird. Wichtig: NULL bedeutet nicht 0 :) Es bedeutet lediglich, dass es hier keinen Wert gibt. Sollte das Durchschnittsalter errechnet werden (was wenig Sinn ergäbe), dann wäre es sogar fatal, wenn hier eine 0 stünde.

Allerdings ist der Datensatz jetzt noch nicht geändert, die Ergebnisse des Ausdrucks müssen in ein neues oder dasselbe Objekt geschrieben werden. Ein neues Objekt hat den Vorteil, dass man noch Zugriff auf die Originaldaten hat, allerdings ist dann der Speicherverbrauch höher.

users <- users %>%
  mutate(Age = as.numeric(Age))
## Warning in mask$eval_all_mutate(quo): NAs introduced by coercion

Die Ergebnisse werden nun nicht mehr angezeigt, da sie ja in das neue Objekt geschrieben werden. Dies lässt sich umgehen, indem man den ganzen Ausdruck in Klammern setzt:

(users2 <- users)
## # A tibble: 278,858 x 3
##    `User-ID` Location                             Age
##        <dbl> <chr>                              <dbl>
##  1         1 nyc, new york, usa                    NA
##  2         2 stockton, california, usa             18
##  3         3 moscow, yukon territory, russia       NA
##  4         4 porto, v.n.gaia, portugal             17
##  5         5 farnborough, hants, united kingdom    NA
##  6         6 santa monica, california, usa         61
##  7         7 washington, dc, usa                   NA
##  8         8 timmins, ontario, canada              NA
##  9         9 germantown, tennessee, usa            NA
## 10        10 albacete, wisconsin, spain            26
## # … with 278,848 more rows

Gelöscht wird übrigens mit rm() für “remove”.

rm(users2)

Danach könnten alle Datensätze herausgefiltert werden, in denen ein Alter angegeben wurde, das unwahrscheinlich ist. Der Operator | heißt hier “oder”:

users %>%
  filter(Age < 100 | Age > 6) 
## # A tibble: 168,096 x 3
##    `User-ID` Location                                 Age
##        <dbl> <chr>                                  <dbl>
##  1         2 stockton, california, usa                 18
##  2         4 porto, v.n.gaia, portugal                 17
##  3         6 santa monica, california, usa             61
##  4        10 albacete, wisconsin, spain                26
##  5        11 melbourne, victoria, australia            14
##  6        13 barcelona, barcelona, spain               26
##  7        18 rio de janeiro, rio de janeiro, brazil    25
##  8        19 weston, ,                                 14
##  9        20 langhorne, pennsylvania, usa              19
## 10        21 ferrol / spain, alabama, spain            46
## # … with 168,086 more rows

Allerdings heißt ein falsch angegebenes Alter nicht, dass das Rating, das der Nutzer angegeben hat, auch falsch ist. Eine Möglichkeit ist, dass in diesen Datensätzen das Alter durch NA ersetzt wird:

users %>% 
  mutate(Age = replace(Age, which(Age>99), NA)) %>%
  mutate(Age = replace(Age, which(Age<6), NA))
## # A tibble: 278,858 x 3
##    `User-ID` Location                             Age
##        <dbl> <chr>                              <dbl>
##  1         1 nyc, new york, usa                    NA
##  2         2 stockton, california, usa             18
##  3         3 moscow, yukon territory, russia       NA
##  4         4 porto, v.n.gaia, portugal             17
##  5         5 farnborough, hants, united kingdom    NA
##  6         6 santa monica, california, usa         61
##  7         7 washington, dc, usa                   NA
##  8         8 timmins, ontario, canada              NA
##  9         9 germantown, tennessee, usa            NA
## 10        10 albacete, wisconsin, spain            26
## # … with 278,848 more rows

(bitte beachten, dass der Datensatz jetzt noch nicht geändert wurde!)

Im “puren” R ginge das so:

users$Age[users$Age > 99] <- NA
users$Age[users$Age < 6] <- NA

Der Books-Datensatz ist etwas komplexer zu importieren, da hier Backslashes als Escape-Zeichen konfiguriert werden müssen:

books <- read_delim("BX-CSV-Dump/BX-Books.csv", 
    ";", escape_backslash = TRUE, escape_double = FALSE, 
    trim_ws = TRUE)

Das Verb ‘select’ kann gut an dem books-Dataframe demonstriert werden, denn dieser hat ein paar Spalten, die wir in der Regel nicht benötigen werden:

str(books)
## spec_tbl_df[,8] [271,379 × 8] (S3: spec_tbl_df/tbl_df/tbl/data.frame)
##  $ ISBN               : chr [1:271379] "0195153448" "0002005018" "0060973129" "0374157065" ...
##  $ Book-Title         : chr [1:271379] "Classical Mythology" "Clara Callan" "Decision in Normandy" "Flu: The Story of the Great Influenza Pandemic of 1918 and the Search for the Virus That Caused It" ...
##  $ Book-Author        : chr [1:271379] "Mark P. O. Morford" "Richard Bruce Wright" "Carlo D'Este" "Gina Bari Kolata" ...
##  $ Year-Of-Publication: num [1:271379] 2002 2001 1991 1999 1999 ...
##  $ Publisher          : chr [1:271379] "Oxford University Press" "HarperFlamingo Canada" "HarperPerennial" "Farrar Straus Giroux" ...
##  $ Image-URL-S        : chr [1:271379] "http://images.amazon.com/images/P/0195153448.01.THUMBZZZ.jpg" "http://images.amazon.com/images/P/0002005018.01.THUMBZZZ.jpg" "http://images.amazon.com/images/P/0060973129.01.THUMBZZZ.jpg" "http://images.amazon.com/images/P/0374157065.01.THUMBZZZ.jpg" ...
##  $ Image-URL-M        : chr [1:271379] "http://images.amazon.com/images/P/0195153448.01.MZZZZZZZ.jpg" "http://images.amazon.com/images/P/0002005018.01.MZZZZZZZ.jpg" "http://images.amazon.com/images/P/0060973129.01.MZZZZZZZ.jpg" "http://images.amazon.com/images/P/0374157065.01.MZZZZZZZ.jpg" ...
##  $ Image-URL-L        : chr [1:271379] "http://images.amazon.com/images/P/0195153448.01.LZZZZZZZ.jpg" "http://images.amazon.com/images/P/0002005018.01.LZZZZZZZ.jpg" "http://images.amazon.com/images/P/0060973129.01.LZZZZZZZ.jpg" "http://images.amazon.com/images/P/0374157065.01.LZZZZZZZ.jpg" ...
##  - attr(*, "spec")=
##   .. cols(
##   ..   ISBN = col_character(),
##   ..   `Book-Title` = col_character(),
##   ..   `Book-Author` = col_character(),
##   ..   `Year-Of-Publication` = col_double(),
##   ..   Publisher = col_character(),
##   ..   `Image-URL-S` = col_character(),
##   ..   `Image-URL-M` = col_character(),
##   ..   `Image-URL-L` = col_character()
##   .. )

Wir können mit select() zum einen die Spalten auswählen, die wir haben wollen:

books %>%
  select(ISBN,`Book-Title`,`Book-Author`,`Year-Of-Publication`,Publisher)
## # A tibble: 271,379 x 5
##    ISBN   `Book-Title`             `Book-Author`   `Year-Of-Public… Publisher   
##    <chr>  <chr>                    <chr>                      <dbl> <chr>       
##  1 01951… Classical Mythology      Mark P. O. Mor…             2002 Oxford Univ…
##  2 00020… Clara Callan             Richard Bruce …             2001 HarperFlami…
##  3 00609… Decision in Normandy     Carlo D'Este                1991 HarperPeren…
##  4 03741… Flu: The Story of the G… Gina Bari Kola…             1999 Farrar Stra…
##  5 03930… The Mummies of Urumchi   E. J. W. Barber             1999 W. W. Norto…
##  6 03991… The Kitchen God's Wife   Amy Tan                     1991 Putnam Pub …
##  7 04251… What If?: The World's F… Robert Cowley               2000 Berkley Pub…
##  8 06718… PLEADING GUILTY          Scott Turow                 1993 Audioworks  
##  9 06794… Under the Black Flag: T… David Cordingly             1996 Random House
## 10 07432… Where You'll Find Me: A… Ann Beattie                 2002 Scribner    
## # … with 271,369 more rows

Man kann das aber auch einfacher haben, indem man die Spalten angibt, die man nicht haben will:

(books <- books %>%
  select(-`Image-URL-S`,-`Image-URL-M`,-`Image-URL-L`))
## # A tibble: 271,379 x 5
##    ISBN   `Book-Title`             `Book-Author`   `Year-Of-Public… Publisher   
##    <chr>  <chr>                    <chr>                      <dbl> <chr>       
##  1 01951… Classical Mythology      Mark P. O. Mor…             2002 Oxford Univ…
##  2 00020… Clara Callan             Richard Bruce …             2001 HarperFlami…
##  3 00609… Decision in Normandy     Carlo D'Este                1991 HarperPeren…
##  4 03741… Flu: The Story of the G… Gina Bari Kola…             1999 Farrar Stra…
##  5 03930… The Mummies of Urumchi   E. J. W. Barber             1999 W. W. Norto…
##  6 03991… The Kitchen God's Wife   Amy Tan                     1991 Putnam Pub …
##  7 04251… What If?: The World's F… Robert Cowley               2000 Berkley Pub…
##  8 06718… PLEADING GUILTY          Scott Turow                 1993 Audioworks  
##  9 06794… Under the Black Flag: T… David Cordingly             1996 Random House
## 10 07432… Where You'll Find Me: A… Ann Beattie                 2002 Scribner    
## # … with 271,369 more rows

Ein weiteres Beispiel soll die Funktionen von summarize und arrange demonstrieren. Hierzu wird zunächst der Ratings-Datensatz eingelesen:

ratings <- read_delim("BX-CSV-Dump/BX-Book-Ratings.csv", 
    ";", escape_double = FALSE, trim_ws = TRUE)
ratings %>%
  group_by(ISBN) %>%
  summarize(AnzahlRatings = n()) %>%
  arrange(desc(AnzahlRatings))
## # A tibble: 340,553 x 2
##    ISBN       AnzahlRatings
##    <chr>              <int>
##  1 0971880107          2502
##  2 0316666343          1295
##  3 0385504209           883
##  4 0060928336           732
##  5 0312195516           723
##  6 044023722X           647
##  7 0679781587           639
##  8 0142001740           615
##  9 067976402X           614
## 10 0671027360           586
## # … with 340,543 more rows

Das Verb group_by() wurde bereits oben einmal erklärt. Es gehört zwar nicht zu den Grundverben, aber es ist ungemein nützlich. Man könnte den Ausdruck oben wie folgt formulieren:

  • Zeig mir den Datensatz ratings an
  • und dann schaue für jeden einzelnen unique Wert
  • wie oft er vorkommt
  • und zeige das dann absteigend an

Dieser Datensatz offenbart auch ein Problem, denn nun sind nur die ISBN-Nummern zu sehen und nicht die Titel der Bücher. Idealerweise wäre beides in einem Data Frame. Hier helfen join-Befehle. In diesem Fall sollen dem Data Frame books die Ratings hinzugefügt werden. Bildlich vorgestellt steht books links und nun wird für jedes Buch geschaut, welche Ratings vorhanden sind und für jedes Rating eine Zeile zu dem Buch erstellt. Ist in books ein Buch nur einmal im Datensatz, so sind hinterher sieben Zeilen für dieses Buch vorhanden, sofern es auch sieben Ratings gibt:

books %>%
  left_join(ratings)
## Joining, by = "ISBN"
## # A tibble: 1,032,398 x 7
##    ISBN    `Book-Title`   `Book-Author`  `Year-Of-Publica… Publisher   `User-ID`
##    <chr>   <chr>          <chr>                      <dbl> <chr>           <dbl>
##  1 019515… Classical Myt… Mark P. O. Mo…              2002 Oxford Uni…         2
##  2 000200… Clara Callan   Richard Bruce…              2001 HarperFlam…         8
##  3 000200… Clara Callan   Richard Bruce…              2001 HarperFlam…     11400
##  4 000200… Clara Callan   Richard Bruce…              2001 HarperFlam…     11676
##  5 000200… Clara Callan   Richard Bruce…              2001 HarperFlam…     41385
##  6 000200… Clara Callan   Richard Bruce…              2001 HarperFlam…     67544
##  7 000200… Clara Callan   Richard Bruce…              2001 HarperFlam…     85526
##  8 000200… Clara Callan   Richard Bruce…              2001 HarperFlam…     96054
##  9 000200… Clara Callan   Richard Bruce…              2001 HarperFlam…    116866
## 10 000200… Clara Callan   Richard Bruce…              2001 HarperFlam…    123629
## # … with 1,032,388 more rows, and 1 more variable: Book-Rating <dbl>

R erkennt automatisch den passenden Schlüssel zum Zusammenführen, die ISBN-Nummer. Ist dies nicht der Fall, so kann der Schlüssel auch definiert werden. Interessanterweise sind nun weniger Zeilen vorhanden als Ratings vorhanden waren, nicht jedes Rating hat es also geschafft, in books gefunden zu werden. Dies kann auch daran liegen, dass die ISBN-Nummern noch nicht gesäubert wurden.

Eine Möglichkeit, einen Datensatz zu erstellen, den man immer wieder verwenden kann (noch ohne Reinigung), sieht so aus

ratings <- ratings %>%
  left_join(books)
## Joining, by = "ISBN"
ratings <- ratings %>%
  filter(!is.na(`Book-Title`))
users <- users %>%
  #filter(!is.na(Age)) %>%
  mutate(country = str_remove(Location,".*,")) 
all <- ratings %>%
  left_join(users) %>%
  select(-Location)
## Joining, by = "User-ID"
write_csv(all, "all.csv")

Reguläre Ausdrücke

Reguläre Ausdrücke sind Zeichenketten, mit denen man Muster erkennen kann. In dem folgenden Beispiel werden Zahlen gesucht, auf die ein oder mehrere Buchstaben folgen können und diese dann extrahiert und als neuer Wert eingesetzt:

books %>%
  mutate(ISBN = str_extract(ISBN, "[0-9]*X*"))
## # A tibble: 271,379 x 5
##    ISBN   `Book-Title`             `Book-Author`   `Year-Of-Public… Publisher   
##    <chr>  <chr>                    <chr>                      <dbl> <chr>       
##  1 01951… Classical Mythology      Mark P. O. Mor…             2002 Oxford Univ…
##  2 00020… Clara Callan             Richard Bruce …             2001 HarperFlami…
##  3 00609… Decision in Normandy     Carlo D'Este                1991 HarperPeren…
##  4 03741… Flu: The Story of the G… Gina Bari Kola…             1999 Farrar Stra…
##  5 03930… The Mummies of Urumchi   E. J. W. Barber             1999 W. W. Norto…
##  6 03991… The Kitchen God's Wife   Amy Tan                     1991 Putnam Pub …
##  7 04251… What If?: The World's F… Robert Cowley               2000 Berkley Pub…
##  8 06718… PLEADING GUILTY          Scott Turow                 1993 Audioworks  
##  9 06794… Under the Black Flag: T… David Cordingly             1996 Random House
## 10 07432… Where You'll Find Me: A… Ann Beattie                 2002 Scribner    
## # … with 271,369 more rows

Auch die Daten zu den Nutzern können noch zu dem Datensatz hinzugefügt werden oder ein ganz neuer Datensatz erstellt werden. Der Books Crossing-Datensatz wird noch mehrmals in der Veranstaltung verwendet werden.

Weitere Informationen zum tidyverse

Ein weiteres, informatives Video mit Hadley Wickham ist hier zu finden: