TAI BUI
Back to curriculum
BuildPython

K-Nearest Neighbors and Distances

Store everything. Predict by looking at your neighbors. The simplest algorithm that actually works.

TIME: ~90 minutesPREREQUISITES: Phase 1 (Lesson 14 Norms and Distances)

Learning Objectives

  • Implement KNN classification and regression from scratch with configurable K and distance-weighted voting
  • Compare L1, L2, cosine, and Minkowski distance metrics and select the appropriate one for a given data type
  • Explain the curse of dimensionality and demonstrate why KNN degrades in high-dimensional spaces
  • Build a KD-tree for efficient nearest neighbor search and analyze when it outperforms brute-force

The Problem

K-nearest neighbors (KNN) predicts a new point by finding the K closest points in your training data and letting them vote on the answer. There is no training phase, no parameters to learn, no loss function to minimize: you store the entire training set and compute distances at prediction time, the opposite of what linear regression or an SVM does.

It sounds too simple to work. It isn't. KNN holds up well against far more sophisticated methods, especially on small to medium datasets, and digging into why forces you to confront ideas that matter well beyond this one algorithm: the choice of distance metric (this loops back to Phase 1 Lesson 14), the curse of dimensionality, and the split between lazy and eager learning.

KNN also shows up everywhere in modern AI, just under different names. Vector databases run KNN search over embeddings. Retrieval-augmented generation (RAG) finds the K nearest document chunks. Recommendation systems find similar users or items. Same algorithm every time. Only the scale and the data structures change.

Warm-up0/3
1. What is the straight-line distance between the points (0, 0) and (3, 4)?Warm-up
A grid from 0 to 5 on both axes with a point at (0, 0), a point at (3, 4), a dashed straight line joining them and a question mark for the length.
2. A model scores 100% on its training data but much worse on new data. What is this called?Warm-up
Two bars: the training-data bar reaches 100 percent and the new-data bar is drawn dashed and much shorter.
Only the 100 percent figure comes from the question. The height of the second bar is not a measured value.
3. You search a sorted list of 1,000,000 numbers for one value. Why is binary search much faster than checking items one by one?Warm-up
A row of cells for item 1, item 2, item 3, then ellipsis, then item 1,000,000, with an arrow marking ascending order and a question mark for the target value.
Check: The Problem0/1
1. What is K-nearest neighbors (KNN)?Define
A scatter of six class A circles and six class B squares in a plane of feature 1 against feature 2, plus one new point marked with a question mark, and the number K = 3 written beside it.
Toy points, seeded.

The Concept

How KNN works

Given a dataset of labeled points and a new query point:

  1. Compute the distance from the query to every point in the dataset
  2. Sort by distance
  3. Take the K closest points
  4. For classification: majority vote among the K neighbors
  5. For regression: average (or weighted average) of the K neighbors' values

Mermaid diagram

That is the entire algorithm. No fitting. No gradient descent. No epochs.

Choosing K

K is the single hyperparameter. It controls the bias-variance trade-off:

KBehavior
K = 1Decision boundary follows every point. Zero training error. High variance. Overfits
Small K (3-5)Sensitive to local structure. Can capture complex boundaries
Large KSmoother boundaries. More robust to noise. May underfit
K = NPredicts the majority class for every point. Maximum bias

A common starting point is K = sqrt(N) for a dataset of N points. Use odd K for binary classification to avoid ties.

Mermaid diagram

Distance metrics

The distance function defines what "near" means. Different metrics produce different neighbors, different predictions.

L2 (Euclidean) is the default. Straight-line distance.

d(a, b) = sqrt(sum((a_i - b_i)^2))

Sensitive to feature scale. Always standardize features before using L2 with KNN.

L1 (Manhattan) sums absolute differences. More robust to outliers than L2 because it does not square the differences.

d(a, b) = sum(|a_i - b_i|)

Cosine distance measures the angle between vectors, ignoring magnitude. Essential for text and embedding data.

d(a, b) = 1 - (a . b) / (||a|| * ||b||)

Minkowski generalizes L1 and L2 with parameter p.

d(a, b) = (sum(|a_i - b_i|^p))^(1/p)

p=1: Manhattan
p=2: Euclidean
p->inf: Chebyshev (max absolute difference)

