Index Ai Academy
Phase 3 · Machine learning · Models

Nearest neighbors

Predict from the most similar examples.

About 25 minutes · A free account opens the whole lesson

Create a free account Try part of it here

Already have an account? Sign in to open this lesson

Why it matters

"Customers like this one usually..." is one of the oldest ways to predict anything. k-nearest neighbors turns it into a precise recipe. To predict for a new customer, it finds the most similar customers whose outcomes you already know. Then it goes with what most of them did. It's the logic behind lookalike audiences and "people who bought this also bought". It's also what retrieval does in RAG. There, the customers are chunks of text turned into embeddings, and the customer table is a vector database.

What you'll be able to do

  • Predict by finding the most similar past examples and letting them vote
  • Explain why scaling is essential for distance-based models
  • Choose k, and see how it trades overfitting against underfitting
  • Connect nearest neighbors to embedding search and lookalike audiences

What it covers

  1. Ask the neighbors
  2. Measuring distance
  3. Let the three nearest customers vote
  4. Scaling isn't optional here
  5. Choosing k
  6. The same idea at scale: vector search

Ask the neighbors

Say you're picking a restaurant in a new town. One way: find the five people whose taste is most like yours, and go where most of them go. You don't need a theory of good food. You need people like you, and what they chose.

k-nearest neighbors, or k-NN, predicts the same way. You give it past examples with known answers. To predict for a new example, it:

  1. measures the distance from the new example to every stored example,
  2. picks the kk closest ones, where kk is a number you choose, like 5 or 15,
  3. lets them decide.

How they decide depends on the task:

  • Classification (churn or stay): it predicts the answer most of the neighbors had. That's a majority vote.
  • Regression (a number, like spend): it predicts the average of their answers. If the 3 nearest customers spent $40, $50 and $90, it predicts $60 for the new one.

k-NN doesn't learn any weights. "Training" means storing the examples, nothing more. All the work happens when you ask for a prediction. That's why it's called a lazy learner.

Measuring distance

Each customer is a vector: a list of numbers, one per feature. To compare two customers, k-NN uses the Euclidean distance from the Vector arithmetic lesson. Subtract, square, add up, then take the square root.

For two features, it looks like this:

d=(a1−b1)2+(a2−b2)2d = \sqrt{(a_1 - b_1)^2 + (a_2 - b_2)^2}

One symbol at a time:

  • dd is the distance between the two customers.
  • a1a_1 is the new customer's first feature, such as orders last year.
  • b1b_1 is the stored customer's value for that same feature.
  • The small 2 means the second feature, such as support tickets. So a2a_2 is the new customer's ticket count.
  • b2b_2 is the stored customer's ticket count.

With more features, you add one more squared gap inside the square root for each feature.

In plain words: for each feature, find the gap between the two customers and square it. Add the squares, then take the square root. A small result means the customers are alike.

Quick check
What does k-NN do during training?
  1. It stores the examples, and that's all
  2. It runs gradient descent
  3. It learns a weight for each feature
  4. It builds a decision tree
Show the answer

It stores the examples, and that's all

All the work happens at prediction time, when it finds the nearest stored examples.

Worked example

A new customer placed 4 orders last year and opened 1 support ticket. Stored customer C placed 4 orders and opened 3 tickets.

  1. Gaps: 4 − 4 = 0 orders, and 1 − 3 = −2 tickets.
  2. Squares: 0 × 0 = 0, and −2 × −2 = 4.
  3. Add them: 0 + 4 = 4.
  4. Square root: √4 = 2.

Stored customer E placed 0 orders and opened 4 tickets. The gaps are 4 and −3. The squares are 16 and 9, which add up to 25. The square root is 5. So C is much more like the new customer than E is.

Go deeper The same formula for any number of features

With many features, the formula is written with a sum:

d=∑i(ai−bi)2d = \sqrt{\sum_i (a_i - b_i)^2}

∑i\sum_i is the Greek letter sigma. It means "add this up over every feature ii", like a for loop. In Python it's math.sqrt(sum((x - y) ** 2 for x, y in zip(a, b))), which is the distance function in the example below.

Quick check
What does this print?
import math
a, b = (1, 1), (4, 5)
print(math.dist(a, b))
  1. 5.0
  2. 7.0
  3. 3.0
  4. 25.0
Show the answer

5.0

Across 3 and up 4 gives a straight-line distance of 5, and math.dist returns it as 5.0.

Worked example

Let the three nearest customers vote

