Aufgabe: Vorhersage von Verspätungen der Flüge von den Flughäfen von New York City im Jahr 2013.
Pakete laden:
library(mosaic)
library(tidyverse)
library(lubridate)
library(corrr)
library(caret)
library(doMC)
library(ranger)
library(sjmisc)
Daten laden:
library(nycflights13)
data(flights)
glimpse(flights)
#> Observations: 336,776
#> Variables: 19
#> $ year <int> 2013, 2013, 2013, 2013, 2013, 2013, 2013, 2013, 2…
#> $ month <int> 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…
#> $ dep_time <int> 517, 533, 542, 544, 554, 554, 555, 557, 557, 558,…
#> $ sched_dep_time <int> 515, 529, 540, 545, 600, 558, 600, 600, 600, 600,…
#> $ dep_delay <dbl> 2, 4, 2, -1, -6, -4, -5, -3, -3, -2, -2, -2, -2, …
#> $ arr_time <int> 830, 850, 923, 1004, 812, 740, 913, 709, 838, 753…
#> $ sched_arr_time <int> 819, 830, 850, 1022, 837, 728, 854, 723, 846, 745…
#> $ arr_delay <dbl> 11, 20, 33, -18, -25, 12, 19, -14, -8, 8, -2, -3,…
#> $ carrier <chr> "UA", "UA", "AA", "B6", "DL", "UA", "B6", "EV", "…
#> $ flight <int> 1545, 1714, 1141, 725, 461, 1696, 507, 5708, 79, …
#> $ tailnum <chr> "N14228", "N24211", "N619AA", "N804JB", "N668DN",…
#> $ origin <chr> "EWR", "LGA", "JFK", "JFK", "LGA", "EWR", "EWR", …
#> $ dest <chr> "IAH", "IAH", "MIA", "BQN", "ATL", "ORD", "FLL", …
#> $ air_time <dbl> 227, 227, 160, 183, 116, 150, 158, 53, 140, 138, …
#> $ distance <dbl> 1400, 1416, 1089, 1576, 762, 719, 1065, 229, 944,…
#> $ hour <dbl> 5, 5, 5, 5, 6, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 5, 6…
#> $ minute <dbl> 15, 29, 40, 45, 0, 58, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
#> $ time_hour <dttm> 2013-01-01 05:00:00, 2013-01-01 05:00:00, 2013-0…
Es gibt zwei Variablen, die Verspätung anzeigen: arr_delay
(Ankunft) und dep_delay
.
favstats(arr_delay ~ 1, data = flights)
favstats(dep_delay ~ 1, data = flights)
Nehmen wir arr_delay
, da die Streuung in dieser Variable höher ist.
Möchte man einen Befehl auf mehrere Spalten anwenden, so kann man dafür den Befehl map()
verwendet. map()
führt ein Befehl auf jede Spalte eines Dataframes aus. Damit man da Ergebnis in Form eines Dataframes (Tabelle) bekommt, fügt man _df
an map()
an:
flights %>%
select(arr_delay, dep_delay) %>%
map_df(favstats)
Für den IQR:
flights %>%
select(arr_delay, dep_delay) %>%
drop_na() %>%
map_df(iqr)
gf_histogram( ~ arr_delay, data = flights)
gf_violin(arr_delay ~ 1, data = flights)
Aufgrund des langen rechten Randbereichs (hohe Verspätungswerte) ist das Diagramm nicht sher hilfreich.
Begrenzen wir uns besser auf den “inneren” Teil der Flüge (was die Verspätung betrifft).
flights %>%
filter(arr_delay < 120) %>%
gf_violin(arr_delay ~ 1, data = .)
Es gibt sehr viele potenzielle Ursachen für die Verspätung eines Flugzeugs bzw. eines Flugs. Zu einigen Kandidaten liegen uns Daten vor. Eine naheliegende (obwohl nicht tiefer theoretisch fundierte) Annahme ist, dass es saisonale Einflüsse auf die Verspätung gibt. So könnte Schnee im Winter oder Weihnachtsstress zum Jahreswechsel für Verspätung sorgen. Am Wochenende sind die Menschen entspannter und es wird weniger gereist. Daher könnte es Samstags und Sonntags zu weniger Verspätung kommen.
Berechnen wir die Jahreszeiten:
flights2 <- flights %>%
mutate(season = case_when(
month %in% c(11, 12, 1, 2, 3) ~ "winter",
month %in% c(6,7,9) ~ "summer",
month %in% c(4, 5) ~ "spring",
TRUE ~ "autumn"
))
Verspätungen nach Jahreszeiten:
favstats(arr_delay ~ season, data = flights2)
Im Sommer ist die Verspätung am höchsten. Vielleicht ist es besser, gleich auf Monate hin zu untersuchen:
favstats(arr_delay ~ month, data = flights2)
Tatsächlich ist die Verspätung im Mittelwert am höchsten im Juni und Juli. Dabei ist zu beachten, dass die mediane Verspätung nur im Dezember positiv ist: Nur im Dezember haben die Flüge in New York im Median eine Verspätung.
Liegt es an Weihnachten? Schauen wir uns die Tage im Dezember (und Januar?) genauer an. Dazu berechnen wir zuerst einen Spalte, die den Tag (und die Woche) berechnet.
flights3 <- flights2 %>%
mutate(dayinyear = yday(time_hour),
day_id = 365-(365-dayinyear),
week = week(time_hour))
flights3 %>%
filter( (time_hour > "2013-11-30 23:59:59") ) %>%
group_by(dayinyear) %>%
summarise(arr_delay = median(arr_delay, na.rm = TRUE)) %>%
gf_line(arr_delay ~ dayinyear, data = .)
Etwa zwei Wochen vor Jahresende, also noch deutlich vor den Feiertagen, kommt es zu den Verspätungsspitzen. Ob zu dieser Zeit die meisten Menschen in den Weihnachtsurlaub fliegen? Insgesamt lässt diese Betrachtung offenbar keine starken Schlüsse zu.
Vielleicht sind die Wochentage die entspannten Tage ohne Verspätung? Schauen wir nach. Man beachte, dass die Woche in Amerika mit Sonntag (1) beginnt, demzufolge ist der Samstag der 7. Tag.
flights3 %>%
mutate(weekend = if_else(wday(time_hour) %in% c(1,7), TRUE, FALSE)) %>%
group_by(weekend) %>%
summarise(arr_delay_md = median(arr_delay, na.rm = TRUE))
Aha, das sind 4 Minuten weniger im Median am Wochenende (im Vergleich zu werktags). Im Verhältnis zur Streuung von 31 Minuten (IQR) ist das nicht Nichts, aber auch nicht die Welt.
flights4 <- flights3 %>%
mutate(day_rounded = round_date(time_hour, "day"))
flights4 %>%
group_by(day_rounded) %>%
summarise(arr_delay_md = median(arr_delay, na.rm = TRUE)) %>%
gf_line(arr_delay_md ~ day_rounded, data = .) %>%
gf_smooth()
Die Spitzen sind so nicht direkt erschließbar. Betrachten wir abschließend die Verspätungen pro Woche.
flights4 %>%
mutate(week_rounded = round_date(time_hour, "week")) %>%
group_by(week_rounded) %>%
summarise(arr_delay_md = median(arr_delay, na.rm = TRUE)) %>%
gf_line(arr_delay_md ~ week_rounded, data = .) %>%
gf_smooth()
Der Zacken im Juli könnte mit dem Nationalfeierag in den USA zusammenhängen. Lassen wir diese Untersuchungen an dieser Stelle.
Die Wetterdaten sind in einer anderen Tabelle (weather
), auch im Paket nycflights13
gespeichert. Über Datum/Zeit können wir die Wetterdaten mit den Flugdaten zusammenführen. Dabei begnügen wir uns mit einer tagesgenauen Präzision, da die Wetterdaten nicht jede Stunde (Minute, Sekunde) abdecken.
data(weather)
glimpse(weather)
#> Observations: 26,115
#> Variables: 15
#> $ origin <chr> "EWR", "EWR", "EWR", "EWR", "EWR", "EWR", "EWR", "EWR…
#> $ year <dbl> 2013, 2013, 2013, 2013, 2013, 2013, 2013, 2013, 2013,…
#> $ month <dbl> 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,…
#> $ hour <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 14, 15, 16, 17…
#> $ temp <dbl> 39.02, 39.02, 39.02, 39.92, 39.02, 37.94, 39.02, 39.9…
#> $ dewp <dbl> 26.06, 26.96, 28.04, 28.04, 28.04, 28.04, 28.04, 28.0…
#> $ humid <dbl> 59.37, 61.63, 64.43, 62.21, 64.43, 67.21, 64.43, 62.2…
#> $ wind_dir <dbl> 270, 250, 240, 250, 260, 240, 240, 250, 260, 260, 260…
#> $ wind_speed <dbl> 10.35702, 8.05546, 11.50780, 12.65858, 12.65858, 11.5…
#> $ wind_gust <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
#> $ precip <dbl> 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.…
#> $ visib <dbl> 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 1…
#> $ time_hour <dttm> 2013-01-01 01:00:00, 2013-01-01 02:00:00, 2013-01-01…
weather2 <- weather %>%
mutate(date_time = round_date(time_hour, "hour"),
day_rounded = round_date(time_hour, "day")) %>%
group_by(day_rounded) %>%
summarise_at(vars(temp, humid, wind_speed, precip, visib), median, na.rm = TRUE)
flights5 <- flights4 %>%
inner_join(weather2, by = c("day_rounded" = "day_rounded"))
Wie ist die Korrelation der Wetterdaten mit der Verspätung?
flights5 %>%
select(temp, humid, wind_speed, precip, visib, arr_delay) %>%
correlate() %>%
focus(arr_delay)
Gut, etwas Zusammenhang mit Luftfeuchtigkeit (humid
), aber ansonsten nicht viel zu sehen. Apropos sehen: Schlechte Sicht geht mit weniger Verspätung einher (?).
Variablen ohne Varianz sind wertlos für die Vorhersage, also entfernen wir sie. caret
bietet dazu nearZeroVar
. Natürlich kann man sich auch die Daten mit bloßem Auge ansehen, dann fällt auf, dass year
den konstanten Wert 2013 aufweist.
flights6 <- flights5 %>%
select(-year)
Probieren wir die rabiate Methode:
flights7 <- flights6 %>%
drop_na()
Wie viel Prozent der Fälle haben wir verloren?
nrow(flights7)/nrow(flights6)
#> [1] 0.9719892
Etwa 3%, das verschmerzen wir.
Für viele Algorithmen ist es nötig (z.B. neuronale Netze), die Prädiktoren vorab zu standardisieren hinsichtlich Mittelwert und Streuung (z-Transformation). Das kann man z.B. so erreichen (via Paket sjmisc
):
flights7a <- std(flights7, suffix = "")
Um Überanpassung zu vermeiden, verwenden wir eine 5-fach-Kreuzvalidierung.
my_crossval <- trainControl(method = "cv",
number = 5,
allowParallel = TRUE,
verboseIter = FALSE)
allowParallel
erlaubt die Verwendung mehrerer Rechenkerne, sofern initialisiert:
doMC::registerDoMC(cores = 2)
Achtung: Verdoppeln wir die Anzahl der Kerne, verdoppeln wir damit auch die Menge des benötigten Speichers.
Große Datensätze bringen einen Rechner leicht aus der Ruhe. Besonders kategoriale Variablen mit vielen Stufen sind schwierig, da sie (manuell oder je nach Funktion automatisch) in Dummy-Variablen umgewandelt werden müssen.
Begrenzen wir uns daher auf metrische Variablen.
flights8 <- flights7a %>%
select_if(is.numeric)
Außerdem dürfen wir nicht vergessen, die andere Verspätungsvariable zu entferne (dep_delay
).
flights9 <- flights8 %>%
select(-dep_delay)
Haben wir noch redundante Variablen?
findLinearCombos(flights9)
#> $linearCombos
#> $linearCombos[[1]]
#> [1] 12 4 11
#>
#> $linearCombos[[2]]
#> [1] 14 13
#>
#>
#> $remove
#> [1] 12 14
Ja!
Entfernen wir sie:
flights9a <- flights9 %>%
select(-c(12,14))
Teilen wir den Datensatz zu 80% in einen Übungsteil bzw. zu 20% in einen Testdatensatz auf.
n_uebung <- round(.8 * nrow(flights9a), digits = 0)
uebung_index <- sample(1:nrow(flights9a), size = n_uebung)
uebung_df <- filter(flights9a, row_number() %in% uebung_index)
test_df <- filter(flights9a, !(row_number() %in% uebung_index))
Die Gesamtfallzahl muss der Summe aus Übungs- und Test-Datensatz entsprechen:
(nrow(uebung_df) + nrow(test_df)) == nrow(flights9a)
#> [1] TRUE
Passt.
Beginnen wir mit einer einfachen Regression (ohne Interaktionen, Polynome, etc.).1
Eine Regression hat keine Tuningparameter.
start <- Sys.time()
lm_fit1 <- train(arr_delay ~ .,
data = uebung_df,
method = "lm",
trControl = my_crossval)
end <- Sys.time()
(time_taken_lm1 <- end - start)
#> Time difference of 16.67416 secs
#saveRDS(lm_fit1, file = "lm_fit1.rds")
Ohne die Begrenzung auf numerische Variablen hat meine Maschine (16GB Speicher) einen Asthmaanfall bekommen und ist steckengeblieben.2
Das erzeugte Modell hatte in der Datei eine Größe von ca. 360MB.
Die Koeffizienten des Modells lassen sich auf übliche Weise bestimmen:
summary(lm_fit1)
#>
#> Call:
#> lm(formula = .outcome ~ ., data = dat)
#>
#> Residuals:
#> Min 1Q Median 3Q Max
#> -2.1075 -0.4652 -0.1807 0.1795 25.6674
#>
#> Coefficients:
#> Estimate Std. Error t value Pr(>|t|)
#> (Intercept) -1.736e-06 1.761e-03 -0.001 0.99921
#> month 8.164e+00 3.014e-01 27.082 < 2e-16 ***
#> day 7.014e-01 2.547e-02 27.542 < 2e-16 ***
#> dep_time 7.888e-01 6.099e-03 129.345 < 2e-16 ***
#> sched_dep_time -5.888e-01 4.338e-02 -13.574 < 2e-16 ***
#> arr_time -2.634e-01 2.903e-03 -90.755 < 2e-16 ***
#> sched_arr_time 8.686e-02 3.582e-03 24.252 < 2e-16 ***
#> flight 3.899e-02 2.017e-03 19.328 < 2e-16 ***
#> air_time 1.643e+00 1.410e-02 116.532 < 2e-16 ***
#> distance -1.664e+00 1.415e-02 -117.593 < 2e-16 ***
#> hour 1.158e-01 4.282e-02 2.705 0.00683 **
#> dayinyear -8.561e+00 3.172e-01 -26.992 < 2e-16 ***
#> week 3.548e-01 9.062e-02 3.915 9.03e-05 ***
#> temp 7.449e-02 2.372e-03 31.405 < 2e-16 ***
#> humid 1.530e-01 2.201e-03 69.524 < 2e-16 ***
#> wind_speed 9.915e-02 1.958e-03 50.640 < 2e-16 ***
#> precip -2.175e-03 1.933e-03 -1.125 0.26042
#> visib 6.001e-03 2.198e-03 2.730 0.00632 **
#> ---
#> Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
#>
#> Residual standard error: 0.9006 on 261512 degrees of freedom
#> Multiple R-squared: 0.1871, Adjusted R-squared: 0.1871
#> F-statistic: 3541 on 17 and 261512 DF, p-value: < 2.2e-16
Die Prädiktorenrelevanz kann man über varImp()
abfragen.
varImp(lm_fit1)
#> lm variable importance
#>
#> Overall
#> dep_time 100.000
#> distance 90.834
#> air_time 90.007
#> arr_time 69.903
#> humid 53.345
#> wind_speed 38.617
#> temp 23.616
#> day 20.602
#> month 20.244
#> dayinyear 20.173
#> sched_arr_time 18.036
#> flight 14.196
#> sched_dep_time 9.709
#> week 2.176
#> visib 1.252
#> hour 1.232
#> precip 0.000
Im Gegensatz zur Regression gibt es bei Random-Forest-Modellen Tuningparameter, und zwar die Anzahl der Variablen pro Baum, hier mit .mtry
bezeichnet. Eine Faustregel für diesen Parameter ist \(\sqrt(k)\), hier also etwa oder 6.
rf_grid <- data.frame(
.mtry = c(4, 5, 6, 7),
.splitrule = "variance",
.min.node.size = 5)
rf_grid
Um Zeit zu sparen, verringern wir die Stichprobengröße auf 1000:
uebung_df_small <- sample_n(uebung_df, size = 1000)
Dann berechnen wir das Modell; gibt man keine Hinweise auf Variation von Tuningparametern, so wählt die Funktion Standardwerte.
start <- Sys.time()
rf_fit1 <- train(arr_delay ~ .,
data = uebung_df_small,
method = "ranger",
trControl = my_crossval)
end <- Sys.time()
(time_taken <- end - start)
#> Time difference of 17.81419 secs
#saveRDS(rf_fit1, file = "lm_fit1.rds")
#readRDS("lm_fit1.rds")
Einen Überblick über das berechnete Modell kann man sich so ausgeben lassen:
rf_fit1
#> Random Forest
#>
#> 1000 samples
#> 17 predictor
#>
#> No pre-processing
#> Resampling: Cross-Validated (5 fold)
#> Summary of sample sizes: 800, 800, 799, 801, 800
#> Resampling results across tuning parameters:
#>
#> mtry splitrule RMSE Rsquared MAE
#> 2 variance 0.7633726 0.3607519 0.5009789
#> 2 extratrees 0.7907793 0.3392968 0.5121332
#> 9 variance 0.6610500 0.5292866 0.4392290
#> 9 extratrees 0.6827074 0.5194783 0.4581119
#> 17 variance 0.6429513 0.5607234 0.4243621
#> 17 extratrees 0.6495064 0.5659727 0.4361470
#>
#> Tuning parameter 'min.node.size' was held constant at a value of 5
#> RMSE was used to select the optimal model using the smallest value.
#> The final values used for the model were mtry = 17, splitrule =
#> variance and min.node.size = 5.
Im resultierenden Objekt sind eine Vielzahl von Informationen zu finden. So kann man sich den Modellkandidaten mit den besten Werten ausgeben lassen:
rf_fit1$bestTune
Aber was ist das Kriteriem, das optimiert wird?
rf_fit1$metric
#> [1] "RMSE"
Es wird nach dem Root Mean Square Error optimiert.
Weitere Infos zum Algorithmus bekommt man z.B. so:
modelLookup("ranger")
Wir sehen, dass das Modell drei Tuningparameter hat (wobei caret
den Parameter min.node.size
konstant hielt).
plot(rf_fit1)
Möchte man eine bestimmte Anzahl an Kandidatenmodelle prüfen lassen, so kann man das mit tuneLength
tun:
start <- Sys.time()
rf_fit2 <- train(arr_delay ~ .,
data = uebung_df_small,
method = "ranger",
trControl = my_crossval,
tuneLength = 4)
end <- Sys.time()
(time_taken <- end - start)
#> Time difference of 22.06099 secs
saveRDS(rf_fit2, file = "lm_fit2.rds")
rf_fit2
#> Random Forest
#>
#> 1000 samples
#> 17 predictor
#>
#> No pre-processing
#> Resampling: Cross-Validated (5 fold)
#> Summary of sample sizes: 800, 801, 801, 799, 799
#> Resampling results across tuning parameters:
#>
#> mtry splitrule RMSE Rsquared MAE
#> 2 variance 0.7623641 0.3546775 0.5016084
#> 2 extratrees 0.7818499 0.3589807 0.5089059
#> 7 variance 0.6833994 0.4963581 0.4493724
#> 7 extratrees 0.6949830 0.5088313 0.4633939
#> 12 variance 0.6557907 0.5389356 0.4313334
#> 12 extratrees 0.6622967 0.5540862 0.4441157
#> 17 variance 0.6490689 0.5545829 0.4246735
#> 17 extratrees 0.6419074 0.5859532 0.4320974
#>
#> Tuning parameter 'min.node.size' was held constant at a value of 5
#> RMSE was used to select the optimal model using the smallest value.
#> The final values used for the model were mtry = 17, splitrule =
#> extratrees and min.node.size = 5.
Mit tuneGrid
kann man die Werte der Modellkandidaten genau einstellen.
start <- Sys.time()
rf_fit3 <- train(arr_delay ~ .,
data = uebung_df_small,
method = "ranger",
trControl = my_crossval,
tuneGrid = rf_grid)
end <- Sys.time()
(time_taken <- end - start)
#> Time difference of 8.216766 secs
# saveRDS(rf_fit3, file = "lm_fit3.rds")
rf_fit3
#> Random Forest
#>
#> 1000 samples
#> 17 predictor
#>
#> No pre-processing
#> Resampling: Cross-Validated (5 fold)
#> Summary of sample sizes: 799, 800, 800, 800, 801
#> Resampling results across tuning parameters:
#>
#> mtry RMSE Rsquared MAE
#> 4 0.7116300 0.4554067 0.4709582
#> 5 0.6922898 0.4903751 0.4593296
#> 6 0.6816250 0.5020712 0.4519450
#> 7 0.6710419 0.5200150 0.4457520
#>
#> Tuning parameter 'splitrule' was held constant at a value of
#> variance
#> Tuning parameter 'min.node.size' was held constant at a value
#> of 5
#> RMSE was used to select the optimal model using the smallest value.
#> The final values used for the model were mtry = 7, splitrule =
#> variance and min.node.size = 5.
Neuronale Netze benötigen große Datensätze, daher ist unser kleiner Datensatz mit \(n=1000\) sicher zu klein (gerade in Anbetracht zur Zahl der Features). Aus Vergleichsbarkeitsgründen und um die Rechenkosten zu schätzen, bietet es sich aber an, zunächst mit einem kleinen Datensatz zu arbeiten:
start <- Sys.time()
nn_fit1 <- train(arr_delay ~ .,
data = uebung_df_small,
method = "nnet",
trControl = my_crossval,
linout = TRUE)
#> # weights: 96
#> initial value 1251.713470
#> iter 10 value 752.392005
#> iter 20 value 629.992664
#> iter 30 value 510.727200
#> iter 40 value 370.835270
#> iter 50 value 303.223878
#> iter 60 value 262.263392
#> iter 70 value 231.974569
#> iter 80 value 191.799761
#> iter 90 value 163.597567
#> iter 100 value 143.767454
#> final value 143.767454
#> stopped after 100 iterations
end <- Sys.time()
(time_taken <- end - start)
#> Time difference of 3.906937 secs
saveRDS(nn_fit1, file = "nn_fit1.rds")
Der Parameter linout = TRUE
verhindert eine Aktivierungsfunktion, die den Wertebereich auf [0,1] beschränken würde.
Das ging schnell. Vergrößen wir den Datensatz:
start <- Sys.time()
nn_fit2 <- train(arr_delay ~ .,
data = uebung_df,
method = "nnet",
trControl = my_crossval,
linout = TRUE)
#> # weights: 96
#> initial value 296093.851805
#> iter 10 value 206345.983463
#> iter 20 value 123079.214569
#> iter 30 value 100052.275388
#> iter 40 value 90280.334351
#> iter 50 value 81869.501514
#> iter 60 value 73615.105786
#> iter 70 value 62928.199709
#> iter 80 value 52795.472294
#> iter 90 value 47514.499738
#> iter 100 value 43863.841483
#> final value 43863.841483
#> stopped after 100 iterations
end <- Sys.time()
(time_taken <- end - start)
#> Time difference of 12.04127 mins
# saveRDS(nn_fit2, file = "nn_fit2.rds")
Es gibt verschiedene Implementierungen von neuronalen Netzen, die in caret
angesteuert werden können, z.B. neuralnet
. Es verfügt über 3 Modellparameter:
modelLookup("neuralnet")
getModelInfo("neuralnet")
#> $neuralnet
#> $neuralnet$label
#> [1] "Neural Network"
#>
#> $neuralnet$library
#> [1] "neuralnet"
#>
#> $neuralnet$loop
#> NULL
#>
#> $neuralnet$type
#> [1] "Regression"
#>
#> $neuralnet$parameters
#> parameter class label
#> 1 layer1 numeric #Hidden Units in Layer 1
#> 2 layer2 numeric #Hidden Units in Layer 2
#> 3 layer3 numeric #Hidden Units in Layer 3
#>
#> $neuralnet$grid
#> function(x, y, len = NULL, search = "grid") {
#> if(search == "grid") {
#> out <- expand.grid(layer1 = ((1:len) * 2) - 1, layer2 = 0, layer3 = 0)
#> } else {
#> out <- data.frame(layer1 = sample(2:20, replace = TRUE, size = len),
#> layer2 = sample(c(0, 2:20), replace = TRUE, size = len),
#> layer3 = sample(c(0, 2:20), replace = TRUE, size = len))
#> }
#> out
#> }
#>
#> $neuralnet$fit
#> function(x, y, wts, param, lev, last, classProbs, ...) {
#> colNames <- colnames(x)
#> dat <- if(is.data.frame(x)) x else as.data.frame(x)
#> dat$.outcome <- y
#> form <- as.formula(paste(".outcome ~",paste(colNames, collapse = "+")))
#> if(param$layer1 == 0) stop("the first layer must have at least one hidden unit")
#> if(param$layer2 == 0 & param$layer2 > 0) stop("the second layer must have at least one hidden unit if a third layer is specified")
#> nodes <- c(param$layer1)
#> if(param$layer2 > 0) {
#> nodes <- c(nodes, param$layer2)
#> if(param$layer3 > 0) nodes <- c(nodes, param$layer3)
#> }
#> neuralnet::neuralnet(form, data = dat, hidden = nodes, ...)
#> }
#>
#> $neuralnet$predict
#> function(modelFit, newdata, submodels = NULL) {
#> newdata <- newdata[, modelFit$model.list$variables, drop = FALSE]
#> neuralnet::compute(modelFit, covariate = newdata)$net.result[,1]
#> }
#>
#> $neuralnet$prob
#> NULL
#>
#> $neuralnet$tags
#> [1] "Neural Network"
#>
#> $neuralnet$sort
#> function(x) x[order(x$layer1, x$layer2, x$layer3),]
Wie gut sagen die Modelle den Test-Datensatz vorher? Vergleichen wir die Modelle.
Erstellen wir uns einen Datensatz mit den Vorhersagen (und den beobachteten Werten):
test_preds <- test_df %>%
select(arr_delay) %>%
mutate(lm1_pred = predict(lm_fit1, newdata = test_df))
test_preds <- test_df %>%
select(arr_delay) %>%
mutate(lm1_pred = predict(lm_fit1, newdata = test_df),
rf1_pred = predict(rf_fit2, newdata = test_df),
rf2_pred = predict(rf_fit3, newdata = test_df),
nn1_pred = predict(nn_fit1, newdata = test_df),
nn2_pred = predict(nn_fit2, newdata = test_df))
Jetzt lassen wir uns typische Kennzahlen der Modellgüte ausgeben:
postResample(pred = test_preds$lm1_pred, obs = test_preds$arr_delay)
#> RMSE Rsquared MAE
#> 0.9037563 0.1906945 0.5425576
Der Vektor der Modellnamen lautet:
model_names <- names(test_preds)
Das wiederholen wir in einer Schleife für jedes Modell:
test_pred_df <- test_preds %>%
map_df(~ postResample(pred = ., obs = test_preds$arr_delay)) %>%
mutate(statistic = c("RMSE", "Rsquared", "MAE")) %>%
select(statistic, everything())
test_pred_df
Formen wir diese Tabelle in Langform (Normalform um):
test_pred_df_t <- test_pred_df %>%
gather(key = "model_name", value = "value", -c(statistic))
Eine andere Form wäre:
test_pred_df %>%
gather(key = "model_name", value = "value", -c(statistic)) %>%
spread(key = statistic, value = value)
Der kleinste RMSE-Wert (nach dem Modell, dass als vorhergesagten Werte die beobachteten nimmt, also einen RSME von Null hat):
test_pred_df_t %>%
filter(statistic == "RMSE") %>%
top_n(2, wt = -value)
Der größte R^2-Wert:
test_pred_df_t %>%
filter(statistic == "Rsquared") %>%
top_n(2, wt = value)
test_pred_df_t %>%
group_by(statistic) %>%
mutate(is_max = value == max(value),
is_min = value == min(value)) %>%
ggplot(aes(y = model_name, x = value, color = is_max, shape = is_min)) +
geom_point(size = 5) +
facet_wrap(~statistic, scales = "free_x") +
theme(legend.position = "bottom")
Damit hat das neuronale Netz “gewonnen”.
Es darf nicht vergessen werden, dass wir nur einen Teil des Datensatzes verwendet haben - schlicht aus Gründen der komputationalen Kostensparung. Insofern sind die Modellgüten nur bedingt für bare Münze zu nehmen. Diese Fallstudie hat nur einen Teil der Möglichkeiten einer ernsthaften Modellierung aufgenommen, so dass die Ergebnisse schon aus diesem Grund mit einem großen Gramm Salz zu betrachten sind. Außerdem fanden nur relativ weniger Modell Eingang; es bleibt also offen, ob nicht andere Modelle “besser” sind. Beim Wort “besser” muss man immer im Kopf behalten, dass “besser” eine bedingte Aussage ist: besser vor dem Hintergrund gewisser anderer Modelle, gewisser Transformationen, gewisser Implementierungen, gewisser Stichprobenmerkmale, gewisser Implementierungsspezifika und so weiter.