| x | -5 | -3 | -1 | 0 | 1 | 2 | 3 | 4 | 6 | 8 | 9 | 10 | 12 | 15 |
| y | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 1 | 0 | 1 | 0 | 0 | 0 | 0 |
H. Sherry Zhang
Department of Statistics and Data Sciences
University of Texas at Austin
library(usethis) create_from_github("SDS322E-26FALL/1102-knn", fork = FALSE)
tidymodels: nearest_neighbor() and the kknn enginevfold_cv(), tune(), grid_regular(), tune_grid(), select_best(), finalize_workflow(), last_fit()| x | -5 | -3 | -1 | 0 | 1 | 2 | 3 | 4 | 6 | 8 | 9 | 10 | 12 | 15 |
| y | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 1 | 0 | 1 | 0 | 0 | 0 | 0 |
Previously from a logistic regression

for x = 3.5, y = 0.72, predict 1.
Maybe there are other things to make a prediction …
How about we look at the 3 points that are closest to x = 3.5:
| x | 2 | 3 | 4 |
| y | 1 | 0 | 1 |
K-nearest neighbor predict based on the “average”/ majority vote: here 1 is the majority, so predict 1.
What would you predict at x = 3 with k = 1?
Applicable both for regression and classification.
Goal: predict sex based on bill_len and bill_dep with KNN.
# A tibble: 3 × 8
species island bill_len bill_dep flipper_len body_mass sex year
<fct> <fct> <dbl> <dbl> <int> <int> <fct> <int>
1 Adelie Torgersen 39.1 18.7 181 3750 male 2007
2 Adelie Torgersen 39.5 17.4 186 3800 female 2007
3 Adelie Torgersen 40.3 18 195 3250 female 2007
The same tidymodels workflow as week 10 b (Oct 28), Practice: Introduction to tidymodels, with two changes:
nearest_neighbor() with the kknn engine, in "classification" modestep_normalize()Step 1: Specify the model, with \(k = 5\) neighbors:
Step 2: Specify the pre-processing recipe:
Step 3: Build a workflow:
Step 4: Fit the model:
Step 5: Predict:
knn_pred <- augment(knn_fit, penguins_clean)
knn_pred |> select(sex, .pred_class, .pred_female, .pred_male) |> head(5)# A tibble: 5 × 4
sex .pred_class .pred_female .pred_male
<fct> <fct> <dbl> <dbl>
1 male male 0.12 0.88
2 female female 1 0
3 female female 0.64 0.36
4 female female 0.52 0.48
5 male male 0.28 0.72
Step 6: Calculate classification metrics:
We picked \(k = 5\), but nothing in the data told us to. \(k\) is a hyperparameter: a setting we choose before fitting. Each value of \(k\) gives a different model:
Try \(k = 1, 2, \ldots, 20\), and compute the accuracy on the same data we fit on:
With \(k = 1\), every penguin’s nearest neighbor is itself, so the training accuracy is almost perfect. The training data always rewards the most flexible model: it can’t tell us how well the model predicts new penguins.
To choose \(k\), we need to score the model on penguins it has not been fit on. First, set aside a test set for the final evaluation. Then split the remaining training set into, say, 10 folds. Each fold takes one turn as the validation set, while the model is fit on the other 9:
For each \(k\), average the 10 validation accuracies, then pick the \(k\) with the best average:
Step 0: Training-testing split
Step 1: Specify the model, with the number of neighbors left to tune
Step 2: Specify the pre-processing recipe
Step 3: Build a workflow
Step 4: Fit the model
Step 5/6: Predict and calculate classification metrics on the testing data
Step 0: Training-testing split: new
<Training/Testing/Total>
<249/84/333>
249 / 333 = 0.747, 84 / 333 = 0.252
penguins_train <- training(penguins_split)
penguins_test <- testing(penguins_split)
head(penguins_train, 5)# A tibble: 5 × 8
species island bill_len bill_dep flipper_len body_mass sex year
<fct> <fct> <dbl> <dbl> <int> <int> <fct> <int>
1 Gentoo Biscoe 59.6 17 230 6050 male 2007
2 Adelie Torgersen 34.4 18.4 184 3325 female 2007
3 Gentoo Biscoe 45.2 15.8 215 5300 male 2008
4 Chinstrap Dream 49 19.5 210 3950 male 2008
5 Adelie Torgersen 41.4 18.5 202 3875 male 2009
[1] 249 8
[1] 84 8
Step 1/2/3: Specify the model, the pre-processing recipe, and the workflow: can you spot the differences from the previous workflow?
# step 1: specify the model
knn_tune_mod <- nearest_neighbor(mode = "classification", neighbors = tune()) |> set_engine("kknn")
# step 2: specify the pre-processing recipe
knn_rec <- recipe(sex ~ bill_len + bill_dep, data = penguins_train) |> step_normalize(all_predictors())
# step 3: build a workflow
knn_tune_wf <- workflow() |> add_recipe(knn_rec) |> add_model(knn_tune_mod)neighbors = tune() is a placeholder: we don’t pick \(k\) now, cross-validation fills it in at step 4penguins_train as the template.Step 4a: Create the cross-validation folds from the training data
# 10-fold cross-validation
# A tibble: 10 × 2
splits id
<list> <chr>
1 <split [224/25]> Fold01
2 <split [224/25]> Fold02
3 <split [224/25]> Fold03
4 <split [224/25]> Fold04
5 <split [224/25]> Fold05
6 <split [224/25]> Fold06
7 <split [224/25]> Fold07
8 <split [224/25]> Fold08
9 <split [224/25]> Fold09
10 <split [225/24]> Fold10
<split [224/25]>: the model is fit on 224 penguins and validated on the other 25penguins_train is split: the test set stays asideStep 4b: Set up the hyperparameter grid: the values of \(k\) to try
# A tibble: 20 × 1
neighbors
<int>
1 1
2 2
3 3
4 4
5 5
6 6
7 7
8 8
9 9
10 10
11 11
12 12
13 13
14 14
15 15
16 16
17 17
18 18
19 19
20 20
tune(): neighborsrange = c(1, 20): the smallest and largest \(k\); levels = 20: how many evenly spaced values, so \(k = 1, 2, \ldots, 20\)levels = 5, we would get \(k\) = 1, 5, 10, 15, 20Step 4c: Fit the model for every \(k\) on every fold
# Tuning results
# 10-fold cross-validation
# A tibble: 10 × 4
splits id .metrics .notes
<list> <chr> <list> <list>
1 <split [224/25]> Fold01 <tibble [60 × 5]> <tibble [0 × 4]>
2 <split [224/25]> Fold02 <tibble [60 × 5]> <tibble [0 × 4]>
3 <split [224/25]> Fold03 <tibble [60 × 5]> <tibble [0 × 4]>
4 <split [224/25]> Fold04 <tibble [60 × 5]> <tibble [0 × 4]>
5 <split [224/25]> Fold05 <tibble [60 × 5]> <tibble [0 × 4]>
6 <split [224/25]> Fold06 <tibble [60 × 5]> <tibble [0 × 4]>
7 <split [224/25]> Fold07 <tibble [60 × 5]> <tibble [0 × 4]>
8 <split [224/25]> Fold08 <tibble [60 × 5]> <tibble [0 × 4]>
9 <split [224/25]> Fold09 <tibble [60 × 5]> <tibble [0 × 4]>
10 <split [225/24]> Fold10 <tibble [60 × 5]> <tibble [0 × 4]>
step_normalize()) and fit KNN on 224 penguins, then predict the 25 held-out penguins and compute the metrics# A tibble: 600 × 8
splits id neighbors .metric .estimator .estimate .config
<list> <chr> <int> <chr> <chr> <dbl> <chr>
1 <split [224/25]> Fold01 1 accuracy binary 0.8 pre0_mod0…
2 <split [224/25]> Fold01 1 roc_auc binary 0.801 pre0_mod0…
3 <split [224/25]> Fold01 1 brier_class binary 0.2 pre0_mod0…
4 <split [224/25]> Fold01 2 accuracy binary 0.8 pre0_mod0…
5 <split [224/25]> Fold01 2 roc_auc binary 0.885 pre0_mod0…
6 <split [224/25]> Fold01 2 brier_class binary 0.152 pre0_mod0…
7 <split [224/25]> Fold01 3 accuracy binary 0.8 pre0_mod0…
8 <split [224/25]> Fold01 3 roc_auc binary 0.917 pre0_mod0…
9 <split [224/25]> Fold01 3 brier_class binary 0.123 pre0_mod0…
10 <split [224/25]> Fold01 4 accuracy binary 0.88 pre0_mod0…
# ℹ 590 more rows
# ℹ 1 more variable: .notes <list>
.metrics: 60 rows for each fold = 20 values of \(k\) \(\times\) 3 default metrics (accuracy, roc_auc, brier_class)Step 4d: Average the metrics over the 10 folds
# A tibble: 20 × 7
neighbors .metric .estimator mean n std_err .config
<int> <chr> <chr> <dbl> <int> <dbl> <chr>
1 1 accuracy binary 0.796 10 0.0200 pre0_mod01_post0
2 2 accuracy binary 0.796 10 0.0200 pre0_mod02_post0
3 3 accuracy binary 0.796 10 0.0200 pre0_mod03_post0
4 4 accuracy binary 0.851 10 0.0135 pre0_mod04_post0
5 5 accuracy binary 0.859 10 0.0124 pre0_mod05_post0
6 6 accuracy binary 0.863 10 0.00907 pre0_mod06_post0
7 7 accuracy binary 0.871 10 0.0133 pre0_mod07_post0
8 8 accuracy binary 0.863 10 0.0137 pre0_mod08_post0
9 9 accuracy binary 0.867 10 0.0146 pre0_mod09_post0
10 10 accuracy binary 0.867 10 0.0146 pre0_mod10_post0
11 11 accuracy binary 0.871 10 0.0143 pre0_mod11_post0
12 12 accuracy binary 0.871 10 0.0143 pre0_mod12_post0
13 13 accuracy binary 0.880 10 0.0133 pre0_mod13_post0
14 14 accuracy binary 0.880 10 0.0119 pre0_mod14_post0
15 15 accuracy binary 0.880 10 0.0119 pre0_mod15_post0
16 16 accuracy binary 0.880 10 0.0119 pre0_mod16_post0
17 17 accuracy binary 0.880 10 0.0119 pre0_mod17_post0
18 18 accuracy binary 0.880 10 0.0119 pre0_mod18_post0
19 19 accuracy binary 0.876 10 0.0126 pre0_mod19_post0
20 20 accuracy binary 0.872 10 0.0116 pre0_mod20_post0
mean: the average accuracy over the n = 10 folds, for each \(k\); std_err: how much it varies across foldsStep 4e: Find the best \(k\)
# A tibble: 5 × 7
neighbors .metric .estimator mean n std_err .config
<int> <chr> <chr> <dbl> <int> <dbl> <chr>
1 14 accuracy binary 0.880 10 0.0119 pre0_mod14_post0
2 15 accuracy binary 0.880 10 0.0119 pre0_mod15_post0
3 16 accuracy binary 0.880 10 0.0119 pre0_mod16_post0
4 17 accuracy binary 0.880 10 0.0119 pre0_mod17_post0
5 18 accuracy binary 0.880 10 0.0119 pre0_mod18_post0
# A tibble: 1 × 2
neighbors .config
<int> <chr>
1 14 pre0_mod14_post0
show_best(): the top \(k\) values, sorted by mean accuracy. Here several \(k\) tie, and select_best() returns the first oneselect_best(): a one-row tibble with the best \(k\), ready to plug into the workflow in step 5Step 4f: Plug in the best \(k\)
══ Workflow ════════════════════════════════════════════════════════════════════
Preprocessor: Recipe
Model: nearest_neighbor()
── Preprocessor ────────────────────────────────────────────────────────────────
1 Recipe Step
• step_normalize()
── Model ───────────────────────────────────────────────────────────────────────
K-Nearest Neighbor Model Specification (classification)
Main Arguments:
neighbors = 14
Computational engine: kknn
finalize_workflow() replaces the tune() placeholder with the best \(k\)Step 4f: Refit on the whole training data
Example: the ROC curve on the testing data
roc_curve() uses the predicted probability (.pred_male), not the predicted class.sex (female) as the event by default, so we set event_level = "second" to make it male