← All tutorials

k-Nearest Neighbors (k-NN)

The classifier that trains nothing.

On this page

1 The laziest classifier that works 2 Distance first, then a vote 3 Classifying one query, step by step 4 Choosing k trades bias for variance 5 Weighted votes, regression, other distances, faster search 6 k-NN in practice 7 When k-NN misleads 8 Naive Bayes needs probability first 9 References

1 The laziest classifier that works

You move to a new street and want to know which bakery is best. You could build a statistical model of bakeries, or ask your five nearest neighbors and go with the majority. k-NN is the second option: to classify a new point, find the k training points closest to it and take a majority vote. Cover and Hart (1967) showed that, with enough data, the one-nearest-neighbor rule makes at most twice the error of the best possible classifier.

"Training" is just memorizing the dataset: the model is the data, and all the thinking happens at query time. The main setting you choose before predicting, a hyperparameter, is k.

stepwhat it givesdistanceevery point rankedk nearestthe votersmajority votethe labelthe k dialjagged or smoothvariantsweights, fast searchtrapswhen it misleads
Figure 1. From one query to a label, with what each step gives.

2 Distance first, then a vote

The neighbors are set by the straight-line (Euclidean) distance from the query q to each training point x across every feature (a feature is one measured number that describes a point).

The straight-line distance adds the squared gap on every feature, so a training point counts as near only when it is close on all features at once.
d(x, q) = √Σj (xj − qj)²
j numbers the features. For each feature, square the gap between training point x and query q, add the squares (Σ, sigma, means sum), and take the square root: Pythagoras, carried to as many features as you like. Squaring drops the sign of a gap and makes one big gap count more than several small ones. Ranking by the squared value gives the same order, so the root can be skipped when only the order matters.

The distance adds raw feature values, so a feature measured in thousands drowns one measured in fractions. Standardizing every feature first fixes that; scikit-learn does it with StandardScaler, and PCA (principal component analysis) needs the same fix.

Standardizing gives every feature a mean of 0 and a standard deviation of 1 before any distance is measured.
x′j = xj − μjσj
x′j is feature j after rescaling. μj (mu) is the feature’s mean over the training points. σj (sigma) is its standard deviation, the typical size of its gap from the mean. Subtracting the mean centers the feature on 0, and dividing by the standard deviation measures every gap in standard deviations, so a feature counted in thousands and one counted in fractions add gaps of the same size and none wins just because its numbers are bigger.

The k nearest points then form the set Nk(q), and the prediction ŷ is their most common label.

The snippet is the whole algorithm on five stored points: one distance line, then the sort and the vote.

import numpy as np
X_train = np.array([[1.0, 1.2], [0.8, 0.9], [3.1, 3.0], [2.9, 3.3], [1.1, 0.7]])
labels = np.array([0, 0, 1, 1, 0])
q = np.array([1.0, 1.0])
k = 3
dist = np.sqrt(((X_train - q) ** 2).sum(axis=1))
nearest = np.argsort(dist)[:k]
votes = np.bincount(labels[nearest])
y_hat = votes.argmax()
print(y_hat)  # 0
  1. 1Makes five labeled points, the query q and k.
  2. 2Measures d(x, q) to every point, eq card in Section 2, line 1.
  3. 3Keeps the k closest points, the set Nk(q) from Section 2.
  4. 4Counts the labels and returns the most common one, the prediction ŷ.

With two classes an odd k can never tie; with three or more, ties can still happen and need a rule, such as the nearest voter decides.

3 Classifying one query, step by step

Distance picks the voters; the vote picks the label.
d(x, q) = √Σj (xj − qj)²
The k points with the smallest d become the voters, joined to the query inside the dashed circle.
ŷ = mode{yi : i ∈ Nk(q)}
The prediction ŷ is the most common label yi (the mode) among Nk(q), the k nearest points; each voter counts once.

Figure 2. Blue wins with 5 voters, orange with 15.

4 Choosing k trades bias for variance

One noisy neighbor can swing a small vote, while many voters average the noise out. That is the bias-variance tradeoff: bias is the error from a boundary too smooth to follow the true one (large k), and variance is how much the boundary changes when the noisy points change (small k).

k = 3, jaggedk = 17, smooth
Figure 3. Two noisy classes, voted on by 3 neighbors (left) and 17 (right).
17

Figure 4. At k = 1 or 3 orange noise points win; from 5 on, blue wins.

5 Weighted votes, regression, other distances, faster search

The plain vote treats the nearest neighbor and the farthest voter as equals, and it only predicts labels.

