EDA zu Flugverspätungen

1 Pakete laden

library(tidyverse)  # data wrangling
library(fastDummies)  # nur für "Dummyisierung"
library(skimr)  # viele Statistiken auf einmal
library(corrr)  # komfortabel Korrelationen ausrechnen

2 Hintergrund und Ziel

Dieser Post zeigt einige mögliche/typische Schritte der explorativen Datenanalyse (EDA) im Hinblick auf die Forschungsfrage “Welche Variablen steht in Zusammenhang mit Flugverspätungen?”.

3 Daten laden

Der Datensatz kann z.B. hier bezogen werden:

flights <- read_csv("https://vincentarelbundock.github.io/Rdatasets/csv/nycflights13/flights.csv")

Der Datensatz ist relativ groß:

dim(flights)
#> [1] 336776     20

Alternativ findet man den Datensatz auch im Paket nycflights13.

data(flights, package = "nycflights13")

4 Was ist Verspätung?

Schauen wir uns den Datensatz mal näher an, um die Zielvariable “Verspätung” zu beleuchten.

glimpse(flights)
#> Rows: 336,776
#> Columns: 20
#> $ X1             <dbl> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, …
#> $ year           <dbl> 2013, 2013, 2013, 2013, 2013, 2013, 2013, 2013, 2013, 2…
#> $ month          <dbl> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1…
#> $ day            <dbl> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1…
#> $ dep_time       <dbl> 517, 533, 542, 544, 554, 554, 555, 557, 557, 558, 558, …
#> $ sched_dep_time <dbl> 515, 529, 540, 545, 600, 558, 600, 600, 600, 600, 600, …
#> $ dep_delay      <dbl> 2, 4, 2, -1, -6, -4, -5, -3, -3, -2, -2, -2, -2, -2, -1…
#> $ arr_time       <dbl> 830, 850, 923, 1004, 812, 740, 913, 709, 838, 753, 849,…
#> $ sched_arr_time <dbl> 819, 830, 850, 1022, 837, 728, 854, 723, 846, 745, 851,…
#> $ arr_delay      <dbl> 11, 20, 33, -18, -25, 12, 19, -14, -8, 8, -2, -3, 7, -1…
#> $ carrier        <chr> "UA", "UA", "AA", "B6", "DL", "UA", "B6", "EV", "B6", "…
#> $ flight         <dbl> 1545, 1714, 1141, 725, 461, 1696, 507, 5708, 79, 301, 4…
#> $ tailnum        <chr> "N14228", "N24211", "N619AA", "N804JB", "N668DN", "N394…
#> $ origin         <chr> "EWR", "LGA", "JFK", "JFK", "LGA", "EWR", "EWR", "LGA",…
#> $ dest           <chr> "IAH", "IAH", "MIA", "BQN", "ATL", "ORD", "FLL", "IAD",…
#> $ air_time       <dbl> 227, 227, 160, 183, 116, 150, 158, 53, 140, 138, 149, 1…
#> $ distance       <dbl> 1400, 1416, 1089, 1576, 762, 719, 1065, 229, 944, 733, …
#> $ hour           <dbl> 5, 5, 5, 5, 6, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 5, 6, 6, 6…
#> $ minute         <dbl> 15, 29, 40, 45, 0, 58, 0, 0, 0, 0, 0, 0, 0, 0, 0, 59, 0…
#> $ time_hour      <dttm> 2013-01-01 05:00:00, 2013-01-01 05:00:00, 2013-01-01 0…

Es gibt zwei Spalten, die auf Verspätung hinzielen:

flights %>% 
  select(contains("delay")) %>% 
  head()
dep_delay arr_delay
2 11
4 20
2 33
-1 -18
-6 -25
-4 12

Ein Blick in die Hilfe lässt uns mehr lernen, was die Spalten bedeuten: help(flights) (wenn das Paket nycflights13 geladen ist; alternativ kann man z.B. hier nachlesen):

dep_delay, arr_delay

Departure and arrival delays, in minutes. 
Negative times represent early departures/arrivals.

4.1 Wie ähnlich sind Ankunfts- und Abflugsverspätung?

Da der Datensatz so groß ist, ziehen wir eine Stichprobe, dann geht alles schneller. Hier nicht wichtig, nur um etwas Zeit beim Plotten zu sparen. In der Praxis würde ich in an dieser Stelle keine Stichprobe ziehen, bzw. mit dem Gesamtdatensatz weiterarbeiten (was wir ja auch im Folgenden tun).

flights %>% 
  sample_n(size = 1000) %>% 
  ggplot() +
  aes(y = dep_delay, x = arr_delay) +
  geom_point()

Oder vielleicht so:

flights %>%
  drop_na(dep_delay, arr_delay) %>% 
  summarise(sd(dep_delay),
            sd(arr_delay))
sd(dep_delay) sd(arr_delay)
40.06569 44.63329

