The first instance-based animal
Session 1 drew the zoo map with two families: model-based animals compress the data into learned numbers, instance-based animals keep the data and look things up. kNN is the purest instance-based animal there is - there is no training step at all. .fit() just files the rows away. All the work happens at prediction time: measure the distance from the new point to every stored point, take the k closest, and vote (classification) or average (regression). No loss function, no descent, no coefficients to read - which means every ounce of its behavior comes from three choices you make: k, the distance metric, and how you scale the features.
The algorithm with no training step 7 min live
Here is the entire mechanism. Store every training row. When a new session arrives, compute its distance to all stored rows, keep the k nearest, and let them decide: majority vote for a class, average for a number. The "model" you deploy is literally the training set plus a lookup rule - which is why session 1 called kNN "non-generalizing": it never compresses the data into a generalization, it remembers.
LiveThe vote, and how to weight it2 min▶
Default kNN uses weights="uniform": each of the k neighbors gets exactly one vote, whether it sits right on top of the query or way out at the edge of the neighborhood. The alternative, weights="distance", weights each vote by 1/distance - closer neighbors count more, far ones fade.
Distance weighting softens the biggest annoyance of uniform voting: a neighborhood where 51 lukewarm far-away neighbors outvote 50 near-identical ones. It also makes predictions vary more smoothly as k grows, because adding one more distant neighbor barely moves the answer. You will try it in the homework at k=25 and see whether it buys anything on Lumen.
LiveWhat "distance" means - euclidean, manhattan, minkowski2 min▶
sklearn's default metric is minkowski with p=2, which is just euclidean distance - the straight-line ruler. Set p=1 and you get manhattan distance: sum of the absolute differences per feature, the "walk the city grid" ruler that is gentler on single-feature outliers. Any p in between (or beyond) is the general minkowski family.
On Lumen the choice barely moves the needle - the features are few and, after scaling, comparable. The metric matters more in high dimensions or on features with heavy tails. The knob to remember: KNeighborsClassifier(metric="minkowski", p=1) flips you to manhattan without changing anything else.
Self-studyThe tie warning in the docs1 min▶
A subtle honesty note straight from the sklearn neighbors guide: if the k-th and (k+1)-th neighbors sit at exactly the same distance but carry different labels, which one makes the cut depends on the ordering of the training data. Rare with continuous features, more common with one-hot grids where many points share coordinates - and worth knowing before you chase a "non-deterministic model" ghost in a debugging session.
Practical hygiene: prefer odd k for binary problems so the vote itself cannot tie.
Self-studyProduction memory - the bill for skipping training2 min▶
No training step sounds free until deployment day: kNN ships the training data as the model. Every prediction scans (or tree-searches) the stored rows, so prediction cost grows with training size - the exact opposite of logistic regression, which trains slowly-ish once and then predicts with a handful of multiplications forever.
This is session 1's fraud-team story coming home: the 40 GB kNN scorer next to the kilobyte logistic model. Same accuracy conversation, wildly different memory bill.
k is the bias-variance dial 9 min live
Every algorithm has a flexibility knob - intro-ml drew the U-curve with tree depth, session 2 blew it up with polynomial degree. kNN's knob is k, and it runs backwards from the others: small k = maximum flexibility (each prediction trusts one loud neighbor), large k = maximum smoothing (each prediction polls a whole district). Here is the dial swept on Lumen, full features, scaled, test ROC-AUC:
| k | Test ROC-AUC | What the dial is doing |
|---|---|---|
| 1 | 0.504 | a coin flip - pure memorization of noise |
| 5 | 0.533 | still trusting tiny, noisy crowds |
| 25 | 0.592 | smoothing starts to pay |
| 101 | 0.640 | kNN's ceiling on this data |
LivePlay: the k dial on the boundary explorer3 min▶
Drag k on the moons - watch the boundary wrap around every stray point at k=1 (memorization) and blur into an over-smoothed blob by k=75.
LiveWhy k=1 memorizes - and why that reads as a coin flip2 min▶
At k=1, every training point's nearest neighbor is itself, so training accuracy is essentially perfect - the model has memorized the data, noise and all. But a new session lands next to whichever old session happens to sit closest, and at Lumen's ~3% conversion rate in a crowded feature space, that single neighbor is nearly uninformative: test AUC 0.504, statistically a coin flip.
This is the sharpest overfitting demonstration in the whole course precisely because nothing was "trained": memorization is not a training pathology, it is a flexibility pathology. Any model flexible enough to store the data will happily do so.
LiveBig k smooths - the sklearn guidance2 min▶
The sklearn guide's advice on choosing k is refreshingly plain: a larger k suppresses the effects of noise, but makes the classification boundaries less distinct. On clean, balanced data you would tune k somewhere modest. On imbalanced data like Lumen - roughly 1 converter per 30 sessions - a converter's local neighborhood is mostly non-converters, so small k drowns the signal in noise and large k keeps helping: 0.504 → 0.533 → 0.592 → 0.640 across our sweep.
But the dial does not rise forever - crank k far enough and every neighborhood contains the same city-wide average, boundaries blur, and performance slides back down. You saw exactly that over-smoothing on the moons at k=75. Where the peak sits is an empirical question, which is homework.
Self-studykNN regression - the whole dial in one row2 min▶
Swap the vote for an average and kNN predicts numbers: KNeighborsRegressor on the 1,306 Lumen buyers, predicting order value. The k sweep tells the entire bias-variance story in four values of R²:
| k | Test R² | Reading |
|---|---|---|
| 1 | -0.10 | worse than predicting the mean! |
| 5 | 0.40 | usable |
| 25 | 0.46 | the peak |
| 101 | 0.39 | over-smoothed - averaging too broad a crowd |
Note the two failure modes bracket the peak: k=1 memorizes (negative R² - you would score better predicting $73 for everyone), k=101 over-smooths (every prediction drifts toward the global mean). Same U-curve as tree depth and polynomial degree, third costume.
Scaling and the curse 7 min live
Distance treats every unit as equal. On Lumen, prior_30d_spend runs 0-400 while pages_viewed runs 1-15 - so in raw units, a $50 spend difference dwarfs a 10-page browsing difference, and "nearest" quietly becomes "nearest in spend, pages be damned". Standardize both to mean 0 and SD 1 and the vote becomes fair. This is not a style preference; on Lumen it is worth six points of AUC: scaled k=101 hits 0.64, unscaled 0.58.
LiveThe scaling proof, in numbers3 min▶
Same pipeline, same k=101, one change - pass the numerics through raw instead of standardizing them - and test AUC drops from 0.64 to 0.58. Nothing else moved. In the unscaled run, prior_30d_spend's 0-400 range dominates every distance computation, so the neighbor sets are effectively chosen by spend alone; pages, hour, and most of the one-hot signal get shouted down.
The fix is one line in the ColumnTransformer, and the discipline you already own from intro-ml applies: the scaler lives inside the pipeline and is fit on train only, so no test-set statistics leak in.
Self-studybrute, kd_tree, ball_tree - how sklearn finds neighbors fast2 min▶
Naively, each prediction compares the query to all N stored rows - algorithm="brute", exact and fine for small data. For bigger data sklearn can pre-organize the rows into a kd_tree (splits space along coordinate axes; shines in low dimensions) or a ball_tree (nests points inside hyperspheres; holds up better as dimensions climb). Both prune away whole regions that provably cannot contain a nearest neighbor.
The default algorithm="auto" chooses for you, and its rules are worth knowing: it falls back to brute force when the data is sparse or when dimensionality is high (above 15 or so, tree structures stop paying for themselves - our one-hot-heavy Lumen matrix is exactly that case). The leaf_size knob (default 30) tunes when the trees stop splitting and hand over to brute force at the leaves - it affects speed and memory, never the predictions.
LiveThe curse of dimensionality2 min▶
Distance-based reasoning degrades as dimensions pile up. In high-dimensional space, everything is far from everything - the volume grows so fast that your training points become lonely, and the gap between the nearest and farthest neighbor shrinks until "nearest" barely means anything. The vote is only as good as the neighbors' relevance, and in high dimensions relevance thins out.
Lumen after one-hot encoding lives in roughly 20 dimensions - modest, but enough to blunt the tool. That, plus the harsh class imbalance, is the honest reason kNN tops out at 0.64 here while logistic - which learns a global pattern instead of trusting local crowds - reaches 0.69. Not a broken model; a mismatched one. Session 9 turns this instinct into a checklist.
The k sweep, scaled and unscaled, both tasks ★ 18 min · everyone builds
One script, three experiments: sweep k on the classifier and watch the dial climb from coin flip to ceiling, rerun the winner without scaling to price the mistake, then sweep the regressor on the 1,306 buyers and watch the same U-curve appear in R².
Build the pipeline. The same ColumnTransformer as session 1 - one-hot the categoricals, standardize the numerics - with KNeighborsClassifier at the end. The scaler inside the pipeline is the whole ballgame today.
Sweep the classifier. Loop k over 1, 5, 25, 101 and print test ROC-AUC. Watch memorization at k=1 become smoothing by k=101.
Price the unscaled sin. Swap the scaler for "passthrough", rerun k=101, and watch six points of AUC evaporate.
Sweep the regressor. Same dial on the buyers subset with KNeighborsRegressor - k=1 goes negative, k=25 peaks.
"passthrough" actually replaced the scaler - a scaler accidentally left upstream of the split is the classic false negative here.Before session 6 ◐ 25 min
- Find the k that maximizes test AUC somewhere between 25 and 101: sweep a finer grid and plot AUC vs k. Does the curve keep climbing to 101, or peak and dip before it?
- Rerun k=25 with
weights="distance". Does letting closer neighbors count more help on Lumen? One sentence on why or why not. - Count the nonzero one-hot dimensions for a typical Lumen row (hint:
prep.fit_transform(X_tr)[0]). How many of the ~20 dimensions does one session actually occupy? That sparsity is your curse-of-dimensionality intuition made concrete.
Three questions before you go 🎯 ◐ 90 seconds
1 · kNN with k=1 scores ~0.50 test AUC on Lumen - a coin flip. Why?
There is no training to go wrong - k=1 is a flexibility pathology. Train accuracy is essentially perfect (each point predicts itself), while a new session inherits the label of one nearly-random neighbor: 0.504 AUC, statistically a coin flip.
2 · Standardizing the numerics moved k=101 from 0.58 to 0.64 AUC. What was going wrong before?
Distance treats every unit as equal, so the feature with the biggest range picks the neighbors. Standardizing puts spend, pages, and hour on the same footing - worth six points of AUC on Lumen, measured, not asserted.
3 · In the regression sweep, k=101 (R² 0.39) fell below k=25 (R² 0.46). What happened?
The dial has two bad ends: k=1 memorizes (R² -0.10, worse than predicting the mean) and k=101 over-smooths. The peak in between - k=25, R² 0.46 - is the bias-variance sweet spot, found by validation, never by faith.
What this session covers
Session 5 covers the kNN material from the anchor sources - the StatQuest episode and the sklearn nearest-neighbors guide. Videos and full API references stay with the official providers.