Weighted k-NN lets each voter count by how close it is, and k-NN regression replaces the vote with an average.
ŷ = argmaxc
Σi ∈ Nk(q) 1d(xi, q) · [yi = c]
Each voter i adds 1 over its distance to the query to the tally of its own label, so a voter twice as close counts twice as much. c is a candidate label. [yi = c] is 1 when voter i has label c and 0 otherwise, so each label’s tally gathers only its own voters, and argmaxc picks the label with the biggest tally. A far neighbor that kept spoiling a good prediction now adds only a little.
ŷ = 1k Σi ∈ Nk(q) yi
For numbers, the prediction is the mean of the k neighbors’ values yi: the same neighbors, with an average in place of the count. A larger k averages more neighbors, so the prediction gets smoother and slower to follow sharp changes.
Manhattan and cosine distance change what near means: the total gap along the axes, or the angle between two arrows.
d(x, q) = Σj |xj − qj|
Manhattan distance adds up the gap along each feature j, where |·| is the size of a gap with its sign dropped. It walks city blocks, one feature at a time, so it fits grid-like features. Gaps add without squaring, so one odd feature pulls the distance less than in straight-line distance.
d(x, q) = 1 − x · q‖x‖ ‖q‖
In cosine distance, x · q is the dot product, the sum of xj times qj over the features, and ‖x‖ is the length of arrow x. The fraction is the cosine of the angle between the arrows, 1 when they point the same way, so such arrows sit at distance 0 whatever their lengths. That suits text, where direction matters more than size: a long and a short document on one topic point the same way.

Plain k-NN scans every stored point per query. KD-trees and ball trees (nested boxes or balls of points) skip most of that scan, and graph indexes such as HNSW (hierarchical navigable small world graphs, which link each point to a few near points) trade a little accuracy for a lot of speed.

This snippet builds four variants with the same 17 voters, each behind a StandardScaler, and prints each one’s accuracy on held-out test points; the one changed argument is the whole difference.

from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.neighbors import KNeighborsClassifier as KNN
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
X, y = make_classification(n_samples=400, n_features=2, n_informative=2, n_redundant=0, random_state=2)
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
k = 17
variants = {
    "weighted by 1/d": KNN(k, weights="distance"),
    "manhattan": KNN(k, metric="manhattan"),
    "cosine": KNN(k, metric="cosine"),
    "kd-tree search": KNN(k, algorithm="kd_tree"),
}
for name, knn in variants.items():
    model = make_pipeline(StandardScaler(), knn).fit(X_train, y_train)
    print(name, model.score(X_test, y_test))
  1. 1Makes 400 toy points and holds a quarter out for testing.
  2. 2Builds the four variants: the 1/d weights of the weighted card, the two distances of the Manhattan and cosine card, and a KD-tree search.
  3. 3Standardizes, fits each variant on the training points and scores it on the test points.

k-NN regression is KNeighborsRegressor(k), the same call with numbers for labels.

6 k-NN in practice

In scikit-learn, k-NN sits after a scaler in one pipeline; fit stores the scaled training points once, and predict or score runs the distance and the vote for every new point.

from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.neighbors import KNeighborsClassifier
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
X, y = make_classification(n_samples=400, n_features=2, n_informative=2, n_redundant=0, random_state=2)
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
model = make_pipeline(StandardScaler(), KNeighborsClassifier(n_neighbors=5))
model.fit(X_train, y_train)
print(model.score(X_test, y_test))  # accuracy on new points
  1. 1Makes 400 toy points and holds a quarter out for testing.
  2. 2Chains StandardScaler, the standardizing card in Section 2, with KNeighborsClassifier.
  3. 3Fit only stores the scaled training points.
  4. 4Votes for every test point, eq card in Section 3, and prints the accuracy.
stepwhen, on what, and its cost
fitOnce, storing the training points with no learning.
predictPer query, one distance per stored point over every feature, then the vote; time grows with the training set.
stateThe whole training set.
settingraise it, lower itdefault
n_neighbors, k in Nk(q)Up smooths the boundary; k equal to the number of training points predicts the majority class everywhere. Down makes it jagged; k = 1 copies the nearest point and overfits.5
weights, the 1/d factor"distance" lets near voters count more; "uniform" counts each voter once."uniform"
metric, p, the d(x, q) cardChanges what near means. The default Minkowski distance raises each feature gap to the power p; p = 2 is straight-line distance, p = 1 Manhattan."minkowski", p=2

Defaults from the KNeighborsClassifier docs; the per-query cost, linear in the training set, is from Manning, Raghavan and Schütze (2008). Gotchas are in Section 7.

7 When k-NN misleads

With no training step, overfitting never shows up in a training curve. It shows up at query time as a jagged boundary: small k wraps noisy points, big k averages them away.

Near means similar only in a few meaningful dimensions. With a thousand raw features everything is far from everything, and the nearest neighbors are barely nearer than the rest, the curse of dimensionality. Cutting the features down to a few directions first, as PCA does, gives the distances their meaning back.

8 Naive Bayes needs probability first

The same nearest-point idea runs without labels in k-means, where each point joins its nearest center. k-NN votes with neighbors. The next classifier, Naive Bayes, votes with word counts and rests on Bayes rule.

9 References

  1. Manning, C. D., Raghavan, P., and Schütze, H. (2008). Introduction to Information Retrieval, section 14.3. Cambridge University Press. (per-query cost)
  2. Cover, T. M., and Hart, P. E. (1967). Nearest neighbor pattern classification. IEEE Transactions on Information Theory, 13(1), 21 to 27.