Das sind ca. 10% Differenz in der Skalierung; wir können die Skalierung komplett angleichen, um Abweichungen, die auf unterschiedlichen Mustern beruhen, besser zu sehen. Dazu hilft uns die z-Transformation.

flights %>% 
  select(contains("delay")) %>% 
  drop_na() %>% 
  mutate(dep_delay = scale(dep_delay),  # z-Transformation
         arr_delay = scale(arr_delay)) %>%   # z-Transformation
  ggplot() +
  aes(x = arr_delay, y = dep_delay) +
  geom_bin2d() +
  geom_abline(linetype = "dashed",
              color = "grey60")

Die beiden Variablen scheinen ziemlich stark korreliert zu sein.

flights %>% 
  drop_na(dep_delay, arr_delay) %>% 
  summarise(cor(dep_delay, arr_delay))
cor(dep_delay, arr_delay)
0.9148028

Ja, sind sie. Dann ist es vielleicht egal, welche der beiden Variablen wir verwenden. Nehmen wir dep_delay.

5 Verteilung der Verspätung

flights %>% 
  ggplot() +
  aes(x = dep_delay) %>% 
  geom_density()

Ein sehr langer rechter Rand; die meisten Flüge sind nicht/kaum verspätet; aber einige wenige sind sehr stark verspätet.

Zentrale deskriptive Statisitken könnte man sich mit summary ausgeben lassen:

flights %>%
  filter(!is.na(dep_delay)) %>%  # keine fehlenden Werte
  summarise(depdelay_mean = mean(dep_delay),
            depdelay_sd = sd(dep_delay),
            depdelay_md = median(dep_delay),
            depdelay_iqr = IQR(dep_delay)) 
depdelay_mean depdelay_sd depdelay_md depdelay_iqr
12.63907 40.21006 -2 16

Oder man benutzt den Befehl across, der es erlaubt, eine oder mehrere Funktionen auf eine oder mehrere Spalten anzuwenden. In diesem Beispiel wenden wir mehrere Funktionen (adressiert mit .fns) auf eine Spalte (dep_delay), adressiert mit dem Argument .cols an. Außerdem kann man die Namen der resultierenden Spalten bestimmen mit dem Argument .names. In der geschweiften Klammer steht eine interne Variable, die den Namen der jeweils berechneten Funktion ({fn}) an den Namen der neu erstellten Spalte anfügt; in der Ausgabe sieht man das gut.

flights %>% 
  summarise(across(
    .cols = dep_delay,
    .fns = list(mean = mean, 
                md = median, 
                sd = sd, 
                iqr = IQR), na.rm = TRUE,
    .names = "depdelay_{fn}"
  ))
depdelay_mean depdelay_md depdelay_sd depdelay_iqr
12.63907 -2 40.21006 16

5.1 flights2: Extremwerte (der Verspätung) definieren

Es gibt keinen sicheren Weg, mit Extremwerten umzugehen. Häufig macht es Sinn, die Ergebnisse zu vergleichen mit oder ohne Extremwerten.

"Wann ist ein Flug sehr verspätet?

5.1.1 Boxplot-Methode

Eine Möglichkeit ist die “Boxplot-Methode”: Entferne alle Flüge, die mehr verspätet sind als: \(q75+1.5iqr\)

Definieren wir als Extremwert, wenn eine Verspätung zu den 25% Flügen mit der größten Verspätung gehört.

flights %>% 
  summarise(q75 = quantile(dep_delay, 
                           prob = .75, 
                           na.rm = TRUE))
q75
11

Das sind also etwa 11 Minuten, die die Grenzlinie zwischen den 75% weniger bzw. den 25% stärker verspäteten Flügen markieren.

Dann berechnen wir den IQR:

flights %>% 
  summarise(depdelay_iqr = IQR(dep_delay, na.rm = TRUE))
depdelay_iqr
16

Der Grenzwert liegt dem zufolge bei:

grenzwert <- 11 + 1.5*16

Das ist kein “gottgegebener” Wert, sondern ein pragmatischer Versuch, einen Grenzwert zu finden. Die Nützlichkeit dieses Grenzwerts müsste sich noch erweisen. Viele andere Grenzwerte lassen sich verteidigen.

flights2 <-
  flights %>% 
  mutate(is_extreme = case_when(
    dep_delay > 11 + 1.5 * 16 ~ TRUE, # Verspätung > 35 Min.
    dep_delay <= 35 ~ FALSE  # in den anderen Fällen (<= 35 Min.), dann kein Extremwert
  ))

5.2 Fehlende Werte berechnen

Wie viele fehlende Werte gibt es eigentlich?

flights %>% 
  summarise(sum(is.na(dep_delay)))  # fehlende Werte zählen
sum(is.na(dep_delay))
8255

So geht es für alle Spalten auf einmal:

flights %>% 
  summarise(across(everything(), ~ sum(is.na(.x))))
