22 min read

Fallstudie: Modellierung von Flugverspätungen

1 Hintergrund und Forschungsfrage

Wir untersuchen die Forschungsfrage Was sind Prädiktoren von Flugverspätungen. Dazu nutzen wir lineare Modelle als Modellierungsmethoden.

Dieser Post knüpft an den Post zur explorativen Datenanalyse der Flugverspätungen an (es gibt auch hier, Teil 1 und hier, Teil 2 ein Video zu diesem EDA-Post).

2 Pakete laden

library("tidymodels")  # Datenmodellierung
library("tidyverse")  # data wrangling
library("conflicted")  # Name clashes finden
library("leaps")  # Rohe Gewalt
library("skimr")  # deskriptive Statistiken komfortabel
library("ggfortify")  # Modellannahmen grafisch prüfen
library("tictoc")  # Rechenzeit messen

3 Daten laden

library("nycflights13")
data(flights)

4 flights2: Nicht benötigte Variablen entfernen und ID hinzufügen

flights2 <-
  flights %>% 
  select(-c(year, arr_delay)) %>% 
  drop_na(dep_delay) %>% 
  mutate(id = row_number()) %>% 
  select(id, everything())  # id nach vorne ziehen

5 Aufteilung in Train- und Testsample

Der Hintergrund zur Idee der Aufteilung in Train- und Test-Stichprobe kann z.b. hier oder hier, Kapitel 15, nachgelesen werden.

flights_split <- initial_split(flights2, 
                               strata = dep_delay,
                               na.rm = TRUE)

6 flights_train2, flights_test2

set.seed(42)  # Reproduzierbarkeit
flights_train2 <- training(flights_split)
flights_test2 <- testing(flights_split)

Die “wirkliche Welt” (was immer das ist) besorgt die Aufteilung von Train- und Test-Sampel für Sie automatisch. Sagen wir, Sie arbeiten für die Flughafen-Aufsicht von New York. Dann haben Sie einen Erfahrungsschatz an Flügen aus der Vergangenheit in Ihrer Datenbank (Train-Sample). Einige Tages kommt Ihr Chef zu Ihnen und sagt: “Rechnen Sie mir mal die zu erwartende Verspätung der Flüge im nächsten Monat aus!”. Da heute nicht klar ist, wie die Verspätung der Flüge in der Zukunft (nächsten Monat) sein wird, stellen die Flüge des nächsten Monats das Test-Sample dar.

Übrigens: In der Prüfung besorgt das Aufteilen von Train- und Test-Sample netterweise Ihr Dozent…

7 lm0: Nullmodell

Eigentlich nicht nötig, das Nullmodell, primär aus didaktischen Gründen berechnet, um zu zeigen, dass in diesem Fall \(R^2\) wirklich gleich Null ist.

lm0 <- lm(dep_delay ~ 1, data = flights_train2)
glance(lm0)
r.squaredadj.r.squaredsigmastatisticp.valuedflogLikAICBICdeviancedf.residualnobs
0040.14855NANANA-125941825188402518861397152640246387246388

Mit glance bekommt man das \(R^2\) (neben einem Haufen anderen Zeugs, was wir jetzt nicht brauchen). Man könnte z.B. auch summary(lm1) out arm::display(lm1). Es gibt verschiedene Möglichkeiten.

8 lm1: origin

lm1 <- lm(dep_delay ~ origin, data = flights_train2)
tidy(lm1)  # tidy zeit die (geschätzten) Regressionsgewichte (Betas)
termestimatestd.errorstatisticp.value
(Intercept)15.1327230.1350185112.078900
originJFK-3.0848970.1944907-15.861410
originLGA-4.8463990.1983607-24.432260

Man vergleiche:

flights_train2 %>% 
  drop_na(dep_delay) %>% 
  group_by(origin) %>% 
  summarise(delay_avg = mean(dep_delay)) %>% 
  mutate(delay_delta = delay_avg - delay_avg[1])
origindelay_avgdelay_delta
EWR15.132720.000000
JFK12.04783-3.084897
LGA10.28632-4.846399

Der Mittelwertsvergleich und das Modell lm1 sind faktisch informationsgleich.

Aber leider ist es um die Modellgüte nicht so gut bestellt (eigentlich eher “grottenschlecht”):

glance(lm1)[1]
r.squared
0.0025138

Die eckigen Klammern erlauben in R ein bestimmtes Element von mehreren auszuwählen. glance(lm1) liefert eine Tabelle mit mehreren Spalten (und einer Zeile) zurück; mit glance(lm1)[1] bekommt man das erste Element dieser Tabelle, das heißt die erste Spalte. Natürlich können Sie einfach glance(lm1) eingeben, wenn Sie das lieber mögen.

