15 min read

Fallstudie (YACSDA) zur praktischen Datenanalyse mit dplyr


Case study in data analysis using R package dplyr in German language.


Praktische Datenanalyse mit dplyr

Das R-Paket dplyr von Hadley Wickham ist ein Stargast auf der R-Showbühne; häufig diskutiert in einschlägigen Foren. Mit dyplr kann man Daten “verhackstücken” - umformen und aufbereiten (“to wrangle” auf Englisch); “praktische Datenanalyse” ist vielleicht eine gute Bezeichnung. Es finden sich online viele Einführungen, z.B. hier oder hier.

Dieser Text ist nicht als Einführung oder Erläuterung gedacht, sondern als Übung, um (neu erworbenen Fähigkeiten) in der praktischen Datenanalyse im Rahmen einer Fallstudie auszuprobieren.


Stellen Sie sich folgendes Szenario zur Fallstudie vor: Sie sind Unternehmensberäter bei einer (nach eigenen Angaben) namhaften Gesellschaft. Ihr erster Auftrag führt Sie direkt nach New York City (normal). Ihr Chef hat irgendwie den Eindruck, dass Sie Zahlen- und Computer-affin sind… “Sag mal, schon mal von R gehört?”, fragt er mal eines Abends (22h, noch voll bei der Arbeit). “Eine Programmiersprache zur Datenanalyse und -visualisierung”, antworten Sie wie aus der Pistole geschossen. Das gefällt Ihrem Chef. “Pass mal auf. Bis morgen brauche ich eine Analyse aller Flüge von NYC, Anzahl, nach Origin, nach Destination.. Du weißt schon…”. Natürlich wissen Sie.. “Reicht bis morgen früh um acht”, sagt er noch, bevor er das Büro verlässt.

Ok, also dann sollten wir keine Zeit verlieren…


Aufgaben (und Lösungen)

Laden wir zuerst die nötigen Pakete und Daten; denken Sie daran, dass R-Pakete zuerst installiert werden müssen (einmalig), bevor Sie sie laden können.

# install.packages("nycflights13")
library(dplyr)
library(ggplot2) # Diagramme
data(flights, package = "nycflights13")

Wie viele Flüge starteten in den NYC-Flughäfen in 2013?

flights %>%
  summarise(n_flights = n())
## # A tibble: 1 × 1
##   n_flights
##       <int>
## 1    336776

Ah, eine Menge :-).

Welche Flughäfen gibt es in NYC? Wie viele Flüge starteten von dort jeweils?

flights %>%
  group_by(origin) %>% 
  summarise(Anzahl = n()) 
## # A tibble: 3 × 2
##   origin Anzahl
##    <chr>  <int>
## 1    EWR 120835
## 2    JFK 111279
## 3    LGA 104662

Das könnten wir auch plotten. Allerdings… 3 Zahlen, das kann man auch ohne Diagramm gut erkennen…

Die internationalen Codes von Flughäfen können z.B. hier nachgelesen werden.

Wie viele Flughäfen starteten pro Monat aus NYC?

Das ist praktische die gleiche Frage in grün…

flights %>%
  group_by(month) %>% 
  summarise(Anzahl = n()) 
## # A tibble: 12 × 2
##    month Anzahl
##    <int>  <int>
## 1      1  27004
## 2      2  24951
## 3      3  28834
## 4      4  28330
## 5      5  28796
## 6      6  28243
## 7      7  29425
## 8      8  29327
## 9      9  27574
## 10    10  28889
## 11    11  27268
## 12    12  28135

Das lohnt sich schon eher als Diagramm:

flights %>%
  group_by(month) %>% 
  summarise(Anzahl = n()) %>% 
  ggplot(aes(x = month, y = Anzahl)) + geom_point(color = "firebrick") +
  geom_line()

plot of chunk unnamed-chunk-5

Welche Ziele wurden angeflogen? Wurden MUC und FRA angeflogen?

flights %>%
  group_by(dest) %>% 
  summarise(Anzahl = n()) %>% 
  select(dest) %>% print(n = 200)