Which metric to use depends on the data:

Data typeBest metricWhy
Numeric features, similar scaleL2 (Euclidean)Default, works for spatial data
Numeric features, outliersL1 (Manhattan)Robust, does not amplify large differences
Text embeddingsCosineMagnitude is noise, direction is meaning
High-dimensional sparseCosine or L1L2 suffers from curse of dimensionality
Mixed typesCustom distanceCombine metrics per feature type

Weighted KNN

Standard KNN gives equal weight to all K neighbors. But a neighbor at distance 0.1 should matter more than one at distance 5.0.

Distance-weighted KNN weights each neighbor inversely by distance:

weight_i = 1 / (distance_i + epsilon)

For classification: weighted vote
For regression:     weighted average = sum(w_i * y_i) / sum(w_i)

The epsilon prevents division by zero when a query point exactly matches a training point.

Weighted KNN is less sensitive to the choice of K because distant neighbors barely register regardless.

The curse of dimensionality

KNN performance degrades in high dimensions. This is not a vague concern. It is a mathematical fact.

Problem 1: distances converge. As dimensionality increases, the ratio of the maximum distance to the minimum distance approaches 1. All points become equally "far" from the query.

In d dimensions, for random uniform points:

d=2:    max_dist / min_dist = varies widely
d=100:  max_dist / min_dist ~ 1.01
d=1000: max_dist / min_dist ~ 1.001

When all distances are nearly equal, "nearest" is meaningless.

Problem 2: volume explodes. To capture K neighbors within a fixed fraction of the data, you need to extend your search radius to cover a much larger fraction of the feature space. The "neighborhood" in high dimensions encompasses most of the space.

Problem 3: corners dominate. In a unit hypercube in d dimensions, most of the volume is concentrated near the corners, not the center. A sphere inscribed in the cube contains a vanishing fraction of the volume as d grows.

Practical consequence: KNN works well up to about 20-50 features. Beyond that, you need dimensionality reduction (PCA, UMAP, t-SNE) before applying KNN, or you need to use tree-based search structures that exploit the data's intrinsic lower dimensionality.

KD-trees: fast nearest neighbor search

Brute-force KNN computes the distance from the query to every training point. That is O(n * d) per query. For large datasets, this is too slow.

A KD-tree recursively partitions the space along feature axes. At each level, it splits along one dimension at the median value.

Mermaid diagram

To find the nearest neighbor, traverse the tree to the leaf containing the query, then backtrack and check neighboring partitions only if they could contain closer points.

Average query time: O(log n) for low dimensions. But KD-trees degrade to O(n) in high dimensions (d > 20) because the backtracking eliminates fewer and fewer branches.

Ball trees: better for moderate dimensions

Ball trees partition data into nested hyperspheres instead of axis-aligned boxes. Each node defines a ball (center + radius) that contains all points in that subtree.

Advantages over KD-trees:

  • Work better in moderate dimensions (up to ~50)
  • Handle non-axis-aligned structure
  • Tighter bounding volumes mean more branches are pruned during search

Both KD-trees and ball trees are exact algorithms. For truly large-scale search (millions of points, hundreds of dimensions), approximate nearest neighbor methods (HNSW, IVF, product quantization) are used instead. These are covered in Phase 1 Lesson 14.

Lazy learning vs eager learning

KNN is a lazy learner: it does no work at training time and all work at prediction time. Most other algorithms (linear regression, SVMs, neural networks) are eager learners: they do heavy computation at training time to build a compact model, then predictions are fast.

AspectLazy (KNN)Eager (SVM, neural net)
Training timeO(1) just store dataO(n * epochs)
Prediction timeO(n * d) per queryO(d) or O(parameters)
Memory at predictionStore entire training setStore model parameters only
Adapts to new dataAdd points instantlyRetrain the model
Decision boundaryImplicit, computed on the flyExplicit, fixed after training

Lazy learning is ideal when:

  • The dataset changes frequently (add/remove points without retraining)
  • You need predictions for very few queries
  • You want zero training time
  • The dataset is small enough that brute-force search is fast

KNN for regression

Instead of majority voting, KNN for regression averages the target values of the K neighbors.

prediction = (1/K) * sum(y_i for i in K nearest neighbors)

