tmdb04

ds1
tidymodels
statlearning
tmdb
random-forest
num
Published

May 17, 2023

Aufgabe

Wir bearbeiten hier die Fallstudie TMDB Box Office Prediction - Can you predict a movie’s worldwide box office revenue?, ein Kaggle-Prognosewettbewerb.

Ziel ist es, genaue Vorhersagen zu machen, in diesem Fall für Filme.

Die Daten können Sie von der Kaggle-Projektseite beziehen oder so:

d_train_path <- "https://raw.githubusercontent.com/sebastiansauer/Lehre/main/data/tmdb-box-office-prediction/train.csv"
d_test_path <- "https://raw.githubusercontent.com/sebastiansauer/Lehre/main/data/tmdb-box-office-prediction/test.csv"

Aufgabe

Reichen Sie bei Kaggle eine Submission für die Fallstudie ein! Berichten Sie den Score!

Hinweise:

  • Sie müssen sich bei Kaggle ein Konto anlegen (kostenlos und anonym möglich); alternativ können Sie sich mit einem Google-Konto anmelden.
  • Halten Sie das Modell so einfach wie möglich. Verwenden Sie als Algorithmus die lineare Regression ohne weitere Schnörkel.
  • Logarithmieren Sie budget und revenue.
  • Minimieren Sie die Vorverarbeitung (steps) so weit als möglich.
  • Verwenden Sie tidymodels.
  • Die Zielgröße ist revenue in Dollars; nicht in “Log-Dollars”. Sie müssen also rücktransformieren, wenn Sie revenue logarithmiert haben, bevor Sie Ihre Prognose einreichen.











Lösung

Vorbereitung

library(tidyverse)
library(tidymodels)
library(finetune)
library(doParallel)
library(tictoc)
d_train_raw <- read_csv(d_train_path)
d_test_raw <- read_csv(d_test_path)

Sicher ist sicher:

d_train_backup <- d_train_raw

Mal einen Blick werfen:

glimpse(d_train_raw)

Train-Set verschlanken

d_train_raw_reduced <-
  d_train_raw %>% 
  select(id, popularity, runtime, revenue, budget) 

Test-Set verschlanken

d_test <-
  d_test_raw %>% 
  select(id,popularity, runtime, budget) 

Outcome logarithmieren

Der Outcome sollte nicht im Rezept transformiert werden (vgl. Part 3, S. 30, in dieser Unterlage).

d_train <-
  d_train_raw_reduced %>% 
  mutate(revenue = if_else(revenue < 10, 10, revenue)) %>% 
  mutate(revenue = log(revenue)) 

Prüfen, ob das funktioniert hat:

d_train$revenue %>% is.infinite() %>% any()

Keine unendlichen Werte mehr, auf dieser Basis können wir weitermachen.

Fehlende Werte prüfen

Welche Spalten haben viele fehlende Werte?

library(easystats)
describe_distribution(d_train)
sum_isna <- function(x) {sum(is.na(x))}
d_train %>% 
  summarise(across(everything(), sum_isna))

Rezept

Rezept definieren

rec2 <-
  recipe(revenue ~ ., data = d_train) %>% 
  step_mutate(budget = ifelse(budget == 0, NA, budget)) %>%  # log mag keine 0
  step_log(budget) %>% 
  step_impute_knn(all_predictors()) %>% 
  step_dummy(all_nominal_predictors())  %>% 
  update_role(id, new_role = "id")

rec2

Schauen Sie mal, der Log mag keine Nullen:

x <- c(1,2, NA, 0)

log(x)

Da \(log(0) = -\infty\). Aus dem Grund wandeln wir 0 lieber in NA um.

tidy(rec2)

Check das Rezept

Wir berechnen das Rezept:

rec2_prepped <-
  prep(rec2, verbose = TRUE)

rec2_prepped

Das ist noch nicht auf einen Datensatz angewendet! Lediglich die steps wurden vorbereitet, “präpariert”: z.B. “Diese Dummy-Variablen impliziert das Rezept”.

So sieht das dann aus, wenn man das präparierte Rezept auf das Train-Sample anwendet:

d_train_baked2 <-
  rec2_prepped %>% 
  bake(new_data = NULL) 