9 lm2: All in

# NICHT AUSFÜHREN
#lm2_all_in <- lm(dep_delay ~ ., data = flights_train2)

Modell lm2_all_in ist hier keine gute Idee, da nominale Prädiktoren in Indikatorvariablen umgewandelt werden. Hat ein nominaler Prädiktor sehr viele Stufen (wie hier), so resultieren sehr viele Indikatorvariablen, was dem Regressionsmodell Probleme bereiten kann (bei mir hängt sich R auf). Besser ist es in dem Fall, die Anzahl der Stufen von nominalskalierten Variablen vorab zu begrenzen.

Bei kleineren Datensätzen (weniger Variablen, weniger Fälle) lohnt es sich aber oft, das “All-in-Modell” auszuprobieren, als Referenzmaßstab für andere Modelle.

10 flights_train3: Textvariablen in Faktorvariablen umwandeln

Begrenzen wir zunächst die Anzahl der Stufen der nominal skalierten Variablen:

flights_train3 <- 
  flights_train2 %>% 
  mutate(across(
    .cols = where(is.character),
    .fns = as.factor))

Wem das across zu kompliziert ist, der kann auch alternativ (synonym) jede Variable einzeln in einen Faktor umwandeln und zwar so:

flights_train3a <- 
  flights_train2 %>% 
  mutate(tailnum = as.factor(tailnum),
         origin = as.factor(origin),
         dest = as.factor(dest),
         carrier = as.factor(carrier)
      )

Das ist einfacher als mit across, aber dafür mehr Tipperei.

Wir müssen die Transformationen, die wir auf das Train-Sample anwenden, auch auf das Test-Sample anwenden:

10.1 flights_test3

flights_test3 <- 
  flights_test2 %>% 
  mutate(across(
    .cols = where(is.character),
    .fns = as.factor))
flights_train3 %>% 
  select(where(is.factor)) %>% 
  names()
#> [1] "carrier" "tailnum" "origin"  "dest"

Z.B. dest hat viele Stufen:

flights_train3 %>% 
  count(dest, sort = TRUE)
destn
ORD12592
ATL12573
LAX12120
BOS11311
MCO10445
CLT10275
SFO9890
FLL8866
MIA8715
DCA6872
DTW6813
DFW6325
RDU5846
TPA5544
IAH5370
DEN5300
MSP5287
PBI4849
BNA4582
LAS4504
SJU4356
IAD4029
PHX3469
BUF3422
CLE3312
STL3135
MDW3057
SEA2884
CVG2792
MSY2762
RSW2648
CMH2563
PIT2103
CHS2075
SAN2069
MKE2039
JAX1952
SLC1886
BTV1873
AUS1790
RIC1760
ROC1760
PWM1733
HOU1542
IND1493
MCI1420
BWI1288
MEM1275
SYR1259
PHL1180
GSO1133
ORF1091
DAY1026
PDX999
SRQ887
SDF843
XNA765
MHT689
BQN653
CAK639
SNA621
GSP618
OMA587
SAV565
GRR549
HNL549
SAT504
LGB499
TYS436
MSN400
DSM398
STT391
BDL317
ALB315
BGR293
PSE288
BUR278
OKC262
PVD260
SJC256
TUL232
SMF224
OAK223
BHM205
ACK199
ABQ196
AVL175
EGE165
MVY152
CRW105
ILM78
CAE74
TVC72
MYR42
CHO31
BZN23
EYW14
PSP14
JAC12
HDN11
MTJ9
SBN9
ANC6
LEX1
flights_train3 %>%
  count(dest) %>%
  ggplot() +
  aes(y = fct_reorder(dest, n), x = n) +
  geom_col()

11 flights_train4: Faktorstufen zusammenfassen

flights_train4 <-
  flights_train3 %>% 
  mutate(across(
    .cols = where(is.factor),
    .fns = fct_lump_prop, prop = .025
  ))
flights_train4a <-
  flights_train3 %>% 
  mutate(dest  = fct_lump_prop(f = dest, 
                               prop = .025))
flights_train4a %>% 
  count(dest)
destn
ATL12573
BOS11311
CLT10275
DCA6872
DFW6325
DTW6813
FLL8866
LAX12120
MCO10445
MIA8715
ORD12592
SFO9890
Other129591
flights_train4 %>%
  count(dest) %>%
  ggplot() +
  aes(y = fct_reorder(dest, n), x = n) +
  geom_col()

Check:

flights_train4 %>% 
  select(where(is.factor)) %>% 
  summarise(nlevels_dest = nlevels(dest))