Five stored customers, each with two features and a known outcome:

customerorders last yearsupport ticketsoutcome
A51stay
B13churn
C43churn
D30stay
E04churn

A new customer has 4 orders and 1 ticket. The code measures the distance to each stored customer, sorts them nearest first, and lets the 3 nearest vote. It uses plain Python, with no NumPy.

import math
from collections import Counter

new = (4, 1)  # orders last year, support tickets
stored = {
    "A": ((5, 1), "stay"),
    "B": ((1, 3), "churn"),
    "C": ((4, 3), "churn"),
    "D": ((3, 0), "stay"),
    "E": ((0, 4), "churn"),
}

def distance(a, b):
    return math.sqrt(sum((x - y) ** 2 for x, y in zip(a, b)))

ranked = sorted(stored, key=lambda name: distance(new, stored[name][0]))
for name in ranked:
    point, label = stored[name]
    print(name, round(distance(new, point), 2), label)

k = 3
votes = [stored[name][1] for name in ranked[:k]]
print("votes:", votes)
print("prediction:", Counter(votes).most_common(1)[0][0])
It runs right here in your browser, with nothing to install.

It lists the customers nearest first: A at 1.0, D at 1.41, C at 2.0, B at 3.61 and E at 5.0. The three nearest are A, D and C. That's two votes for stay and one for churn, so the prediction is stay. sorted with key= orders the names by their distance. Counter counts the votes, and most_common(1) returns the winner.

Quick check
Same five customers and the same new customer, but now k = 5, so every stored customer votes. What does k-NN predict?
  1. churn
  2. It's a tie
  3. stay
  4. It can't predict with k = 5
Show the answer

churn

All five vote: A and D say stay, while B, C and E say churn. Churn wins 3 to 2. When k equals the number of stored customers, k-NN always predicts the most common outcome overall, whoever the new customer is.

Scaling isn't optional here

Imagine comparing houses by price and number of bedrooms. A $1,000 price gap is tiny in real life. But as a number, 1,000 dwarfs a gap of 3 bedrooms. A distance that adds up raw gaps would treat the bedrooms as if they barely mattered.

The same thing happens with customers. In this shop's data, days since last order runs from 6 to

  1. Membership in the Trail Club loyalty program (trail_club) is 0 or 1. Say two customers

are 100 days apart, and only one is a member. The days add 100 × 100 = 10,000 to the squared distance. Membership adds 1 × 1 = 1. So the neighbors are chosen almost entirely by recency, and membership is ignored.

The fix is to standardize each feature first. That rescales a column so its average is 0 and its typical spread (its standard deviation) is 1. After that, a typical gap in days counts about as much as a typical gap in membership. Every feature gets a fair say.

The code below loads the shop's 600 customers and holds back 30% of them for testing (test_size=0.3). Then it trains two k-NN models with k=15k = 15. One uses the raw numbers. The other puts StandardScaler in front. make_pipeline chains the two steps. Because the pipeline is fit on the training rows, the scaler learns its averages from the training data only.

import pandas as pd
import requests
from sklearn.metrics import roc_auc_score
from sklearn.model_selection import train_test_split
from sklearn.neighbors import KNeighborsClassifier
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler

df = pd.DataFrame(requests.get("https://api.gradient.test/v1/customers").json()["data"])
features = ["days_since_last_order", "orders_last_year", "support_tickets", "trail_club", "tenure_months"]
X_tr, X_te, y_tr, y_te = train_test_split(df[features], df["churned"], test_size=0.3, random_state=0, stratify=df["churned"])

raw = KNeighborsClassifier(n_neighbors=15).fit(X_tr, y_tr)
scaled = make_pipeline(StandardScaler(), KNeighborsClassifier(n_neighbors=15)).fit(X_tr, y_tr)
for name, model in [("unscaled", raw), ("scaled", scaled)]:
    auc = roc_auc_score(y_te, model.predict_proba(X_te)[:, 1])
    print(f"{name:<9} accuracy {model.score(X_te, y_te):.3f}   AUC {auc:.3f}")

The unscaled model scores accuracy 0.817 and AUC 0.756. The scaled one scores 0.822 and 0.813.

Accuracy barely moves. About 81% of the customers stay, so a model that says "stays" for everyone already scores about 0.81. So accuracy hardly tells these two models apart.

AUC looks at ranking instead. Each model gives every customer a churn score: here, the share of their 15 neighbors who churned. AUC is the chance that a random churner gets a higher score than a random customer who stayed. 0.5 is guessing and 1 is perfect. It gets its own lesson later. Scaling lifts AUC from 0.756 to 0.813. The scaled model is clearly better at putting likely churners at the top of the list.