X1 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
0 0 0 0 8255 0 8255 8713 0 9430 0 0 2512 0 0 9430 0 0 0 0

Übrigens hätten wir die Anzahl der fehlenden Werte auch mit skim() in Erfahrung bringen können.

Wie viele Fälle gingen verloren, wenn wir die Fälle mit fehlenden Werten bei dep_delay entfernten?

flights %>% 
  drop_na(dep_delay) %>% 
  nrow()
#> [1] 328521

Und wenn wir alle fehlenden Werte entfernen würden?

flights %>% 
  drop_na() %>% 
  nrow()
#> [1] 327346

Wir verlieren nicht viele Fälle mehr, wenn wir die fehlenden Werte aller Variablen (Spalten) entfernen. Also machen wir das mal.

5.3 flights3

Achtung: dieses Vorgehen hier ist gefährlich. U.U. verliert man sehr viele Zeilen (Beobachtungen).

flights3 <-
  flights2 %>% 
  drop_na() %>% 
  select(-year)

Die Spalte year ist kontant (immer der Wert “2013”); daher ist die Spalte nutzlos, sie birgt keine Information. Wir können sie gefahrlos löschen.

6 Deskriptive Statistiken

6.1 Mit summarise

Das kann machen mit summarise. Einfach, kann aber viel Tipperei bedeuten:

flights2 %>% 
  summarise(mean(dep_delay),
            sd(dep_delay),
            mean(arr_delay),
            sd(arr_delay))  # und so weiter
mean(dep_delay) sd(dep_delay) mean(arr_delay) sd(arr_delay)
NA NA NA NA

6.2 Mit skimr

skimr ist sehr praktisch; man bekommt viele Statistiken auf einmal gezeigt; das spart viel Tipperei.

flights %>% 
  skim()
Table 6.1: Data summary
Name Piped data
Number of rows 336776
Number of columns 20
_______________________
Column type frequency:
character 4
numeric 15
POSIXct 1
________________________
Group variables None

Variable type: character

skim_variable n_missing complete_rate min max empty n_unique whitespace
carrier 0 1.00 2 2 0 16 0
tailnum 2512 0.99 5 6 0 4043 0
origin 0 1.00 3 3 0 3 0
dest 0 1.00 3 3 0 105 0

Variable type: numeric

skim_variable n_missing complete_rate mean sd p0 p25 p50 p75 p100 hist
X1 0 1.00 168388.50 97219.00 1 84194.75 168388.5 252582.2 336776 ▇▇▇▇▇
year 0 1.00 2013.00 0.00 2013 2013.00 2013.0 2013.0 2013 ▁▁▇▁▁
month 0 1.00 6.55 3.41 1 4.00 7.0 10.0 12 ▇▆▆▆▇
day 0 1.00 15.71 8.77 1 8.00 16.0 23.0 31 ▇▇▇▇▆
dep_time 8255 0.98 1349.11 488.28 1 907.00 1401.0 1744.0 2400 ▁▇▆▇▃
sched_dep_time 0 1.00 1344.25 467.34 106 906.00 1359.0 1729.0 2359 ▁▇▇▇▃
dep_delay 8255 0.98 12.64 40.21 -43 -5.00 -2.0 11.0 1301 ▇▁▁▁▁
arr_time 8713 0.97 1502.05 533.26 1 1104.00 1535.0 1940.0 2400 ▁▃▇▇▇
sched_arr_time 0 1.00 1536.38 497.46 1 1124.00 1556.0 1945.0 2359 ▁▃▇▇▇
arr_delay 9430 0.97 6.90 44.63 -86 -17.00 -5.0 14.0 1272 ▇▁▁▁▁
flight 0 1.00 1971.92 1632.47 1 553.00 1496.0 3465.0 8500 ▇▃▃▁▁
air_time 9430 0.97 150.69 93.69 20 82.00 129.0 192.0 695 ▇▂▂▁▁
distance 0 1.00 1039.91 733.23 17 502.00 872.0 1389.0 4983 ▇▃▂▁▁
hour 0 1.00 13.18 4.66 1 9.00 13.0 17.0 23 ▁▇▇▇▅
minute 0 1.00 26.23 19.30 0 8.00 29.0 44.0 59 ▇▃▆▃▅

Variable type: POSIXct

skim_variable n_missing complete_rate min max median n_unique
time_hour 0 1 2013-01-01 05:00:00 2013-12-31 23:00:00 2013-07-03 10:00:00 6936

7 Korrelate von Verspätung

Schauen wir, welche Variablen mit dep_delay, der Verspätung der Flüge also, korrelieren.

7.1 Metrische Prädiktoren

7.1.1 Nur mit cor

Am einfachsten geht es so. Der Nachteil ist mehr (viel) Tipperei:

