H. Sherry Zhang
Department of Statistics and Data Sciences
University of Texas at Austin
library(usethis) create_from_github("SDS322E-26FALL/1302-compare-models", fork = FALSE)
tidymodelsWhich model predicts penguin sex best, from bill_len and bill_dep?
We have seen three models for a binary response:
logistic_reg(), no hyperparametersnearest_neighbor(), tune neighborsrand_forest(), tune mtry and min_nMost of the code is what you have already seen in each lecture. What’s new is putting them together fairly:
strata = sex: the training and the testing data keep the same proportion of females and males
strata beforestrata: otherwise some folds may have very few of the rare classsex is binarytune()Different models need different pre-processing:
step_normalize(), because distances need all predictors on the same scalespecies), logistic regression and KNN would also need step_dummy(); the random forest can use them directlyWhat does the KNN recipe do? Use prep() and bake() to look at the processed training data:
# A tibble: 5 × 3
bill_len bill_dep sex
<dbl> <dbl> <fct>
1 -0.748 0.434 female
2 -1.41 1.08 female
3 -1.01 0.335 female
4 -0.601 0.237 female
5 -1.43 0.335 female
bill_len and bill_dep now have mean 0 and standard deviation 1prep() and bake() to fit a model, the workflow does it for you. But they are useful to check what your recipe doesEach workflow keeps a model with its own recipe, so the right pre-processing always goes with the right model.
# 10-fold cross-validation using stratification
# A tibble: 10 × 2
splits id
<list> <chr>
1 <split [223/26]> Fold01
2 <split [223/26]> Fold02
3 <split [223/26]> Fold03
4 <split [224/25]> Fold04
5 <split [224/25]> Fold05
6 <split [224/25]> Fold06
7 <split [225/24]> Fold07
8 <split [225/24]> Fold08
9 <split [225/24]> Fold09
10 <split [225/24]> Fold10
penguins_folds: each model is validated on exactly the same penguins, so their cross-validation results can be compared directlystrata = sex again: every fold keeps the same proportion of females and malesknn_grid <- grid_regular(neighbors(range = c(1, 30)), levels = 30)
rf_grid <- grid_regular(mtry(range = c(1, 2)), min_n(range = c(2, 20)), levels = c(2, 4))
rf_grid# A tibble: 8 × 2
mtry min_n
<int> <int>
1 1 2
2 2 2
3 1 8
4 2 8
5 1 14
6 2 14
7 1 20
8 2 20
mtry can only be 1 or 2: 2 \(\times\) 4 values of min_n = 8 combinationsChoose the metrics with metric_set() (new), and use the same ones for all models:
cls_metrics <- metric_set(accuracy, roc_auc)
log_res <- fit_resamples(log_wf, resamples = penguins_folds, metrics = cls_metrics,
control = control_resamples(save_pred = TRUE))
knn_res <- tune_grid(knn_wf, resamples = penguins_folds, grid = knn_grid, metrics = cls_metrics,
control = control_grid(save_pred = TRUE))
set.seed(5)
rf_res <- tune_grid(rf_wf, resamples = penguins_folds, grid = rf_grid, metrics = cls_metrics,
control = control_grid(save_pred = TRUE))fit_resamples() (new): like tune_grid(), but for a model with nothing to tune: it fits the workflow on each fold and computes the validation metricssave_pred = TRUE: keep the predictions on each validation fold, so we can draw ROC curves later# A tibble: 2 × 6
.metric .estimator mean n std_err .config
<chr> <chr> <dbl> <int> <dbl> <chr>
1 accuracy binary 0.773 10 0.0292 pre0_mod0_post0
2 roc_auc binary 0.848 10 0.0294 pre0_mod0_post0
# A tibble: 3 × 7
neighbors .metric .estimator mean n std_err .config
<int> <chr> <chr> <dbl> <int> <dbl> <chr>
1 1 accuracy binary 0.786 10 0.0196 pre0_mod01_post0
2 2 accuracy binary 0.786 10 0.0196 pre0_mod02_post0
3 3 accuracy binary 0.786 10 0.0196 pre0_mod03_post0
mean: the average over the n = 10 folds; std_err: how much it varies from fold to fold. We will need both to compare models# A tibble: 3 × 7
neighbors .metric .estimator mean n std_err .config
<int> <chr> <chr> <dbl> <int> <dbl> <chr>
1 12 accuracy binary 0.852 10 0.0221 pre0_mod12_post0
2 11 accuracy binary 0.848 10 0.0255 pre0_mod11_post0
3 13 accuracy binary 0.848 10 0.0227 pre0_mod13_post0
# A tibble: 3 × 8
mtry min_n .metric .estimator mean n std_err .config
<int> <int> <chr> <chr> <dbl> <int> <dbl> <chr>
1 1 2 accuracy binary 0.828 10 0.0292 pre0_mod1_post0
2 2 2 accuracy binary 0.828 10 0.0225 pre0_mod5_post0
3 1 20 accuracy binary 0.824 10 0.0241 pre0_mod4_post0
Take the best version of each model, and compare their cross-validation accuracy:
cv_compare <- bind_rows(
collect_metrics(log_res) |> mutate(model = "Logistic regression"),
show_best(knn_res, metric = "accuracy", n = 1) |> mutate(model = "KNN"),
show_best(rf_res, metric = "accuracy", n = 1) |> mutate(model = "Random forest")
) |>
filter(.metric == "accuracy") |>
select(model, mean, std_err, n)
cv_compare# A tibble: 3 × 4
model mean std_err n
<chr> <dbl> <dbl> <int>
1 Logistic regression 0.773 0.0292 10
2 KNN 0.852 0.0221 10
3 Random forest 0.828 0.0292 10
Accuracy depends on the 0.5 cutoff. The ROC curve compares the models over all cutoffs, using the predictions on the validation folds:

Plug the best \(k\) into the KNN workflow, refit on all the training data, and evaluate once on the testing data. last_fit() does the refit and the evaluation in one step:
best_k <- select_best(knn_res, metric = "accuracy")
knn_final <- last_fit(finalize_workflow(knn_wf, best_k), split = penguins_split, metrics = cls_metrics)
collect_metrics(knn_final)# A tibble: 2 × 4
.metric .estimator .estimate .config
<chr> <chr> <dbl> <chr>
1 accuracy binary 0.857 pre0_mod0_post0
2 roc_auc binary 0.921 pre0_mod0_post0
If your response is numeric (e.g. predict body_mass), the same steps apply, with a few changes:
Models: linear_reg() instead of logistic_reg(), and mode = "regression" for KNN and random forest
Split: strata is optional: for a numeric response, strata = body_mass splits it into bins so every fold covers light and heavy penguins
Metrics: RMSE (in the units of the response) and \(R^2\), with metric_set(rmse, rsq): for RMSE, lower is better. Use select_best(metric = "rmse")
Recipes: the same rules. Dummy variables for categorical predictors in linear regression and KNN, normalization for KNN, nothing for the random forest
Don’t assume the flexible model wins: if the response is close to a straight-line function of the predictors (as body mass is of flipper length), linear regression can do as well as KNN and random forest, and is much easier to explain. Compare, and let the cross-validation results decide
Use this lecture as a template: