Problem
Most models compress the training set into parameters: a line, a tree, a network. That compression is an assumption about the shape of the answer. Sometimes we would rather not assume a shape at all and just say "points that look alike should get alike labels". Nearest neighbors does exactly that. It needs a way to say what "look alike" means (a distance), a rule to turn nearby labels into a prediction, and a way to find nearby points fast once the training set has millions of rows.
Intuition
To label a new point, find the k training points closest to it and let them vote. With k = 1 the prediction copies the single closest point, so the decision boundary bends around every training example, noise included. Larger k averages over a wider neighborhood, giving smoother boundaries but blurring small regions of the other class. This is the bias-variance tradeoff with one knob. Training is free (store the data); all the work happens at prediction time.
The diagram shows 16 training points in two classes and a query at (4.8, 5.0). Each dashed circle encloses the k nearest points for one value of k, and the vote changes as the circle grows.
Mechanics
Distance
Almost everything uses the Minkowski distance with exponent p:
dp(a, b) = (Σj |aj − bj|p)1/p
p = 2 is Euclidean distance (scikit-learn's default, metric="minkowski", p=2),
p = 1 is Manhattan distance, and p → ∞ is Chebyshev distance, the largest
single-coordinate gap. For p ≥ 1 these are true metrics (the triangle inequality holds),
which is what lets trees prune. Each coordinate contributes on its own scale, so a feature measured in
thousands swamps one measured in fractions. That is why k-NN almost always sits behind
standardization. For text and
embeddings, cosine distance on normalized vectors is more common.
It ranks neighbors the same way as Euclidean distance on unit vectors (see
vectors and dot products).
Vote and weights
Given the k nearest training points with labels yi and distances di, a classifier returns the label with the largest total weight, and a regressor returns the weighted mean:
ŷ = argmaxc Σi wi·[yi = c] ŷ = Σi wi yi / Σi wi
With weights="uniform", every wi = 1. With
weights="distance", scikit-learn's _get_weights sets
wi = 1/di. If the query lands exactly on one or more
training points, those points get weight 1 and the rest get 0, which avoids dividing by zero.
predict_proba returns each class's share of the total weight. When two labels tie on
uniform votes, predict takes the mode, which picks the class that sorts first. The docstring
also warns that when neighbors k and k+1 are equally far away but have different
labels, the result depends on the order of the training data.
Finding the neighbors
Brute force measures the query against all n training points: O(n·d) per query, and trivially parallel. Trees spend O(n log n) once, at fit time, so that most queries can skip most of the data.
-
A KD tree splits the node's points at the median of the dimension with the largest
spread (
find_node_split_dim, thenpartition_node_indices), and stores each node's bounding box. Nodes live in a flat array, and node i's children are 2i+1 and 2i+2. - A ball tree uses the same splitting, but each node stores a centroid and a radius, the largest distance from the centroid to any of the node's points. Its lower bound, max(0, d(q, centroid) − radius), works for any true metric, not just axis-aligned ones.
Before building, the tree fixes its depth: n_levels = int(log2(max(1, (n − 1) / leaf_size)) + 1)
and n_nodes = 2**n_levels − 1. Splitting at the median then leaves every leaf with between
leaf_size and 2·leaf_size points. The query is depth-first with a max-heap of
the best k distances so far. A node whose lower bound already exceeds the
k-th best distance is trimmed with all its points. A leaf is scanned point by point.
Otherwise the query visits the nearer child first, so the heap tightens early and prunes more.
Internally the KD tree compares "reduced" distances (squared Euclidean, which keeps the same order) to
skip square roots.
The left half splits on y at 4 and the right half at 7, so each leaf box shown here is the
tight box around its own four points, which is what init_node stores and
min_rdist measures against.
| Component | Shape/Params | Meaning | Source |
|---|---|---|---|
KNeighborsClassifier.__init__ |
n_neighbors=5, weights="uniform", algorithm="auto", leaf_size=30, p=2, metric="minkowski" |
The defaults: five neighbors, equal votes, Euclidean distance, the search method picked at fit time. | _classification.py 198-219 |
fit / predict |
X (n, d) → labels (m,) |
Fit stores the data (and builds a tree). Predict calls kneighbors, then takes the mode or the weighted mode of the neighbor labels. |
_classification.py 225-312 |
predict_proba |
(m, n_classes) |
Each class's share of the (weighted) votes among the k neighbors. | _classification.py 314-417 |
KNeighborsRegressor.predict |
(m,) or (m, n_outputs) |
Mean of the neighbor targets, or their weighted sum divided by the sum of weights. | _regression.py 229-272 |
_get_weights |
dist (m, k) |
"uniform" gives no weights; "distance" gives 1/d, with exact matches getting weight 1 and the others 0. |
_base.py 74-117 |
algorithm="auto" |
metric, d, k, n | Brute force if the metric is precomputed, d > 15, or k ≥ n/2. Otherwise a KD tree when the metric allows it, else a ball tree. | _base.py 615-648 |
kneighbors |
→ dist, ind (m, k) |
The shared neighbor search behind every k-NN estimator and NearestNeighbors. |
_base.py 756-958 |
NearestNeighbors |
unsupervised | The same search with no labels: return neighbors, for retrieval or as a building block. | _unsupervised.py 10-179 |
| Tree size | n_levels, n_nodes = 2**n_levels − 1 |
The depth is fixed before building, so leaves end up with between leaf_size and 2·leaf_size points. | _binary_tree.pxi.tp 870-877 |
find_node_split_dim |
→ j | The dimension with the largest spread (max − min) among the node's points. | _binary_tree.pxi.tp 595-645 |
_recursive_build |
node i → 2i+1, 2i+2 | Stop at a leaf; otherwise partition at the median n_mid = n_points / 2 and recurse. |
_binary_tree.pxi.tp 1033-1083 |
_query_single_depthfirst |
heap of k | Trim a node whose bound exceeds the heap's largest, scan a leaf, or visit the nearer child first. | _binary_tree.pxi.tp 1603-1656 |
KD init_node / min_rdist |
bounds (2, n_nodes, d) |
Per-node lower and upper corners, and the reduced distance from a point to that box. | _kd_tree.pyx.tp 73-147 |
Ball init_node / min_dist |
centroid (d,), radius |
Centroid is the mean of the node's points, radius the farthest one. The bound is max(0, d(q, c) − r). | _ball_tree.pyx.tp 86-156 |
Worked example
One pair, four distances. From (0, 0) to (3, 4), the Minkowski distance shrinks as p grows:
| Metric | p | Distance |
|---|---|---|
| manhattan | 1 | 7.000 |
| euclidean | 2 | 5.000 |
| minkowski | 3 | 4.498 |
| chebyshev | ∞ | 4.000 |
The neighbor order can change with p, so the metric is a modelling choice, not a detail.
The vote at (4.8, 5.0). The seven nearest training points, closest first:
| Rank | Point | Class | Distance | 1/d |
|---|---|---|---|---|
| 1 | (5.0, 4.0) | 0 | 1.020 | 0.981 |
| 2 | (6.0, 4.5) | 1 | 1.300 | 0.769 |
| 3 | (3.0, 6.0) | 0 | 2.059 | 0.486 |
| 4 | (7.0, 5.0) | 1 | 2.200 | 0.455 |
| 5 | (6.0, 7.0) | 1 | 2.332 | 0.429 |
| 6 | (3.0, 3.0) | 0 | 2.691 | 0.372 |
| 7 | (2.0, 4.0) | 0 | 2.973 | 0.336 |
With k = 5 and uniform weights the vote is 2 to
3 for class 1. With distance weights it is
1.466 to 1.653, still class 1, so
predict_proba gives 0.530 for class 1 instead of
0.600. At k = 7 two more class-0 points join and the uniform vote
becomes 4 to 3 for class 0.
Matching scikit-learn's docstrings. With training inputs [0], [1], [2], [3] and labels 0, 0, 1, 1:
- A classifier with k = 3 predicts class 0 at 1.1. The neighbors are 1, 2 and 0, with labels 0, 1, 0.
- At 0.9 the neighbors are 1, 0 and 2, so
predict_probais [0.667, 0.333]. - A regressor with k = 2 predicts 0.5 at 1.5, the mean of the targets 0 and 1.
These are the outputs printed in the KNeighborsClassifier and KNeighborsRegressor docstrings.
Choosing k. Leave-one-out error on the 16 points: predict each point from the other 15.
| k | Errors | Error rate |
|---|---|---|
| 1 | 2 / 16 | 0.125 |
| 3 | 0 / 16 | 0.000 |
| 5 | 0 / 16 | 0.000 |
| 7 | 1 / 16 | 0.062 |
| 9 | 1 / 16 | 0.062 |
| 15 | 16 / 16 | 1.000 |
k = 1 overfits: two points near the boundary copy a neighbor from the other class. Errors are lowest at 3 and 5. At k = 15 every point is wrong. With one point held out, its own class has 7 left and the other class has 8, so the vote always goes to the other class. A large k turns k-NN into "predict the majority of the rest". In practice, pick k with cross-validation, and prefer odd k for two classes to avoid ties.
The curse of dimensionality. Draw 200 uniform points in the unit cube [0, 1]d and one more as a query (fixed seed). Then compare the nearest and farthest distances:
| d | Nearest | Farthest | Nearest / farthest |
|---|---|---|---|
| 1 | 0.001 | 0.843 | 0.001 |
| 2 | 0.045 | 1.101 | 0.041 |
| 10 | 0.628 | 1.588 | 0.395 |
| 100 | 3.240 | 4.549 | 0.712 |
| 1000 | 12.396 | 13.578 | 0.913 |
In one dimension the nearest point is over a thousand times closer than the farthest. In 1000 dimensions it is only 1.10 times closer, so "nearest" barely means anything. Here is a second view. A sub-cube that holds 1% of uniform data needs edge 0.011/d: 0.01 for d = 1, 0.10 for d = 2, 0.63 for d = 10, 0.95 for d = 100. In 100 dimensions the "local" neighborhood spans 95% of every axis. k-NN works in high dimensions only when the data actually lies near a low-dimensional structure, which is what learned embeddings try to produce.
The tree at work.
- Size: with 16 points and
leaf_size=2,n_levels= 3 andn_nodes= 7. That is four leaves of four points, within [2, 4]. With 100,000 points and the defaultleaf_size=30, it is 12 levels and 4,095 nodes. - Build: the root's points spread 8 along both axes, so the first dimension wins the tie. The root splits at x = 5.
- Query with k = 1: the search goes to node 1, then leaf 4, and finds point (5.0, 4.0) at distance 1.020. Leaves 3, 5, 6 are trimmed because their boxes are farther away than that. It computes 4 of 16 distances.
- Query with k = 5: the fifth-best distance stays large, so no box can be ruled out and the tree scans all 16 points. That is why
"auto"uses brute force once k ≥ n/2. - Ball bound: the class-0 points have centroid (2.625, 3.375) and radius 2.652. That gives the query a lower bound of 0.063, below the true nearest distance 1.020. It is a valid bound, but a loose one.
Implementation
from sklearn.model_selection import GridSearchCV
from sklearn.neighbors import KNeighborsClassifier, KDTree
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
# Scale first: every feature contributes to the distance on its own scale.
model = make_pipeline(StandardScaler(), KNeighborsClassifier())
search = GridSearchCV(
model,
{"kneighborsclassifier__n_neighbors": [1, 3, 5, 7, 9, 15],
"kneighborsclassifier__weights": ["uniform", "distance"]},
cv=5,
)
search.fit(X_train, y_train)
proba = search.predict_proba(X_test) # (m, n_classes), vote shares
# The tree on its own, for retrieval.
tree = KDTree(X_train, leaf_size=30)
dist, ind = tree.query(X_test[:3], k=5) # each (3, 5), nearest first
Fitting the scaler inside the pipeline keeps the test folds out of the means and variances (see
data leakage). The tree is pure Cython with no learned parameters.
Its only choices are the metric and leaf_size, which trades tree depth against per-leaf
brute force. Larger leaf_size means a shallower tree and more scanning, smaller means
more pruning overhead. Results are identical either way.
Tradeoffs
- No training, slow prediction. Fit is cheap, but the model is the data: memory grows with n, and each query costs anywhere from O(log n) (a tree in low dimension) to O(n·d) (brute force).
- Flexible boundaries. k-NN can draw any shape of class region and adapts as data is added, but small k memorizes noise (overfitting) and large k washes out minority regions.
- Everything rides on the distance. Unscaled or irrelevant features distort neighborhoods. k-NN has no built-in feature selection, so one noisy column hurts every prediction.
- Exact trees fade with dimension. As distances concentrate, box and ball bounds stop ruling nodes out, which is why
"auto"switches to brute force above 15 features. Embedding search at hundreds of dimensions and millions of vectors uses approximate indexes instead. They trade a little recall for orders of magnitude in speed. - Calibration.
predict_probacan only take values j/k with uniform weights, so with k = 5 every probability is a multiple of 0.2.
Why does k-NN need feature scaling while a decision tree does not?
A tree compares one feature at a time against a threshold, so rescaling a feature just moves the threshold. k-NN adds all features into one distance, so a feature with a larger numeric range dominates that sum.
Why can a KD tree prune when k = 1 but not when k = 5 in the worked example?
A node is trimmed only when its box is farther away than the current k-th best distance. With k = 1 that distance is 1.02, and three leaf boxes lie beyond it. With k = 5 it is 2.33, which reaches into every box, so every leaf must be scanned.
What does weights="distance" predict for a query that exactly matches a training point?
The matching point (or points) gets weight 1 and every other neighbor gets weight 0, so the prediction is that point's label or target, not an average.
Connections
Read first
Read next
- Clustering intro
- Similarity search intro
References
- Cover and Hart. Nearest Neighbor Pattern Classification. IEEE Transactions on Information Theory, 1967.
- Bentley. Multidimensional Binary Search Trees Used for Associative Searching. Communications of the ACM, 1975 (the KD tree).
- Friedman, Bentley and Finkel. An Algorithm for Finding Best Matches in Logarithmic Expected Time. ACM Transactions on Mathematical Software, 1977.
- Omohundro. Five Balltree Construction Algorithms. ICSI Technical Report, 1989.
- Beyer, Goldstein, Ramakrishnan and Shaft. When Is "Nearest Neighbor" Meaningful? ICDT, 1999.
- Hastie, Tibshirani and Friedman. The Elements of Statistical Learning, 2nd ed., Springer, 2009: section 2.5 (local methods in high dimensions) and chapter 13.
- scikit-learn 1.9.1:
KNeighborsClassifier,KNeighborsRegressor,NearestNeighbors,_binary_tree.pxi.tp.