nlevels_dest
13

Oder alle Faktorvariablen auf einmal:

flights_train4 %>% 
  select(where(is.factor)) %>% 
  map_dfc(nlevels)
carriertailnumorigindest
101313

Das sind jetzt Zahlen von Faktorstufen, mit denen wir arbeiten können.

11.1 flights_test4

Vergessen wir nicht, die Transformation auch auf das Test-Sample anzuwenden:

flights_test4 <-
  flights_test3 %>% 
  mutate(across(
    .cols = where(is.factor),
    .fns = fct_lump_prop, prop = .025
  ))

12 lm3: Alle zusammengefassten Faktorvariablen

lm3 <- flights_train4 %>% 
  select(dep_delay, where(is.factor), -tailnum, -id) %>% 
  lm(dep_delay ~ ., data = .)

Der Punkt bei dep_delay ~ . meint “nimm alle Variablen im Datensatz (bis auf dep_delay)”.

Der Punkt bei data = . nimm die Tabelle, wie sie dir im letzten Schritt mundgerecht aufbereitet wurde. Man hätte hier auch flights_train4 schreiben können, aber dann hätten wir noch tailnum etc. entfernen müssen.

Eigentlich brauchen wir nicht so viele Dezimalstellen …

options(digits = 2)

glance zeigt das R-Quadrat (und einiges mehr, was hier jetzt nicht brauchen).

glance(lm3)  # R^2
r.squaredadj.r.squaredsigmastatisticp.valuedflogLikAICBICdeviancedf.residualnobs
0.01201330.011921139.90852130.2453023-125792925159082516168392381512246364246388
tidy(lm3)  # Modellkoeffizienten, also die Beta-Gewichte ("estimate")
termestimatestd.errorstatisticp.value
(Intercept)18.61108010.563218233.0441710.0000000
carrierAA-7.78409540.4917308-15.8299940.0000000
carrierB6-3.56705250.4116207-8.6658730.0000000
carrierDL-7.04951360.4322994-16.3070180.0000000
carrierEV2.98601880.45985766.4933560.0000000
carrierMQ-5.92042470.4724111-12.5323560.0000000
carrierUA-4.93839770.4607246-10.7187620.0000000
carrierUS-12.47253800.5694244-21.9037650.0000000
carrierWN1.48536700.58314052.5471850.0108602
carrierOther-1.67945450.6073453-2.7652380.0056885
originJFK-0.31883480.2827839-1.1274860.2595383
originLGA-1.63867430.2444494-6.7035310.0000000
destBOS-2.51965020.5619247-4.4839640.0000073
destCLT-0.67337370.6234772-1.0800290.2801302
destDCA-0.67587580.6672185-1.0129750.3110730
destDFW-1.89253190.6953841-2.7215630.0064978
destDTW-2.28274420.6088426-3.7493170.0001774
destFLL-0.79024330.5797189-1.3631490.1728368
destLAX-3.70379200.5541243-6.6840460.0000000
destMCO-1.56425090.5531661-2.8278140.0046871
destMIA-1.29000420.6062971-2.1276770.0333649
destORD1.57057860.55007062.8552310.0043009
destSFO-0.75712200.5814538-1.3021190.1928770
destOther-1.62646650.4091865-3.9748780.0000704

Ein mageres R-Quadrat.

13 lm4: Alle metrischen Variablen

Was sind noch mal unsere metrischen Variablen:

flights_train4 %>% 
  select(where(is.numeric)) %>% 
  names()
#>  [1] "id"             "month"          "day"            "dep_time"      
#>  [5] "sched_dep_time" "dep_delay"      "arr_time"       "sched_arr_time"
#>  [9] "flight"         "air_time"       "distance"       "hour"          
#> [13] "minute"

Ok, jetzt eine Regression mit diesen Variablen (ober ohne die ID-Variable):

lm4 <- 
  flights_train4 %>% 
  select(dep_delay, where(is.numeric), -id) %>% 
  lm(dep_delay ~ ., data = .)
glance(lm4)
r.squaredadj.r.squaredsigmastatisticp.valuedflogLikAICBICdeviancedf.residualnobs
0.13818790.138152837.144523936.327010-123578624715972471722338706325245490245501

Tja, das \(R^2\) hat einen nicht gerade um …

14 lm5: Alle metrischen und alle (zusammengefassten) nominalen Variablen

Welche Variablen sind jetzt alle an Bord?

flights_train4 %>% 
  names()