flights3 %>% 
  select(where(is.numeric)) %>% 
  summarise(cor_month = cor(dep_delay, month),
            cor_day = cor(dep_delay, day),
            cor_dep_time = cor(dep_delay, dep_time))  # etc
cor_month cor_day cor_dep_time
-0.0200547 0.0005914 0.2596127

7.1.2 Mit across

Berechnen wir die Korrelationen jetzt mit dem Befehl across. Der Punkt . spricht hier jeweils eine Spalte an, die von across ausgewählt wurde. Der Effekt ist, dass eine Korrelation von jeder Spalte mit dep_delay berechnet wird.

flights3 %>% 
  select(where(is.numeric)) %>%  # nur die numerischen Spalten auswählen
  summarise(across(
    .cols = everything(),
    .fns = ~ cor(., dep_delay))) %>% 
  pivot_longer(everything()) %>%  # von breit auf lang
  arrange(-value)  # absteigend sortieren
name value
dep_delay 1.0000000
arr_delay 0.9148028
dep_time 0.2596127
sched_dep_time 0.1989235
hour 0.1982692
sched_arr_time 0.1604972
flight 0.0539697
X1 0.0491868
arr_time 0.0294210
minute 0.0282514
day 0.0005914
month -0.0200547
distance -0.0216809
air_time -0.0224051

7.1.3 Mit correlate

Oder so, das ist vielleicht einfacher:

flights2 %>% 
  select(where(is.numeric)) %>%  # alle metrischen Variablen, aber sonst keine
  correlate() %>%   # korreliere, was du hast
  focus(dep_delay) %>%  # beschränke (fokussiere) dich auf `dep_delay`
  arrange(-dep_delay)
term dep_delay
arr_delay 0.9148028
dep_time 0.2602312
sched_dep_time 0.1988867
hour 0.1982259
sched_arr_time 0.1604885
flight 0.0547337
X1 0.0497132
arr_time 0.0287288
minute 0.0284409
day 0.0004200
month -0.0200570
distance -0.0216708
air_time -0.0224051
year NA

7.2 Nominale Prädiktoren

flights2 %>% 
  select(where(negate(is.numeric))) %>% 
  names()
#> [1] "carrier"    "tailnum"    "origin"     "dest"       "time_hour" 
#> [6] "is_extreme"
flights2 %>% 
  select(where(negate(is.numeric))) %>% 
  summarise(across(
    .cols = everything(),
    .fns = list(ndist = n_distinct)
  )) %>% 
  pivot_longer(everything())
name value
carrier_ndist 16
tailnum_ndist 4044
origin_ndist 3
dest_ndist 105
time_hour_ndist 6936
is_extreme_ndist 3

Geht auch mit skim, s. oben, ist einfacher.

7.2.1 Carrier

carrier ist die Fluggesellschaft. Wie viele verschiedene Fluggesellschaften gibt es? Wie viele Flüge hat jede davon ausgeführt? Welche Fluggesellschaft hat die meisten Flüge ausgeführt? Gibt es große Unterschiede in de Zahl der ausgeführten Flüge. Wer hat eigentlich die flüssige Seife erfunden? Fragen über Fragen.

flights2 %>% 
  select(carrier) %>% 
  count(carrier, sort = TRUE) 
carrier n
UA 58665
B6 54635
EV 54173
DL 48110
AA 32729
MQ 26397
US 20536
9E 18460
WN 12275
VX 5162
FL 3260
AS 714
F9 685
YV 601
HA 342
OO 32

Wir brauchen eine Visualisierung dazu; das beantwortet vielleicht einen Teil der obigen Fragen.

plot1 <- flights2 %>% 
  select(carrier) %>% 
  count(carrier, sort = TRUE) %>% 
  ggplot() +
  aes(y = carrier, x = n) +
  geom_point(color = "red") +
  geom_line(group = 1)

plot1

Wir müssen die Werte von carrier sortieren anhand der Anzahl der Flüge, sonst ist es zu unübersichtlich.

7.2.2 Achsen-Labels anpassen

Dieser Abschnitt ist zur Vertiefung, er ist nicht inhaltlich wichtig

Sagen wir, wir möchten die Labels der X-Achse anpassen, und zwar möchten wir die Werte 25.000, 50.000, und 75.0000.

plot1 +
  scale_x_continuous(breaks = c(0, 25000, 50000, 75000),
                     limits = c(0, 100000),
                     labels = c("keine", "wenig", "mittel", "viel"))

Hm, schön sieht es noch nicht aus; die limits machen nicht unbedingt Sinn. Die labels sind auch wenig sinnvoll.

Mehr zum Thema “Achsen aufhübschen” findet sich z.B. hier.

7.2.3 “Lumpsensammler-Kategorie”

flights2 <-
  flights2 %>% 
  mutate(carrier = factor(carrier)) %>%   # nicht character, sondern factor
  mutate(carrier_lump = fct_lump(carrier, n = 8))