## # A tibble: 105 × 1
##      dest
##     <chr>
## 1     ABQ
## 2     ACK
## 3     ALB
## 4     ANC
## 5     ATL
## 6     AUS
## 7     AVL
## 8     BDL
## 9     BGR
## 10    BHM
## 11    BNA
## 12    BOS
## 13    BQN
## 14    BTV
## 15    BUF
## 16    BUR
## 17    BWI
## 18    BZN
## 19    CAE
## 20    CAK
## 21    CHO
## 22    CHS
## 23    CLE
## 24    CLT
## 25    CMH
## 26    CRW
## 27    CVG
## 28    DAY
## 29    DCA
## 30    DEN
## 31    DFW
## 32    DSM
## 33    DTW
## 34    EGE
## 35    EYW
## 36    FLL
## 37    GRR
## 38    GSO
## 39    GSP
## 40    HDN
## 41    HNL
## 42    HOU
## 43    IAD
## 44    IAH
## 45    ILM
## 46    IND
## 47    JAC
## 48    JAX
## 49    LAS
## 50    LAX
## 51    LEX
## 52    LGA
## 53    LGB
## 54    MCI
## 55    MCO
## 56    MDW
## 57    MEM
## 58    MHT
## 59    MIA
## 60    MKE
## 61    MSN
## 62    MSP
## 63    MSY
## 64    MTJ
## 65    MVY
## 66    MYR
## 67    OAK
## 68    OKC
## 69    OMA
## 70    ORD
## 71    ORF
## 72    PBI
## 73    PDX
## 74    PHL
## 75    PHX
## 76    PIT
## 77    PSE
## 78    PSP
## 79    PVD
## 80    PWM
## 81    RDU
## 82    RIC
## 83    ROC
## 84    RSW
## 85    SAN
## 86    SAT
## 87    SAV
## 88    SBN
## 89    SDF
## 90    SEA
## 91    SFO
## 92    SJC
## 93    SJU
## 94    SLC
## 95    SMF
## 96    SNA
## 97    SRQ
## 98    STL
## 99    STT
## 100   SYR
## 101   TPA
## 102   TUL
## 103   TVC
## 104   TYS
## 105   XNA

Eine lange Liste… wäre vielleicht übersichtlicher, die nicht abzubilden ;)

Schauen wir mal, ob MUC (München) oder FRA (Frankfurt) dabei waren.

flights %>%
  filter(dest == "MUC" | dest == "FRA")
## # A tibble: 0 × 19
## # ... with 19 variables: year <int>, month <int>, day <int>,
## #   dep_time <int>, sched_dep_time <int>, dep_delay <dbl>, arr_time <int>,
## #   sched_arr_time <int>, 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>

Die resultierende Tabelle (“tibble”) hat 0 Zeilen. Diese Ziele wurden also nicht angeflogen.

Das Zeichen “|” bedeutet “oder” (im logischen Sinne). Demnach kann man die die ganze “Pfeife” so lesen: Nimm flights Filter Zeilen mit Ziel gleich MUC oder Zeilen mit Ziel gleich FRA.

Welche Ziele am häufigsten angeflogen?

flights %>%
  group_by(dest) %>%
  summarise(n_per_dest = n()) %>%
  arrange(desc(n_per_dest))
## # A tibble: 105 × 2
##     dest n_per_dest
##    <chr>      <int>
## 1    ORD      17283
## 2    ATL      17215
## 3    LAX      16174
## 4    BOS      15508
## 5    MCO      14082
## 6    CLT      14064
## 7    SFO      13331
## 8    FLL      12055
## 9    MIA      11728
## 10   DCA       9705
## # ... with 95 more rows

Das könnte man auch wieder “plotten”, aber lieber nur die Top-10.

flights %>%
  group_by(dest) %>%
  summarise(n_per_dest = n()) %>%
  arrange(desc(n_per_dest)) %>% 
  filter(min_rank(n_per_dest) < 11) %>% 
  ggplot(aes(x = dest, y = n_per_dest)) + geom_bar(stat = "identity")

plot of chunk unnamed-chunk-9

Der Befehl min_rank(n_per_dest) < 11 liefert die 10 kleinsten Rangplätze der Variablen n_per_dest zurück.

Beim Plotten brauchen wir beim Geom bar (Balken) den Zusatz stat = "identity", weil das Geom bar standardgemäß zählen möchte, wie viele Zeilen z.B. “LGA” enthalten. Wir haben aber das Zählen der Zeilen schon vorher mit n() gemacht, so dass der Befehl einfach den Wert, so wie er in unserem Dataframe steht (daher identity) nehmen soll.

Welche Ziele werden mehr als 10000 Mal pro Jahr angeflogen?

flights %>%
  group_by(dest) %>%
  summarise(n_dest = n()) %>%
  filter(n_dest > 10000)