Or with distance weighting:
prediction = sum(w_i * y_i) / sum(w_i)
where w_i = 1 / distance_i

KNN regression produces piecewise-constant (or piecewise-smooth with weighting) predictions. It cannot extrapolate beyond the range of the training data. If the training targets are all between 0 and 100, KNN will never predict 200.

Check: The Concept0/34
1. KNN is called a 'lazy learner.' What does that mean?Define
Two boxes joined by an arrow: fit(X, y) when training data is given, then predict(x) when a new point arrives, each with the text work done here and a question mark.
2. What is eager learning?Define
A table with rows SVM and neural network and columns for work during fit and work during predict, all four cells containing a question mark.
3. What is the curse of dimensionality for KNN?Define
Three rows of small cells representing tables with 2, 100 and 1,000 features, growing in width, and a line asking about the nearest neighbors of a query in each case.
Cell counts are schematic.
4. The Minkowski distance (sum(|a_i - b_i|^p))^(1/p) generalizes which two named metrics as special cases of p?Define
The Minkowski formula in a box above a two-row table with p equal to 1 and 2, each with a question mark for the named metric.
5. What is feature scaling?Define
A three-row table of feature A on a 0 to 1000 range and feature B on a 0 to 1 range, with a line asking for the step applied before KNN.
Row values typed for illustration.
6. In KNN classification, what is a majority vote?Define
A table of the five nearest neighbors of a query, giving rank, class and distance, next to a question mark for the majority-vote prediction.
Distances typed for illustration.
7. How does a KD-tree partition the data at each level?Define
Eight points plotted on feature 1 against feature 2 axes from 0 to 10 with the prompt that a tree is built over them and a question mark for how each level splits.
8. How do ball trees differ from KD-trees, and when are they preferred?Define
Two identical plots of eight points side by side, captioned KD-tree regions with a question mark and ball tree regions with a question mark.
9. In distance-weighted KNN regression, how is the prediction computed?Define
A line with the query as a question mark at the left and three neighbors at distances 0.5, 1 and 2 carrying target values y = 12, 20 and 30, followed by a prompt for the prediction.
Distances and targets typed for illustration.
10. What is brute-force nearest neighbor search?Define
A cloud of 30 stored points with one query marked by a question mark, and a prompt asking for the search method for the K nearest neighbors.
Seeded random points.
11. What is approximate nearest neighbor search?Define
A dense cloud of small grey dots standing for a very large collection of stored vectors, with one query marked by a question mark and a prompt to find its K nearest neighbors.
Dot positions are random and schematic.
12. What is a Voronoi diagram, and how does it relate to K=1 KNN?Define
Six points on a 0 to 10 plane, three class A circles and three class B squares, with a prompt asking about the Voronoi diagram of these points and a question mark.
Point positions typed for illustration.
13. Why is feature scaling critical for KNN?Explain
A wide plot with feature 1 from 0 to 1000 and feature 2 from 0 to 1 holding a query Q and two stored points P1 and P2, and the Euclidean distance formula beneath.
Point values typed for illustration.
14. Why is L1 (Manhattan) distance more robust to outliers than L2?Explain
A bar chart of five per-feature gaps between two vectors, 1, 1, 1, 1 and 20, with the L1 distance and L2 distance each shown as a question mark.
Gaps typed for illustration.
15. Why is cosine distance a good fit for comparing text documents represented as TF-IDF vectors?Explain
A two-row table of TF-IDF weights for doc 1 and doc 2 over eight terms with mostly zero entries, and a prompt for which distance to use.
Weights typed for illustration.
16. Why does weighted KNN use weight = 1 / (distance + epsilon) instead of 1 / distance?Explain
Two code lines: version A weight equals 1 over distance and version B weight equals 1 over distance plus epsilon, with a note that epsilon is a small constant.
17. What happens to the KNN decision boundary as K increases from 1 to N (the full dataset size)?Explain
A scatter of 22 class A circles and 14 class B squares that overlap, with a note that K runs from 1 to N = 36 and a question mark for the boundary.
Toy points, seeded.
18. What is a common starting heuristic for choosing K in KNN, and why use an odd K for binary classification?Explain
Two chips reading N = 100 training points and 2 classes next to prompts for a starting value of K and for the parity of K.
N = 100 is an illustrative size.
19. All training targets for a KNN regressor lie between 0 and 100. Why can it never predict 200?Explain
A scatter of 24 training points with targets between 0 and 100 on a target axis that runs up to 220, a dashed vertical line at feature 5 for the query, and a question mark for its prediction.
Seeded toy data.
20. In 100 dimensions with uniform random points, what happens to the ratio of max distance to min distance?Explain
A table of dimensions 2 and 100 with the ratio of max distance to min distance as a question mark in each row.
21. In high dimensions, what happens to the neighborhood needed to capture K neighbors?Explain
A unit square with 60 random points, a query at the center marked by a question mark and a small circle around its 5 nearest points, next to a prompt about the neighborhood at 100 features.
Seeded random points. The K = 5 figure is illustrative.
22. Why does a KD-tree degrade toward O(n) queries when d is above about 20?Explain
A small KD-tree with a root splitting on f1, two nodes splitting on f2 and four leaves. An orange path leads to the leftmost leaf where the query lands, and the other branches are dashed.
Schematic tree, not drawn from data.
23. When should you reach for approximate nearest neighbor methods like HNSW or IVF instead of KD-trees or ball trees?Explain
Two boxes, KD-tree or ball tree labelled exact and HNSW or IVF labelled approximate, each with a use when prompt and a question mark.
24. A weighted KNN regressor (w = 1/distance) has two neighbors: y = 10 at distance 1, and y = 40 at distance 2. What is the prediction?Apply
A line with the query as a question mark and two neighbors, one at distance 1 with target 10 and one at distance 2 with target 40, with the rule weight equals 1 over distance and a prompt for the prediction.
25. K = 3 neighbors: class A at distance 0.1, class B at distance 2, class B at distance 2. Which class wins by plain majority vote, and which with 1/distance weights?Apply
A table of three neighbors: class A at distance 0.1 and two class B at distance 2, with prompts for the plain vote winner and the weighted vote winner.
26. Your dataset has 50,000 points in 5 dimensions, and you switch from brute-force KNN to a KD-tree. What should you expect?Apply
A brute-force box and a KD-tree box, both with n equal to 50,000 points and d equal to 5, joined by an arrow, with a prompt for query time after the switch.
27. After swapping an embedding model from 384 to 1536 dimensions, retrieval quality drops with nothing else changed. What should you check first?Analyze
Two boxes for an embedding model, 384 dimensions with good retrieval quality and 1536 dimensions with dropped quality, with a note that nothing else changed and a prompt for what to check first.
28. A Euclidean KNN on age (20 to 60) and income (20,000 to 200,000) seems to ignore age. What is the likely cause and fix?Analyze
A table of a query Q and two stored people P1 and P2 with age on a 20 to 60 range and income on a 20,000 to 200,000 range, and a note that the model seems to ignore age.
Rows typed for illustration.
29. Team A runs a KD-tree on 100-dimensional embeddings. Team B first reduces them to 10 dimensions with PCA, then runs a KD-tree. Which is more likely to get fast exact queries?Analyze
Two boxes: Team A runs a KD-tree on 100-dimensional embeddings and Team B reduces them to 10 dimensions with PCA before a KD-tree, with a prompt about which keeps exact queries fast.
30. A KNN classifier with K = 1 scores 100% on training data but poorly on validation data. Which change addresses this?Analyze
A two-class scatter with the jagged K = 1 decision regions shaded behind the training points, and notes that training accuracy is 100 percent, validation is poor, and a question mark for the change to try.
Toy points, seeded; regions computed with numpy.
31. Embeddings of two documents on the same topic have very different magnitudes, and L2 KNN does not treat them as neighbors. What fits best?Analyze
Three arrows from the origin in a plane: doc 1 short and doc 2 long, both labelled same topic, and doc 3 a different short arrow labelled another topic, with a note that L2 KNN does not pair doc 1 and doc 2.
Vectors typed for illustration. Doc 3 is added to show a different topic.
32. A recommender with 20,000 items in 10 features adds and removes items hundreds of times a day, and serves a few queries an hour. Which design fits?Design
Four cards: 20,000 items, 10 features per item, hundreds of items added or removed a day, and a few queries an hour, followed by a prompt for the design that fits.
33. You must search 50 million 768-dimensional embeddings with low latency. Which approach fits?Design
Two cards reading 50,000,000 stored vectors and 768 dimensions each, with a low latency requirement and a prompt for the search approach.
34. Your tabular data has 5,000 rows and 300 numeric features on very different ranges. You want KNN. Which pipeline fits?Design
A grid block standing for a table of 5,000 rows by 300 numeric features with very different ranges, and a prompt for the pipeline to run before KNN.
Schematic block, not data.