Hier fassen wir mit fct_lump alle Stufen von carrier zu acht Stufen (daher n = 8) zusammen plus einer “Lumpensammler-Kategorie” zusammen. Dazu muss die Variable aber als factor vorliegen, was wir in der Zeile davor erledigt haben.

Jetzt haben wir noch nur 9 (8 plus Lumpensammler-Gruppe) Gruppen:

flights2 %>% 
  count(carrier_lump)
carrier_lump n
9E 18460
AA 32729
B6 54635
DL 48110
EV 54173
MQ 26397
UA 58665
US 20536
Other 23071

Liniendiagramm:

flights2 %>% 
  select(carrier) %>% 
  count(carrier, sort = TRUE) %>% 
  ggplot() +
  aes(y = fct_reorder(carrier, n), x = n) +
  geom_point(color = "red") +
  geom_line(group = 1)

Mit fct_reorder haben wir die Werte von carrier (UA, B6, AA, …) sortiert und zwar anhand der Werte von n, also anhand der Häufigkeit. Es resultiert eine Rangfolge: UA > B6 > EV > DL > ... etc. (Nur) Mit einer sortierten Faktorvariable lässt sich so eine Art von Diagramm gut sortiert darstellen.

Ah, schon besser. Aber recht informationsarm, das Diagramm. Informationsreicher ist ein Boxplot:

flights2 %>% 
  filter(!is_extreme) %>%  # keine Extremwerte
  ggplot() +
  aes(x = fct_reorder(carrier_lump, dep_delay, 
                      na.rm = TRUE), 
      y = dep_delay) %>% 
  geom_boxplot()

Eine alternative Darstellung wäre ein Letter Value Plot.

Schauen wir uns mal die Mediane genauer an:

flights2 %>% 
  filter(!is_extreme) %>% 
  group_by(carrier_lump) %>% 
  summarise(dep_delay = median(dep_delay, na.rm = TRUE)) %>% 
  arrange(dep_delay)
carrier_lump dep_delay
US -5
MQ -4
9E -3
AA -3
DL -3
EV -3
B6 -2
UA -1
Other 0

Die Reihenfolge entspricht der dem obigen Diagramm.

7.3 Korrelation von carrier mit Verspätung

Hier mit “Dummysierung” aller nicht-numerischer Spalten. Ein Beispiel zur Verdeutlichung:

flights2 <- 
  flights2 %>% 
  mutate(
    originJFK = case_when(
      origin == "JFK" ~ 1,
      origin != "JFK" ~ 0
    ),
    originLGA = case_when(
      origin == "LGA" ~ 1,
      TRUE ~ 0,
    )
  )
flights2 %>% 
  select(origin, originJFK, originLGA) %>% 
  slice(1:5)
origin originJFK originLGA
EWR 0 0
LGA 0 1
JFK 1 0
JFK 1 0
LGA 0 1

Diese Art der Umwandlung von mehrstufig-nominal in eine binäre Varialbe (0-1-Variable, oder “Indikatorvariable”) kann auch z.B. mit der Funktion dummy_cols() (auf “fastDummies”) bewerkstelligen lassen:

flights2 %>% 
  select(origin, dep_delay) %>% 
  dummy_cols() %>%  # aus dem Paket `fastDummies`
  head()  # slice(1:6)
origin dep_delay origin_EWR origin_JFK origin_LGA
EWR 2 1 0 0
LGA 4 0 0 1
JFK 2 0 1 0
JFK -1 0 1 0
LGA -6 0 0 1
EWR -4 1 0 0

Mit den “dummyisierten” Spalten können wir jetzt Korrelationen rechnen, denn jetzt haben wir Zahlen. Achtung: Die Variablen bleiben nominalskaliert, trotz der 0-1-Transformation. Auf diese Art Korrelationen zu berechnen ist nur für dummysierte Variablen (“Indikatorvariablen”) sinnvoll. Die Schiefe der Verteilung begrenzt hier übrigens die Stärke der Korrelation.

flights2 %>% 
  select(dep_delay, carrier) %>% 
  dummy_cols() %>%   # "Dummysierung"
  select(-carrier) %>% 
  pivot_longer(-dep_delay,
               names_to = "carrier",
               values_to = "value") %>% 
  group_by(carrier) %>% 
  summarise(cor_depdelay_carrier = cor(dep_delay, value,
                                       use = "complete.obs")) %>% 
  arrange(-abs(cor_depdelay_carrier)) %>% 
  filter(abs(cor_depdelay_carrier) > 0.10)
carrier cor_depdelay_carrier

Keine Korrelation war (im Betrag) größer als 0.1.

Zur Erinnerung: Es ist nicht unbedingt nötig, die “Dummyisierung” durchzuführen, ein einfaches Vergleichen der Mittelwerte (oder Mediane) mit ihrer Streuung führt zu einem ähnlichen Ergebnis. Die Regression mit lm führt für Sie automatisch die Dummyisierung durch.