## # A tibble: 9 × 2
##    dest n_dest
##   <chr>  <int>
## 1   ATL  17215
## 2   BOS  15508
## 3   CLT  14064
## 4   FLL  12055
## 5   LAX  16174
## 6   MCO  14082
## 7   MIA  11728
## 8   ORD  17283
## 9   SFO  13331

Welche Flüge gingen von JFK nach PWM (Portland) im Januar zwischen Mitternach und 5 Uhr?

library(knitr)
filter(flights, origin == "JFK" & month == 1, dest == "PWM", dep_time < 500) %>% 
  kable
yearmonthdaydep_timesched_dep_timedep_delayarr_timesched_arr_timearr_delaycarrierflighttailnumorigindestair_timedistancehourminutetime_hour
20131410622451412012356125B6608N192JBJFKPWM4427322452013-01-04 22:00:00
20131315422501241522359113B6608N281JBJFKPWM4127322502013-01-31 22:00:00

Der Befehl knitr::kable erstellt eine (einigermaßen) schöne Tabelle (man muss aber das Paket knitr vorher geladen haben.)

Warum Ihr Chef das wissen will, weiß er nur allein…

Welche Flüge starteten von JFK, dieeine Ankunftsverspätung hatten doppelt so groß wie die Abflugverspätung, und die nach Atlanta geflogen sind?

Selten eine Aufgabe gelesen, die aus so einem langen Satz bestand …

filter(flights, origin == "JFK", arr_delay > 2 * dep_delay, month == 1, dest == "ATL") %>% 
  kable
yearmonthdaydep_timesched_dep_timedep_delayarr_timesched_arr_timearr_delaycarrierflighttailnumorigindestair_timedistancehourminutetime_hour
201311807810-3104310430DL269N308DEJFKATL1267608102013-01-01 08:00:00
20131113251330-5160616051DL2043N318USJFKATL13176013302013-01-01 13:00:00
201312606610-48468451DL1743N387DAJFKATL1297606102013-01-02 06:00:00
201312808810-2104910454DL269N971DLJFKATL1247608102013-01-02 08:00:00
201312155115483183818308DL95N702TWJFKATL11976015482013-01-02 15:00:00
20131220272030-323092314-5DL1447N947DLJFKATL12776020302013-01-02 20:00:00
201313612615-391285022DL2057N707TWJFKATL1357606152013-01-03 06:00:00
20131381181011053104211DL269N319NBJFKATL1347608102013-01-03 08:00:00
20131318471855-821302142-12DL951N181DNJFKATL12976018552013-01-03 18:00:00
201315805810-510391041-2DL269N339NBJFKATL1167608102013-01-05 08:00:00
20131515401548-818201829-9DL95N710TWJFKATL11976015482013-01-05 15:00:00
201316612615-3846848-2DL2057N397DAJFKATL1267606152013-01-06 06:00:00
201316809810-1104410422DL269N316NBJFKATL1257608102013-01-06 08:00:00
20131613261330-4160516050DL2043N3734BJFKATL12676013302013-01-06 13:00:00
20131615451548-31842183012DL95N387DAJFKATL13976015482013-01-06 15:00:00
201317803810-710291042-13DL269N344NBJFKATL1057608102013-01-07 08:00:00
201318612615-390185569E3856N153PQJFKATL1247606152013-01-08 06:00:00
201318823810131112104329DL269N377NWJFKATL1267608102013-01-08 08:00:00
2013181549154811846183016DL95N376DAJFKATL11776015482013-01-08 15:00:00
20131818431855-12214221420DL951N1611BJFKATL12676018552013-01-08 18:00:00
201319807810-3104510423DL269N316NBJFKATL1247608102013-01-09 08:00:00
201319185718552215121429DL951N173DZJFKATL12076018552013-01-09 18:00:00
2013110809810-110411042-1DL269N361NBJFKATL1117608102013-01-10 08:00:00
2013111807810-31056104214DL269N345NBJFKATL1177608102013-01-11 08:00:00
2013113612615-3853855-29E3856N146PQJFKATL1187606152013-01-13 06:00:00
201311318481855-72204214222DL951N1609JFKATL11676018552013-01-13 18:00:00
2013115612615-3927855329E3856N181PQJFKATL1347606152013-01-15 06:00:00
201311513241330-6160516050DL2043N704XJFKATL13376013302013-01-15 13:00:00
20131166156150905855109E3856N232PQJFKATL1317606152013-01-16 06:00:00
201311613241330-616031605-2DL2043N3763DJFKATL12976013302013-01-16 13:00:00
2013117612615-3906855119E3856N176PQJFKATL1297606152013-01-17 06:00:00
20131178108100104810426DL269N340NBJFKATL1277608102013-01-17 08:00:00
201311713251330-516001605-5DL2043N3756JFKATL12776013302013-01-17 13:00:00
2013119614615-185785529E3856N187PQJFKATL1277606152013-01-19 06:00:00
2013119803810-710311041-10DL269N320NBJFKATL1157608102013-01-19 08:00:00
201312018511855-421352142-7DL951N1605JFKATL11476018552013-01-20 18:00:00
201312117231730-720102017-7DL951N175DNJFKATL13176017302013-01-21 17:00:00
2013122614615-185785529E3856N153PQJFKATL1327606152013-01-22 06:00:00
2013122820810101107104225DL269N359NBJFKATL1307608102013-01-22 08:00:00
2013124612615-385585509E3856N197PQJFKATL1177606152013-01-24 06:00:00
201312581081001056104214DL269N366NBJFKATL1237608102013-01-25 08:00:00
201312518501855-5215121429DL951N646DLJFKATL15076018552013-01-25 18:00:00
201312615431548-518211829-8DL95N723TWJFKATL10776015482013-01-26 15:00:00
2013128610615-585685519E3856N187PQJFKATL1267606152013-01-28 06:00:00
2013128808810-210411042-1DL269N327NBJFKATL1207608102013-01-28 08:00:00
201312813201330-1015471605-18DL2043N3748YJFKATL11776013302013-01-28 13:00:00
201312815461548-2183518305DL95N3745BJFKATL16076015482013-01-28 15:00:00
201312913251330-515591605-6DL2043N712TWJFKATL11976013302013-01-29 13:00:00
2013130807810-31105104223DL269N361NBJFKATL1287608102013-01-30 08:00:00
201313018531855-22152214210DL951N686DAJFKATL13176018552013-01-30 18:00:00
20131316166151905855109E3856N181PQJFKATL1267606152013-01-31 06:00:00
201313181081001054104212DL269N355NBJFKATL1277608102013-01-31 08:00:00
201313118531855-2214921427DL951N175DZJFKATL12976018552013-01-31 18:00:00