#>  [1] "id"             "month"          "day"            "dep_time"      
#>  [5] "sched_dep_time" "dep_delay"      "arr_time"       "sched_arr_time"
#>  [9] "carrier"        "flight"         "tailnum"        "origin"        
#> [13] "dest"           "air_time"       "distance"       "hour"          
#> [17] "minute"         "time_hour"

time_hour nehmen wir noch einmal raus, da es zum einen redundant ist zu hour etc. und zum anderen noch zusätzlicher Aufbereitung bedarf.

flights_train4 %>% 
  select(minute) %>% 
  skim()
Table 14.1: Data summary
NamePiped data
Number of rows246388
Number of columns1
_______________________
Column type frequency:
numeric1
________________________
Group variablesNone

Variable type: numeric

skim_variablen_missingcomplete_ratemeansdp0p25p50p75p100hist
minute0126.2519.3108294459▇▃▆▃▅
lm5 <- 
  flights_train4 %>% 
  select(-time_hour, -tailnum, -id, -minute) %>%   # "minute" machte Probleme, besser rausnehmen
  lm(dep_delay ~ ., data = .)
glance(lm5)
r.squaredadj.r.squaredsigmastatisticp.valuedflogLikAICBICdeviancedf.residualnobs
0.14471690.144601937.005281258.602033-123485324697762470140336140327245467245501

Der Vorhersage-Gott ist nicht mit uns. Vielleicht sollten wir zu einem ehrlichen Metier als Schuhverkäufer umsatteln …

15 Wetter-Daten ergänzen

15.1 Wetterdaten laden

data("weather")
glimpse(weather)
#> Rows: 26,115
#> Columns: 15
#> $ origin     <chr> "EWR", "EWR", "EWR", "EWR", "EWR", "EWR", "EWR", "EWR", "EW…
#> $ year       <int> 2013, 2013, 2013, 2013, 2013, 2013, 2013, 2013, 2013, 2013,…
#> $ month      <int> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,…
#> $ day        <int> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,…
#> $ hour       <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 14, 15, 16, 17, 18, …
#> $ temp       <dbl> 39.02, 39.02, 39.02, 39.92, 39.02, 37.94, 39.02, 39.92, 39.…
#> $ dewp       <dbl> 26.06, 26.96, 28.04, 28.04, 28.04, 28.04, 28.04, 28.04, 28.…
#> $ humid      <dbl> 59.37, 61.63, 64.43, 62.21, 64.43, 67.21, 64.43, 62.21, 62.…
#> $ wind_dir   <dbl> 270, 250, 240, 250, 260, 240, 240, 250, 260, 260, 260, 330,…
#> $ wind_speed <dbl> 10.35702, 8.05546, 11.50780, 12.65858, 12.65858, 11.50780, …
#> $ wind_gust  <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, 20.…
#> $ precip     <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
#> $ pressure   <dbl> 1012.0, 1012.3, 1012.5, 1012.2, 1011.9, 1012.4, 1012.2, 101…
#> $ visib      <dbl> 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10,…
#> $ time_hour  <dttm> 2013-01-01 01:00:00, 2013-01-01 02:00:00, 2013-01-01 03:00…
skim(weather)
Table 15.1: Data summary
Nameweather
Number of rows26115
Number of columns15
_______________________
Column type frequency:
character1
numeric13
POSIXct1
________________________
Group variablesNone

Variable type: character

skim_variablen_missingcomplete_rateminmaxemptyn_uniquewhitespace
origin0133030

Variable type: numeric

skim_variablen_missingcomplete_ratemeansdp0p25p50p75p100hist
year01.002013.000.002013.002013.002013.002013.002013.00▁▁▇▁▁
month01.006.503.441.004.007.009.0012.00▇▆▆▆▇
day01.0015.688.761.008.0016.0023.0031.00▇▇▇▇▆
hour01.0011.496.910.006.0011.0017.0023.00▇▇▆▇▇
temp11.0055.2617.7910.9439.9255.4069.98100.04▂▇▇▇▁
dewp11.0041.4419.39-9.9426.0642.0857.9278.08▁▆▇▇▆
humid11.0062.5319.4012.7447.0561.7978.79100.00▁▆▇▇▆
wind_dir4600.98199.76107.310.00120.00220.00290.00360.00▆▂▆▇▇
wind_speed41.0010.528.540.006.9010.3613.811048.36▇▁▁▁▁
wind_gust207780.2025.495.9516.1120.7124.1728.7766.75▇▅▁▁▁
precip01.000.000.030.000.000.000.001.21▇▁▁▁▁
pressure27290.901017.907.42983.801012.901017.601023.001042.10▁▁▇▆▁
visib01.009.262.060.0010.0010.0010.0010.00▁▁▁▁▇

Variable type: POSIXct

skim_variablen_missingcomplete_rateminmaxmediann_unique
time_hour012013-01-01 01:00:002013-12-30 18:00:002013-07-01 14:00:008714

wind_gust hat zu viele fehlende Werte; diese Variable dürfen wir nicht verwenden.

15.2 flights_train5: Wetterdaten mit Flugdaten verheiraten

flights_train5 <- 
  flights_train4 %>% 
  left_join(weather) %>%  # "verheiraten"
  select(-wind_gust)

Möchte man explizit angeben, anhand welcher Variablen man zusammenfügen möchte, so kann man dies mit dem Parameter by tun:

by = c("origin", "month" , "day", "hour")

Unter der Hilfeseite von left_join findet man mehr Infos: ?left_join.

flights_test5 <- 
  flights_test4 %>% 
  left_join(weather) %>% 
  select(-wind_gust)

16 flights_train6

Bei der Gelegenheit löschen wir noch zwei Variablen, die wir bisher nicht benötigt haben.

flights_train6 <- 
  flights_train5 %>% 
  select(-time_hour, -tailnum, -minute)
flights_test6 <- 
  flights_test5 %>% 
  select(-time_hour, -tailnum, -minute)

17 lm6: Plus Wetterdaten

lm6 <- update(lm5, . ~ . + humid + wind_speed + precip + pressure + visib,
              data = flights_train6)
glance(lm6)
r.squaredadj.r.squaredsigmastatisticp.valuedflogLikAICBICdeviancedf.residualnobs
0.16203160.161885834.260881110.918038-108150021630802163492256265775218320218359

Ah. Etwas besser. Wettervorhersage macht’s möglich.

\(R^2\) kann man auch so berechnen:

flights_train6_pred <- 
  flights_train6 %>% 
  mutate(lm6_pred = predict(lm6, newdata = .))

flights_train6_pred %>% 
  rsq(truth = dep_delay,
      estimate = lm6_pred)
.metric.estimator.estimate
rsqstandard0.1620316

17.1 R2 im Testsample

Berechnen wir jetzt die Modellgüte im Testsample.

Fügen wir die Vorhersagewerte dem Testsample dazu:

flights_test6_pred <-
  flights_test6 %>% 
  mutate(pred_lm6 = predict(lm6, newdata = flights_test6))
flights_test6_pred %>% 
  select(id, dep_delay, pred_lm6) %>%
  head()
iddep_delaypred_lm6
120.3247318
24-1.4677357
11-2-0.1192573
1901.6611710
3004.2525720
49-22.6402972
test_rsq <- 
 tibble(model = "lm6") %>% 
  mutate(rsq = rsq_vec(truth = flights_test6_pred$dep_delay,
                       estimate = flights_test6_pred$pred_lm6))

test_rsq
modelrsq
lm60.1534751

Prüfen wir noch, wie viele fehlende Werte es bei den vorhergesagten Werten gibt:

flights_test6_pred %>% 
  summarise(pred_isna = sum(is.na(pred_lm6)),
            pred_isna_prop = pred_isna / nrow(flights_test6_pred))  # prop wie "proportion" (Anteil)
pred_isnapred_isna_prop
93520.1138641

Da fehlende Werte u.U. mit dem Mittelwert (der übrigen prognostizierten Werte) aufgefüllt werden, erledigen wir das gleich, um den Effekt auf \(R^2\) abzuschätzen:

flights_test6_pred2 <- 
flights_test6_pred %>% 
  mutate(pred_lm6 = replace_na(pred_lm6, mean(pred_lm6, na.rm = TRUE)))
flights_test6_pred2 %>% 
  summarise(sum(is.na(pred_lm6)))
sum(is.na(pred_lm6))
0

Keine fehlenden Werte mehr.

Wie sieht \(R^2\) jetzt aus?

flights_test6_pred2 %>% 
  rsq(truth = dep_delay, estimate = pred_lm6)
.metric.estimator.estimate
rsqstandard0.1204287

Etwas schlechter; das liegt an den vielen Werten, die jetzt eine schlechte Vorhersage machen, da sie mit dem Mittelwert aufgefüllt werden mussten.

18 lm7: Rohe Gewalt

Wir könnten einfach, etwas stumpfsinnig, alle Regressionsmodelle ausprobieren: Jeder der \(p\) Prädiktoren könnte also entweder enthalten sein oder nicht. Natürlich würde das schnell zu Arbeit auswachsen, wenn man so viele Regressionsmodelle – \(2^p\) – durchprobieren würde. Außerdem wird die Gefahr von Zufallsbefunden schnell riesig. Aber gut, man kann stumpfsinnige Arbeiten ja gut an den Computer delegieren. Und zum Schutz vor Überanpassung gibt es ein paar Behelfe. Probieren wir es aus; dazu nutzen wir das Paket {leaps}.