7.3.1 Hour

flights2 %>% 
  filter(!is_extreme) %>% 
  select(dep_delay, hour) %>% 
  mutate(hour = factor(hour)) %>% 
  ggplot() +
  aes(x = fct_reorder(hour, dep_delay,
                      na.rm = TRUE), 
      y = dep_delay) +
  geom_boxplot()

7.3.2 Origin

flights2 %>% 
  filter(!is_extreme) %>% 
  select(origin, dep_delay) %>% 
  ggplot() +
  aes(x = origin, y = dep_delay) %>% 
  geom_boxplot()

7.4 Drei Variablen: Origin, hour, dep_delay

flights2 %>% 
  filter(!is_extreme) %>% 
  select(origin, dep_delay, hour) %>% 
  mutate(hour = factor(hour, levels = 5:23)) %>% 
  ggplot() +
  aes(x = hour, y = dep_delay) +
  geom_boxplot() +
  facet_wrap(~ origin)

7.5 Alle nominale Variablen

Natürlich könnte man “händisch” alle nominalskalierten Variablen explizit benennen, etwa so:

flights3 %>% 
  select(carrier, tailnum, origin, dest, time_hour) %>% 
  slice(1:3)
carrier tailnum origin dest time_hour
UA N14228 EWR IAH 2013-01-01 05:00:00
UA N24211 LGA IAH 2013-01-01 05:00:00
AA N619AA JFK MIA 2013-01-01 05:00:00

Aber es geht auch etwas “cooler” mit weniger Tipperei:

flights3 %>% 
  select(where(~ !is.numeric(.))) %>% 
  names()
#> [1] "carrier"    "tailnum"    "origin"     "dest"       "time_hour" 
#> [6] "is_extreme"

7.6 flights4

flights4 <-
flights3 %>% 
  mutate(dest = fct_lump_prop(dest, prop = .025)) 

Mit fct_lump_prop fassen wir alle Stufen zu einer zusammen, die jeweils weniger als 2.5% ausmachen.

flights4 %>% 
  count(dest, sort = T)
dest n
Other 172061
ATL 16837
ORD 16566
LAX 16026
BOS 15022
MCO 13967
CLT 13674
SFO 13173
FLL 11897
MIA 11593
DCA 9111
DTW 9031
DFW 8388
flights4 %>% 
  filter(!is_extreme) %>% 
  select(dep_delay, dest, origin, carrier) %>% 
  group_by(dest, origin, carrier) %>% 
  summarise(depdelay_md = median(dep_delay, na.rm = T)) %>% 
  ggplot() +
  aes(x = origin, y = depdelay_md, color = origin) +
  facet_grid(dest ~ carrier) +
  geom_point()
  

Puh, das Diagramm ist nicht sehr aussagekräftig. Vielleicht besser als Tabelle?

flights4 %>% 
  filter(!is_extreme) %>% 
  select(dep_delay, dest, origin, carrier) %>% 
  group_by(dest, origin, carrier) %>% 
  summarise(depdelay_md = median(dep_delay, na.rm = T))
#> # A tibble: 128 x 4
#> # Groups:   dest, origin [37]
#>    dest  origin carrier depdelay_md
#>    <fct> <chr>  <chr>         <dbl>
#>  1 ATL   EWR    9E               -6
#>  2 ATL   EWR    DL               -3
#>  3 ATL   EWR    EV               -2
#>  4 ATL   EWR    UA               -1
#>  5 ATL   JFK    9E               -2
#>  6 ATL   JFK    DL               -1
#>  7 ATL   LGA    DL               -3
#>  8 ATL   LGA    EV               30
#>  9 ATL   LGA    FL                0
#> 10 ATL   LGA    MQ               -4
#> # … with 118 more rows

Hm, ist auch nicht gerade nützlich.

Das Beispiel zeigt, dass die Datenvisualisierung bei einer größeren Zahl an Dimensionen und/oder vielen Werten an ihre Grenzen kommen kann.

7.7 Anzahl von Flüge

flights4_sum <- 
  flights4 %>% 
  filter(!is_extreme) %>% 
  select(month, origin, dep_delay) %>% 
  drop_na() %>% 
  group_by(month, origin) %>% 
  summarise(delay_md = median(dep_delay),
            delay_iqr = IQR(dep_delay),
            delay_n = n()) %>% 
  mutate(month = factor(month),
         delay_n = as.numeric(delay_n))
  
flights4 %>% 
  filter(!is_extreme) %>% 
  select(month, origin, dep_delay) %>% 
  mutate(month = factor(month)) %>% 
  drop_na() %>% 
  ggplot() +
  aes(x = month, y = dep_delay, color = origin) +
  geom_violin() +
  geom_point(data = flights4_sum, 
             aes(y = delay_md,
                 x = month)) +
  facet_wrap( ~ origin)

8 Fazit

