Practical Machine Learning with tidymodels
set.seed(429)
class_data <-
# Nonlinear classification with a low event rate
sim_classification(5000, intercept = -10) |>
# Add some noise predictors that are correlated with one another
bind_cols(sim_noise(5000, num_var = 15, cov_type = "toeplitz", cov_param = 0.5))
sim_split <- initial_split(class_data, prop = 0.75, strata = class)
sim_train <- training(sim_split)
sim_test <- testing(sim_split)
set.seed(523)
sim_rs <- vfold_cv(sim_train, v = 10, strata = class)Some models automatically remove predictors by never using them in the model:
glmnet)Sometimes using irrelevant predictors hurts model performance.
wrappers: a sequential algorithm proposes feature subsets, fits the model with these subsets, and then determines a better subset from the results.
filters: screen predictors before adding them to the model.
tidymodels doesn’t have any wrappers (but see the caret documentation for them)
The new important package does have filters via recipes.
tidymodels has always contained some “hidden guardrails” that should prevent practitioners from making subtle (but consequential) methodological mistakes.
Feature selection is a good example. Based on the literature, it is easily done wrong.
The selection process should take place inside a resampling loop so that the workflow does not overfit the predictors.
We released two packages this year that enable supervised feature selection:
Let’s look at the help page for important::step_predictor_best().
rec <-
recipe(class ~ ., data = sim_train) |>
step_predictor_best(
all_predictors(),
score = "imp_rf",
prop_terms = tune(),
id = "filter"
) |>
step_normalize(all_numeric_predictors())
knn_spec <-
nearest_neighbor(neighbors = tune(), weight_func = tune()) |>
set_mode("classification")
thrsh_tlr <-
tailor() |>
adjust_probability_threshold(threshold = tune()) knn_fit <- fit_best(knn_res, metric = "brier_class")
filter_info <-
knn_fit |>
extract_recipe() |>
tidy(id = "filter")
filter_info
#> # A tibble: 30 × 4
#> terms removed score id
#> <chr> <lgl> <dbl> <chr>
#> 1 two_factor_1 FALSE 0.137 filter
#> 2 two_factor_2 FALSE 0.140 filter
#> 3 non_linear_1 FALSE 0.00591 filter
#> 4 non_linear_2 TRUE 0.00144 filter
#> 5 non_linear_3 TRUE 0.000402 filter
#> 6 linear_01 TRUE 0.000163 filter
#> 7 linear_02 TRUE -0.0000237 filter
#> 8 linear_03 TRUE 0.0000385 filter
#> 9 linear_04 TRUE -0.0000384 filter
#> 10 linear_05 TRUE -0.000333 filter
#> # ℹ 20 more rowsThe data were simulated and 15 out of 30 predictors were uninformative (and highly correlated). How did we do?
| noise | real | |
|---|---|---|
| kept | 0 | 3 |
| removed | 15 | 12 |
It was good at removing noise but not keeping the real predictors.
The simulation system is documented here with method = "caret". The two most important predictors being retained correspond to:
Most of the others are small linear effects and tree-based models are not great at modeling those.
Also, the noise predictors were simulated to have fairly high correlations with one another. That can often compromise random forest importance scores.
The important package has two other feature selection steps that can be used with multiple scores:
step_predictor_retain(): choose predictors based on a logical statement. Example:step_predictor_desirability(): choose multiple scores to compute then use desirability functions to rank them:We already have our fitted model and, if we are happy with it:
Resampling estimates:
#> # A tibble: 4 × 4
#> .metric mean n std_err
#> <chr> <dbl> <int> <dbl>
#> 1 sensitivity 0.987 10 0.00319
#> 2 specificity 0.620 10 0.0169
#> 3 brier_class 0.0854 10 0.00220
#> 4 roc_auc 0.951 10 0.00212
Similar to fit_best(), there is a convenience function that can be used to get the final model and the test set results.
We have to start with a finalized workflow (i.e., no tune() values):
last_fit() uses the original split object to fit, predict, and measure the model using the test set:
knn_test_res <-
knn_last_wflow |>
last_fit(sim_split, metrics = cls_mtr)
knn_test_res
#> # Resampling results
#> # Manual resampling
#> # A tibble: 1 × 6
#> splits id .metrics .notes .predictions .workflow
#> <list> <chr> <list> <list> <list> <list>
#> 1 <split [3749/1251]> train/test split <tibble> <tibble> <tibble> <workflow>We can pick out the parts that we want:
knn_final_fit <- knn_test_res |> extract_workflow()
knn_test_pred <- knn_test_res |> collect_predictions()
knn_test_mtr <- knn_test_res |> collect_metrics()
knn_test_mtr
#> # A tibble: 4 × 4
#> .metric .estimator .estimate .config
#> <chr> <chr> <dbl> <chr>
#> 1 sensitivity binary 0.936 pre0_mod0_post0
#> 2 specificity binary 0.825 pre0_mod0_post0
#> 3 brier_class binary 0.0485 pre0_mod0_post0
#> 4 roc_auc binary 0.946 pre0_mod0_post0Easy peasy!