tic()  # Zeitmessung t1
lm7 <- regsubsets(dep_delay ~ ., 
                  nvmax = 15,  # max. Anzahl von Prädiktoren
                  data = flights_train6)
#> Reordering variables and trying again:
toc()  # Zeitmessung t2
#> 41.618 sec elapsed

Der Punkt in der Regressionsformel

dep_delay ~ .,

bedeutet “nimm alle übrigen Variablen” (also alle außer der abhängigen Variablen).

Je größer nvmax, desto länger die Rechenzeit, z.B. \(2^{15}=32768\) Modelle, die berechnet werden müssen.

Achtung: Einige Statistiker (siehe auch hier) stehen solchen automatischen Verfahren kritisch gegenüber, sind also der Meinung, das sei Quatsch (andere wiederum nutzen diese Verfahren).

18.1 Zeitabschätzung

Ein kurze Sch+ätzung der Rechenzeit in Abhängikeit der Anzahl der Prädiktoren bei der Best-Subset-Methode; gehen wir von 1, 2 oder 5 Sekunden Rechenzeit (exec_time) pro Modell aus.

n_preds <- 1:20
exec_time <- c(1, 2, 5)
exec_time_df <-
  expand_grid(n_preds = n_preds,
              exec_time_per_run = exec_time)
exec_time_df <- 
  exec_time_df %>% 
  mutate(exec_time = 2^n_preds * exec_time_per_run) %>% 
  group_by(exec_time_per_run) %>% 
  mutate(exec_time_cum = cumsum(exec_time) / 60 / 60 / 60)
head(exec_time_df)
#> # A tibble: 6 x 4
#> # Groups:   exec_time_per_run [3]
#>   n_preds exec_time_per_run exec_time exec_time_cum
#>     <int>             <dbl>     <dbl>         <dbl>
#> 1       1                 1         2    0.00000926
#> 2       1                 2         4    0.0000185 
#> 3       1                 5        10    0.0000463 
#> 4       2                 1         4    0.0000278 
#> 5       2                 2         8    0.0000556 
#> 6       2                 5        20    0.000139
exec_time_df %>% 
  ggplot() +
  aes(x = n_preds, 
      y = exec_time_cum, 
      color = factor(exec_time_per_run)) +
  geom_line() +
  labs(y = "GesamtRechenzeit in Stunden",
       x = "Anzahl von Prädiktoren pro Modell",
       col = "Rechenzeit pro Modell",
       title = "Schätzung der Rechenzeit für eine Best-Subset-Regression") +
  theme(legend.position = "bottom")

Puh, das kann dauern, wenn man viele Prädiktoren hat.

18.2 Best Subsets – Ergebnisse

Schauen wir uns die Ergebnisse an:

lm7_summary <- summary(lm7)

tibble(adjr2 = lm7_summary$adjr2,
       r2 = lm7_summary$rsq,
       bic = lm7_summary$bic,
       id = 1:length(lm7_summary$adjr2)) %>% 
  pivot_longer(-id) %>% 
  ggplot() +
  aes(x = id, y = value, color = name) +
  geom_point() +
  geom_line() +
  facet_wrap(~ name, scales = "free", nrow = 3) +
  scale_x_continuous(breaks = 1:16)

Das Modell mit 7 Prädiktoren könnte sinnvoll sein. Mit coef(modellname, k) können wir uns ausgeben lassen, welche Prädiktoren in dem Modell mit k Prädiktoren beinhaltet waren.

coef(lm7, 7) %>% names()
#> [1] "(Intercept)"    "dep_time"       "sched_dep_time" "arr_time"      
#> [5] "sched_arr_time" "carrierEV"      "wind_dir"       "visib"

18.3 Vorwärts-/Rückwärts-Schrittweise Regression

Man kann regsubsets übrigens auch für eine schrittweise Regression benutzen:

lm7a <- regsubsets(dep_delay ~ ., 
           method = "forward",  # Vorwärts-Schrittweise-Regression
           nvmax = 15,  # max. Anzahl von Prädiktoren
           data = flights_train6)
#> Reordering variables and trying again:

Das Modell mit einem Prädiktor findet das folgende Modell als das beste:

coef(lm7a, 1)
#>  (Intercept)     dep_time 
#> -15.31028825   0.01944815

Und entsprechend:

coef(lm7a, 2)
#> (Intercept)    dep_time    arr_time 
#> -6.10576901  0.03280038 -0.01805599

