| 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 |
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 |
What’s the “average”/ majority vote?
It should get predicted to 1
This is k-nearest neighbor with k = 3.
What would you predict at x = 3 with k = 1?
Follow along the class example with
usethis::create_from_github("SDS322E-26FALL/1102-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
Goal: predict sex based on bill_len and bill_dep with KNN.
The same tidymodels workflow as last time, 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.
Split the data, fit on the training set, and compute the accuracy on the test set for each \(k\). Here are three different random splits:
Instead, keep the test set aside and split the 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. This uses the training set only, and averaging over 10 folds is much more stable than a single test set.
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
Step 1/2/3: Specify the model, the pre-processing recipe, and the workflow - same, except neighbors = tune()
Step 4: Fit the model: cross validation folds, hyperparameter grid, fit on both, and find the best model
penguins_folds <- vfold_cv(penguins_train, v = 10)
knn_grid <- grid_regular(neighbors(range = c(1, 20)), levels = 20)
knn_tune_res <- tune_grid(knn_tune_wf, resamples = penguins_folds, grid = knn_grid)
best_k <- select_best(knn_tune_res, metric = "accuracy")
best_k# A tibble: 1 × 2
neighbors .config
<int> <chr>
1 14 pre0_mod14_post0
knn_tune_res |>
collect_metrics() |>
filter(.metric == "accuracy") |>
ggplot(aes(x = neighbors, y = mean)) +
geom_line() +
geom_point() +
geom_point(data = \(d) filter(d, neighbors == best_k$neighbors), size = 5, shape = 21, stroke = 1.5) +
scale_x_continuous(breaks = 1:20) +
labs(x = "k", y = "Cross validation accuracy")Step 5/6: Predict and calculate classification metrics on the testing data: plug the best \(k\) into the workflow, refit on the whole training data, and evaluate once on the testing data
final_wf <- finalize_workflow(knn_tune_wf, best_k)
final_fit <- fit(final_wf, data = penguins_train)
res_final <- augment(final_fit, penguins_test)
metrics(res_final, truth = sex, estimate = .pred_class)# A tibble: 2 × 3
.metric .estimator .estimate
<chr> <chr> <dbl>
1 accuracy binary 0.798
2 kap binary 0.593
last_fit(final_wf, split = penguins_split) does the refit and the testing-data evaluation in one step.
This follows the big picture:
step_normalize()), list candidate values of \(k\), choose the best with cross validation, refit