Quick summary
Summarize this blog with AI
Use nearest neighbors to predict a known class
In this tutorial, “nearest neighbor” means supervised classification with class::knn(). It predicts a new observation's class from nearby labelled training observations. K-means is a different, unsupervised clustering algorithm; it does not replace a classifier that needs training labels.
The complete example uses the built-in iris data, R 4.5.1 and class 7.3-23. All blocks run in order. The held-out results are a teaching demonstration on this small dataset, not a production performance estimate.
Split rows before preprocessing
data(iris)
# Deterministic teaching split: every fifth row within each species is held out.
within_species <- ave(seq_len(nrow(iris)), iris$Species, FUN = seq_along)
test_rows <- within_species %% 5 == 0
train_rows <- !test_rows
features <- c("Sepal.Length", "Sepal.Width", "Petal.Length", "Petal.Width")
x_train <- as.matrix(iris[train_rows, features])
x_test <- as.matrix(iris[test_rows, features])
y_train <- iris$Species[train_rows]
y_test <- iris$Species[test_rows]
stopifnot(nrow(x_train) == 120, nrow(x_test) == 30)
stopifnot(ncol(x_train) == 4, !("Species" %in% colnames(x_train)))
stopifnot(!anyNA(x_train), !anyNA(x_test))
Species is the answer to predict and must not appear among the predictors. Including it leaks the answer into the distance calculation. This transparent split leaves ten test rows per species. For a real evaluation, choose a split that matches deployment: random stratification for suitable independent data, or time/group-aware separation when observations are dependent.
Scale with training statistics and predict
z_train <- scale(x_train)
centers <- attr(z_train, "scaled:center")
spreads <- attr(z_train, "scaled:scale")
stopifnot(all(is.finite(spreads)), all(spreads > 0))
z_test <- scale(x_test, center = centers, scale = spreads)
RNGkind("Mersenne-Twister", "Inversion", "Rejection")
set.seed(2026)
predicted <- class::knn(train = z_train, test = z_test,
cl = y_train, k = 5, use.all = TRUE)
stopifnot(length(predicted) == length(y_test))
stopifnot(all(as.character(predicted) %in% levels(y_train)))
The function uses Euclidean distances between predictor rows. Standardization prevents a predictor's numerical units from automatically dominating distance; whether that is appropriate still depends on the task. The training centers and spreads are applied unchanged to the test rows. Do not scale the test set independently or estimate the transformation from the combined data.
Evaluate on labels the classifier did not receive
confusion <- table(actual = y_test, predicted = predicted)
print(confusion)
accuracy <- mean(predicted == y_test)
print(round(accuracy, 6)) # 0.933333 (28 correct out of 30)
expected_confusion <- matrix(c(10L, 0L, 0L, 0L, 10L, 0L, 0L, 2L, 8L),
nrow = 3, byrow = TRUE)
stopifnot(identical(unname(confusion), expected_confusion) ||
identical(as.vector(confusion), as.vector(expected_confusion)))
stopifnot(sum(confusion) == 30)
stopifnot(isTRUE(all.equal(accuracy, sum(diag(confusion)) / 30)))
The table has diagonal counts 10, 10 and 8; two virginica observations are predicted as versicolor. Rows are actual species and columns are predicted species. Diagonal entries are correct predictions. Accuracy is the fraction of the 30 held-out rows classified correctly. Report the per-class table as well as the single accuracy value; an aggregate can conceal which classes are confused.
Keep the evaluation honest
The value k = 5 is fixed for this demonstration. To choose k, compare candidates using validation splits inside the training data, refitting preprocessing in every split. Keep the test set for the final evaluation. Do not repeatedly adjust k in response to the displayed test accuracy and then report that accuracy as an untouched test result.
Odd k does not eliminate every tie, especially with multiple classes. With use.all = TRUE, observations tied at the kth distance can make the voting set larger than k; tied class votes may be resolved randomly. Record the seed and package version for reproducibility. Before deployment, also define missing-value handling, detect unseen feature columns and assess class imbalance.
Classify a new measurement with the same contract
new_flower <- data.frame(Sepal.Length = 5.1, Sepal.Width = 3.5,
Petal.Length = 1.4, Petal.Width = 0.2)
stopifnot(identical(names(new_flower), features))
new_scaled <- scale(as.matrix(new_flower), center = centers, scale = spreads)
set.seed(2026)
new_prediction <- class::knn(z_train, new_scaled, y_train, k = 5)
print(new_prediction) # setosa
stopifnot(as.character(new_prediction) == "setosa")
The new row has no Species column. Keep the predictor schema, training data, preprocessing values, k and version information together if this demonstration becomes an application. See using scale in R for the preprocessing details.