Und so weiter für 3, 4 oder mehr Prädiktoren.

Im Unterschied zur Best-Subsets-Methode verbleiben Prädiktoren, wenn einmal aufgenommen ins Modell, immer im Modell.

Die Schrittweise-Regression ist weniger rechenintiv, da weniger Modelle berechnet werden müssen und kann daher u.U. das beste Modell übersehen.

Beide Verfahren der automatisierten Prädiktorenauswahl – Best-Subset und Schrittweise-Regression – werden von einigen Statistikern kritisch gesehen.

19 lm8: Bestes Modell aus der Best-Subset-Analyse

flights_train7 <-
  flights_train6 %>% 
  mutate(carrierEV = fct_other(carrier, 
                                  keep = "EV")) 
flights_test7 <-
  flights_test6 %>% 
  mutate(carrierEV = fct_other(carrier, 
                                  keep = "EV")) 
lm8 <- 
  lm(dep_delay ~ dep_time + sched_dep_time + arr_time + carrierEV + wind_dir + sched_arr_time + visib,
          data = flights_train7)

glance(lm8)
r.squaredadj.r.squaredsigmastatisticp.valuedflogLikAICBICdeviancedf.residualnobs
0.14998410.149959236.900936018.73907-120039024007982400891325132132238773238781

Hm; nicht so gut.

19.1 R2 im Testdatensatz

flights_test7_pred <-
  flights_test7 %>% 
  mutate(pred_lm8 = predict(lm8, newdata = flights_test7)) %>% 
  select(id, dep_delay, pred_lm8)
test_rsq <- 
  test_rsq %>% 
  bind_rows(tibble(
    model = "lm8",
    rsq = rsq_vec(truth = flights_test7_pred$dep_delay,
                  estimate = flights_test7_pred$pred_lm8)))

test_rsq
modelrsq
lm60.1534751
lm80.1396282

Ein typischer Befund bei Regressionsanalysen (ohne Polynome): Im Testdatensatz ist \(R^2\) etwas geringer als im Train-Datensatz. Bei Modellen mit Polynomen oder anderen flexiblen Modellen kann der Unterschied zwischen Train- und Test-Sample noch viel größer sein.

20 Prüfen der Modellqualität

autoplot(lm6, which = 1)

Abweichungen von der Linearität finden sich besonders bei sehr kleinen und sehr großen Werten; das ist ein Hinweise, dass unser Modell nicht-lineare Zusammenhänge nicht erkannt. Das ist kein Wunder, schließlich besteht unser Modell nur aus linearen Termen. Interaktionen, Polynome oder logarithmische Transformationen könnten das Modell verbessern und eröffnen Raum für weitere Analysen.

21 Einreichen

Das beste Modell im Trainsample reichen wir ein; in diesem Fall lm6. Wenn die Maßgabe ist, für alle Fälle des Datensatzes eine Vorhersage einzureichen, tun wir das natürlich:

flights6 <- 
flights_train6 %>% 
  bind_rows(flights_test6) 
flights6_pred <- 
  flights6 %>% 
  mutate(pred = predict(lm6, newdata = .)) %>% 
  select(id, pred)
head(flights6_pred)
idpred
5-0.1549456
70.8112611
21-1.7284946
31-4.1875960
346.2202257
35-1.1899645

Wie viele vorhergesagte Werte haben wir, bzw. welchen Anteil?

flights6_pred %>% 
  drop_na() %>% 
  nrow() / nrow(flights6_pred)
#> [1] 0.8862143

21.1 CSV-Datei erstellen zum Einreichen

write_csv(flights6_pred, file = "Sauer_Sebastian_0123456_Prognose.csv")

22 Was noch?

Ein nächster Schritt könnte sein, sich folgende Punkte anzuschauen:

  • Interaktionen
  • Polynome
  • Voraussetzungen

Eine Faustregel zu Interaktionen lautet: Wenn zwei Variablen jeweils einen starken Haupteffekt haben, lohnt es sich u.U., den Interaktionseffekt anzuschauen (vgl. Gelman & Hill, 2007, S. 69).

23 Tidymodels

Das ständige Updaten des Test-Datensatzes nervt; mit tidymodels wird es komfortabler und man hat Zugang zu leistungsfähigeren Prognosemodellen. Hier findet sich ein Einstieg und hier eine Fallstudie mit Tutorial.

24 Reproducibility