Build It

Step 1: Distance functions

Implement L1, L2, cosine, and Minkowski distances. These connect directly to Phase 1 Lesson 14.

python
import math
 
def l2_distance(a, b):
    return math.sqrt(sum((ai - bi) ** 2 for ai, bi in zip(a, b)))
 
def l1_distance(a, b):
    return sum(abs(ai - bi) for ai, bi in zip(a, b))
 
def cosine_distance(a, b):
    dot_val = sum(ai * bi for ai, bi in zip(a, b))
    norm_a = math.sqrt(sum(ai ** 2 for ai in a))
    norm_b = math.sqrt(sum(bi ** 2 for bi in b))
    if norm_a == 0 or norm_b == 0:
        return 1.0
    return 1.0 - dot_val / (norm_a * norm_b)
 
def minkowski_distance(a, b, p=2):
    if p == float('inf'):
        return max(abs(ai - bi) for ai, bi in zip(a, b))
    return sum(abs(ai - bi) ** p for ai, bi in zip(a, b)) ** (1 / p)

Step 2: KNN classifier and regressor

Build the full KNN with configurable K, distance metric, and optional distance weighting.

python
class KNN:
    def __init__(self, k=5, distance_fn=l2_distance, weighted=False,
                 task="classification"):
        self.k = k
        self.distance_fn = distance_fn
        self.weighted = weighted
        self.task = task
        self.X_train = None
        self.y_train = None
 
    def fit(self, X, y):
        self.X_train = X
        self.y_train = y
 
    def predict(self, X):
        return [self._predict_one(x) for x in X]