Auch diese Tabelle ist recht lang. Aber sei’s drum :)

Welche Airlines hatten die meiste “Netto-Verspätung”?

f_2 <- group_by(flights, carrier)
f_3 <- mutate(f_2, delay = dep_delay - arr_delay)
f_4 <- filter(f_3, !is.na(delay))
f_5 <- summarise(f_4, delay_mean = mean(delay))
arrange(f_5, delay_mean) 
## # A tibble: 16 × 2
##    carrier delay_mean
##      <chr>      <dbl>
## 1       F9 -1.7195301
## 2       FL -1.5099213
## 3       MQ -0.3293526
## 4       OO  0.6551724
## 5       US  1.6150976
## 6       YV  3.3419118
## 7       B6  3.5095746
## 8       EV  4.0424982
## 9       DL  7.5796089
## 10      WN  8.0125374
## 11      AA  8.2048393
## 12      UA  8.4588972
## 13      9E  9.0599052
## 14      VX 10.9921814
## 15      HA 11.8157895
## 16      AS 15.7616361

Etwas umständlich mit den ganzen Zwischenspeichern… Vielleicht besser so:

flights %>% 
  group_by(carrier) %>% 
  mutate(delay = dep_delay - arr_delay) %>% 
  filter(!is.na(delay)) %>% 
  summarise(delay_mean = mean(delay)) %>% 
  arrange(-delay_mean)
## # A tibble: 16 × 2
##    carrier delay_mean
##      <chr>      <dbl>
## 1       AS 15.7616361
## 2       HA 11.8157895
## 3       VX 10.9921814
## 4       9E  9.0599052
## 5       UA  8.4588972
## 6       AA  8.2048393
## 7       WN  8.0125374
## 8       DL  7.5796089
## 9       EV  4.0424982
## 10      B6  3.5095746
## 11      YV  3.3419118
## 12      US  1.6150976
## 13      OO  0.6551724
## 14      MQ -0.3293526
## 15      FL -1.5099213
## 16      F9 -1.7195301

Das könnten wir mal wieder visualisieren:

flights %>% 
  group_by(carrier) %>% 
  mutate(delay = dep_delay - arr_delay) %>% 
  filter(!is.na(delay)) %>% 
  summarise(delay_mean = mean(delay)) %>% 
  arrange(-delay_mean) -> f_summarised

  ggplot(f_summarised, aes(x = carrier, y = delay_mean)) + geom_point(color = "firebrick") 

plot of chunk unnamed-chunk-15

ggplot2 ordnet die X-Achse hier automatisch alphanumerisch. Wenn wir wollen, dass die Achse nach den Werten der Y-Achse (delay_mean) geordnet wird (was sinnvoll ist), können wir das so erreichen:

  ggplot(f_summarised, aes(x = reorder(carrier, delay_mean), y = delay_mean)) + 
    geom_point(color = "firebrick") 

plot of chunk unnamed-chunk-16

Der Befehl reorder(carrier, delay_mean) ordnet die Werte der Varialbne carrier anhand der Werte der Variablen delay_mean.

Berechnen Sie die mittlere Verspätung aller Flüge mit deutlicher Verspätung (> 1 Stunde)!

flights %>%
  mutate(delay = dep_delay - arr_delay) %>% 
  filter(delay > 60) %>%
  summarise(delay_mean = mean(delay),
            n = n()) %>%  # Anzahl
  arrange(delay_mean)
## # A tibble: 1 × 2
##   delay_mean     n
##        <dbl> <int>
## 1   65.18182   154

Wie sind die Verspätungen verteilt?

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

plot of chunk unnamed-chunk-18

Hängen Flugzeit und Verspätung zusammen?

flights %>%
  mutate(delay = dep_delay - arr_delay) %>% 
  na.omit() %>% 
  ggplot(aes(x = distance, y = delay)) + 
  geom_point(alpha = .1) +
  geom_smooth()
## `geom_smooth()` using method = 'gam'

plot of chunk unnamed-chunk-19

Sag mal, plotten wir gerade wirklich 300.000 Punkte??? Das kann dauern…

Das alpha = .1 macht die Punkte blässlich, fast durchsichtig. Ganz praktisch, wenn viele Punkte aufeinander liegen.

Hängen Verspätung und Jahreszeit zusammen?

Auch eine ganz interessante Frage. Schauen wir mal:

cor(flights$month, flights$dep_delay, use = "complete") 
## [1] -0.02005702

Das use = complete sagt, dass wir Zeilen mit fehlenden Werten ignorieren.

Sieht also nicht nach einem Zusammenhang aus. Das sollte uns ein Diagramm auch bestätigen:

flights %>% 
  group_by(month) %>% 
  na.omit() %>%  # alle Zeilen mit fehlenden Werten löschen
  mutate(delay = dep_delay - arr_delay) %>% 
  ggplot(aes(x = month, y = delay)) + geom_boxplot()
## Warning: Continuous x aesthetic -- did you forget aes(group=...)?

plot of chunk unnamed-chunk-21

Upps, das sieht ja komisch aus… Hm..ggplot schlägt vor, wir sollen irgendwie group mit reinwursten… Naja, unsere Gruppen könnten die Monate sein. Also probieren wir’s mal…

flights %>% 
  group_by(month) %>% 
  na.omit() %>%  # alle Zeilen mit fehlenden Werten löschen
  mutate(delay = dep_delay - arr_delay) %>% 
  ggplot(aes(x = month, y = delay, group = month)) + geom_boxplot()

plot of chunk unnamed-chunk-22

Die X-Achse sieht noch nicht so toll aus (mit den Nachkommastellen), aber das heben wir uns für eine andere Gelegenheit auf :-)

Noch ein kleiner Bonus zum Abschluss: Interaktive Diagramme!

Dazu müssen wir erstmal ein neues Paket laden: plotly (und ggf. vorher installieren).

# install.packages("plotly")
library(plotly)

plotly kann man ein ggplot-Objekt übergeben, welches dann automatisch in ein interaktives Diagramm übersetzt wird. Macht natürlich nur Sinn, wenn man das am Computer anschaut; ausgedruckt ist es dann nicht interaktiv…

flights %>% 
  group_by(month) %>% 
  na.omit() %>%  # alle Zeilen mit fehlenden Werten löschen
  mutate(delay = dep_delay - arr_delay) %>% 
  ggplot(aes(x = month, y = delay, group = month, color = month)) + geom_boxplot() -> flights_plot

ggplotly(flights_plot)

plot of chunk unnamed-chunk-24

Für heute reicht’s!