#> ─ 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-24                  
#> 
#> ─ 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)                
#>  base64enc      0.1-3      2015-07-28 [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)                
#>  class          7.3-19     2021-05-03 [2] 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)                
#>  conflicted   * 1.0.4      2019-06-21 [1] CRAN (R 4.1.0)                
#>  crayon         1.4.1      2021-02-08 [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)                
#>  dials        * 0.0.9      2020-09-16 [1] CRAN (R 4.1.0)                
#>  DiceDesign     1.9        2021-02-13 [1] 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)                
#>  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)                
#>  foreach        1.5.1      2020-10-15 [2] CRAN (R 4.1.0)                
#>  fs             1.5.0      2020-07-31 [1] CRAN (R 4.1.0)                
#>  furrr          0.2.2      2021-01-29 [1] CRAN (R 4.1.0)                
#>  future         1.21.0     2020-12-10 [1] CRAN (R 4.1.0)                
#>  generics       0.1.0      2020-10-31 [1] CRAN (R 4.1.0)                
#>  ggfortify    * 0.4.11     2020-10-02 [2] CRAN (R 4.1.0)                
#>  ggplot2      * 3.3.4      2021-06-16 [1] CRAN (R 4.1.0)                
#>  globals        0.14.0     2020-11-22 [1] CRAN (R 4.1.0)                
#>  glue           1.4.2      2020-08-27 [1] CRAN (R 4.1.0)                
#>  gower          0.2.2      2020-06-23 [1] CRAN (R 4.1.0)                
#>  GPfit          1.0-8      2019-02-08 [1] CRAN (R 4.1.0)                
#>  gridExtra      2.3        2017-09-09 [2] 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)                
#>  infer        * 0.5.4      2021-01-13 [1] CRAN (R 4.1.0)                
#>  ipred          0.9-11     2021-03-12 [1] CRAN (R 4.1.0)                
#>  iterators      1.0.13     2020-10-15 [2] 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)                
#>  lattice        0.20-44    2021-05-02 [2] CRAN (R 4.1.0)                
#>  lava           1.6.9      2021-03-11 [1] CRAN (R 4.1.0)                
#>  leaps        * 3.1        2020-01-16 [1] CRAN (R 4.1.0)                
#>  lhs            1.1.1      2020-10-05 [1] CRAN (R 4.1.0)                
#>  lifecycle      1.0.0      2021-02-15 [1] CRAN (R 4.1.0)                
#>  listenv        0.8.0      2019-12-05 [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)                
#>  MASS           7.3-54     2021-05-03 [2] CRAN (R 4.1.0)                
#>  Matrix         1.3-4      2021-06-01 [2] CRAN (R 4.1.0)                
#>  memoise        2.0.0      2021-01-26 [2] CRAN (R 4.1.0)                
#>  modeldata    * 0.1.0      2020-10-22 [1] 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)                
#>  nnet           7.3-16     2021-05-03 [2] CRAN (R 4.1.0)                
#>  nycflights13 * 1.0.2      2021-04-12 [1] CRAN (R 4.1.0)                
#>  parallelly     1.25.0     2021-04-30 [1] CRAN (R 4.1.0)                
#>  parsnip      * 0.1.6      2021-05-27 [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)                
#>  plyr           1.8.6      2020-03-03 [1] 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)                
#>  pROC           1.17.0.1   2021-01-13 [1] CRAN (R 4.1.0)                
#>  processx       3.5.2      2021-04-30 [1] CRAN (R 4.1.0)                
#>  prodlim        2019.11.13 2019-11-17 [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)                
#>  recipes      * 0.1.16     2021-04-16 [1] CRAN (R 4.1.0)                
#>  remotes        2.4.0      2021-06-02 [2] CRAN (R 4.1.0)                
#>  repr           1.1.3      2021-01-21 [1] 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)                
#>  rpart          4.1-15     2019-04-12 [2] CRAN (R 4.1.0)                
#>  rprojroot      2.0.2      2020-11-15 [2] CRAN (R 4.1.0)                
#>  rsample      * 0.1.0      2021-05-08 [1] 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)                
#>  skimr        * 2.1.3      2021-03-07 [1] 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)                
#>  survival       3.2-11     2021-04-26 [2] 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)                
#>  tidymodels   * 0.1.3      2021-04-19 [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)                
#>  timeDate       3043.102   2018-02-21 [1] CRAN (R 4.1.0)                
#>  tune         * 0.1.5      2021-04-23 [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)                
#>  workflows    * 0.2.2      2021-03-10 [1] CRAN (R 4.1.0)                
#>  workflowsets * 0.0.2      2021-04-16 [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)
#>  yardstick    * 0.0.8      2021-03-28 [1] CRAN (R 4.1.0)                
#> 
#> [1] /Users/sebastiansaueruser/Library/R/x86_64/4.1/library
#> [2] /Library/Frameworks/R.framework/Versions/4.1/Resources/library