Lab 1 — Build a k-nearest-neighbor classifier
Objective
By the end of this lab you will have a runnable Python program, knn.py, that
loads a small labeled flower dataset from CSV, splits it into train and test
sets, classifies each test flower by a majority vote of its k nearest
neighbors, and reports the classifier's accuracy on the held-out set. It is a
complete supervised-learning loop built entirely by hand — no NumPy, no pandas,
no scikit-learn — using only the Python standard library (csv, math,
collections, pathlib), so it costs nothing and runs anywhere.
Skills exercised:
- Loading a labeled dataset from CSV into feature vectors and labels
- Splitting data into disjoint train and test sets deterministically
- Implementing Euclidean distance and k-nearest-neighbor voting from scratch
- Measuring a classifier with held-out accuracy
Prerequisites and setup
Prerequisites: the three lessons in this module — Python foundations for AI, Working with datasets, and From data to a first model.
Setup: you need Python 3.10 or newer. There are no packages to install; everything uses the standard library. Verify your version and create a working folder.
- macOS / Linux
- Windows (PowerShell)
python3 --version
mkdir ai101-knn-lab && cd ai101-knn-lab
python --version
mkdir ai101-knn-lab; cd ai101-knn-lab
Expected output from the version command is a line like Python 3.11.6. If the
command is not found, install Python from python.org before continuing. In the
steps below, use python3 on macOS/Linux and python on Windows.
Every step writes or edits files in this one folder. If you close your terminal
and come back, re-run the cd ai101-knn-lab command to return to it; all your
files are still there.
Step 1 — Create the dataset
Create the labeled dataset your classifier will learn from: two flower species described by petal length and petal width, twelve examples each.
Save this exactly as flowers.csv:
petal_length,petal_width,species
1.4,0.2,alpina
1.3,0.2,alpina
1.5,0.2,alpina
1.4,0.3,alpina
1.7,0.4,alpina
1.5,0.1,alpina
1.6,0.2,alpina
1.4,0.1,alpina
1.2,0.2,alpina
1.5,0.4,alpina
1.3,0.3,alpina
1.6,0.2,alpina
4.7,1.4,montana
4.5,1.5,montana
4.9,1.5,montana
4.0,1.3,montana
4.6,1.5,montana
4.5,1.3,montana
4.7,1.6,montana
5.0,1.7,montana
4.8,1.4,montana
4.3,1.3,montana
5.1,1.6,montana
4.4,1.4,montana
The two species separate cleanly in these two features — alpina has small
petals, montana has large ones — which is what will let a simple classifier
score well.
Checkpoint: confirm the file has all 24 data rows.
python3 -c "import csv; print(sum(1 for _ in csv.DictReader(open('flowers.csv'))))"
You should see 24. If you see a smaller number, a row is missing — re-save the
file with all 24 lines below the header. If you see a FileNotFoundError, check
that the file is saved in the current folder as exactly flowers.csv.
Step 2 — Load the dataset into features and labels
Goal: create knn.py with a loader that reads each CSV row into a
((petal_length, petal_width), species) pair — the (X, y) shape from the
lessons, one sample per row.
Create knn.py with this content:
import csv
import math
from collections import Counter
from pathlib import Path
K = 3
def load_dataset(path):
"""Load rows as ((petal_length, petal_width), species) tuples."""
samples = []
with open(path, newline="", encoding="utf-8") as f:
for row in csv.DictReader(f):
features = (float(row["petal_length"]), float(row["petal_width"]))
samples.append((features, row["species"]))
return samples
Checkpoint: load the dataset and count the samples.
python3 -c "import knn; from pathlib import Path; print(len(knn.load_dataset(Path('flowers.csv'))))"
You should see 24. If you get ModuleNotFoundError: No module named 'knn', you
are running from the wrong folder — cd into ai101-knn-lab where knn.py
lives.
Step 3 — Split into train and test
Goal: add a deterministic split so evaluation stays honest. Every third sample (indices 0, 3, 6, ...) becomes a test example; the rest are training data.
Add this function to knn.py:
def train_test_split(samples):
"""Split deterministically: every 3rd row (index 0, 3, 6, ...) is test."""
train, test = [], []
for index, sample in enumerate(samples):
if index % 3 == 0:
test.append(sample)
else:
train.append(sample)
return train, test
Checkpoint: split the dataset and print the two sizes.
python3 -c "import knn; from pathlib import Path; tr, te = knn.train_test_split(knn.load_dataset(Path('flowers.csv'))); print(len(tr), len(te))"
You should see 16 8 — sixteen training samples and eight test samples. If you
see 24 0, the split condition is wrong; confirm the test branch uses
index % 3 == 0.
Step 4 — Implement distance and k-NN prediction
Goal: add the classifier core — Euclidean distance between two feature vectors,
and a predict that labels a new point by a majority vote of its k nearest
training neighbors.
Add these two functions to knn.py:
def distance(a, b):
"""Euclidean distance between two feature tuples."""
return math.sqrt(sum((x - y) ** 2 for x, y in zip(a, b)))
def predict(train, features, k=K):
"""Predict a label by majority vote of the k nearest training samples."""
ranked = sorted(train, key=lambda sample: distance(sample[0], features))
top_labels = [label for _, label in ranked[:k]]
return Counter(top_labels).most_common(1)[0][0]
Checkpoint: predict a large-petal flower and a small-petal flower.
python3 -c "import knn; from pathlib import Path; tr, _ = knn.train_test_split(knn.load_dataset(Path('flowers.csv'))); print(knn.predict(tr, (4.6, 1.5)), knn.predict(tr, (1.4, 0.2)))"
You should see montana alpina — the large petals classify as montana and the
small ones as alpina. If both print the same label, check that distance
squares the difference and that predict slices ranked[:k], not the whole list.
Step 5 — Score the classifier and run it end to end
Goal: add an accuracy metric and a main block that loads, splits, evaluates, and prints a result, then run the whole program.
Add this to the bottom of knn.py:
def accuracy(train, test, k=K):
"""Fraction of test samples the classifier labels correctly."""
if not test:
return 0.0
correct = sum(
1 for features, label in test if predict(train, features, k) == label
)
return correct / len(test)
if __name__ == "__main__":
base = Path(__file__).parent
samples = load_dataset(base / "flowers.csv")
train, test = train_test_split(samples)
print("loaded", len(samples))
print("train", len(train), "test", len(test))
print("accuracy", round(accuracy(train, test), 3))
Checkpoint: run the program.
python3 knn.py
You should see three lines ending with accuracy 1.0 — the classifier labels all
eight held-out flowers correctly because the two species separate cleanly. If the
accuracy is below 1.0, re-check distance and the vote in predict; if you get
a FileNotFoundError, run the command from the ai101-knn-lab folder.
Validation
Verify the whole build against the objective: a working classifier that clears a
sensible accuracy bar on the held-out set. Save this as validate.py:
from pathlib import Path
import knn
MIN_ACCURACY = 0.90
base = Path(__file__).parent
samples = knn.load_dataset(base / "flowers.csv")
train, test = knn.train_test_split(samples)
acc = knn.accuracy(train, test)
checks = {
"loaded 24 samples": len(samples) == 24,
"16 train / 8 test": len(train) == 16 and len(test) == 8,
"accuracy at or above 0.90": acc >= MIN_ACCURACY,
}
if all(checks.values()):
print("PASSED - k-NN classifier scored %.2f on the held-out test set" % acc)
else:
failed = [name for name, ok in checks.items() if not ok]
print("FAILED - checks not passed: %s (accuracy=%.2f)" % (failed, acc))
Run it:
python3 validate.py
Checkpoint: the command prints PASSED - k-NN classifier scored 1.00 on the held-out test set. If it prints FAILED, the listed check names tell you what
broke: a wrong sample count points to the flowers.csv file (Step 1), a wrong
split points to Step 3, and a low accuracy points to distance or predict
(Step 4).
Teardown
Nothing is billable — this lab runs entirely on your machine. Keep the
ai101-knn-lab folder if you want to reuse knn.py as a reference for the course
challenges; otherwise remove it with rm -rf ai101-knn-lab (macOS/Linux) or
Remove-Item -Recurse ai101-knn-lab (Windows).
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
ModuleNotFoundError: No module named 'knn' | Running from the wrong folder | cd into ai101-knn-lab, where knn.py lives |
FileNotFoundError: 'flowers.csv' | Dataset missing or misnamed | Re-save the Step 1 file as exactly flowers.csv in the folder |
Checkpoint prints a number other than 24 | A dataset row is missing | Re-save flowers.csv with all 24 data rows below the header |
Split prints 24 0 in Step 3 | Test branch condition inverted | Ensure the test branch uses index % 3 == 0 |
| Both predictions print the same label | Distance or slice wrong in Step 4 | Confirm distance squares differences and predict uses [:k] |
Validation prints FAILED | An earlier step's output is stale | Re-check the named failing step, then re-run python3 validate.py |