Capstone: a regression project
Predict the number, diagnose the residuals, report in minutes
RidgeGridSearchCVresidualsMAER²reporting in unitsWatch it happen
Play it through, or step back and forth yourself.
minutes, not lateno stratify — it is a numberDummyRegressor → R² 0RidgeCV / GridSearchCV on alpharesiduals vs predictionsMAE 3.09 min · 80% within 5The same data, the more useful target. Predicting minutes keeps everything the 30-minute threshold discarded — and it lets you report in a unit anybody understands.
The idea
Same data, better target. late was always minutes > 30 — somebody's threshold — so predicting the number keeps what the threshold discarded, and you can apply any cut you like afterwards.
It also lets you report in minutes, which is a unit the stall owner already thinks in. That matters more than it sounds.
1. Split — and note what changes
X_train, X_test, y_train, y_test = train_test_split(
X, deliveries["minutes"], test_size=0.25, random_state=0)No stratify: the target is continuous, so there are no classes to balance. (If you needed it — a very skewed target, say — you'd bin the target and stratify on the bins.)
2. Baseline
DummyRegressor(strategy="mean") scores R² of essentially 0 by construction, which is R²'s quiet advantage: the baseline is built into the scale, so you don't have to fit one to read the number. Do it anyway for the MAE, which is 4.91 minutes — that's what "no model" costs you.
3. Pipeline and search
pipe = Pipeline([("pre", pre), ("m", Ridge())])
GridSearchCV(pipe, {"m__alpha": np.logspace(-2, 3, 12)},
cv=5, scoring="r2").fit(X_train, y_train)
best alpha 5.337
cv R² 0.5871Ridge rather than plain LinearRegression because the penalty costs nothing when it isn't needed and protects you when it is (lesson 15). Log scale on alpha, because its effect is multiplicative (lesson 23).
Module 4 already established the important negative results here: polynomial features make it worse, and a 200-tree forest scores 0.5198 against linear's 0.5785. This process really is a weighted sum, and it's worth having checked.
4. Diagnose before you report
resid = y_test - model.predict(X_test)
residual mean −0.0236 centred
worst miss 9.93 minutes
within 5 min 80.0%Plot residuals against predictions and you get a flat, shapeless band — no curve, no fan, no drift (lesson 17). The straight-line assumption holds, which independently corroborates why the forest couldn't beat it.
Do this before you report the score. A good R² with a curved residual plot means you have a model that works on average and fails systematically somewhere specific, and the average will not tell you where.
5. Report in units people use
TEST R² 0.5967
TEST MAE 3.087 minutes
within 5 min 80.0%Three numbers doing three different jobs:
- R² — for a technical reader, with its baseline built in.
- MAE — the typical miss, in minutes.
- "80% within five minutes" — the one for the stall owner. It's a promise they can check against reality tomorrow, and it converts a model score into an operational commitment.
That third form is worth constructing deliberately for every regression project. Pick a tolerance that matters to the business and report the fraction inside it.
6. And connect it back
You can recover the classification decision from the regression at any threshold: predicted_minutes > 30. That's often the better architecture — one model, one number, and the operational cut kept as a separate, adjustable decision rather than baked in at training time.
It also means changing the promise from 30 minutes to 25 requires no retraining at all.
The checklist
- Predict the number if you have it; you can always threshold later.
- Split (no stratify), baseline, pipeline.
- Tune alpha on a log scale by cross-validation.
- Plot the residuals before you believe the score.
- Report R², an error in real units, and a tolerance fraction.
See it run
The lesson's code, ready to run and to fiddle with.
Putting the kettle on…
Starting up…
Worked example
not gradedAlready written and ready to go — press Run to see what it does, then change a number, a column name, anything, and run it again.
tryreporting "within 3 minutes" instead of 5 — a tighter promise, and a smaller fraction.
Your turn
3 exercises. Write the code yourself, then press Check — a nudge and the answer are there if you want them.
Tune ridge over np.logspace(-2, 3, 12) with scoring="r2" and return [best_alpha, test_R2, test_MAE], rounded to 3, 4 and 3 places.
The number for the stall owner. Return the fraction of test predictions within 5 minutes of the truth, rounded to 4 places.
Diagnose before reporting. Return [residual_mean, worst_absolute] rounded to 4 and 2 places — the mean should be essentially zero.
