learn-classification-regression-with-phoebe / Session 9 of 10
Learn Classification & Regression with Phoebe · Session 9 of 10

Choosing your model

Seven animals, eight sessions, one leaderboard - and now the session that turns knowledge into judgement. No new algorithms today. Instead: the one axis that lines the whole zoo up (stiffness vs flexibility), a decision guide you can defend in a design review, and the fair-comparison protocol - tune every candidate by cross-validation on train, and spend the test set exactly once. This is the question hiring managers actually probe: not "can you fit a model" but "why THIS model, and how do you know the comparison was fair?" By the end you will have a defensible answer and a harness that produces it.

🟠 Getting real Builders · DA / DE / DS Python · scikit-learn 45 min
0-3 · Setup 3-20 · The map and the guide 20-40 · Build: the fair harness 40-45 · Wrap
Part 0

The judgement session

You have met all seven animals: lines (s2), sigmoids (s3), the engine underneath (s4), neighbors (s5), probability multipliers (s6), rectangles (s7), and margins (s8). Each session ended with a number on the Lumen leaderboard. Today we stop collecting animals and start choosing between them - which is a different skill entirely. Choosing well needs two things: a mental map that says where each algorithm lives, and a protocol that compares them without cheating. Both fit in one session, because you already did the hard part.

Live - built in session Self-study - run after ★ The one big idea The case: Lumen Skincare
★ What you walk out with today A defensible answer to "why THIS model?" - built from the one complexity axis, an honest decision-guide table, and a start-simple rule - plus compare.py, a harness that tunes every candidate by 5-fold CV on train only and leaves the test set sealed for session 10. Fair comparison is not a vibe; it is a protocol.
Part 1 · the map

The zoo on one axis 8 min live

Every algorithm you met sits somewhere on a single axis running from stiff (high bias: a strong opinion about the shape of the truth, hard to bend, hard to fool) to bendy (high variance: few opinions, fits anything, memorizes noise if you let it). And every knob you learned - polynomial degree, k, tree depth, gamma, C - does the same job: it slides its animal along this axis. Eight sessions of knobs were secretly one knob.

One axis to rule the zoo - stiffness vs flexibility UNDERFIT ZONE OVERFIT ZONE STIFF · high bias BENDY · high variance linear / logistic naive Bayes linear SVM shallow tree kNN, mid k deep tree kNN k=1 big-gamma RBF Every knob you met (degree, k, depth, gamma, C) slides its animal along this one axis.
🔍 Click to zoom - the whole zoo on the bias-variance axis
LiveThe knobs are one knob - complexity4 min▶

Slide too far left and the model is too stiff to see the pattern (underfitting: train AND test scores both poor). Slide too far right and it bends around every noisy point (overfitting: train score great, test score collapses). You have already watched every animal fall off the right edge - here is the reunion tour, with the numbers you produced yourself:

Knob pushed to the edgeWhat happened on testSession
Polynomial degree 12 (50 training points)test R² -798,595 - six orders of magnitude worse than the means2
kNN k=1AUC 0.50 - memorization scores a coin flips5
Tree depth Nonetrain 1.00, test 0.51 - a perfect memory of the past, useless on the futures7
RBF gamma cranked upboundary wraps single points (you watched it in the explorer)s8

Four failures, one disease. Diagnosis is now easy: big train-test gap = too far right, slide left. Both scores low = too far left, slide right (or get better features). That is Ng's bias/variance diagnosis in one sentence.

LiveNo free lunch - why OUR leaderboard loves linear4 min▶

All course long, the linear family has topped the Lumen leaderboard (logistic ~0.69, LinearSVC ~0.69) while the bendier animals trailed. Say the honest reason out loud: the Lumen generator is log-linear. The machine that makes the data IS a linear machine, so models that assume linearity start with the right bet. That is not a law of nature - it is a property of this dataset.

On interaction-heavy tabular data (where the effect of one feature flips depending on another), trees and their ensemble descendants routinely win. There is no universally best algorithm - that is the "no free lunch" theorem in practice. The skill is matching the model's assumed shape to the data's true shape, and being suspicious of anyone who has one favorite algorithm for everything.

Where that story continues: boosted tree ensembles (XGBoost, LightGBM) - the tabular champions on nonlinear data - get their own course, learn-ensemble-methods. We name the animal; we do not teach it here.
Part 2 · the guide