Step 3: KD-tree for efficient search

Build a KD-tree from scratch that recursively splits on the median of each dimension.

python
class KDTree:
    def __init__(self, X, indices=None, depth=0):
        # Recursively partition the data
        self.axis = depth % len(X[0])
        # Split on median of the current axis
        ...
 
    def query(self, point, k=1):
        # Traverse to leaf, then backtrack
        ...

See code/knn.py for the complete implementation with all helper methods and demos.

Step 4: Feature scaling

KNN requires feature scaling because distances are sensitive to feature magnitudes. A feature ranging from 0 to 1000 will dominate a feature ranging from 0 to 1.

python
def standardize(X):
    n = len(X)
    d = len(X[0])
    means = [sum(X[i][j] for i in range(n)) / n for j in range(d)]
    stds = [
        max(1e-10, (sum((X[i][j] - means[j]) ** 2 for i in range(n)) / n) ** 0.5)
        for j in range(d)
    ]
    return [[((X[i][j] - means[j]) / stds[j]) for j in range(d)] for i in range(n)], means, stds
Check: Build It0/5
1. What does cosine_distance([1, 0], [0, 1]) return?Apply
The lesson's cosine_distance function in a code block, and below it the call cosine_distance([1, 0], [0, 1]) with a question mark for the return value.
Code as given in the lesson.
2. What does cosine_distance([1, 2], [0, 0]) return?Apply
The first four lines of the lesson's cosine_distance function followed by an ellipsis, and below it the call cosine_distance([1, 2], [0, 0]) with a question mark for the return value.
Code as given in the lesson; the lines after norm_b are not shown here.
3. What does minkowski_distance([0, 0], [3, 4], p=float('inf')) return?Apply
A grid with points (0, 0) and (3, 4) next to a code box showing the call minkowski_distance([0, 0], [3, 4], p=float('inf')) and returns with a question mark.
4. What does l1_distance([1, 2, 3], [4, 0, 3]) return?Apply
The lesson's l1_distance code, a table lining up the vectors a = 1, 2, 3 and b = 4, 0, 3 by feature, and the call with a question mark for the return value.
Code as given in the lesson.
5. What does standardize([[0], [10]]) return for the data and for the means and stds?Apply
A number line with two points at 0 and 10, the call standardize([[0], [10]]), and prompts with question marks for the returned data, means and stds.