Keine heiße Spur bisher. Allerdings erlaubt explorative Datenanalyse nur die gleichzeitige Betrachtung von zwei bis drei, vielleicht vier Variablen. Sind die Zusammenhänge komplizierter in dem Sinne, dass mehrere Variablen für einen Effekt zusammenwirken, so ist es mit explorativen Methoden schwer zu finden.

Der nächste logische Schritt: Wir müssen modellieren. Mit Modellierungsmethoden lassen sich auch hochdimensionale Zusammenhänge finden.

9 Achtung

Diese Analyse ist rein explorativ in dem Sinne, dass keine Hypothesen getestet werden. Es ist damit zu rechnen, dass falsch-positive Befunde auftauchen. Alle Ergebnisse sollten anhand neuer Daten validiert werden.

10 Reproduzierbarkeit

#> ─ Session info ───────────────────────────────────────────────────────────────────────────────────────────────────────
#>  setting  value                       
#>  version  R version 4.1.0 (2021-05-18)
#>  os       macOS Big Sur 10.16         
#>  system   x86_64, darwin17.0          
#>  ui       X11                         
#>  language (EN)                        
#>  collate  en_US.UTF-8                 
#>  ctype    en_US.UTF-8                 
#>  tz       Europe/Berlin               
#>  date     2021-06-22                  
#> 
#> ─ Packages ───────────────────────────────────────────────────────────────────────────────────────────────────────────
#>  package     * version  date       lib source                        
#>  assertthat    0.2.1    2019-03-21 [1] CRAN (R 4.1.0)                
#>  backports     1.2.1    2020-12-09 [1] CRAN (R 4.1.0)                
#>  blogdown      1.3      2021-04-14 [2] CRAN (R 4.1.0)                
#>  bookdown      0.22     2021-04-22 [2] CRAN (R 4.1.0)                
#>  broom         0.7.6    2021-04-05 [1] CRAN (R 4.1.0)                
#>  bslib         0.2.5.1  2021-05-18 [1] CRAN (R 4.1.0)                
#>  cachem        1.0.5    2021-05-15 [2] CRAN (R 4.1.0)                
#>  callr         3.7.0    2021-04-20 [1] CRAN (R 4.1.0)                
#>  cellranger    1.1.0    2016-07-27 [1] CRAN (R 4.1.0)                
#>  cli           2.5.0    2021-04-26 [1] CRAN (R 4.1.0)                
#>  codetools     0.2-18   2020-11-04 [2] CRAN (R 4.1.0)                
#>  colorspace    2.0-1    2021-05-04 [1] CRAN (R 4.1.0)                
#>  crayon        1.4.1    2021-02-08 [1] CRAN (R 4.1.0)                
#>  data.table    1.14.0   2021-02-21 [1] CRAN (R 4.1.0)                
#>  DBI           1.1.1    2021-01-15 [1] CRAN (R 4.1.0)                
#>  dbplyr        2.1.1    2021-04-06 [1] CRAN (R 4.1.0)                
#>  desc          1.3.0    2021-03-05 [2] CRAN (R 4.1.0)                
#>  devtools      2.4.1    2021-05-05 [2] CRAN (R 4.1.0)                
#>  digest        0.6.27   2020-10-24 [1] CRAN (R 4.1.0)                
#>  dplyr       * 1.0.6    2021-05-05 [1] CRAN (R 4.1.0)                
#>  ellipsis      0.3.2    2021-04-29 [1] CRAN (R 4.1.0)                
#>  evaluate      0.14     2019-05-28 [1] CRAN (R 4.1.0)                
#>  fansi         0.5.0    2021-05-25 [1] CRAN (R 4.1.0)                
#>  farver        2.1.0    2021-02-28 [1] CRAN (R 4.1.0)                
#>  fastDummies * 1.6.3    2020-11-29 [1] CRAN (R 4.1.0)                
#>  fastmap       1.1.0    2021-01-25 [2] CRAN (R 4.1.0)                
#>  forcats     * 0.5.1    2021-01-27 [1] CRAN (R 4.1.0)                
#>  fs            1.5.0    2020-07-31 [1] CRAN (R 4.1.0)                
#>  generics      0.1.0    2020-10-31 [1] CRAN (R 4.1.0)                
#>  ggplot2     * 3.3.3    2020-12-30 [1] CRAN (R 4.1.0)                
#>  glue          1.4.2    2020-08-27 [1] CRAN (R 4.1.0)                
#>  gtable        0.3.0    2019-03-25 [1] CRAN (R 4.1.0)                
#>  haven         2.4.1    2021-04-23 [1] CRAN (R 4.1.0)                
#>  highr         0.9      2021-04-16 [1] CRAN (R 4.1.0)                
#>  hms           1.1.0    2021-05-17 [1] CRAN (R 4.1.0)                
#>  htmltools     0.5.1.1  2021-01-22 [1] CRAN (R 4.1.0)                
#>  httr          1.4.2    2020-07-20 [1] CRAN (R 4.1.0)                
#>  jquerylib     0.1.4    2021-04-26 [1] CRAN (R 4.1.0)                
#>  jsonlite      1.7.2    2020-12-09 [1] CRAN (R 4.1.0)                
#>  knitr         1.33     2021-04-24 [1] CRAN (R 4.1.0)                
#>  labeling      0.4.2    2020-10-20 [1] CRAN (R 4.1.0)                
#>  lifecycle     1.0.0    2021-02-15 [1] CRAN (R 4.1.0)                
#>  lubridate     1.7.10   2021-02-26 [1] CRAN (R 4.1.0)                
#>  magrittr      2.0.1    2020-11-17 [1] CRAN (R 4.1.0)                
#>  memoise       2.0.0    2021-01-26 [2] CRAN (R 4.1.0)                
#>  modelr        0.1.8    2020-05-19 [1] CRAN (R 4.1.0)                
#>  munsell       0.5.0    2018-06-12 [1] CRAN (R 4.1.0)                
#>  pillar        1.6.1    2021-05-16 [1] CRAN (R 4.1.0)                
#>  pkgbuild      1.2.0    2020-12-15 [2] CRAN (R 4.1.0)                
#>  pkgconfig     2.0.3    2019-09-22 [1] CRAN (R 4.1.0)                
#>  pkgload       1.2.1    2021-04-06 [2] CRAN (R 4.1.0)                
#>  prettyunits   1.1.1    2020-01-24 [1] CRAN (R 4.1.0)                
#>  printr      * 0.1.1    2021-01-27 [1] CRAN (R 4.1.0)                
#>  processx      3.5.2    2021-04-30 [1] CRAN (R 4.1.0)                
#>  ps            1.6.0    2021-02-28 [1] CRAN (R 4.1.0)                
#>  purrr       * 0.3.4    2020-04-17 [1] CRAN (R 4.1.0)                
#>  R6            2.5.0    2020-10-28 [1] CRAN (R 4.1.0)                
#>  Rcpp          1.0.6    2021-01-15 [1] CRAN (R 4.1.0)                
#>  readr       * 1.4.0    2020-10-05 [1] CRAN (R 4.1.0)                
#>  readxl        1.3.1    2019-03-13 [1] CRAN (R 4.1.0)                
#>  remotes       2.4.0    2021-06-02 [2] CRAN (R 4.1.0)                
#>  reprex        2.0.0    2021-04-02 [1] CRAN (R 4.1.0)                
#>  rlang         0.4.11   2021-04-30 [1] CRAN (R 4.1.0)                
#>  rmarkdown     2.8      2021-05-07 [1] CRAN (R 4.1.0)                
#>  rprojroot     2.0.2    2020-11-15 [2] CRAN (R 4.1.0)                
#>  rstudioapi    0.13     2020-11-12 [1] CRAN (R 4.1.0)                
#>  rvest         1.0.0    2021-03-09 [1] CRAN (R 4.1.0)                
#>  sass          0.4.0    2021-05-12 [1] CRAN (R 4.1.0)                
#>  scales        1.1.1    2020-05-11 [1] CRAN (R 4.1.0)                
#>  sessioninfo   1.1.1    2018-11-05 [2] CRAN (R 4.1.0)                
#>  stringi       1.6.2    2021-05-17 [1] CRAN (R 4.1.0)                
#>  stringr     * 1.4.0    2019-02-10 [1] CRAN (R 4.1.0)                
#>  testthat      3.0.2    2021-02-14 [2] CRAN (R 4.1.0)                
#>  tibble      * 3.1.2    2021-05-16 [1] CRAN (R 4.1.0)                
#>  tidyr       * 1.1.3    2021-03-03 [1] CRAN (R 4.1.0)                
#>  tidyselect    1.1.1    2021-04-30 [1] CRAN (R 4.1.0)                
#>  tidyverse   * 1.3.1    2021-04-15 [1] CRAN (R 4.1.0)                
#>  usethis       2.0.1    2021-02-10 [2] CRAN (R 4.1.0)                
#>  utf8          1.2.1    2021-03-12 [1] CRAN (R 4.1.0)                
#>  vctrs         0.3.8    2021-04-29 [1] CRAN (R 4.1.0)                
#>  withr         2.4.2    2021-04-18 [1] CRAN (R 4.1.0)                
#>  xfun          0.23     2021-05-15 [1] CRAN (R 4.1.0)                
#>  xml2          1.3.2    2020-04-23 [1] CRAN (R 4.1.0)                
#>  yaml          2.2.1.99 2021-06-18 [1] Github (viking/r-yaml@4788abe)
#> 
#> [1] /Users/sebastiansaueruser/Library/R/x86_64/4.1/library
#> [2] /Library/Frameworks/R.framework/Versions/4.1/Resources/library