Quick check
Two customers are 30 days apart, and only one of them is a Trail Club member. With no scaling, how much does each feature add to the squared distance?
  1. Membership adds more, because it's a yes/no feature
  2. Days adds 900, membership adds 1
  3. Days adds 30, membership adds 1
  4. Both add 1
Show the answer

Days adds 900, membership adds 1

Gaps are squared: 30 × 30 = 900 and 1 × 1 = 1. Membership barely affects which neighbors are chosen until you standardize.

Choosing k

Back to the restaurant. Ask one friend, and their personal quirks become your answer. Poll the whole town, and you get the most popular place, whatever your own taste.

  • Small k (like 1): the prediction copies the single nearest customer, noise and all. It's very flexible, and it tends to overfit. That means it memorizes quirks of the training data and does worse on new customers.
  • Large k: the vote averages over many customers, so it drifts toward the overall majority. It's stable, but it can underfit, which means it's too blunt to see the real pattern. Say nn is the number of stored examples. With k=nk = n, it always predicts the majority class.

With two classes, an odd k like 3 or 15 means a vote can never end in a tie.

The code below trains scaled k-NN models for six values of k. For each one, it prints the accuracy (the share of customers predicted correctly) on the training data and on the test data.

import pandas as pd
import requests
from sklearn.model_selection import train_test_split
from sklearn.neighbors import KNeighborsClassifier
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler

df = pd.DataFrame(requests.get("https://api.gradient.test/v1/customers").json()["data"])
features = ["days_since_last_order", "orders_last_year", "support_tickets", "trail_club", "tenure_months"]
X_tr, X_te, y_tr, y_te = train_test_split(df[features], df["churned"], test_size=0.3, random_state=0, stratify=df["churned"])
for k in [1, 3, 9, 25, 75, 200]:
    model = make_pipeline(StandardScaler(), KNeighborsClassifier(n_neighbors=k)).fit(X_tr, y_tr)
    print(f"k={k:<4} train {model.score(X_tr, y_tr):.3f}  test {model.score(X_te, y_te):.3f}")

With k=1k = 1, training accuracy is a perfect 1.000, but test accuracy is only 0.717. Every training customer is its own nearest neighbor, at distance 0. So the model looks up each one's stored answer, and the perfect score means nothing.

Test accuracy climbs as k grows and peaks at 0.833 with k=25k = 25. At k=75k = 75 and k=200k = 200, the scores fall to 0.807 on training and 0.806 on test. That's the same score you'd get by predicting "stays" for everyone.

Pick k by how well it does on validation data (data held back from training), the same way you'd pick a tree's depth.

Try it now

Every spot on the plane takes the majority vote of its k nearest points. Slide k from 1 up to 51.

Quick check
Why does k-NN with k = 1 always score 100% on its own training data?
  1. Each training example's nearest neighbor is itself
  2. k = 1 is always the best choice
  3. Distances are zero for all examples
  4. Training data has no noise
Show the answer

Each training example's nearest neighbor is itself

Each training customer finds itself at distance 0 and copies its own stored answer. That says nothing about how it will do on new data.

k-NN's weakness is speed. Every prediction compares the new example with every stored one. With a million stored customers, that's a million distances for each prediction. That's too slow for a live app.

Vector databases, from Phase 1, solve this problem. They use approximate nearest-neighbor indexes. These find neighbors that are usually the true nearest ones, or very close, far faster than checking everything.

Retrieval in RAG is k-NN. You embed the question, find the k nearest document chunks, and hand them to the model.

Other places it appears:

  • Lookalike audiences: find the people most similar to your best customers.
  • Recommendations: suggest items whose embedding vectors are nearest to what someone liked.
  • Anomaly detection: an example that's far from all its neighbors is unusual.

For marketers: a k-NN prediction comes with a ready explanation: "these 15 similar customers, 11 of whom churned". That transparency is often worth more than a point of accuracy.

Practice what you just read

  • 3 code examples you can edit and run
  • a practice exercise that checks your code
  • a 5-question quiz
  • 4 review cards for spaced repetition

Create a free account

Try a quiz question

What does k-NN do during training?

  1. Stores the training examples
  2. Learns a weight per feature
  3. Builds a tree of questions
  4. Fits a line

Answer it at the end of the lesson to see if you're right, and why.

Before this lesson