Kapitel 5 tidymodels
Benötigte R-Pakete für dieses Kapitel:
tidymodels ist ein Metapaket: Ein (R-)Paket,
das mehrere andere Paket startet und uns damit das Leben einfacher macht.
Eine Liste der R-Pakete, die durch tidymodels
gestartet werden,
findet sich hier.
Probieren Sie auch mal ?tidymodels
.
Eine Liste aller Pakete, die
in Tidymodels benutzt werden, die dependencies
,
kann man sich so ausgeben lassen:
pkg_deps(x = "tidymodels", recursive = FALSE)
5.1 Lernsteuerung
5.1.1 Vorbereitung
- Lesen Sie TMWR, Kapitel 1
- Lesen Sie übrige Literatur zu diesem Thema
5.2 Daten
Dieser Abschnitt bezieht sich auf Kapitel 4 in Silge and Kuhn (2022).
Wir benutzen den Datensatz zu Immobilienpreise aus dem Ames County in Iowa, USA, gelegen im Zentrum des Landes.
data(ames) # Daten wurden über tidymodels mit geladen
ames <- ames %>% mutate(Sale_Price = log10(Sale_Price))
Hier wurde die AV log-transformiert. Das hat zwei (wichtige) Effekte:
- Die Verteilung ist symmetrischer, näher an der Normalverteilung. Damit gibt es mehr Daten im Hauptbereich des Ranges von
Sale_Price
, was die Vorhersagen stabiler machen dürfte. - Logarithmiert man die Y-Variable, so kommt dies einem multiplikativen Modell gleich, s. auch hier.
5.3 Train- vs Test-Datensatz aufteilen
Dieser Abschnitt bezieht sich auf Kapitel 5 in Silge and Kuhn (2022).
Das Aufteilen in Train- und Test-Datensatz ist einer der wesentlichen Grundsätze im maschinellen Lernen. Das Ziel ist, Overfitting abzuwenden.
Im Train-Datensatz werden alle Modelle berechnet. Der Test-Datensatz wird nur einmal verwendet, und zwar zur Überprüfung der Modellgüte.
Praktisch funktioniert das in Silge and Kuhn (2022) wie folgt.
Wir laden die Daten und erstellen einen Index, der jeder Beobachtung die Zuteilung zu Train- bzw. zum Test-Datensatz zuweist:
ames_split <- initial_split(ames, prop = 0.80, strata = Sale_Price)
initial_split()
speichert für spätere komfortable Verwendung auch die Daten.
Aber eben auch der Index, der bestimmt, welche Beobachtung im Train-Set landet:
## [1] 2 27 28 30 31 32 33 35 79 83
length(ames_split$in_id)
## [1] 2342
Praktisch ist auch,
dass die AV-Verteilung in beiden Datensätzen ähnlich gehalten wird (Stratifizierung),
das besorgt das Argument strata
.
Die eigentlich Aufteilung in die zwei Datensätze geht dann so:
ames_train <- training(ames_split)
ames_test <- testing(ames_split)
5.4 Grundlagen der Modellierung mit tidymodels
Dieser Abschnitt bezieht sich auf Kapitel 6 in Silge and Kuhn (2022).
tidymodels
ist eine Sammlung mehrerer, zusammengehöriger Pakete,
eben zum Thema statistische Modellieren.
Das kann man analog zur Sammlung tidyverse
verstehen,
zu der z.B. das R-Paket dplyr
gehört.
Das R-Paket innerhalb von tidymodels
, das zum “Fitten” von Modellen zuständig ist, heißt parsnip.
Eine Liste der verfügbaren Modelltypen, Modellimplementierungen und Modellparameter, die in Parsnip aktuell unterstützt werden, findet sich hier.
5.4.1 Modelle spezifizieren
Ein (statistisches) Modell wird in Tidymodels mit drei Elementen spezifiziert, vgl. Abb. 5.1.
Die Definition eines Modells in tidymodels folgt diesen Ideen:
- Das Modell sollte unabhängig von den Daten spezifiziert sein
- Das Modell sollte unabhängig von den Variablen (AV, UVs) spezifiziert sein
- Das Modell sollte unabhängig von etwaiger Vorverarbeitung (z.B. z-Transformation) spezifiziert sein
Da bei einer linearen Regression nur der Modus “Regression” möglich ist, muss der Modus in diesem Fall nicht angegeben werden. Tidymodels erkennt das automatisch.
lm_model <-
linear_reg() %>% # Algorithmus, Modelltyp
set_engine("lm") # Implementierung
# Modus hier nicht nötig, da lineare Modelle immer numerisch klassifizieren
5.4.2 Modelle berechnen
Nach Rhys (2020) ist ein Modell sogar erst ein Modell, wenn die Koeffizienten berechnet sind. Tidymodels kennt diese Unterscheidung nicht. Stattdessen spricht man in Tidymodels von einem “gefitteten” Modell, sobald es berechnet ist. Ähnlich fancy könnte man von einem “instantiierten” Modell sprechen.
Für das Beispiel der einfachen linearen Regression heißt das, das Modell ist gefittet, sobald die Steigung und der Achsenabschnitt (sowie die Residualstreuung) berechnet sind.
lm_form_fit <-
lm_model %>%
fit(Sale_Price ~ Longitude + Latitude, data = ames_train)
5.4.3 Vorhersagen
Im maschinellen Lernen ist man primär an den Vorhersagen interessiert, häufig nur an Punktschätzungen. Schauen wir uns also zunächst diese an.
Vorhersagen bekommt man recht einfach mit der predict()
Methode:
## # A tibble: 6 × 1
## .pred
## <dbl>
## 1 5.29
## 2 5.28
## 3 5.28
## 4 5.27
## 5 5.25
## 6 5.26
Die Syntax lautet predict(modell, daten_zum_vorhersagen)
.
5.4.4 Vorhersagen im Train-Datensatz
Vorhersagen im Train-Datensatz machen keinen Sinn, da sie nicht gegen Overfitting geschützt sind und daher deutlich zu optimistisch sein können.
Bei einer linearen Regression ist diese Gefahr nicht so hoch, aber bei anderen, flexibleren Modellen, ist diese Gefahr absurd groß.
5.4.5 Modellkoeffizienten im Train-Datensatz
Gibt man den Namen des Modellobjekts ein, so wird ein Überblick an relevanten Modellergebnissen am Bildschirm gedruckt:
lm_form_fit
## parsnip model object
##
##
## Call:
## stats::lm(formula = Sale_Price ~ Longitude + Latitude, data = data)
##
## Coefficients:
## (Intercept) Longitude Latitude
## -311.552 -2.118 2.818
Innerhalb des Ergebnisobjekts findet sich eine Liste namens fit
,
in der die Koeffizienten (der “Fit”) abgelegt sind:
##
## Call:
## stats::lm(formula = Sale_Price ~ Longitude + Latitude, data = data)
##
## Coefficients:
## (Intercept) Longitude Latitude
## -311.552 -2.118 2.818
Zum Herausholen dieser Infos kann man auch die Funktion extract_fit_engine()
verwenden:
lm_fit <-
lm_form_fit %>%
extract_fit_engine()
lm_fit
##
## Call:
## stats::lm(formula = Sale_Price ~ Longitude + Latitude, data = data)
##
## Coefficients:
## (Intercept) Longitude Latitude
## -311.552 -2.118 2.818
Das extrahierte Objekt ist, in diesem Fall,
das typische lm()
Objekt.
Entsprechend kann man daruaf coef()
oder summary()
anwenden.
coef(lm_fit)
## (Intercept) Longitude Latitude
## -311.551792 -2.117896 2.817839
summary(lm_fit)
##
## Call:
## stats::lm(formula = Sale_Price ~ Longitude + Latitude, data = data)
##
## Residuals:
## Min 1Q Median 3Q Max
## -1.02571 -0.09559 -0.01499 0.09805 0.56740
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) -311.5518 14.2734 -21.83 <2e-16 ***
## Longitude -2.1179 0.1282 -16.52 <2e-16 ***
## Latitude 2.8178 0.1778 15.84 <2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.1595 on 2339 degrees of freedom
## Multiple R-squared: 0.18, Adjusted R-squared: 0.1793
## F-statistic: 256.7 on 2 and 2339 DF, p-value: < 2.2e-16
Schicker sind die Pendant-Befehle aus broom
,
die jeweils einen Tibble zuückliefern:
## # A tibble: 3 × 5
## term estimate std.error statistic p.value
## <chr> <dbl> <dbl> <dbl> <dbl>
## 1 (Intercept) -312. 14.3 -21.8 2.75e-96
## 2 Longitude -2.12 0.128 -16.5 4.69e-58
## 3 Latitude 2.82 0.178 15.8 8.74e-54
glance(lm_fit) # Modellgüte
## # A tibble: 1 × 12
## r.squared adj.r.squared sigma statistic p.value df logLik AIC BIC
## <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 0.180 0.179 0.160 257. 1.65e-101 2 978. -1947. -1924.
## # … with 3 more variables: deviance <dbl>, df.residual <int>, nobs <int>
5.5 Workflows
Dieser Abschnitt bezieht sich auf Kapitel 7 in Silge and Kuhn (2022).
5.5.2 Einfaches Beispiel
Wir initialisieren einen Workflow, verzichten auf Vorverarbeitung und fügen ein Modell hinzu:
lm_workflow <-
workflow() %>% # init
add_model(lm_model) %>% # Modell hinzufügen
add_formula(Sale_Price ~ Longitude + Latitude) # Modellformel hinzufügen
Werfen wir einen Blick in das Workflow-Objekt:
lm_workflow
## ══ Workflow ════════════════════════════════════════════════════════════════════
## Preprocessor: Formula
## Model: linear_reg()
##
## ── Preprocessor ────────────────────────────────────────────────────────────────
## Sale_Price ~ Longitude + Latitude
##
## ── Model ───────────────────────────────────────────────────────────────────────
## Linear Regression Model Specification (regression)
##
## Computational engine: lm
Wie man sieht,
gehört die Modellformel (y ~ x
) zur Vorverarbeitung
aus Sicht von Tidymodels.
Was war nochmal im Objekt lm_model
enthalten?
lm_model
## Linear Regression Model Specification (regression)
##
## Computational engine: lm
Jetzt können wir das Modell berechnen (fitten):
lm_fit <-
lm_workflow %>%
fit(ames_train)
Natürlich kann man synonym auch schreiben:
lm_fit <- fit(lm_wflow, ames_train)
Schauen wir uns das Ergebnis an:
lm_fit
## ══ Workflow [trained] ══════════════════════════════════════════════════════════
## Preprocessor: Formula
## Model: linear_reg()
##
## ── Preprocessor ────────────────────────────────────────────────────────────────
## Sale_Price ~ Longitude + Latitude
##
## ── Model ───────────────────────────────────────────────────────────────────────
##
## Call:
## stats::lm(formula = ..y ~ ., data = data)
##
## Coefficients:
## (Intercept) Longitude Latitude
## -311.552 -2.118 2.818
5.5.3 Vorhersage mit einem Workflow
Die Vorhersage mit einem Tidymodels-Workflow ist einerseits komfortabel, da man einfach sagen kann:
“Nimm die richtigen Koeffizienten des Modells aus dem Train-Set und wende sie auf das Test-Sample an. Berechne mir die Vorhersagen und die Modellgüte.”
So sieht das aus:
final_lm_res <- last_fit(lm_workflow, ames_split)
final_lm_res
## # Resampling results
## # Manual resampling
## # A tibble: 1 × 6
## splits id .metrics .notes .predictions .workflow
## <list> <chr> <list> <list> <list> <list>
## 1 <split [2342/588]> train/test split <tibble> <tibble> <tibble> <workflow>
Anderseits wird auch ein recht komplexes Objekt zurückgeliefert, das man erst mal durchschauen muss.
Wie man sieht, gibt es mehrere Listenspalten.
Besonders interessant erscheinen natürlich die Listenspalten .metrics
und .predictions
.
Schauen wir uns die Vorhersagen an.
Es gibt auch eine Funktion, die obige Zeile vereinfacht (also synonym ist):
lm_preds <- collect_predictions(final_lm_res)
lm_preds %>% slice_head(n = 5)
## # A tibble: 5 × 5
## id .pred .row Sale_Price .config
## <chr> <dbl> <int> <dbl> <chr>
## 1 train/test split 5.29 6 5.29 Preprocessor1_Model1
## 2 train/test split 5.28 7 5.33 Preprocessor1_Model1
## 3 train/test split 5.28 8 5.28 Preprocessor1_Model1
## 4 train/test split 5.27 12 5.27 Preprocessor1_Model1
## 5 train/test split 5.25 17 5.21 Preprocessor1_Model1
5.5.4 Modellgüte
Dieser Abschnitt bezieht sich auf Kapitel 9 in Silge and Kuhn (2022).
Die Vorhersagen bilden die Basis für die Modellgüte (“Metriken”),
die schon fertig berechnet im Objekt final_lm_res
liegen und mit
collect_metrics
herausgenommen werden können:
lm_metrics <- collect_metrics(final_lm_res)
.metric | .estimator | .estimate | .config |
---|---|---|---|
rmse | standard | 1.67 × 10−1 | Preprocessor1_Model1 |
rsq | standard | 1.44 × 10−1 | Preprocessor1_Model1 |
Man kann auch angeben, welche Metriken der Modellgüte man bekommen möchte:
ames_metrics <- metric_set(rmse, rsq, mae)
ames_metrics(data = lm_preds,
truth = Sale_Price,
estimate = .pred)
## # A tibble: 3 × 3
## .metric .estimator .estimate
## <chr> <chr> <dbl>
## 1 rmse standard 0.167
## 2 rsq standard 0.144
## 3 mae standard 0.127
5.5.5 Vorhersage von Hand
Man kann sich die Metriken auch von Hand ausgeben lassen,
wenn man direktere Kontrolle haben möchte als mit last_fit
und collect_metrics
.
## # A tibble: 5 × 1
## .pred
## <dbl>
## 1 5.29
## 2 5.28
## 3 5.28
## 4 5.27
## 5 5.25
Jetzt binden wir die Spalten zusammen, also die “Wahrheit” (\(y\)) und die Vorhersagen:
ames_test_small2 <-
ames_test_small %>%
select(Sale_Price) %>%
bind_cols(predict(lm_form_fit, ames_test_small)) %>%
# Add 95% prediction intervals to the results:
bind_cols(predict(lm_form_fit, ames_test_small, type = "pred_int"))
rsq(ames_test_small2,
truth = Sale_Price,
estimate = .pred
)
## # A tibble: 1 × 3
## .metric .estimator .estimate
## <chr> <chr> <dbl>
## 1 rsq standard 0.804
Andere Koeffizienten der Modellgüte können mit rmse
oder mae
abgerufen werden.
5.6 Rezepte zur Vorverarbeitung
Dieser Abschnitt bezieht sich auf Kapitel 8 in Silge and Kuhn (2022).
5.6.1 Was ist Rezept und wozu ist es gut?
So könnte ein typischer Aufruf von lm()
aussehen:
Neben dem Fitten des Modells besorgt die Formel-Schreibweise noch einige zusätzliche nützliche Vorarbeitung:
- Definition von AV und AV
- Log-Transformation von
Gr_Liv_Area
- Transformation der nominalen Variablen in Dummy-Variablen
Das ist schön und nütlich, hat aber auch Nachteile:
- Das Modell wird nicht nur spezifiziert, sondern auch gleich berechnet. Das ist unpraktisch, weil man die Modellformel vielleicht in anderen Modell wiederverwenden möchte. Außerdem kann das Berechnen lange dauern.
- Die Schritte sind ineinander vermengt, so dass man nicht einfach und übersichtlich die einzelnen Schritte bearbeiten kann.
Praktischer wäre also, die Schritte der Vorverarbeitung zu ent-flechten. Das geht mit einem “Rezept” aus Tidmoodels:
simple_ames <-
recipe(Sale_Price ~ Neighborhood + Gr_Liv_Area + Year_Built + Bldg_Type,
data = ames_train) %>%
step_log(Gr_Liv_Area, base = 10) %>%
step_dummy(all_nominal_predictors())
simple_ames
## Recipe
##
## Inputs:
##
## role #variables
## outcome 1
## predictor 4
##
## Operations:
##
## Log transformation on Gr_Liv_Area
## Dummy variables from all_nominal_predictors()
Ein Rezept berechnet kein Modell. Es macht nichts außer die Vorverarbeitung des Modells zu spezizifieren (inklusive der Modellformel).
5.6.2 Workflows mit Rezepten
Jetzt definieren wir den Workflow nicht nur mit einer Modellformel, sondern mit einem Rezept:
Sonst hat sich nichts geändert.
Wie vorher, können wir jetzt das Modell berechnen.
lm_fit <- fit(lm_workflow, ames_train)
final_lm_res <- last_fit(lm_workflow, ames_split)
final_lm_res
## # Resampling results
## # Manual resampling
## # A tibble: 1 × 6
## splits id .metrics .notes .predictions .workflow
## <list> <chr> <list> <list> <list> <list>
## 1 <split [2342/588]> train/test split <tibble> <tibble> <tibble> <workflow>
##
## There were issues with some computations:
##
## - Warning(s) x1: prediction from a rank-deficient fit may be misleading
##
## Use `collect_notes(object)` for more information.
lm_metrics <- collect_metrics(final_lm_res)
lm_metrics
## # A tibble: 2 × 4
## .metric .estimator .estimate .config
## <chr> <chr> <dbl> <chr>
## 1 rmse standard 0.0822 Preprocessor1_Model1
## 2 rsq standard 0.794 Preprocessor1_Model1
5.6.3 Spaltenrollen
Eine praktische Funktion ist es, bestimmte Spalten nicht als Prädiktor, sondern als ID-Variable zu nutzen. Das kann man in Tidymodels komfortabel wie folgt angeben:
ames_recipe <-
simple_ames %>%
update_role(Neighborhood, new_role = "id")
ames_recipe
## Recipe
##
## Inputs:
##
## role #variables
## id 1
## outcome 1
## predictor 3
##
## Operations:
##
## Log transformation on Gr_Liv_Area
## Dummy variables from all_nominal_predictors()