head(d_train_baked2)
d_train_baked2 %>% 
  map_df(sum_isna)

Keine fehlenden Werte mehr in den Prädiktoren.

Nach fehlenden Werten könnte man z.B. auch so suchen:

datawizard::describe_distribution(d_train_baked2)

So bekommt man gleich noch ein paar Infos über die Verteilung der Variablen. Praktische Sache.

Check Test-Sample

Das Test-Sample backen wir auch mal, um zu prüfen, das alles läuft:

d_test_baked2 <-
  bake(rec2_prepped, new_data = d_test)

d_test_baked2 %>% 
  head()

Sieht soweit gut aus.

Kreuzvalidierung / Resampling

Hier ist nur aus Gründen der Rechenzeit auf kleine Werte von \(v\) und \(r\) ausgewichen worden. Besser wäre z.B. \(v=10\) und \(r=3\).

cv_scheme <- vfold_cv(d_train,
                      v = 3, 
                      repeats = 1)

Modelle

LM

mod_lm <-
  linear_reg()

Workflow-Set

Hier nur ein sehr kleiner Workflow-Set.

Das ist übrigens eine gute Strategie: Erstmal mit einem kleinen Prozess anfangen, und dann sukzessive erweitern.

preproc2 <- list(rec1 = rec2)
models2 <- list(lm1 = mod_lm)
 
 
all_workflows2 <- workflow_set(preproc2, models2)

Fitten und tunen

tmdb_model_set2 <-
    all_workflows2 %>% 
    workflow_map(resamples = cv_scheme,
                 control = control_grid(verbose = TRUE),
                 fn = "tune_race_anova")

Finalisieren

tmdb_model_set2 %>% 
  collect_metrics() %>% 
  arrange(-mean) %>% 
  head(10)
best_model_params2 <-
extract_workflow_set_result(tmdb_model_set2, "rec1_lm1") %>% 
  select_best()

best_model_params2

Finalisieren

Finalisieren bedeutet:

  • Besten Workflow identifizieren (zur Erinnerung: Workflow = Rezept + Modell)
  • Den besten Workflow mit den optimalen Modell-Parametern ausstatten
  • Damit dann den ganzen Train-Datensatz fitten
  • Auf dieser Basis das Test-Sample vorhersagen
best_wf2 <- 
all_workflows2 %>% 
  extract_workflow("rec1_lm1")

best_wf2
best_wf_finalized2 <- 
  best_wf2 %>% 
  finalize_workflow(best_model_params2)

best_wf_finalized2

Final Fit

fit_final2 <-
  best_wf_finalized2 %>% 
  fit(d_train)

fit_final2
preds <- 
fit_final2 %>% 
  predict(new_data = d_test)

head(preds)

Achtung, wenn die Outcome-Variable im Rezept verändert wurde, dann würde obiger Code nicht durchlaufen.

Grund ist hier beschrieben:

When predict() is used, it only has access to the predictors (mirroring how this would work with new samples). Even if the outcome column is present, it is not exposed to the recipe. This is generally a good idea so that we can avoid information leakage.

One approach is the use the skip = TRUE option in step_log() so that it will avoid that step during predict() and/or bake(). However, if you are using this recipe with the tune package, there will still be an issue because the metric function(s) would get the predictions in log units and the observed outcome in the original units.

The better approach is, for simple transformations like yours, to log the outcome outside of the recipe (before data analysis and the initial split).

Submission df

submission_df <-
  d_test %>% 
  select(id) %>% 
  bind_cols(preds) %>% 
  rename(revenue = .pred)

head(submission_df)

Zurücktransformieren

submission_df <-
  submission_df %>% 
  mutate(revenue = exp(revenue)-1)

head(submission_df)

Hier ein Beispiel, warum \(e^x-1\) genauer ist für kleine Zahlen als \(e^x\).

Abspeichern und einreichen:

write_csv(submission_df, file = "submission.csv")

Kaggle Score

Diese Submission erzielte einen Score von Score: 2.46249 (RMSLE).

sol <- 2.5

Categories:

  • ds1
  • tidymodels
  • statlearning
  • tmdb
  • random-forest
  • num