Use It

You just built KNN and a KD-tree by hand. In production you'll almost never repeat that work: scikit-learn already ships tuned, tested versions of both, so reach for those unless you have a specific reason not to.

python
from sklearn.neighbors import KNeighborsClassifier
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
 
clf = Pipeline([
    ("scaler", StandardScaler()),
    ("knn", KNeighborsClassifier(n_neighbors=5, metric="euclidean")),
])
clf.fit(X_train, y_train)
print(f"Accuracy: {clf.score(X_test, y_test):.4f}")

Scikit-learn automatically uses KD-trees or ball trees when the dataset is large enough and the dimensionality is low enough. For high-dimensional data, it falls back to brute force. You can control this with the algorithm parameter.

For large-scale nearest neighbor search (millions of vectors), use FAISS, Annoy, or a vector database:

python
import faiss
 
index = faiss.IndexFlatL2(dimension)
index.add(embeddings)
distances, indices = index.search(query_vectors, k=5)
Check: Use It0/1
1. What does scikit-learn's KNeighborsClassifier do automatically when choosing a search algorithm?Explain
The lesson's scikit-learn pipeline code with StandardScaler and KNeighborsClassifier using 5 neighbors and Euclidean metric, followed by a prompt about which search algorithm it picks.
Code as given in the lesson, trimmed.
Author's Note

The gap between this lesson and a production vector database is smaller than it looks. Strip away the marketing and pgvector, Pinecone, and FAISS are all running some version of what you just built: a distance function, a way to prune the search space, and a K to return.

The curse of dimensionality math above is not academic. I've seen a "why did retrieval quality suddenly drop" bug turn out to be someone swapping an embedding model from 384 dimensions to 1536 without re-checking whether cosine distance still separated anything meaningfully at that size. It did, barely, and only because the new embeddings were dense and well-trained, not because the math got any easier. If your retrieval numbers get worse right after a model upgrade, check dimensionality and distance metric before you touch anything else in the pipeline.

Exercises

  1. Implement KNN classification on a 2D dataset with 3 classes. Plot the decision boundary for K=1, K=5, K=15, and K=N. Observe the transition from overfitting to underfitting.

  2. Generate 1000 random points in 2, 5, 10, 50, 100, and 500 dimensions. For each dimensionality, compute the ratio of the maximum pairwise distance to the minimum pairwise distance. Plot the ratio vs dimensionality to visualize the curse of dimensionality.

  3. Compare L1, L2, and cosine distance for KNN on a text classification problem (use TF-IDF vectors). Which metric gives the best accuracy? Why does cosine tend to win for text?

  4. Implement a KD-tree and measure query time vs brute force for datasets of 1k, 10k, and 100k points in 2D, 10D, and 50D. At what dimensionality does the KD-tree stop being faster than brute force?

  5. Build a weighted KNN regressor for y = sin(x) + noise. Compare it with unweighted KNN for K=3, 10, 30. Show that weighting produces smoother predictions, especially for large K.

Key Terms

TermWhat it actually means
K-nearest neighborsNon-parametric algorithm that predicts by finding the K closest training points to a query
Lazy learningNo computation at training time. All work happens at prediction time. KNN is the canonical example
Eager learningHeavy computation at training time to build a compact model. Most ML algorithms are eager
Curse of dimensionalityIn high dimensions, distances converge and neighborhoods expand to cover most of the space, making KNN ineffective
KD-treeBinary tree that recursively partitions space along feature axes. O(log n) queries in low dimensions
Ball treeTree of nested hyperspheres. Works better than KD-trees in moderate dimensions (up to ~50)
Weighted KNNNeighbors weighted inversely by distance. Closer neighbors have more influence on the prediction
Feature scalingNormalizing features to comparable ranges. Required for distance-based methods like KNN
Majority voteClassification by counting which class is most common among K neighbors
Brute force searchComputing distance to every training point. O(n*d) per query. Exact but slow for large n
Approximate nearest neighborAlgorithms (HNSW, LSH, IVF) that find approximately nearest points much faster than exact search
Voronoi diagramThe partition of space where each region contains all points closer to one training point than any other. K=1 KNN produces Voronoi boundaries

Further Reading

Code

Ships With