The decision guide 9 min live

The axis says how bendy each animal is; this table says what it costs to keep one. Every cell below is something you verified yourself in sessions 2-8 - this is not a blog-post cheat table, it is your own lab notebook, compressed.

AlgorithmNeeds scaling?Trains fast?Interpretable?Handles nonlinearity?Watch out for
Linear regressionNot for plain OLSYes - closed formYes - coefficients in real units ($)Only via built features (poly)Multicollinearity; degree explosion (s2's -798,595)
Logistic regressionHelps solvers + any penaltyYesYes - readable odds ratiosNo - one straight seamRegularized by default; C is INVERSE strength
kNNMandatory (0.64 vs 0.58 AUC)"Trains" instantly - but predicts slowlyNo - the model is the dataYes - follows the crowdShips the training set as the model: memory + slow predict
Naive BayesNoFastest in the zooSomewhat - per-class statsMildly - per-feature onlyLying probabilities: never trust its predict_proba
Decision treeNoYesYes - you can read the rules aloudYes - rectanglesInstability (small data change, new tree) + must cap depth
SVM linearMandatoryYes - LinearSVC scales finePartly - weights, but no probabilitiesNoNo predict_proba; use decision_function
SVM RBFMandatoryNo - roughly O(n²), subsample big dataNoYes - smooth curvesTune gamma and C or lose to linear (0.56 vs 0.69 here)
LiveStart simple - the baseline is the floor3 min▶

The most useful row of that table is the first two. A logistic or linear baseline trains in seconds, explains itself, and sets the floor every fancier model must beat. On Lumen, nothing beat it - and you only know that because you fit the simple thing first.

The professional habit: fit the simple model on day one, ship the number to the team, THEN explore. If a deep tree grid buys you +0.002 AUC over logistic, you have learned something valuable: the signal is linear, stop spending compute. If it buys +0.05, you have also learned something: go nonlinear, and say why.

The one-liner for your next design review: "we start with the model we can explain, and we make the complicated ones earn their keep."
Self-studyInterpretability is a requirement, not a preference3 min▶

In regulated settings - credit, insurance, healthcare, hiring - "why did the model say no?" is a question you must answer to a regulator or a customer, sometimes by law. There, the Interpretable column is not a nice-to-have; it eliminates candidates before accuracy is even discussed. A readable odds ratio ("returning customers have ~2.8x the odds") beats hand-waving at a black box every single time.

Treat interpretability as a requirement you gather up front, like latency or memory. If the answer is "we must explain every decision", your shortlist is logistic, a shallow tree, maybe linear SVM - and the bake-off runs among those.

Real worldA lending team spent a quarter tuning a model they could not deploy: legal required reason codes per decision, and their winner could not produce them. The runner-up - plain logistic, 1 point of AUC behind - shipped in a week. The best model you cannot ship loses to the good model you can.
Self-studyLearning curves and error analysis - the diagnosis toolkit3 min▶

Two habits from Ng's C2 W3 that complete the picture. Learning curves: plot train and CV score against training-set size. If the two curves have converged and both sit low, you have a bias problem - more data will NOT help; better features or a bendier model will. If a big gap remains between them, you have a variance problem - more data (or regularization, or sliding left on the axis) helps. Teams burn months collecting data for bias problems; the curve tells you in minutes.

Error analysis: read a sample of the actual mistakes. On Lumen you might find the model misses converters arriving from a channel it rarely sees - a feature fix, not an algorithm swap. The best model-choice tool is sometimes 30 minutes of looking at what the current one gets wrong.

Part 3 · the protocol

The fair-comparison protocol 8 min live

A comparison is only worth what its protocol guarantees. Ours: every candidate gets its knobs tuned by 5-fold cross-validation on the training set only, each is judged by its CV mean and spread, and the held-out test set is spent exactly once - in session 10, on the finalists. Run our logistic model through it and you get CV AUC 0.706 +/- 0.010 (folds ranging 0.688-0.715, reconciling with intro-ml's "~0.71"). That +/- is not decoration: the spread across folds tells you how much a single lucky or unlucky split could move the number.

The protocol - every candidate tuned on train, test spent once TRAIN - 30,000 rows each candidate tuned by 5-fold CV, inside logistic - no grid needed · 5-fold CV kNN - k in 25 / 51 / 101 · 5-fold CV tree - depth 3 / 5 / 8 · 5-fold CV LinearSVC - C 0.1 / 1 / 10 · 5-fold CV GaussianNB - nothing to tune Pick winners best params per candidate, judged by CV mean +/- std ONE test pass 10,000 sealed rows opened in session 10, once, then reported Report BOTH numbers: CV mean +/- spread (stability) and the single test score (the headline). Tune on CV, spend test once. Peek at test repeatedly and it quietly becomes a validation set.
🔍 Click to zoom - the fair-comparison pipeline, from CV loops to the single sealed test pass
LiveGridSearchCV per candidate, one harness3 min▶

The mechanics are intro-ml's GridSearchCV, applied at zoo scale: one dict of (pipeline, param grid) pairs, one loop, cv=5 and scoring="roc_auc" for all. Because every pipeline carries its own preprocessing, scaling happens inside each CV fold - no leakage, no favoritism. Candidates with nothing to tune (GaussianNB) just run through the same loop with an empty grid, so every number on the board was produced by the identical procedure.

One nice detail: LinearSVC has no predict_proba, but scoring="roc_auc" falls back to decision_function automatically - the s8 lesson, paying rent.

LiveHARD RULE: never tune on test3 min▶

The test set answers one question - "how will the chosen model do on data it has never seen?" - and it can only answer it once. Every time you peek at test, adjust a knob, and peek again, the test set stops being unseen: you are selecting for whatever flatters those 10,000 particular rows. Repeated peeking turns your test set into a validation set, and the final score you report becomes an optimistic lie - one that production traffic will correct, publicly.

This is why compare.py below computes nothing on X_te. Not one number. The cliffhanger is deliberate: session 10 opens the vault.

Self-studyWhy report the +/-2 min▶

Our logistic folds ranged 0.688 to 0.715. If you had judged it on one unlucky split you would call it a 0.69 model; on one lucky split, a 0.72 model. The fold spread is the honest error bar on your estimate - a single split can be lucky, and the +/- is how you confess that.

It also breaks ties like an adult: a candidate at 0.70 +/- 0.01 is a better bet than one at 0.71 +/- 0.04, because the second one might be a 0.67 model wearing a lucky split. Stability is information. Report mean AND spread, always.

Build-along

Build the fair harness ★ 18 min · everyone builds

One script, five candidates, identical treatment: each pipeline gets GridSearchCV with cv=5 and scoring="roc_auc" on the training set. We print each winner's params and CV mean +/- std - and we do not touch the test set. That restraint is the whole point of the session.

Load and split as always. Seed 42, stratified, 25% test - the same vault every session has used. The split happens; the test half then goes untouched.

Declare the candidates. A dict of (estimator, grid) pairs. Grids are small and honest: k in [25, 51, 101], depth in [3, 5, 8], C in [0.1, 1, 10]. GaussianNB and logistic run with empty grids - same loop, same judge.

Run the loop. Every candidate is tuned by the same 5-fold CV on the same folds of the same training data. Print best params, CV mean, CV std.

Stop. Rank by CV mean, note the spreads, write your recommendation - and leave X_te sealed. Session 10 opens it, once.

compare.py - five candidates, one fair judge, zero test peeksimport pandas as pd from sklearn.model_selection import train_test_split, GridSearchCV from sklearn.compose import ColumnTransformer from sklearn.preprocessing import OneHotEncoder, StandardScaler from sklearn.pipeline import Pipeline from sklearn.linear_model import LogisticRegression from sklearn.neighbors import KNeighborsClassifier from sklearn.tree import DecisionTreeClassifier from sklearn.svm import LinearSVC from sklearn.naive_bayes import GaussianNB df = pd.read_parquet("lumen_sessions.parquet") cat = ["channel", "device", "new_vs_returning", "product_category"] num = ["pages_viewed", "session_hour", "prior_30d_spend"] X, y = df[cat + num], df["converted"] X_tr, X_te, y_tr, y_te = train_test_split( X, y, test_size=0.25, random_state=42, stratify=y) # X_te and y_te are now SEALED. Nothing below touches them. prep = ColumnTransformer([ ("cat", OneHotEncoder(handle_unknown="ignore"), cat), ("num", StandardScaler(), num)]) candidates = { "logistic": (LogisticRegression(max_iter=2000), {}), "kNN": (KNeighborsClassifier(), {"model__n_neighbors": [25, 51, 101]}), "tree": (DecisionTreeClassifier(random_state=42), {"model__max_depth": [3, 5, 8]}), "LinearSVC": (LinearSVC(), {"model__C": [0.1, 1, 10]}), "GaussianNB": (GaussianNB(), {}), } for name, (model, grid) in candidates.items(): pipe = Pipeline([("prep", prep), ("model", model)]) search = GridSearchCV(pipe, grid, cv=5, scoring="roc_auc", n_jobs=-1) search.fit(X_tr, y_tr) mean = search.best_score_ std = search.cv_results_["std_test_score"][search.best_index_] print(f"{name:10s} CV AUC {mean:.3f} +/- {std:.3f} {search.best_params_}") # logistic CV AUC 0.706 +/- 0.010 (folds ranged 0.688-0.715) # the other candidates land near their session numbers on your machine # NOBODY scored the test set today - that is session 10's opening move
If kNN takes forever: that is the instance-based tax from session 5 - every CV fold predicts by searching 24,000 training rows. Let it run (a few minutes), and appreciate the logistic column finishing before your coffee cools.
Real worldA growth team once "compared" models by letting each teammate tune their favorite against the test set all week. The winning score was spectacular and the production launch was not - they had collectively memorized the test set, one peek at a time. One shared harness with a sealed test split is not bureaucracy; it is how the number stays true.
Homework

Before session 10 ◐ 30 min

Check yourself

Three questions before you go 🎯 ◐ 90 seconds

1 · Why do we tune hyperparameters with cross-validation on train, instead of just picking whatever scores best on the test set?

Every peek-and-adjust cycle leaks information about the test rows into your choices. CV simulates unseen data using only the training set, so the test set stays a genuine stranger - and its one verdict, in session 10, stays honest.

2 · Your candidate's five CV folds score 0.63, 0.75, 0.66, 0.74, 0.64. What does that large spread tell you?

The fold spread is the error bar on your estimate. A wide spread means one split could easily mislead you - which is exactly why we report CV mean +/- std, and why 0.70 +/- 0.01 can be a better bet than 0.71 +/- 0.04.

3 · The Lumen leaderboard has favored linear models all course. The honest explanation is...

There is no universally best algorithm. Our data's true machine is linear, so linear bets win here - say that out loud in your readout. On nonlinear, interaction-heavy data the ranking flips, which is the ensembles course's opening story.

Source material

What this session covers

Session 9 covers the model-selection layer of the anchor sources: Ng's bias/variance diagnosis week and the practical-choice wisdom scattered across the scikit-learn module pages. Certificates, videos, and graded labs stay with the official providers.

✓Ng C2 W3 - diagnosing bias and variancePart 1 + the axis
✓Ng C2 W3 - regularization vs bias/variancePart 1 knob table
◐Ng C2 W3 - learning curvesPart 2 self-study card
◐Ng C2 W3 - error analysisPart 2 self-study card
✓sklearn - model-choice heuristics distilled from all five module pages (1.1, 1.4, 1.6, 1.9, 1.10)Part 2 decision guide
✓sklearn - GridSearchCV protocol: tune on CV, never on testPart 3 + demo
✓StatQuest - the bias-variance trade-off framing (Regularization Part 1's story)Part 1
◐AutoML / ensembling as the variance fixnot covered by design - named for the ensembles course

Session 9 cheat sheet · pin this

One complexity axisStiff (high bias) to bendy (high variance). Every knob - degree, k, depth, gamma, C - slides its animal along it. Big train-test gap: slide left. Both scores low: slide right or improve features.
The knob failuresDegree 12: test R² -798,595. kNN k=1: AUC 0.50. Depth None: train 1.00 / test 0.51. Big gamma: point-wrapping. Four edges, one disease.
Decision-guide highlightskNN + SVM: scaling mandatory. Trees + NB: no scaling, but trees are unstable and NB's probabilities lie. Logistic: readable odds ratios. RBF: tune or lose.
Start simpleThe logistic/linear baseline is the floor every fancy model must beat. On Lumen, nothing beat it. Make complexity earn its keep.
The CV numberLogistic 5-fold CV AUC 0.706 +/- 0.010 (folds 0.688-0.715). Report mean AND spread - the spread is the error bar, and a single split can be lucky.
Spend test onceTune every candidate by CV on train; the test set answers one question, once, in session 10. Repeated peeking turns test into validation and the score into a lie.