Machine Learning › Regression › Day 154
Hands-on lab — Day 154: A Complete Regression Project
- ← Back to the Day 154 lesson
- Open the hands-on files on GitHub — clone or download them from the public labs repository
- Local path in your clone:
labs/sections/machine-learning/day-154-a-complete-regression-project/
Commands
Setup
cd labs/sections/machine-learning/day-154-a-complete-regression-project
python3 -m venv .venv
.venv/bin/pip install -r requirements/requirements.txt
.venv/bin/python3 -c "import numpy, sklearn; print(numpy.__version__, sklearn.__version__)" Run
.venv/bin/pytest examples -q
.venv/bin/pytest starter -q
.venv/bin/python3 examples/report_measurements.py Test
bash tests/run_tests.sh File tree
examples/regression_lib.py examples/report_measurements.py examples/test_regression_claims.py examples/test_regression_lib.py expected-output/examples-run.txt expected-output/FIELDS.md expected-output/measured-values.txt expected-output/starter-run.txt expected-output/test-run.txt metadata.yml README.md requirements/README.md requirements/requirements.txt security.md starter/00_brief.md starter/regression_lib.py starter/test_regression_claims.py starter/test_regression_lib.py tests/run_tests.sh troubleshooting.md
Lab README
Day 154 lab — A Complete Regression Project
Lesson
- Lesson title: A Complete Regression Project
- Day number: 154 of 365
- Lesson article: https://ai-roadmap-365.github.io/day-154-a-complete-regression-project
- Lab files: everything you need is in this directory — follow “How to run” below.
- Browse the course locally: from the repository root, this lab also appears in the course website at
/labs/day-154-a-complete-regression-projectwhen the site is running.
Purpose
Days 148 through 153 each isolated one discipline in a lab built to show it alone: the one-predictor model and its four assumptions, the loss function as a choice, multicollinearity, ridge and lasso, the metrics that can be gamed or inverted, and OLS built from scratch.
This lab is every one of those disciplines, spent on one real dataset, in the order a working project actually uses them: frame, baseline, split, pipeline, cross-validate, select, one test evaluation, residual diagnostics, a fairness check, prediction intervals, an honest verdict with an interval on the margin.
The dataset is sklearn.datasets.load_diabetes(scaled=False) — the only
regression dataset bundled inside scikit-learn that needs no download.
fetch_california_housing downloads by default and is forbidden by this
lab's offline rule. 442 rows, 10 real-valued measurements in raw units,
and a target with no physical unit — a composite disease-progression
score, not mg/dL.
| quantity | value |
|---|---|
| dataset | load_diabetes(scaled=False) |
| rows / features | 442 / 10 |
| target range / mean | [25.0, 346.0] / 152.1335 |
| split (seed 0, 25 percent test) | 331 train / 111 test |
| mean-predictor baseline RMSE / R2 | 70.4637 / -0.0001 |
| K candidate pipelines | 23 (11 ridge, 11 lasso, 1 OLS) |
| winner (5-fold CV RMSE) | Lasso(alpha=1), 53.8958 |
| ONE test evaluation (RMSE / R2 / MAE) | 56.5566 / 0.3557 / 45.2846 |
| margin over baseline, 95 percent bootstrap interval | 13.9071, [5.5852, 22.3324] |
| leaky-vs-honest gap over 20 seeds | mean +0.5279, never negative |
| prediction-interval coverage (nominal 0.95) | 0.9459 |
Learning objectives
By the end of this lab you will be able to:
- Choose a regression dataset by what is actually available offline, not by habit, and explain plainly what a target with no physical unit means for reading an RMSE.
- Establish a mean-predictor baseline before fitting any model.
- Build a real train/test split and hold the test rows back until one evaluation, using the discipline Days 144 and 147 built.
- Sweep a genuine set of candidate pipelines with scikit-learn's
Pipeline, and count K rather than losing track of it. - Select a winner using cross-validation on training rows only, never on test rows, choosing RMSE as the metric before any model is fitted.
- Enforce a one-evaluation budget on a test set mechanically.
- Compute a bootstrap interval around a model's margin over baseline, and judge whether the improvement is distinguishable from noise at the test-set size actually available.
- Read residual-vs-fitted, curvature and normal-probability diagnostics for what they reveal about a regression model, not only its RMSE.
- Measure whether a model's errors are worse for high-value targets than low-value ones, and read the result honestly whichever way it comes out.
- Reproduce, and recognise, the mistake of selecting a model by scoring every candidate directly against the test set.
- Build a prediction interval from held-out residuals and measure its realised coverage against its nominal rate.
Prerequisites
- Day 141 for what a score means, Day 144 for the three sets and the
GatedTestSetpattern, Day 147 for the full classification protocol this lab mirrors, Day 148 for the one-predictor model and its four assumptions, Day 149 for the loss as a choice, Day 150 for multicollinearity, Day 151 for ridge and lasso, Day 152 for metrics that can be gamed or inverted, and Day 153 for OLS built from scratch. This lab uses every one of them and teaches none of them again. - Comfort with NumPy arrays and reading a pytest failure, and
python33.11 or newer on yourPATH.
Supported operating systems
- macOS (Apple Silicon or Intel) — the capture machine was macOS 26.5.2 on arm64.
- Linux (any distribution with Python 3.11+ and bash).
- Windows via WSL2. The harness is a bash script and uses
mktemp -d,findand process substitution; native PowerShell is not supported.
Hardware requirements
Any machine that can run Python. No GPU is needed or used — this lab is small-array NumPy and scikit-learn on the CPU throughout, and the authoring machine (Apple Silicon, no CUDA GPU) ran the entire harness in under a minute. The heaviest step is the 20-seed leaky-gap comparison in exercise 10b, which cross-validates all 23 candidates 20 times over and took 2.9559 seconds on the capture machine. Around 400 MB of disk for the virtual environment, almost all of it scikit-learn and scipy.
Required software
- Python 3.11 or newer (3.14.0 during capture).
- bash 3.2 or newer (3.2.57 during capture — the macOS system bash).
- The three pinned packages in
requirements/requirements.txt:numpy==2.5.2,scikit-learn==1.9.0,pytest==9.1.1.
find, grep, awk, sed, diff and mktemp are used by the harness
and ship with every supported system.
Free and open-source options
Everything here is free and open source, and there is no paid tier anywhere in this lab.
- NumPy and scikit-learn are BSD 3-Clause licensed.
- pytest is MIT licensed.
- The dataset,
sklearn.datasets.load_diabetes, ships inside the scikit-learn package itself; nothing is downloaded and no dataset licence beyond scikit-learn's own applies to your use of this lab.
The estimators used here — Ridge, Lasso, LinearRegression,
DummyRegressor — and the selection machinery — Pipeline, KFold,
cross_val_score, cross_val_predict, train_test_split — are all part
of scikit-learn. The Q-Q normal-probability check is built from scratch
in regression_lib.py (a rational approximation to the inverse normal
CDF) so this lab needs no scipy dependency.
Installation
From the repository root:
cd labs/sections/machine-learning/day-154-a-complete-regression-project
python3 -m venv .venv
.venv/bin/pip install -r requirements/requirements.txt
.venv/bin/python3 -c "import numpy, sklearn; print(numpy.__version__, sklearn.__version__)"
That last line should print 2.5.2 1.9.0. The install step is the only
part of this lab that needs the network, and it installs into a
lab-local environment — never into your system Python. rm -rf .venv
reverses it completely.
File structure
day-154-a-complete-regression-project/
├── README.md this file
├── metadata.yml how the lab was actually executed
├── security.md what the lab touches, and what it does not
├── troubleshooting.md every failure this lab is known to produce
├── requirements/
│ ├── README.md why the pins are exact
│ └── requirements.txt numpy, scikit-learn, pytest
├── starter/
│ ├── 00_brief.md read this first
│ ├── regression_lib.py complete machinery — not the exercise
│ ├── test_regression_lib.py five machinery checks, already solved
│ └── test_regression_claims.py fourteen exercises, each a skip to replace
├── examples/
│ ├── regression_lib.py identical to the starter copy
│ ├── test_regression_lib.py the same five machinery checks
│ ├── test_regression_claims.py the reference solutions
│ └── report_measurements.py prints every measured pair as one table
├── expected-output/
│ ├── FIELDS.md what is exact everywhere, and what is not
│ ├── measured-values.txt the captured report, compared byte for byte
│ ├── examples-run.txt captured `pytest examples -q`
│ ├── starter-run.txt captured `pytest starter -q`
│ └── test-run.txt captured `bash tests/run_tests.sh`
└── tests/
└── run_tests.sh the harness — the definition of done
starter/regression_lib.py and examples/regression_lib.py are byte
identical on purpose. The library is machinery; the exercises are the
work.
How to run
## the exercises, as you will find them
.venv/bin/pytest starter -q
## the reference solutions
.venv/bin/pytest examples -q
## every measured pair, as one table
.venv/bin/python3 examples/report_measurements.py
## the harness: the only definition of done
bash tests/run_tests.sh
echo "exit=$?"
Run starter and examples as two separate invocations. Both
directories define modules with the same names, and pytest aborts on the
collision with import file mismatch. Check 5 of the harness asserts
that it does, so the behaviour is documented rather than surprising.
Capture the exit status of run_tests.sh itself, as shown. Writing
bash tests/run_tests.sh | tail -3 and then reading $? gives you
tail's exit status, which is essentially always zero — the classic
always-passing test suite.
What the commands do
| Command | What it does |
|---|---|
python3 -m venv .venv |
Creates a lab-local environment so nothing installs into your system Python |
.venv/bin/pip install -r requirements/requirements.txt |
Installs the three pinned packages, plus scipy, joblib and threadpoolctl as scikit-learn's own dependencies |
.venv/bin/pytest starter -q |
Runs your work: five machinery checks pass, fourteen exercises skip until you write them |
.venv/bin/pytest examples -q |
Runs the reference solutions — nineteen assertions about the whole project |
.venv/bin/python3 examples/report_measurements.py |
Recomputes every published number and prints them as one table |
bash tests/run_tests.sh |
Fifteen checks: version pins, every claim reproduced without pytest, both suites, the collision, a byte-comparison of the report, the one-evaluation guarantee, a deliberate self-break, an unquoted-seed re-check, and cleanliness |
Expected output
bash tests/run_tests.sh ends with:
---------------------------------------------------------------
15 checks, 0 failure(s)
and exits 0. pytest examples -q reports 19 passed.
pytest starter -q reports 5 passed, 14 skipped until you start work.
The complete captured runs are in expected-output/. The measurement
table is compared byte for byte by check 6, so if a number in the lesson
ever drifts from the code, the harness fails rather than the lesson
quietly becoming wrong.
Read expected-output/FIELDS.md before concluding that a mismatch on
your machine is a bug. It separates what is exact everywhere — the
dataset's shape, the RMSE and R2 formulas, the leaky RMSE never being
worse than the honest one — from what holds only under the pinned
versions, which is most of the decimals.
Validation steps
bash tests/run_tests.sh; echo "exit=$?"→15 checks, 0 failure(s)andexit=0..venv/bin/pytest examples -q→19 passed..venv/bin/pytest starter -q→5 passed, 14 skippedbefore you start;19 passedwhen you have finished every exercise..venv/bin/python3 examples/report_measurements.py | diff - expected-output/measured-values.txt→ no output.- Break one assertion in
examples/test_regression_claims.pyon purpose, re-run the harness, and confirm it reports failures and exits non-zero. Restore it. A test suite you have never seen fail is not evidence.
Tests
tests/run_tests.sh is a bash assert harness. It prints one ok: or
FAIL: line per check, ends with N checks, M failure(s), and exits
non-zero when M is not zero.
The fifteen checks are:
1-3. The installed numpy, scikit-learn and pytest match the pins exactly.
4. Every published claim reproduced directly against regression_lib,
with no pytest involved — so a broken test file cannot hide a broken
library, and vice versa.
5. pytest examples -q reports 19 passed.
6. pytest starter -q reports 5 passed, 14 skipped.
7. The combined pytest examples starter invocation aborts, as
documented.
8. report_measurements.py output is byte-identical to the captured
table.
9. GatedTestSet permits exactly one evaluation, then refuses five
further attempts in a row without ever advancing its counter.
10-11. A scratch copy of examples/ passes, then fails with a non-zero
exit and the failing test named after one assertion is deliberately
rewritten.
12. The leaky-gap direction and the selection mechanics are re-confirmed
at seeds this lab never quotes, so no directional claim rests on a
single lucky seed.
13-15. No URL appears in any source file; no __pycache__ and no
.pytest_cache are left behind.
Caches are cleared at the start of the run as well as the end, so the cleanliness checks measure what that run left rather than what a previous manual pytest invocation left.
Cleanup
find . -path ./.venv -prune -o -type d -name '__pycache__' -print -exec rm -rf -- {} +
rm -rf .pytest_cache
rm -rf .venv # optional: removes the lab virtual environment
git checkout -- starter/ # optional: reset your work
The harness already removes its own scratch directory. Nothing else is created outside this directory, so those four commands return your machine to exactly the state it was in.
Troubleshooting
See troubleshooting.md, which covers the missing virtual environment,
the import file mismatch collision, a winning configuration that
differs from the lesson's, the bootstrap interval not matching a by-hand
run, residual diagnostics that look different at another seed, a leaky
RMSE that should never come out higher than the honest one, and
convergence warnings from Lasso or Ridge.
Security notes
See security.md. In short: no network after the install, no
credentials, no sudo, no write outside this directory except a
mktemp -d scratch directory the harness removes in the same run, and
everything reversible with rm -rf .venv. It also reads GatedTestSet
as an access-control pattern, and shows explicitly that a budget enforced
only at one call site — as opposed to on the resource itself — can be
bypassed, which the leaky-selection exercise deliberately does.
Extension exercises
- Nested cross-validation. Implement an inner loop that selects among the 23 candidates and an outer loop that scores the winner, so the outer score is never contaminated by the selection. Measure whether the gap between cv_rmse and test RMSE shrinks further, and report the cost in fits.
- A fourth family. Add
ElasticNettocandidate_configs, re-run the sweep, and report whether the winner or its cross-validated RMSE changes at seed 0. - Cost-weighted residual analysis. Exercise 9 splits the test set at the median. Try a more granular split — quartiles instead of halves — and report whether the fairness signal strengthens or weakens toward the extremes.
- Scale the test set. Repeat the leaky-gap comparison (exercise 10b) using a 60/40 train/test split instead of 75/25, so the test set has roughly 177 rows instead of 111. Report whether the mean gap changes.
- A per-row prediction interval. Exercise 11 builds one constant half-width for every prediction. Build a version whose half-width varies with the fitted value (using, for instance, the local density of out-of-fold residuals near each prediction) and compare its coverage and average width to the constant-width version.
- Break the independence assumption on purpose. Duplicate 10 percent of the rows into both the train and test splits before running the sweep, and measure how much the reported test RMSE improves (misleadingly). This is Day 144's group-leakage lesson, reconstructed on real data.
Navigation
- Lab brief:
starter/00_brief.md - Previous lab:
../day-153-linear-regression-from-scratch/ - Week 22 (Regression) ends here. Day 155 begins Week 23.
- Week 22 project:
../projects/week-22/
Expected output
FIELDS.md
# What is exact, what may differ, and why
Everything in this directory is captured from a real run on the authoring
machine on 2026-08-27: macOS 26.5.2 (Apple Silicon, arm64, CPU only -- no
GPU is needed or used), Python 3.14.0, in this lab's own `.venv` built
from `requirements/requirements.txt` -- numpy 2.5.2, scikit-learn 1.9.0,
pytest 9.1.1, with scipy, joblib and threadpoolctl pulled in as
scikit-learn's own dependencies (this lab imports none of them directly).
## Exact on any machine, for any reason
These are arithmetic or structural facts, not measurements that happened
to come out a certain way. Check 9 of the harness confirms the
directional ones at seeds this lab does not quote.
- **The dataset's shape, feature names, and target range** in exercise 1.
`load_diabetes` is bundled data, not a sample drawn at run time -- its
row and feature counts, and the target's min, max and mean, do not
depend on any seed.
- **`fetch_california_housing`'s `download_if_missing` default being
`True`.** A fact about the function's signature, not a measurement.
- **The RMSE, R2 and MAE formulas.** Arithmetic, not measurements.
- **Cross-validation selecting on train rows only, and never on test
rows.** Structural, by construction of `select_best`.
- **`TestSetTouchedTwice` on a second evaluation, and the counter not
advancing on a refused attempt.** Branching logic, asserted
mechanically by check 7 of the harness with five repeated refused
attempts.
- **The leaky RMSE never being worse (higher) than the honest RMSE, at
any seed.** The leaky search considers the honestly selected winner
among its 23 candidates and can only replace it with something that
scored at least as well on the test rows it was allowed to peek at.
## Exact under these pins, and only these
Everything else depends on NumPy's `default_rng` and `RandomState` bit
streams (used, indirectly, through scikit-learn's `random_state=`
parameters, and directly in `margin_bootstrap_interval`'s own
`default_rng(seed)` call) and on scikit-learn's estimator internals.
**NumPy's own documentation states that `Generator` carries no
stream-compatibility guarantee across versions**, so seeding makes these
reproducible under the pins in `requirements/requirements.txt` and not
beyond them.
| Value | Exercise | What it is |
| --- | --- | --- |
| `331` train rows, `111` test rows | 3 | the 75/25 split at seed 0 |
| `70.4637` RMSE, `-0.0001` R2 | 2 | the mean-predictor baseline |
| `('lasso', 1)`, `53.8958` | 5 | the winning configuration and its 5-fold CV RMSE |
| `56.5566` RMSE, `0.3557` R2, `45.2846` MAE | 6 | the one permitted test evaluation |
| `[5.5852, 22.3324]`, margin `13.9071` | 7 | the bootstrap interval around the margin over baseline |
| `-3.6262` mean, `56.4402` sd, `0.2386` heteroscedasticity, `-0.1278` curvature | 8 | the residual-vs-fitted diagnostics |
| `0.9901` Q-Q correlation, five named largest residuals | 8b | the normal-probability check and the worst individual mistakes |
| `55.2464`, `57.8601`, `1.0473` | 9 | RMSE on the below- and above-median halves of the test targets, and their ratio |
| `55.5212` leaky RMSE at seed 0 | 10 | selecting by peeking at the test set |
| `0.5279` mean gap, `0.3686` sd, `0.011` min, `1.1451` max | 10b | the 20-seed leaky-gap distribution |
| `105.8797` half-width, `0.9459` coverage | 11 | the prediction interval and its realised coverage |
## Sampled, and therefore soft even here
- **The bootstrap interval on the margin (exercise 7) resamples the 111
test rows 2000 times with a fixed `default_rng(0)`.** It is
deterministic under this NumPy version because the algorithm's random
draws are seeded and reproducible bit-for-bit, but a different NumPy
version's `Generator` stream is not guaranteed to reproduce the same
sequence, per NumPy's own documentation. The direction -- the interval
excludes zero, so the model is distinguishable from baseline -- is the
claim likely to survive a version change; the exact bounds may not.
- **The predicted-vs-measured leaky-gap distribution in exercise 10b is
averaged over 20 seeds**, for the reason Day 144 gave for averaging
over many replications: one draw of a noisy quantity is an anecdote.
At any single seed the gap's size varies (0.011 to 1.1451 in this
20-seed sweep); the structural claim that survives every seed is the
direction -- never negative.
- **The winning configuration itself, `Lasso(alpha=1)`, is a property of
seed 0.** Several nearby configurations -- `Lasso(alpha=0.3)` at
53.9863, `Ridge(alpha=10)` at 54.0335, plain OLS at 54.0926 -- score
within two-tenths of a point of the winner. Day 145's lesson about
near-tied configurations trading places under resampling applies here
too; the harness does not assert the same winner would hold at every
seed.
- **The realised coverage of the prediction interval, 0.9459 at seed 0,
is itself a sampled quantity on 111 test rows.** A separate 10-seed
check (not asserted by the harness, reported here for context) gives a
mean coverage of 0.9558, ranging from 0.9369 to 0.991 -- close to the
0.95 nominal rate on any single seed, and closer still on average.
## Timings
No timing is asserted anywhere in this lab. On the capture machine, one
seed's frame-to-verdict pipeline (baseline, sweep, cross-validate,
select, one test evaluation, residual diagnostics, prediction interval)
completed in 0.1386 seconds; the 20-seed leaky-gap comparison in exercise
10b, which cross-validates all 23 candidates 20 times over, completed in
2.9559 seconds; the full `report_measurements.py` run, which performs
that 20-seed sweep and the 2000-draw bootstrap, completed in 3.99 seconds
of user CPU time (4.13 seconds wall-clock). All of this runs on the CPU;
no GPU is present, needed, or used. A slower machine will take longer;
nothing here asserts a threshold.
examples-run.txt
................... [100%]
19 passed in 4.72s
measured-values.txt
Day 154 -- a complete regression project, measured
===================================================
1. The dataset: the only bundled regression set
-----------------------------------------------
shape: (442, 10) features: ['age', 'sex', 'bmi', 'bp', 's1', 's2', 's3', 's4', 's5', 's6']
y range: [25.0, 346.0] mean: 152.1335
the target is a composite disease-progression score with no physical unit
split: train=331 test=111 (seed 0, 25 percent test)
2. The baseline, before any model
---------------------------------
mean-predictor baseline: RMSE 70.4637 R2 -0.0001
3. The sweep: cross-validate every candidate on train rows only
---------------------------------------------------------------
K = 23 candidate pipelines: 11 ridge, 11 lasso, 1 plain OLS
winner: lasso (alpha=1) 5-fold CV RMSE = 53.8958
4. ONE test evaluation
----------------------
test RMSE: 56.5566 R2: 0.3557 MAE: 45.2846
second evaluation : TestSetTouchedTwice
the test set has already been used once; any further score is a validation score, not a test score
5. The margin, with a bootstrap interval
----------------------------------------
margin (baseline RMSE - model RMSE): +13.9071
95 percent bootstrap interval on the margin: [5.5852, 22.3324]
distinguishable from baseline at this test-set size: True
6. Residual diagnostics -- the centrepiece
------------------------------------------
residuals: mean -3.6262 sd 56.4402
heteroscedasticity signal, corr(fitted, |residual|): +0.2386
curvature signal, corr(fitted^2, residual): -0.1278
normal-probability (Q-Q) correlation: 0.9901
largest residuals (row, true, predicted, residual):
row 60: true= 52.0 pred= 209.3 residual= -157.3
row 65: true= 302.0 pred= 153.9 residual= +148.1
row 24: true= 68.0 pred= 202.8 residual= -134.8
row 9: true= 99.0 pred= 230.1 residual= -131.1
row 64: true= 132.0 pred= 261.3 residual= -129.3
7. Is the model worse for high-value targets?
---------------------------------------------
RMSE on below-median targets: 55.2464
RMSE on above-median targets: 57.8601
ratio (high / low): 1.0473
8. The leaky version: selecting by peeking at the test set
----------------------------------------------------------
honest (select on CV, look once): 56.5566
leaky (best of 23 scored directly on test): 55.5212
gap (honest - leaky, positive means the leak looked better): +1.0354
8b. The leaky gap, over 20 seeds
--------------------------------
seed honest leaky gap
0 56.5566 55.5212 +1.0354
1 54.1762 53.8807 +0.2955
2 54.3055 53.8764 +0.4291
3 55.0771 54.7174 +0.3597
4 53.8442 53.7681 +0.0761
5 55.8645 55.1545 +0.7100
6 56.1914 55.1399 +1.0515
7 54.3182 53.7125 +0.6057
8 55.6894 54.5492 +1.1402
9 48.3584 48.2618 +0.0966
10 54.5696 54.4307 +0.1389
11 59.0362 58.8067 +0.2295
12 57.8902 57.6206 +0.2696
13 53.9688 53.2935 +0.6753
14 58.1023 57.5956 +0.5067
15 50.7788 49.6337 +1.1451
16 53.6874 53.1124 +0.5750
17 53.5888 52.5993 +0.9895
18 55.0428 54.8246 +0.2182
19 55.1773 55.1663 +0.0110
mean gap: +0.5279 sd 0.3686 min +0.0110 max +1.1451
fraction of seeds where the leak was non-negative: 1.0000
9. Prediction intervals, and their realised coverage
----------------------------------------------------
95 percent prediction interval half-width (from TRAIN out-of-fold residuals): +/-105.8797
realised coverage on the 111 test rows: 0.9459 (nominal: 0.9500)
10. What the whole thing costs
------------------------------
wall-clock cost is machine-dependent and not reproduced here byte for byte;
see metadata.yml and expected-output/FIELDS.md for the captured timing
starter-run.txt
ssssssssssssss..... [100%]
5 passed, 14 skipped in 0.56s
test-run.txt
1. Installed versions match requirements/requirements.txt
numpy 2.5.2
scikit-learn 1.9.0
pytest 9.1.1
ok: numpy 2.5.2 matches the pin
ok: scikit-learn 1.9.0 matches the pin
ok: pytest 9.1.1 matches the pin
2. Every published claim, reproduced directly (no pytest involved)
ok: exercises 1-11 reproduced directly against regression_lib, no pytest involved
3. examples/ passes in full
ok: pytest examples -q -> 19 passed
4. starter/ is an untouched skeleton
ok: pytest starter -q -> 5 passed, 14 skipped (the machinery checks pass; the fourteen exercises are stubs)
5. pytest examples starter (one invocation) aborts on the module-name collision
ok: combined invocation reports import file mismatch, as documented -- never run starter and examples together
6. The report reproduces the captured table exactly
ok: report_measurements.py output is byte-identical to expected-output/measured-values.txt
7. The test set is evaluated EXACTLY ONCE in the reference run
ok: GatedTestSet enforces exactly one evaluation mechanically, not by convention
8. Proof the harness can fail
ok: scratch copy of examples/ passes before it is broken
ok: breaking exercise 8b's assertion produces a non-zero exit and names the failing test
9. The leaky-gap direction holds beyond the quoted seed range
ok: the leaky-gap direction and the selection mechanics hold at seeds this lab does not quote
10. Offline, and nothing left behind
ok: no URLs inside examples/ or starter/ source -- this lab reaches no network beyond the bundled dataset
ok: no __pycache__ left behind (cleaned during this run)
ok: no .pytest_cache left behind (cleaned during this run)
---------------------------------------------------------------
15 checks, 0 failure(s)
exit=0
Source files
examples/regression_lib.py (19560 bytes)
"""One regression project, run properly, once.
Days 148-153 each isolated one discipline: the one-predictor model and its
four assumptions, the loss as a choice, multicollinearity, ridge and lasso,
the metrics that can be gamed or inverted, and OLS built from scratch. This
module spends every one of those disciplines on a single real dataset and
produces one defensible verdict, with residual diagnostics as the
centrepiece no other day owns.
Frame, baseline, split, pipeline, cross-validate, select, ONE test
evaluation, residual diagnostics, a fairness check, prediction intervals,
an honest interval on the margin. Nothing here is taught for the first
time; everything here is used.
"""
from __future__ import annotations
import numpy as np
from sklearn.datasets import load_diabetes
from sklearn.dummy import DummyRegressor
from sklearn.linear_model import Lasso, LinearRegression, Ridge
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
from sklearn.model_selection import KFold, cross_val_predict, cross_val_score, train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
# --------------------------------------------------------------------------
# 1. The dataset -- the only bundled regression set that needs no download
# --------------------------------------------------------------------------
def load_dataset():
"""The Wisconsin/diabetes progression set, in raw measurement units.
``load_diabetes`` is the only regression dataset scikit-learn bundles
offline; ``fetch_california_housing`` downloads and is not used here.
``scaled=False`` keeps the ten features in their original units (age in
years, bmi, average blood pressure, six serum measures) so a coefficient
would still mean something if this project stopped to interpret one --
Day 148's point, carried forward. The target itself is a composite
disease-progression score with no physical unit; it is not measured in
anything, and this project never pretends otherwise.
"""
d = load_diabetes(scaled=False)
return d.data, d.target, list(d.feature_names)
# --------------------------------------------------------------------------
# 2. The frame and the baseline -- before any model
# --------------------------------------------------------------------------
def baseline_metrics(x_train, y_train, x_test, y_test):
"""The mean-predictor baseline: RMSE and R^2, computed before any model.
Day 141's rule, restated for regression: a score is not evidence until
you know what it beats. Predicting the training mean for every row is
the simplest possible non-model.
"""
dummy = DummyRegressor(strategy="mean").fit(x_train, y_train)
pred = dummy.predict(x_test)
rmse = float(np.sqrt(mean_squared_error(y_test, pred)))
r2 = float(r2_score(y_test, pred))
return round(rmse, 4), round(r2, 4)
# --------------------------------------------------------------------------
# 3. The split -- 442 rows is small, so the test set stays small too
# --------------------------------------------------------------------------
def split_once(X, y, seed: int = 0, test_size: float = 0.25):
"""One split. The test half is touched once, later, for scoring.
A 25 percent test set on 442 rows is about 110 rows -- small enough
that every interval in this project is wide, and that is reported
honestly rather than smoothed over.
"""
return train_test_split(X, y, test_size=test_size, random_state=seed)
# --------------------------------------------------------------------------
# 4. The candidate pipelines -- ridge, lasso and plain OLS, K counted
# --------------------------------------------------------------------------
_ALPHAS = [0.001, 0.003, 0.01, 0.03, 0.1, 0.3, 1, 3, 10, 30, 100]
def candidate_configs():
"""23 candidate pipelines: 11 ridge, 11 lasso, 1 plain OLS.
Every candidate is a scikit-learn ``Pipeline`` with a ``StandardScaler``
ahead of the estimator, so cross-validation refits the scaler on each
fold's training rows only -- Day 143's stage-ordering rule, enforced by
the estimator's own contract. Returns ``(family, hyperparameter,
make_pipeline)`` where ``make_pipeline`` is a zero-argument callable
returning a fresh, unfitted ``Pipeline``.
"""
configs = []
for a in _ALPHAS:
configs.append(
("ridge", a, lambda a=a: Pipeline([("scale", StandardScaler()), ("clf", Ridge(alpha=a))]))
)
for a in _ALPHAS:
configs.append(
(
"lasso",
a,
lambda a=a: Pipeline(
[("scale", StandardScaler()), ("clf", Lasso(alpha=a, max_iter=20000))]
),
)
)
configs.append(
("ols", 0.0, lambda: Pipeline([("scale", StandardScaler()), ("clf", LinearRegression())]))
)
return configs
def candidate_count() -> int:
"""K, the number of configurations this project actually tries."""
return len(candidate_configs())
# --------------------------------------------------------------------------
# 5. Cross-validate, then select -- honest spending of the train rows
# --------------------------------------------------------------------------
def cross_validate_configs(x_train, y_train, seed: int = 0, folds: int = 5):
"""5-fold CV RMSE for every candidate, on train rows only.
Returns rows of ``(family, hyperparameter, cv_rmse, cv_std)``, sorted
best (lowest RMSE) first. RMSE is chosen as the selection metric because
it is stated as the metric BEFORE any model is fitted -- Day 152's whole
argument that the metric is a choice, made here in the units the target
is actually in (unitless composite-score points, not mg/dL).
"""
splitter = KFold(n_splits=folds, shuffle=True, random_state=seed)
rows = []
for family, param, make in candidate_configs():
scores = cross_val_score(make(), x_train, y_train, cv=splitter, scoring="neg_root_mean_squared_error")
rows.append((family, param, round(float(-scores.mean()), 4), round(float(scores.std()), 4)))
rows.sort(key=lambda r: r[2])
return rows
def select_best(x_train, y_train, seed: int = 0, folds: int = 5):
"""Fit the winner of the sweep on the full training set.
Returns ``(family, hyperparameter, cv_rmse, fitted_pipeline)``.
"""
rows = cross_validate_configs(x_train, y_train, seed=seed, folds=folds)
winner_family, winner_param, winner_cv, _sd = rows[0]
for family, param, make_fn in candidate_configs():
if family == winner_family and param == winner_param:
fitted = make_fn().fit(x_train, y_train)
return winner_family, winner_param, winner_cv, fitted
raise RuntimeError("winning configuration vanished between sweep and refit")
# --------------------------------------------------------------------------
# 6. The test set -- one look, enforced mechanically
# --------------------------------------------------------------------------
class TestSetTouchedTwice(RuntimeError):
"""Raised when the test set is scored against more than once."""
class GatedTestSet:
"""A test set that permits exactly one scoring call, then refuses.
Day 144's discipline, reused unchanged: the counter does not advance on
a refused attempt, so a caller that never succeeds cannot drain the
budget by retrying. ``evaluate`` returns ``(rmse, r2, mae)`` -- the one
look this project spends. Reading predictions afterward for residual
diagnostics is inspection, not a second selection, exactly as Day 147's
confusion matrix read predictions after its own one evaluation.
"""
def __init__(self, X, y):
self._X = X
self._y = y
self.evaluations = 0
def evaluate(self, model):
if self.evaluations >= 1:
raise TestSetTouchedTwice(
"the test set has already been used once; any further score is a "
"validation score, not a test score"
)
self.evaluations += 1
pred = model.predict(self._X)
rmse = float(np.sqrt(mean_squared_error(self._y, pred)))
r2 = float(r2_score(self._y, pred))
mae = float(mean_absolute_error(self._y, pred))
return round(rmse, 4), round(r2, 4), round(mae, 4)
# --------------------------------------------------------------------------
# 7. The verdict -- a bootstrap interval around the margin
# --------------------------------------------------------------------------
def margin_bootstrap_interval(y_test, pred_baseline, pred_model, n_boot: int = 2000, seed: int = 0):
"""A 95 percent interval around the margin: baseline RMSE minus model RMSE.
Resamples the test rows with replacement ``n_boot`` times and recomputes
both RMSEs on each resample, so the interval reflects how much the
margin itself could plausibly move at this test-set size -- the same
question Day 144's accuracy interval asked, answered here for RMSE
without an analytic formula, because RMSE's sampling distribution has
no equally simple closed form.
"""
rng = np.random.default_rng(seed)
y_test = np.asarray(y_test)
pred_baseline = np.asarray(pred_baseline)
pred_model = np.asarray(pred_model)
n = len(y_test)
margins = np.empty(n_boot)
for i in range(n_boot):
idx = rng.integers(0, n, size=n)
rmse_base = np.sqrt(mean_squared_error(y_test[idx], pred_baseline[idx]))
rmse_model = np.sqrt(mean_squared_error(y_test[idx], pred_model[idx]))
margins[i] = rmse_base - rmse_model
lower = round(float(np.percentile(margins, 2.5)), 4)
upper = round(float(np.percentile(margins, 97.5)), 4)
return lower, upper
def margin_distinguishable(lower: float, upper: float) -> bool:
"""Whether the bootstrap interval around the margin excludes zero.
If the interval spans zero, the honest verdict is "cannot distinguish
this model from the baseline at this test-set size" -- Day 144's
cautionary case, now checked for a real margin instead of assumed away.
"""
return lower > 0.0
# --------------------------------------------------------------------------
# 8. Residual diagnostics -- the centrepiece this day owns
# --------------------------------------------------------------------------
def _normal_ppf(p):
"""The inverse standard-normal CDF, by Acklam's rational approximation.
Built from scratch because scipy is not one of this lab's three pinned
dependencies -- Day 153's habit, applied again: when a tool is not
available, build the minimal piece you need rather than reach past the
pins. Accurate to about 1.15e-9 across the open interval (0, 1); good
enough for a Q-Q correlation on 111 points. Public-domain algorithm,
reference in sources.yml.
"""
a = [-3.969683028665376e01, 2.209460984245205e02, -2.759285104469687e02,
1.383577518672690e02, -3.066479806614716e01, 2.506628277459239e00]
b = [-5.447609879822406e01, 1.615858368580409e02, -1.556989798598866e02,
6.680131188771972e01, -1.328068155288572e01]
c = [-7.784894002430293e-03, -3.223964580411365e-01, -2.400758277161838e00,
-2.549732539343734e00, 4.374664141464968e00, 2.938163982698783e00]
d = [7.784695709041462e-03, 3.224671290700398e-01, 2.445134137142996e00,
3.754408661907416e00]
p_low = 0.02425
p = np.asarray(p, dtype=float)
out = np.empty_like(p)
low = p < p_low
high = p > (1 - p_low)
mid = ~(low | high)
ql = np.sqrt(-2 * np.log(p[low]))
out[low] = (((((c[0] * ql + c[1]) * ql + c[2]) * ql + c[3]) * ql + c[4]) * ql + c[5]) / (
(((d[0] * ql + d[1]) * ql + d[2]) * ql + d[3]) * ql + 1
)
q = p[mid] - 0.5
r = q * q
out[mid] = (((((a[0] * r + a[1]) * r + a[2]) * r + a[3]) * r + a[4]) * r + a[5]) * q / (
(((((b[0] * r + b[1]) * r + b[2]) * r + b[3]) * r + b[4]) * r + 1)
)
qh = np.sqrt(-2 * np.log(1 - p[high]))
out[high] = -(((((c[0] * qh + c[1]) * qh + c[2]) * qh + c[3]) * qh + c[4]) * qh + c[5]) / (
(((d[0] * qh + d[1]) * qh + d[2]) * qh + d[3]) * qh + 1
)
return out
def residual_summary(y_test, pred_test):
"""Mean and standard deviation of the test residuals, y minus prediction."""
resid = np.asarray(y_test, dtype=float) - np.asarray(pred_test, dtype=float)
return round(float(resid.mean()), 4), round(float(resid.std()), 4)
def heteroscedasticity_signal(pred_test, y_test):
"""Correlation between the fitted value and the absolute residual.
A value near zero means the spread of errors does not grow with the
predicted value -- the "fanning out" pattern a residual-vs-fitted plot
is built to catch. This is the from-scratch numeric version of reading
that plot.
"""
pred_test = np.asarray(pred_test, dtype=float)
resid = np.asarray(y_test, dtype=float) - pred_test
corr = np.corrcoef(pred_test, np.abs(resid))[0, 1]
return round(float(corr), 4)
def curvature_signal(pred_test, y_test):
"""Correlation between the squared fitted value and the signed residual.
A value far from zero suggests the model is missing a systematic curve
-- residuals that trend up then down (or the reverse) as the fitted
value rises, rather than scattering evenly around zero.
"""
pred_test = np.asarray(pred_test, dtype=float)
resid = np.asarray(y_test, dtype=float) - pred_test
corr = np.corrcoef(pred_test**2, resid)[0, 1]
return round(float(corr), 4)
def normal_probability_correlation(y_test, pred_test):
"""A from-scratch normal-probability (Q-Q) check, as one correlation.
Standardises the residuals, sorts them, and correlates them against the
theoretical normal quantiles a perfectly Gaussian set of residuals
would produce. 1.0 is a perfectly straight Q-Q line; values noticeably
below 1.0 flag departures from normality that a plotted Q-Q line would
show as curvature at the tails.
"""
resid = np.asarray(y_test, dtype=float) - np.asarray(pred_test, dtype=float)
n = len(resid)
std_resid = (resid - resid.mean()) / resid.std()
sorted_resid = np.sort(std_resid)
probs = (np.arange(1, n + 1) - 0.5) / n
theoretical = _normal_ppf(probs)
corr = np.corrcoef(theoretical, sorted_resid)[0, 1]
return round(float(corr), 4)
def largest_residuals(y_test, pred_test, n: int = 5):
"""The n largest-magnitude residuals, inspected individually.
Returns rows of ``(test_row_index, true_value, predicted_value,
residual)``, sorted by |residual| descending. A confusion matrix reads
the specific mistakes a classifier makes; this is that discipline's
regression counterpart.
"""
y_test = np.asarray(y_test, dtype=float)
pred_test = np.asarray(pred_test, dtype=float)
resid = y_test - pred_test
order = np.argsort(-np.abs(resid))[:n]
return [
(int(i), round(float(y_test[i]), 4), round(float(pred_test[i]), 4), round(float(resid[i]), 4))
for i in order
]
# --------------------------------------------------------------------------
# 9. Is the model worse for high-value targets? Measure it.
# --------------------------------------------------------------------------
def error_by_target_level(y_test, pred_test):
"""RMSE on the below-median half of test targets against the above-median half.
Returns ``(rmse_low, rmse_high, ratio)`` where ``ratio`` is
``rmse_high / rmse_low``. On a disease-progression score this is a
fairness-relevant question, not only a statistical one: are errors
worse for patients whose true progression is more severe?
"""
y_test = np.asarray(y_test, dtype=float)
pred_test = np.asarray(pred_test, dtype=float)
median_y = float(np.median(y_test))
low_mask = y_test <= median_y
high_mask = ~low_mask
rmse_low = float(np.sqrt(mean_squared_error(y_test[low_mask], pred_test[low_mask])))
rmse_high = float(np.sqrt(mean_squared_error(y_test[high_mask], pred_test[high_mask])))
return round(rmse_low, 4), round(rmse_high, 4), round(rmse_high / rmse_low, 4)
# --------------------------------------------------------------------------
# 10. The leaky version -- selecting by peeking at the test set
# --------------------------------------------------------------------------
def leaky_selection_test_rmse(x_train, y_train, x_test, y_test):
"""Select the winner by fitting every candidate and scoring it on TEST.
Returns the winning (lowest) test RMSE -- K looks disguised as one,
Day 147's mistake, reconstructed for regression. Lower RMSE is better,
so the leaky search can only match or beat the honestly selected
model's own test RMSE, never lose to it.
"""
best_rmse = None
for _family, _param, make in candidate_configs():
pipe = make().fit(x_train, y_train)
pred = pipe.predict(x_test)
rmse = float(np.sqrt(mean_squared_error(y_test, pred)))
if best_rmse is None or rmse < best_rmse:
best_rmse = rmse
return round(best_rmse, 4)
def leaky_vs_honest_over_seeds(X, y, seeds=range(20), folds: int = 5):
"""The gap between peeking at the test set and looking at it once.
Returns rows of ``(seed, honest_rmse, leaky_rmse, gap)`` where
``gap = honest_rmse - leaky_rmse``. Because lower RMSE is better, a
positive gap means the leak reported a lower (better-looking) error
than the honest evaluation -- the leak can only help the reported
number, never hurt it.
"""
rows = []
for seed in seeds:
x_train, x_test, y_train, y_test = split_once(X, y, seed=seed)
_family, _param, _cv, fitted = select_best(x_train, y_train, seed=seed, folds=folds)
honest_rmse = float(np.sqrt(mean_squared_error(y_test, fitted.predict(x_test))))
leaky_rmse = leaky_selection_test_rmse(x_train, y_train, x_test, y_test)
rows.append((seed, round(honest_rmse, 4), leaky_rmse, round(honest_rmse - leaky_rmse, 4)))
return rows
# --------------------------------------------------------------------------
# 11. Prediction intervals -- not just a point, and measured coverage
# --------------------------------------------------------------------------
def prediction_interval_coverage(x_train, y_train, x_test, y_test, fitted, seed: int = 0, folds: int = 5):
"""A constant-width 95 percent prediction interval, and its realised coverage.
The half-width comes from the standard deviation of out-of-fold
residuals on the TRAINING rows only (``cross_val_predict``), never from
the test residuals themselves -- using the test residuals to size the
test interval would be circular. Returns ``(half_width, coverage)``
where ``coverage`` is the fraction of test targets that actually fall
inside ``prediction +/- half_width``, measured against the nominal 0.95.
"""
splitter = KFold(n_splits=folds, shuffle=True, random_state=seed)
oof_pred = cross_val_predict(fitted, x_train, y_train, cv=splitter)
oof_resid = np.asarray(y_train, dtype=float) - oof_pred
half_width = round(float(1.96 * oof_resid.std()), 4)
pred_test = fitted.predict(x_test)
y_test = np.asarray(y_test, dtype=float)
lower = pred_test - half_width
upper = pred_test + half_width
coverage = round(float(np.mean((y_test >= lower) & (y_test <= upper))), 4)
return half_width, coverage
examples/report_measurements.py (5580 bytes)
#!/usr/bin/env python3
"""Print every measured pair in this lab as one table.
The harness compares this output byte for byte against
expected-output/measured-values.txt, so the report is not a convenience:
it is how the lab notices that a number in the lesson has gone stale.
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
import numpy as np # noqa: E402
from sklearn.dummy import DummyRegressor # noqa: E402
import regression_lib as r # noqa: E402
def rule(title: str) -> None:
print()
print(title)
print("-" * len(title))
def main() -> None:
print("Day 154 -- a complete regression project, measured")
print("=" * 51)
rule("1. The dataset: the only bundled regression set")
X, y, names = r.load_dataset()
print(f" shape: {X.shape} features: {names}")
print(f" y range: [{float(y.min()):.1f}, {float(y.max()):.1f}] mean: {float(y.mean()):.4f}")
print(" the target is a composite disease-progression score with no physical unit")
x_train, x_test, y_train, y_test = r.split_once(X, y, seed=0)
print(f" split: train={x_train.shape[0]} test={x_test.shape[0]} (seed 0, 25 percent test)")
rule("2. The baseline, before any model")
base_rmse, base_r2 = r.baseline_metrics(x_train, y_train, x_test, y_test)
print(f" mean-predictor baseline: RMSE {base_rmse:.4f} R2 {base_r2:.4f}")
rule("3. The sweep: cross-validate every candidate on train rows only")
k = r.candidate_count()
print(f" K = {k} candidate pipelines: 11 ridge, 11 lasso, 1 plain OLS")
family, param, cv_rmse, fitted = r.select_best(x_train, y_train, seed=0)
print(f" winner: {family} (alpha={param}) 5-fold CV RMSE = {cv_rmse:.4f}")
rule("4. ONE test evaluation")
gate = r.GatedTestSet(x_test, y_test)
test_rmse, test_r2, test_mae = gate.evaluate(fitted)
print(f" test RMSE: {test_rmse:.4f} R2: {test_r2:.4f} MAE: {test_mae:.4f}")
try:
gate.evaluate(fitted)
print(" second evaluation: NO ERROR RAISED")
except r.TestSetTouchedTwice as exc:
print(f" second evaluation : {type(exc).__name__}")
print(f" {exc}")
rule("5. The margin, with a bootstrap interval")
baseline_model = DummyRegressor(strategy="mean").fit(x_train, y_train)
pred_baseline = baseline_model.predict(x_test)
pred_model = fitted.predict(x_test)
margin = round(base_rmse - test_rmse, 4)
lower, upper = r.margin_bootstrap_interval(y_test, pred_baseline, pred_model, seed=0)
print(f" margin (baseline RMSE - model RMSE): {margin:+.4f}")
print(f" 95 percent bootstrap interval on the margin: [{lower:.4f}, {upper:.4f}]")
print(f" distinguishable from baseline at this test-set size: {r.margin_distinguishable(lower, upper)}")
rule("6. Residual diagnostics -- the centrepiece")
resid_mean, resid_std = r.residual_summary(y_test, pred_model)
het = r.heteroscedasticity_signal(pred_model, y_test)
curv = r.curvature_signal(pred_model, y_test)
qq = r.normal_probability_correlation(y_test, pred_model)
print(f" residuals: mean {resid_mean:+.4f} sd {resid_std:.4f}")
print(f" heteroscedasticity signal, corr(fitted, |residual|): {het:+.4f}")
print(f" curvature signal, corr(fitted^2, residual): {curv:+.4f}")
print(f" normal-probability (Q-Q) correlation: {qq:.4f}")
print(" largest residuals (row, true, predicted, residual):")
for row_idx, true_val, pred_val, resid in r.largest_residuals(y_test, pred_model, n=5):
print(f" row {row_idx:3d}: true={true_val:7.1f} pred={pred_val:7.1f} residual={resid:+8.1f}")
rule("7. Is the model worse for high-value targets?")
rmse_low, rmse_high, ratio = r.error_by_target_level(y_test, pred_model)
print(f" RMSE on below-median targets: {rmse_low:.4f}")
print(f" RMSE on above-median targets: {rmse_high:.4f}")
print(f" ratio (high / low): {ratio:.4f}")
rule("8. The leaky version: selecting by peeking at the test set")
leaky_rmse = r.leaky_selection_test_rmse(x_train, y_train, x_test, y_test)
print(f" honest (select on CV, look once): {test_rmse:.4f}")
print(f" leaky (best of {k} scored directly on test): {leaky_rmse:.4f}")
print(f" gap (honest - leaky, positive means the leak looked better): {round(test_rmse - leaky_rmse, 4):+.4f}")
rule("8b. The leaky gap, over 20 seeds")
rows = r.leaky_vs_honest_over_seeds(X, y, seeds=range(20))
gaps = np.array([row[3] for row in rows])
print(" seed honest leaky gap")
for seed, honest_s, leaky_s, gap_s in rows:
print(f" {seed:5d} {honest_s:.4f} {leaky_s:.4f} {gap_s:+.4f}")
print(f" mean gap: {float(gaps.mean()):+.4f} sd {float(gaps.std()):.4f} min {float(gaps.min()):+.4f} max {float(gaps.max()):+.4f}")
print(f" fraction of seeds where the leak was non-negative: {float((gaps >= 0).mean()):.4f}")
rule("9. Prediction intervals, and their realised coverage")
half_width, coverage = r.prediction_interval_coverage(x_train, y_train, x_test, y_test, fitted, seed=0)
print(f" 95 percent prediction interval half-width (from TRAIN out-of-fold residuals): +/-{half_width:.4f}")
print(f" realised coverage on the 111 test rows: {coverage:.4f} (nominal: 0.9500)")
rule("10. What the whole thing costs")
print(" wall-clock cost is machine-dependent and not reproduced here byte for byte;")
print(" see metadata.yml and expected-output/FIELDS.md for the captured timing")
if __name__ == "__main__":
main()
examples/test_regression_claims.py (9147 bytes)
"""The reference solutions: one regression project, run properly, once.
Every number here was captured from a real run of this file on the
authoring machine. If a number changes, the claim in the lesson is wrong
and one of the two must be fixed.
"""
import inspect
import numpy as np
import pytest
from sklearn.datasets import fetch_california_housing, load_diabetes
from sklearn.dummy import DummyRegressor
import regression_lib as r
@pytest.fixture(scope="module")
def dataset():
return r.load_dataset()
@pytest.fixture(scope="module")
def split(dataset):
X, y, _names = dataset
return r.split_once(X, y, seed=0)
# --- 1. The dataset, and why it is the only one used ------------------------
def test_01_the_dataset_is_the_only_bundled_regression_set(dataset):
X, y, names = dataset
assert X.shape == (442, 10)
assert names == ["age", "sex", "bmi", "bp", "s1", "s2", "s3", "s4", "s5", "s6"]
assert round(float(y.min()), 4) == 25.0
assert round(float(y.max()), 4) == 346.0
assert round(float(y.mean()), 4) == 152.1335
# fetch_california_housing downloads by default -- forbidden by this
# lab's offline rule -- while load_diabetes never touches the network.
sig = inspect.signature(fetch_california_housing)
assert sig.parameters["download_if_missing"].default is True
def test_01b_raw_units_are_not_the_scikit_learn_default(dataset):
X, y, names = dataset
# This lab's loader asks for scaled=False. Compare against scikit-learn's
# own default (scaled=True) to see why: the default is mean-centred and
# variance-scaled to tiny floats, not the years and mg/dL a reader could
# sanity-check a coefficient against.
default = load_diabetes(scaled=True)
age_index = names.index("age")
assert X[:, age_index].min() == 19.0
assert X[:, age_index].max() == 79.0
assert -0.2 < default.data[:, age_index].min() < 0
assert 0 < default.data[:, age_index].max() < 0.2
# --- 2. The frame and the baseline -----------------------------------------
def test_02_the_baseline_before_any_model(split):
x_train, x_test, y_train, y_test = split
rmse, r2 = r.baseline_metrics(x_train, y_train, x_test, y_test)
assert rmse == 70.4637
assert r2 == -0.0001
# Every model below has to beat 70.4637 RMSE to be worth building.
# --- 3. The split -------------------------------------------------------
def test_03_the_split_holds_the_test_rows_back(dataset):
X, y, _names = dataset
x_train, x_test, y_train, y_test = r.split_once(X, y, seed=0)
assert x_train.shape == (331, 10)
assert x_test.shape == (111, 10)
# 111 test rows on a 442-row dataset: small, and every interval below
# is wide because of it -- that is the point, not a flaw to fix.
# --- 4. The sweep --------------------------------------------------------
def test_04_the_sweep_counts_twenty_three_candidate_pipelines():
assert r.candidate_count() == 23
families = [family for family, _param, _make in r.candidate_configs()]
assert families.count("ridge") == 11
assert families.count("lasso") == 11
assert families.count("ols") == 1
# --- 5. Cross-validate, then select --------------------------------------
def test_05_cross_validation_selects_the_winner_on_train_rows_only(split):
x_train, _x_test, y_train, _y_test = split
family, param, cv_rmse, fitted = r.select_best(x_train, y_train, seed=0)
assert (family, param) == ("lasso", 1)
assert cv_rmse == 53.8958
assert hasattr(fitted, "predict")
# The winner was chosen on cross-validated train rows; test has not
# been touched.
# --- 6. The gate: one look at the test set ----------------------------------
def test_06_the_gate_permits_exactly_one_test_evaluation(split):
x_train, x_test, y_train, y_test = split
_family, _param, _cv, fitted = r.select_best(x_train, y_train, seed=0)
gate = r.GatedTestSet(x_test, y_test)
assert gate.evaluations == 0
rmse, r2, mae = gate.evaluate(fitted)
assert rmse == 56.5566
assert r2 == 0.3557
assert mae == 45.2846
assert gate.evaluations == 1
with pytest.raises(r.TestSetTouchedTwice) as excinfo:
gate.evaluate(fitted)
assert "validation score" in str(excinfo.value)
assert gate.evaluations == 1
# --- 7. The margin, with a bootstrap interval ------------------------------
def test_07_the_margin_has_a_bootstrap_interval(split):
x_train, x_test, y_train, y_test = split
baseline = DummyRegressor(strategy="mean").fit(x_train, y_train)
_family, _param, _cv, fitted = r.select_best(x_train, y_train, seed=0)
pred_baseline = baseline.predict(x_test)
pred_model = fitted.predict(x_test)
lower, upper = r.margin_bootstrap_interval(y_test, pred_baseline, pred_model, seed=0)
assert lower == 5.5852
assert upper == 22.3324
rmse_base, _r2_base = r.baseline_metrics(x_train, y_train, x_test, y_test)
rmse_model = float(np.sqrt(np.mean((y_test - pred_model) ** 2)))
margin = round(rmse_base - rmse_model, 4)
assert margin == 13.9071
# The margin (13.9071) clears the interval's lower bound (5.5852): this
# model IS distinguishable from the baseline at this test-set size.
assert r.margin_distinguishable(lower, upper) is True
# --- 8. Residual diagnostics -- the centrepiece ----------------------------
def test_08_the_residual_vs_fitted_diagnostic(split):
x_train, x_test, y_train, y_test = split
_family, _param, _cv, fitted = r.select_best(x_train, y_train, seed=0)
pred = fitted.predict(x_test)
resid_mean, resid_std = r.residual_summary(y_test, pred)
assert resid_mean == -3.6262
assert resid_std == 56.4402
het = r.heteroscedasticity_signal(pred, y_test)
curv = r.curvature_signal(pred, y_test)
assert het == 0.2386
assert curv == -0.1278
# A modest positive heteroscedasticity signal (errors fan out a little
# as predictions rise) and a weak curvature signal (no strong missed
# trend) -- neither dramatic, both worth reporting rather than ignoring.
def test_08b_the_normal_probability_check_and_the_largest_residuals(split):
x_train, x_test, y_train, y_test = split
_family, _param, _cv, fitted = r.select_best(x_train, y_train, seed=0)
pred = fitted.predict(x_test)
qq = r.normal_probability_correlation(y_test, pred)
assert qq == 0.9901
rows = r.largest_residuals(y_test, pred, n=5)
assert len(rows) == 5
assert rows[0] == (60, 52.0, 209.3314, -157.3314)
assert rows[1] == (65, 302.0, 153.8865, 148.1135)
# 0.9901 is close to a perfectly straight Q-Q line: these residuals are
# not wildly non-normal, even on real data with only 111 test rows.
# --- 9. Is the model worse for high-value targets? Measure it. -------------
def test_09_error_by_target_level(split):
x_train, x_test, y_train, y_test = split
_family, _param, _cv, fitted = r.select_best(x_train, y_train, seed=0)
pred = fitted.predict(x_test)
rmse_low, rmse_high, ratio = r.error_by_target_level(y_test, pred)
assert rmse_low == 55.2464
assert rmse_high == 57.8601
assert ratio == 1.0473
# Only 4.73 percent worse on the more-severe half: not the dramatic
# fairness problem this exercise sets out to check for, at this seed --
# and that is exactly why the check has to run rather than be assumed.
# --- 10. The leaky version --------------------------------------------------
def test_10_the_leaky_version_selects_by_peeking_at_the_test_set(split):
x_train, x_test, y_train, y_test = split
_family, _param, _cv, fitted = r.select_best(x_train, y_train, seed=0)
honest_rmse = round(float(np.sqrt(np.mean((y_test - fitted.predict(x_test)) ** 2))), 4)
leaky_rmse = r.leaky_selection_test_rmse(x_train, y_train, x_test, y_test)
assert honest_rmse == 56.5566
assert leaky_rmse == 55.5212
# Lower RMSE is better: the leak can only match or beat the honest
# score, never lose to it.
assert leaky_rmse <= honest_rmse
def test_10b_the_leaky_gap_over_twenty_seeds(dataset):
X, y, _names = dataset
rows = r.leaky_vs_honest_over_seeds(X, y, seeds=range(20))
assert len(rows) == 20
gaps = np.array([row[3] for row in rows])
assert round(float(gaps.mean()), 4) == 0.5279
assert round(float(gaps.std()), 4) == 0.3686
assert round(float(gaps.min()), 4) == 0.011
assert round(float(gaps.max()), 4) == 1.1451
# The leak never once hurt the reported number, across 20 independent
# seeds: the mechanism, not luck.
assert (gaps >= 0).all()
# --- 11. Prediction intervals, and their realised coverage -----------------
def test_11_prediction_interval_coverage(split):
x_train, x_test, y_train, y_test = split
_family, _param, _cv, fitted = r.select_best(x_train, y_train, seed=0)
half_width, coverage = r.prediction_interval_coverage(
x_train, y_train, x_test, y_test, fitted, seed=0
)
assert half_width == 105.8797
assert coverage == 0.9459
# Nominal is 0.95; measured is 0.9459 -- close, on only 111 test rows.
assert 0.85 < coverage <= 1.0
examples/test_regression_lib.py (2331 bytes)
"""Machinery checks: the helpers behave, before any claim is made.
These five tests are solved in both `starter/` and `examples/`. They exist
so that a broken helper reports itself as a broken helper rather than as a
surprising scientific result.
"""
import numpy as np
import pytest
import regression_lib as r
def test_the_dataset_loads_offline_and_matches_its_shape():
X, y, names = r.load_dataset()
assert X.shape == (442, 10)
assert y.shape == (442,)
assert names == ["age", "sex", "bmi", "bp", "s1", "s2", "s3", "s4", "s5", "s6"]
assert float(y.min()) == 25.0
assert float(y.max()) == 346.0
def test_candidate_configs_really_are_twenty_three_distinct_pipelines():
configs = r.candidate_configs()
assert len(configs) == 23
seen = set()
for family, param, make in configs:
pipe = make()
assert hasattr(pipe, "fit") and hasattr(pipe, "predict")
seen.add((family, param))
# No two configs share a (family, hyperparameter) pair.
assert len(seen) == 23
def test_the_gated_test_set_counts_and_refuses():
class AlwaysMean:
def predict(self, X):
return np.full(len(X), 150.0)
y = np.array([100.0, 200.0, 150.0, 150.0])
gate = r.GatedTestSet(np.zeros((4, 2)), y)
rmse, r2, mae = gate.evaluate(AlwaysMean())
assert rmse > 0
with pytest.raises(r.TestSetTouchedTwice):
gate.evaluate(AlwaysMean())
# A fresh gate is a fresh budget; the class holds no global state.
fresh_rmse, _r2, _mae = r.GatedTestSet(np.zeros((4, 2)), y).evaluate(AlwaysMean())
assert fresh_rmse == rmse
def test_the_normal_ppf_matches_known_reference_points():
# Well-known standard-normal quantiles, to a few decimals.
assert abs(r._normal_ppf(np.array([0.5]))[0] - 0.0) < 1e-6
assert abs(r._normal_ppf(np.array([0.975]))[0] - 1.959964) < 1e-4
assert abs(r._normal_ppf(np.array([0.025]))[0] - (-1.959964)) < 1e-4
def test_largest_residuals_returns_them_sorted_by_magnitude():
y_test = np.array([10.0, 20.0, 30.0, 40.0])
pred_test = np.array([10.0, 25.0, 10.0, 41.0])
rows = r.largest_residuals(y_test, pred_test, n=2)
assert len(rows) == 2
# Row 2 (residual +20) is the largest in magnitude; row 3 (residual -1) is smallest.
assert rows[0][0] == 2
assert rows[0][3] == 20.0
metadata.yml (8295 bytes)
lesson_id: D154
day: 154
kind: guided-build
languages:
- python
- bash
setup_commands:
- cd labs/sections/machine-learning/day-154-a-complete-regression-project
- python3 -m venv .venv
- .venv/bin/pip install -r requirements/requirements.txt
- >-
.venv/bin/python3 -c "import numpy, sklearn; print(numpy.__version__,
sklearn.__version__)"
run_commands:
- .venv/bin/pytest examples -q
- .venv/bin/pytest starter -q
- .venv/bin/python3 examples/report_measurements.py
test_commands:
- bash tests/run_tests.sh
cleanup_commands:
- >-
find . -path ./.venv -prune -o -type d -name '__pycache__' -print -exec rm -rf -- {}
+
- rm -rf .pytest_cache
- 'rm -rf .venv # optional: removes the lab virtual environment'
- 'git checkout -- starter/ # optional: reset your work'
requires_network: true
requires_api_key: false
estimated_minutes: 70
last_executed: '2026-08-27'
executed_on: >-
macOS 26.5.2 (Apple Silicon, arm64, CPU only -- no GPU is needed or used), Python
3.14.0, numpy 2.5.2, scikit-learn 1.9.0, pytest 9.1.1, bash 3.2.57 -- bash
tests/run_tests.sh -> 15 checks, 0 failure(s), exit 0. pytest examples -q -> 19
passed. pytest starter -q -> 5 passed, 14 skipped (the five machinery checks in
test_regression_lib.py are solved in both directories; the fourteen exercise stubs
in starter/test_regression_claims.py are untouched). Everything ran through a real
lab-local .venv created by the documented setup commands; scikit-learn pulled in
scipy, joblib and threadpoolctl as its own dependencies, none of which this lab
imports directly (the Q-Q normal-probability check is built from scratch instead of
importing scipy). The lab is fully offline after the pip install -- the dataset is
bundled inside scikit-learn itself (load_diabetes), nothing is downloaded, and
harness check 10 confirms no URL appears anywhere in starter/ or examples/ source.
Section 8 of the harness copies examples/ into a mktemp -d scratch directory,
confirms 19 passed, rewrites the largest-residuals assertion in exercise 8b to a
value that cannot hold, confirms a non-zero exit naming the failing test, and
removes the scratch directory. Separately, by hand, the winning-configuration
assertion in exercise 5 was changed from ('lasso', 1) to ('ridge', 1) in
examples/test_regression_claims.py and the whole harness re-run: it reported 15
checks, 2 failure(s) and exited 1 (both the pytest run in section 3 and the
pytest-free direct reproduction in section 2 caught it); the file was restored and
the harness returned to 15 checks, 0 failure(s), exit 0. Measured wall-clock time
for one seed's frame-to-verdict pipeline: 0.1386s. The 20-seed leaky-gap comparison
in exercise 10b: 2.9559s. The full report_measurements.py run (which performs that
20-seed sweep and a 2000-draw bootstrap): 3.99s user CPU / 4.13s wall-clock.
THE DATASET: sklearn.datasets.load_diabetes(scaled=False), the only regression
dataset bundled inside scikit-learn -- fetch_california_housing downloads by
default (download_if_missing=True) and is forbidden by this lab's offline rule.
442 rows, 10 real-valued measurements in raw units (age in years, not a
mean-centred fraction), target range [25.0, 346.0], mean 152.1335. THE TARGET HAS
NO PHYSICAL UNIT -- it is a composite disease-progression score, and this lab says
so plainly rather than inventing one, following Day 152's precedent. MEASURED
PAIRS, all captured verbatim in expected-output/measured-values.txt. (1) Split
(seed 0, 25 percent test): 331 train rows, 111 test rows. (2) Mean-predictor
baseline, computed BEFORE any model: RMSE 70.4637, R2 -0.0001. (3) K = 23 candidate
pipelines (11 ridge alphas, 11 lasso alphas, 1 plain OLS), 5-fold cross-validation
on train rows only, scored by RMSE (the metric chosen before modelling, in the
target's own unitless composite-score points); the winner at seed 0 is
Lasso(alpha=1) at cv_rmse 53.8958 -- plain OLS scores 54.0926 at the same seed, only
0.1968 RMSE points behind, so regularisation's benefit on PREDICTION accuracy here
is real but modest, even though Day 150 already showed multicollinearity makes the
raw OLS COEFFICIENTS unstable. (4) The ONE test evaluation, enforced by a
GatedTestSet that raises TestSetTouchedTwice on any further attempt without
advancing its counter: test RMSE 56.5566, R2 0.3557, MAE 45.2846. (5) The margin
over baseline (70.4637 - 56.5566 = 13.9071 RMSE points) with a 2000-draw bootstrap
95 percent interval of [5.5852, 22.3324] (seeded, deterministic under the pinned
NumPy) -- the interval excludes zero, so the honest verdict is DISTINGUISHABLE, not
the "cannot distinguish" case a smaller improvement or a smaller test set could have
forced. (6) RESIDUAL DIAGNOSTICS, the centrepiece: residual mean -3.6262, sd
56.4402; heteroscedasticity signal (corr of fitted value with absolute residual)
+0.2386 -- a modest fanning-out, not dramatic; curvature signal (corr of squared
fitted value with signed residual) -0.1278 -- weak, no strong missed trend; a
from-scratch Q-Q normal-probability correlation of 0.9901 -- close to a straight
line, these residuals are close to normal even on real data with only 111 test
rows. The five largest residuals were inspected individually; the worst is test row
60, true value 52.0, predicted 209.3314, a residual of -157.3314 points. (7) ERROR
BY TARGET LEVEL, a fairness-relevant measurement on a disease-progression score:
RMSE 55.2464 on the below-median half of test targets, 57.8601 on the above-median
half, a ratio of 1.0473 -- only 4.73 percent worse on the more severe half at this
seed, not the dramatic asymmetry the check sets out to look for, and that is a real
finding rather than a disappointing one. (8) The leaky version -- selecting by
fitting all 23 candidates and scoring each directly on the test set instead of
selecting on cross-validated train rows: at seed 0 the leaky RMSE is 55.5212 against
the honest 56.5566, a gap of 1.0354 points in the leak's favour; over 20 seeds the
mean gap is 0.5279 (sd 0.3686, min 0.011, max 1.1451) and the gap is NEVER negative
at any seed -- the leak can only make the reported error look as good or better,
never worse. (9) PREDICTION INTERVALS: a constant 95 percent half-width of 105.8797,
built from the standard deviation of TRAINING out-of-fold residuals only (never from
the test residuals, which would be circular); realised coverage on the 111 test
rows was 0.9459 against the 0.95 nominal rate -- close, and a separate 10-seed check
(not asserted by the harness, reported here for context) gives a mean coverage of
0.9558, range 0.9369 to 0.991. FOUR HONESTY CALLS. FIRST: the margin over baseline
(13.9071 RMSE points) comfortably clears its own bootstrap interval's lower bound
(5.5852), so this project's verdict is DISTINGUISHABLE -- this is reported as the
measured outcome, not adjusted to manufacture the "cannot distinguish" case the
brief flagged as the more valuable one; on this dataset, at this seed, the model
genuinely beats the baseline by more than sampling noise could explain, and the
arithmetic that could have said otherwise is run rather than skipped. SECOND: the
heteroscedasticity and curvature signals (+0.2386 and -0.1278) are real but modest
-- neither is dramatic, and the lab reports them as such rather than either
overstating a problem or hiding a real (if mild) fanning-out pattern. THIRD: the
fairness check by target level came back nearly flat (ratio 1.0473) -- a genuinely
boring result at this seed, reported plainly rather than searched for a seed where
it would look more dramatic. FOURTH: the prediction-interval half-width is built
exclusively from TRAINING out-of-fold residuals, never from the test set's own
residuals; using test residuals to size an interval evaluated on the same test set
would be circular, and this lab does not do that even though it would have made
coverage numbers easier to hit exactly. Harness check 9 re-runs the leaky-gap
direction and the selection mechanics at seeds this lab does not quote (20-24 and
seed 41), so the headline directions are confirmed beyond the seeds reported in
prose.
requirements/README.md (2073 bytes)
# Requirements
`requirements.txt` pins the three packages this lab imports directly, at
the exact versions the captured output in `expected-output/` was produced
with:
```
numpy==2.5.2
scikit-learn==1.9.0
pytest==9.1.1
```
Installing scikit-learn also pulls in scipy, joblib and threadpoolctl as
its own dependencies. This lab imports none of them directly and does not
pin them; the versions present during capture are recorded in
`../expected-output/FIELDS.md`. The Q-Q normal-probability check is built
from scratch inside `regression_lib.py` specifically so this lab does not
need scipy.
## Why the versions are pinned exactly
`KFold(shuffle=True, random_state=...)`, `train_test_split`, and the
bootstrap resampling in `margin_bootstrap_interval` all depend on NumPy's
`Generator` bit stream, which NumPy's own documentation states carries no
cross-version compatibility guarantee. Different pinned versions can
legitimately shuffle the same seed into a different order, and every
downstream number -- the winning configuration, its cross-validated RMSE,
the test score, every residual diagnostic -- would move with it.
What does not depend on the pins: the RMSE and R2 formulas, which are
arithmetic; the direction of every structural result -- the leaky
selection score is never worse than the honest one, cross-validation
selects on train rows only, a gated test set refuses a second look; and
structural facts, such as the dataset used here having 442 rows and 10
features. `expected-output/FIELDS.md` separates the two categories in
full.
## Installing
From the lab directory:
```bash
python3 -m venv .venv
.venv/bin/pip install -r requirements/requirements.txt
```
The install step needs the network. Everything after it is offline: the
dataset is bundled inside scikit-learn itself, and nothing else is
downloaded.
## Free and open-source status
All three packages are free and open source -- NumPy and scikit-learn
under the BSD 3-Clause licence, pytest under the MIT licence. There is no
paid tier, no account and no API key anywhere in this lab.
requirements/requirements.txt (47 bytes)
numpy==2.5.2
scikit-learn==1.9.0
pytest==9.1.1
starter/00_brief.md (6464 bytes)
# Day 154 lab brief — A Complete Regression Project
Days 148 through 153 each isolated one discipline in a lab built to show
it in isolation: the one-predictor model and its four assumptions, the
loss function as a choice, multicollinearity, ridge and lasso, the
metrics that can be gamed or inverted, and OLS built from scratch.
This lab is not a new discipline. It is all six of them, spent on one
real dataset, in the order a working project actually uses them: frame,
baseline, split, pipeline, cross-validate, select, **one** test
evaluation, residual diagnostics, a fairness check, prediction intervals,
an honest verdict with an interval on the margin.
## The dataset, and why there is only one choice
`sklearn.datasets.load_diabetes` is the only regression dataset that
ships inside scikit-learn and needs no download.
`fetch_california_housing` downloads on first use and is forbidden by
this lab's offline rule. Exercise 1 checks both facts directly.
`load_diabetes(scaled=False)` gives 442 rows of 10 real-valued
measurements — age, sex, bmi, average blood pressure, and six serum
measures — in their **raw units**: age in years, not a mean-centred
fraction. scikit-learn's own default (`scaled=True`) mean-centres and
variance-scales every column to tiny floats; exercise 1b measures the
difference directly.
**The target has no physical unit.** It is a composite disease-
progression score assembled from clinical measurements a year after
baseline — not mg/dL, not a count, not anything with a name. Every RMSE
and MAE in this lab is in those same unitless composite-score points, and
this brief says so plainly rather than inventing a unit that does not
exist.
442 rows is small. A 25 percent test set is 111 rows — small enough that
every interval this lab computes is wide, and that is the point, not a
flaw: an honest verdict on data this size can legitimately come out
"cannot distinguish," and this lab's own margin check (exercise 7)
reports what actually happened rather than assuming an answer.
## What the honest run measures
Twenty-three candidate pipelines — 11 ridge regularisation strengths, 11
lasso regularisation strengths, and 1 plain OLS — are cross-validated
five ways on the training rows only, scored by RMSE. The winner is
`Lasso(alpha=1)`, at a cross-validated RMSE of **53.8958**. Fitted on the
full training set and evaluated **exactly once** against the test rows,
it scores **56.5566** RMSE (R2 0.3557, MAE 45.2846).
## The margin, and whether it survives an interval
The mean-predictor baseline scores 70.4637 RMSE. The winning model's
margin over that baseline is **13.9071** RMSE points. A 2000-draw
bootstrap resample of the 111 test rows gives a 95 percent interval on
that margin of **[5.5852, 22.3324]** — the interval excludes zero, so
this project's honest verdict is that the model IS distinguishable from
the baseline at this test-set size. It could easily have come out the
other way on a smaller improvement or a smaller test set, which is
exactly why the bootstrap runs rather than being assumed.
## Residual diagnostics — the centrepiece of this lab
No other day in this course owns reading the residuals themselves.
Exercise 8 measures whether the errors fan out as predictions rise
(a **heteroscedasticity signal** of 0.2386 — mild, not dramatic), whether
there is a missed curve in the fit (a **curvature signal** of -0.1278 —
weak), and how close the residuals are to normally distributed (a
**Q-Q correlation** of 0.9901, built from scratch with no scipy
dependency, since scipy is not one of this lab's three pinned packages).
Exercise 8b names the five largest individual mistakes: the biggest is a
patient whose true score was 52 and whose prediction was 209.3 — a
residual of -157.3 points.
## Is the model worse for high-value targets? Measured, not assumed.
Exercise 9 splits the test set at the median true value and compares
RMSE on each half: 55.2464 on the below-median half, 57.8601 on the
above-median half — a ratio of **1.0473**. Only 4.73 percent worse on the
more severe half at this seed: not the dramatic fairness problem the
exercise sets out to check for, and that is a real finding, not a
disappointing one — it had to be measured to be known.
## The leak this lab lets you cause on purpose
Exercise 10 rebuilds the mistake Day 147 spent a whole lesson on, for
regression: selecting a model by fitting every candidate and scoring it
**on the test set directly**, keeping whichever scores the lowest RMSE,
instead of selecting on cross-validated train rows and looking at test
once.
At the reported seed the leak reports 55.5212 RMSE against the honest
56.5566 — a gap of 1.0354 points in the leak's favour. Over 20 seeds the
mean gap is **0.5279** (sd 0.3686, min 0.011, max 1.1451), and it is
**never negative** — the leak can only make the reported error look as
good or better than the honest one, never worse. That asymmetry is the
mechanism, not luck.
## Prediction intervals, and their realised coverage
Exercise 11 builds a constant-width 95 percent prediction interval from
the standard deviation of **training** out-of-fold residuals — never from
the test residuals themselves, which would be circular — and checks what
fraction of the 111 test targets actually fall inside it: **0.9459**,
against a 0.95 nominal rate. Close, on 111 rows.
## How to work
1. Build the environment (see the lab `README.md`).
2. Run `.venv/bin/pytest starter -q`. You will see five passes (the
machinery checks in `test_regression_lib.py`) and fourteen skips.
3. Replace one `pytest.skip(...)` at a time with real code. The skip text
names the exact helper and the exact value to assert.
4. When you want the whole measured table at once, run
`.venv/bin/python3 examples/report_measurements.py`.
Do not run `pytest starter examples` in one invocation. Both directories
define `regression_lib.py`, `test_regression_lib.py` and
`test_regression_claims.py`; pytest aborts on the module-name collision.
Run them separately, always.
## And the rule, made mechanical, again
Exercise 6 wraps the test set in the same `GatedTestSet` pattern Day 144
built and Day 147 reused: exactly one evaluation, `TestSetTouchedTwice`
on the second, and a counter that does not advance on a refused attempt.
Thirteen days in, this is not a new idea — it is the same discipline,
proven on a regression problem instead of a classification one.
starter/regression_lib.py (19560 bytes)
"""One regression project, run properly, once.
Days 148-153 each isolated one discipline: the one-predictor model and its
four assumptions, the loss as a choice, multicollinearity, ridge and lasso,
the metrics that can be gamed or inverted, and OLS built from scratch. This
module spends every one of those disciplines on a single real dataset and
produces one defensible verdict, with residual diagnostics as the
centrepiece no other day owns.
Frame, baseline, split, pipeline, cross-validate, select, ONE test
evaluation, residual diagnostics, a fairness check, prediction intervals,
an honest interval on the margin. Nothing here is taught for the first
time; everything here is used.
"""
from __future__ import annotations
import numpy as np
from sklearn.datasets import load_diabetes
from sklearn.dummy import DummyRegressor
from sklearn.linear_model import Lasso, LinearRegression, Ridge
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
from sklearn.model_selection import KFold, cross_val_predict, cross_val_score, train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
# --------------------------------------------------------------------------
# 1. The dataset -- the only bundled regression set that needs no download
# --------------------------------------------------------------------------
def load_dataset():
"""The Wisconsin/diabetes progression set, in raw measurement units.
``load_diabetes`` is the only regression dataset scikit-learn bundles
offline; ``fetch_california_housing`` downloads and is not used here.
``scaled=False`` keeps the ten features in their original units (age in
years, bmi, average blood pressure, six serum measures) so a coefficient
would still mean something if this project stopped to interpret one --
Day 148's point, carried forward. The target itself is a composite
disease-progression score with no physical unit; it is not measured in
anything, and this project never pretends otherwise.
"""
d = load_diabetes(scaled=False)
return d.data, d.target, list(d.feature_names)
# --------------------------------------------------------------------------
# 2. The frame and the baseline -- before any model
# --------------------------------------------------------------------------
def baseline_metrics(x_train, y_train, x_test, y_test):
"""The mean-predictor baseline: RMSE and R^2, computed before any model.
Day 141's rule, restated for regression: a score is not evidence until
you know what it beats. Predicting the training mean for every row is
the simplest possible non-model.
"""
dummy = DummyRegressor(strategy="mean").fit(x_train, y_train)
pred = dummy.predict(x_test)
rmse = float(np.sqrt(mean_squared_error(y_test, pred)))
r2 = float(r2_score(y_test, pred))
return round(rmse, 4), round(r2, 4)
# --------------------------------------------------------------------------
# 3. The split -- 442 rows is small, so the test set stays small too
# --------------------------------------------------------------------------
def split_once(X, y, seed: int = 0, test_size: float = 0.25):
"""One split. The test half is touched once, later, for scoring.
A 25 percent test set on 442 rows is about 110 rows -- small enough
that every interval in this project is wide, and that is reported
honestly rather than smoothed over.
"""
return train_test_split(X, y, test_size=test_size, random_state=seed)
# --------------------------------------------------------------------------
# 4. The candidate pipelines -- ridge, lasso and plain OLS, K counted
# --------------------------------------------------------------------------
_ALPHAS = [0.001, 0.003, 0.01, 0.03, 0.1, 0.3, 1, 3, 10, 30, 100]
def candidate_configs():
"""23 candidate pipelines: 11 ridge, 11 lasso, 1 plain OLS.
Every candidate is a scikit-learn ``Pipeline`` with a ``StandardScaler``
ahead of the estimator, so cross-validation refits the scaler on each
fold's training rows only -- Day 143's stage-ordering rule, enforced by
the estimator's own contract. Returns ``(family, hyperparameter,
make_pipeline)`` where ``make_pipeline`` is a zero-argument callable
returning a fresh, unfitted ``Pipeline``.
"""
configs = []
for a in _ALPHAS:
configs.append(
("ridge", a, lambda a=a: Pipeline([("scale", StandardScaler()), ("clf", Ridge(alpha=a))]))
)
for a in _ALPHAS:
configs.append(
(
"lasso",
a,
lambda a=a: Pipeline(
[("scale", StandardScaler()), ("clf", Lasso(alpha=a, max_iter=20000))]
),
)
)
configs.append(
("ols", 0.0, lambda: Pipeline([("scale", StandardScaler()), ("clf", LinearRegression())]))
)
return configs
def candidate_count() -> int:
"""K, the number of configurations this project actually tries."""
return len(candidate_configs())
# --------------------------------------------------------------------------
# 5. Cross-validate, then select -- honest spending of the train rows
# --------------------------------------------------------------------------
def cross_validate_configs(x_train, y_train, seed: int = 0, folds: int = 5):
"""5-fold CV RMSE for every candidate, on train rows only.
Returns rows of ``(family, hyperparameter, cv_rmse, cv_std)``, sorted
best (lowest RMSE) first. RMSE is chosen as the selection metric because
it is stated as the metric BEFORE any model is fitted -- Day 152's whole
argument that the metric is a choice, made here in the units the target
is actually in (unitless composite-score points, not mg/dL).
"""
splitter = KFold(n_splits=folds, shuffle=True, random_state=seed)
rows = []
for family, param, make in candidate_configs():
scores = cross_val_score(make(), x_train, y_train, cv=splitter, scoring="neg_root_mean_squared_error")
rows.append((family, param, round(float(-scores.mean()), 4), round(float(scores.std()), 4)))
rows.sort(key=lambda r: r[2])
return rows
def select_best(x_train, y_train, seed: int = 0, folds: int = 5):
"""Fit the winner of the sweep on the full training set.
Returns ``(family, hyperparameter, cv_rmse, fitted_pipeline)``.
"""
rows = cross_validate_configs(x_train, y_train, seed=seed, folds=folds)
winner_family, winner_param, winner_cv, _sd = rows[0]
for family, param, make_fn in candidate_configs():
if family == winner_family and param == winner_param:
fitted = make_fn().fit(x_train, y_train)
return winner_family, winner_param, winner_cv, fitted
raise RuntimeError("winning configuration vanished between sweep and refit")
# --------------------------------------------------------------------------
# 6. The test set -- one look, enforced mechanically
# --------------------------------------------------------------------------
class TestSetTouchedTwice(RuntimeError):
"""Raised when the test set is scored against more than once."""
class GatedTestSet:
"""A test set that permits exactly one scoring call, then refuses.
Day 144's discipline, reused unchanged: the counter does not advance on
a refused attempt, so a caller that never succeeds cannot drain the
budget by retrying. ``evaluate`` returns ``(rmse, r2, mae)`` -- the one
look this project spends. Reading predictions afterward for residual
diagnostics is inspection, not a second selection, exactly as Day 147's
confusion matrix read predictions after its own one evaluation.
"""
def __init__(self, X, y):
self._X = X
self._y = y
self.evaluations = 0
def evaluate(self, model):
if self.evaluations >= 1:
raise TestSetTouchedTwice(
"the test set has already been used once; any further score is a "
"validation score, not a test score"
)
self.evaluations += 1
pred = model.predict(self._X)
rmse = float(np.sqrt(mean_squared_error(self._y, pred)))
r2 = float(r2_score(self._y, pred))
mae = float(mean_absolute_error(self._y, pred))
return round(rmse, 4), round(r2, 4), round(mae, 4)
# --------------------------------------------------------------------------
# 7. The verdict -- a bootstrap interval around the margin
# --------------------------------------------------------------------------
def margin_bootstrap_interval(y_test, pred_baseline, pred_model, n_boot: int = 2000, seed: int = 0):
"""A 95 percent interval around the margin: baseline RMSE minus model RMSE.
Resamples the test rows with replacement ``n_boot`` times and recomputes
both RMSEs on each resample, so the interval reflects how much the
margin itself could plausibly move at this test-set size -- the same
question Day 144's accuracy interval asked, answered here for RMSE
without an analytic formula, because RMSE's sampling distribution has
no equally simple closed form.
"""
rng = np.random.default_rng(seed)
y_test = np.asarray(y_test)
pred_baseline = np.asarray(pred_baseline)
pred_model = np.asarray(pred_model)
n = len(y_test)
margins = np.empty(n_boot)
for i in range(n_boot):
idx = rng.integers(0, n, size=n)
rmse_base = np.sqrt(mean_squared_error(y_test[idx], pred_baseline[idx]))
rmse_model = np.sqrt(mean_squared_error(y_test[idx], pred_model[idx]))
margins[i] = rmse_base - rmse_model
lower = round(float(np.percentile(margins, 2.5)), 4)
upper = round(float(np.percentile(margins, 97.5)), 4)
return lower, upper
def margin_distinguishable(lower: float, upper: float) -> bool:
"""Whether the bootstrap interval around the margin excludes zero.
If the interval spans zero, the honest verdict is "cannot distinguish
this model from the baseline at this test-set size" -- Day 144's
cautionary case, now checked for a real margin instead of assumed away.
"""
return lower > 0.0
# --------------------------------------------------------------------------
# 8. Residual diagnostics -- the centrepiece this day owns
# --------------------------------------------------------------------------
def _normal_ppf(p):
"""The inverse standard-normal CDF, by Acklam's rational approximation.
Built from scratch because scipy is not one of this lab's three pinned
dependencies -- Day 153's habit, applied again: when a tool is not
available, build the minimal piece you need rather than reach past the
pins. Accurate to about 1.15e-9 across the open interval (0, 1); good
enough for a Q-Q correlation on 111 points. Public-domain algorithm,
reference in sources.yml.
"""
a = [-3.969683028665376e01, 2.209460984245205e02, -2.759285104469687e02,
1.383577518672690e02, -3.066479806614716e01, 2.506628277459239e00]
b = [-5.447609879822406e01, 1.615858368580409e02, -1.556989798598866e02,
6.680131188771972e01, -1.328068155288572e01]
c = [-7.784894002430293e-03, -3.223964580411365e-01, -2.400758277161838e00,
-2.549732539343734e00, 4.374664141464968e00, 2.938163982698783e00]
d = [7.784695709041462e-03, 3.224671290700398e-01, 2.445134137142996e00,
3.754408661907416e00]
p_low = 0.02425
p = np.asarray(p, dtype=float)
out = np.empty_like(p)
low = p < p_low
high = p > (1 - p_low)
mid = ~(low | high)
ql = np.sqrt(-2 * np.log(p[low]))
out[low] = (((((c[0] * ql + c[1]) * ql + c[2]) * ql + c[3]) * ql + c[4]) * ql + c[5]) / (
(((d[0] * ql + d[1]) * ql + d[2]) * ql + d[3]) * ql + 1
)
q = p[mid] - 0.5
r = q * q
out[mid] = (((((a[0] * r + a[1]) * r + a[2]) * r + a[3]) * r + a[4]) * r + a[5]) * q / (
(((((b[0] * r + b[1]) * r + b[2]) * r + b[3]) * r + b[4]) * r + 1)
)
qh = np.sqrt(-2 * np.log(1 - p[high]))
out[high] = -(((((c[0] * qh + c[1]) * qh + c[2]) * qh + c[3]) * qh + c[4]) * qh + c[5]) / (
(((d[0] * qh + d[1]) * qh + d[2]) * qh + d[3]) * qh + 1
)
return out
def residual_summary(y_test, pred_test):
"""Mean and standard deviation of the test residuals, y minus prediction."""
resid = np.asarray(y_test, dtype=float) - np.asarray(pred_test, dtype=float)
return round(float(resid.mean()), 4), round(float(resid.std()), 4)
def heteroscedasticity_signal(pred_test, y_test):
"""Correlation between the fitted value and the absolute residual.
A value near zero means the spread of errors does not grow with the
predicted value -- the "fanning out" pattern a residual-vs-fitted plot
is built to catch. This is the from-scratch numeric version of reading
that plot.
"""
pred_test = np.asarray(pred_test, dtype=float)
resid = np.asarray(y_test, dtype=float) - pred_test
corr = np.corrcoef(pred_test, np.abs(resid))[0, 1]
return round(float(corr), 4)
def curvature_signal(pred_test, y_test):
"""Correlation between the squared fitted value and the signed residual.
A value far from zero suggests the model is missing a systematic curve
-- residuals that trend up then down (or the reverse) as the fitted
value rises, rather than scattering evenly around zero.
"""
pred_test = np.asarray(pred_test, dtype=float)
resid = np.asarray(y_test, dtype=float) - pred_test
corr = np.corrcoef(pred_test**2, resid)[0, 1]
return round(float(corr), 4)
def normal_probability_correlation(y_test, pred_test):
"""A from-scratch normal-probability (Q-Q) check, as one correlation.
Standardises the residuals, sorts them, and correlates them against the
theoretical normal quantiles a perfectly Gaussian set of residuals
would produce. 1.0 is a perfectly straight Q-Q line; values noticeably
below 1.0 flag departures from normality that a plotted Q-Q line would
show as curvature at the tails.
"""
resid = np.asarray(y_test, dtype=float) - np.asarray(pred_test, dtype=float)
n = len(resid)
std_resid = (resid - resid.mean()) / resid.std()
sorted_resid = np.sort(std_resid)
probs = (np.arange(1, n + 1) - 0.5) / n
theoretical = _normal_ppf(probs)
corr = np.corrcoef(theoretical, sorted_resid)[0, 1]
return round(float(corr), 4)
def largest_residuals(y_test, pred_test, n: int = 5):
"""The n largest-magnitude residuals, inspected individually.
Returns rows of ``(test_row_index, true_value, predicted_value,
residual)``, sorted by |residual| descending. A confusion matrix reads
the specific mistakes a classifier makes; this is that discipline's
regression counterpart.
"""
y_test = np.asarray(y_test, dtype=float)
pred_test = np.asarray(pred_test, dtype=float)
resid = y_test - pred_test
order = np.argsort(-np.abs(resid))[:n]
return [
(int(i), round(float(y_test[i]), 4), round(float(pred_test[i]), 4), round(float(resid[i]), 4))
for i in order
]
# --------------------------------------------------------------------------
# 9. Is the model worse for high-value targets? Measure it.
# --------------------------------------------------------------------------
def error_by_target_level(y_test, pred_test):
"""RMSE on the below-median half of test targets against the above-median half.
Returns ``(rmse_low, rmse_high, ratio)`` where ``ratio`` is
``rmse_high / rmse_low``. On a disease-progression score this is a
fairness-relevant question, not only a statistical one: are errors
worse for patients whose true progression is more severe?
"""
y_test = np.asarray(y_test, dtype=float)
pred_test = np.asarray(pred_test, dtype=float)
median_y = float(np.median(y_test))
low_mask = y_test <= median_y
high_mask = ~low_mask
rmse_low = float(np.sqrt(mean_squared_error(y_test[low_mask], pred_test[low_mask])))
rmse_high = float(np.sqrt(mean_squared_error(y_test[high_mask], pred_test[high_mask])))
return round(rmse_low, 4), round(rmse_high, 4), round(rmse_high / rmse_low, 4)
# --------------------------------------------------------------------------
# 10. The leaky version -- selecting by peeking at the test set
# --------------------------------------------------------------------------
def leaky_selection_test_rmse(x_train, y_train, x_test, y_test):
"""Select the winner by fitting every candidate and scoring it on TEST.
Returns the winning (lowest) test RMSE -- K looks disguised as one,
Day 147's mistake, reconstructed for regression. Lower RMSE is better,
so the leaky search can only match or beat the honestly selected
model's own test RMSE, never lose to it.
"""
best_rmse = None
for _family, _param, make in candidate_configs():
pipe = make().fit(x_train, y_train)
pred = pipe.predict(x_test)
rmse = float(np.sqrt(mean_squared_error(y_test, pred)))
if best_rmse is None or rmse < best_rmse:
best_rmse = rmse
return round(best_rmse, 4)
def leaky_vs_honest_over_seeds(X, y, seeds=range(20), folds: int = 5):
"""The gap between peeking at the test set and looking at it once.
Returns rows of ``(seed, honest_rmse, leaky_rmse, gap)`` where
``gap = honest_rmse - leaky_rmse``. Because lower RMSE is better, a
positive gap means the leak reported a lower (better-looking) error
than the honest evaluation -- the leak can only help the reported
number, never hurt it.
"""
rows = []
for seed in seeds:
x_train, x_test, y_train, y_test = split_once(X, y, seed=seed)
_family, _param, _cv, fitted = select_best(x_train, y_train, seed=seed, folds=folds)
honest_rmse = float(np.sqrt(mean_squared_error(y_test, fitted.predict(x_test))))
leaky_rmse = leaky_selection_test_rmse(x_train, y_train, x_test, y_test)
rows.append((seed, round(honest_rmse, 4), leaky_rmse, round(honest_rmse - leaky_rmse, 4)))
return rows
# --------------------------------------------------------------------------
# 11. Prediction intervals -- not just a point, and measured coverage
# --------------------------------------------------------------------------
def prediction_interval_coverage(x_train, y_train, x_test, y_test, fitted, seed: int = 0, folds: int = 5):
"""A constant-width 95 percent prediction interval, and its realised coverage.
The half-width comes from the standard deviation of out-of-fold
residuals on the TRAINING rows only (``cross_val_predict``), never from
the test residuals themselves -- using the test residuals to size the
test interval would be circular. Returns ``(half_width, coverage)``
where ``coverage`` is the fraction of test targets that actually fall
inside ``prediction +/- half_width``, measured against the nominal 0.95.
"""
splitter = KFold(n_splits=folds, shuffle=True, random_state=seed)
oof_pred = cross_val_predict(fitted, x_train, y_train, cv=splitter)
oof_resid = np.asarray(y_train, dtype=float) - oof_pred
half_width = round(float(1.96 * oof_resid.std()), 4)
pred_test = fitted.predict(x_test)
y_test = np.asarray(y_test, dtype=float)
lower = pred_test - half_width
upper = pred_test + half_width
coverage = round(float(np.mean((y_test >= lower) & (y_test <= upper))), 4)
return half_width, coverage
starter/test_regression_claims.py (7892 bytes)
"""Fourteen exercises: one regression project, run properly, once.
Read `00_brief.md` first. Each function below is a `pytest.skip` naming
exactly what to build and what to assert; replace the skip with real code.
`regression_lib.py` is complete -- it is the machinery, not the exercise.
Run this suite on its own:
.venv/bin/pytest starter -q
Never run `pytest starter examples` in one invocation: both directories
define modules with the same names and pytest aborts on the collision.
"""
import inspect
import numpy as np # noqa: F401 (you will need it)
import pytest
from sklearn.datasets import fetch_california_housing, load_diabetes # noqa: F401
from sklearn.dummy import DummyRegressor # noqa: F401
import regression_lib as r # noqa: F401 (you will need it)
@pytest.fixture(scope="module")
def dataset():
return r.load_dataset()
@pytest.fixture(scope="module")
def split(dataset):
X, y, _names = dataset
return r.split_once(X, y, seed=0)
def test_01_the_dataset_is_the_only_bundled_regression_set(dataset):
pytest.skip(
"Unpack (X, y, names) from the dataset fixture. Assert X.shape == "
"(442, 10), names == ['age', 'sex', 'bmi', 'bp', 's1', 's2', 's3', "
"'s4', 's5', 's6'], y.min() rounds to 25.0, y.max() rounds to 346.0, "
"and y.mean() rounds to 152.1335. Then assert "
"inspect.signature(fetch_california_housing).parameters"
"['download_if_missing'].default is True -- the reason that dataset "
"is forbidden here and load_diabetes is used instead."
)
def test_01b_raw_units_are_not_the_scikit_learn_default(dataset):
pytest.skip(
"Unpack (X, y, names) from the dataset fixture. Find age's column "
"index and assert X[:, age_index].min() == 19.0 and .max() == 79.0 "
"-- real years. Then load load_diabetes(scaled=True) (scikit-learn's "
"own default) and assert its age column's min is between -0.2 and 0, "
"and its max is between 0 and 0.2 -- tiny centred floats. This is "
"why this lab asks for scaled=False."
)
def test_02_the_baseline_before_any_model(split):
pytest.skip(
"Unpack (x_train, x_test, y_train, y_test) from the split fixture. "
"Call r.baseline_metrics and assert the RMSE rounds to 70.4637 and "
"R2 rounds to -0.0001. Every model in this exercise has to beat "
"70.4637 RMSE to be worth building at all."
)
def test_03_the_split_holds_the_test_rows_back(dataset):
pytest.skip(
"Unpack (X, y, _names) from the dataset fixture and call "
"r.split_once(X, y, seed=0). Assert x_train.shape == (331, 10) and "
"x_test.shape == (111, 10). 111 test rows on 442 total is small -- "
"every interval computed later in this exercise is wide because of "
"it, and that is the point, not a bug."
)
def test_04_the_sweep_counts_twenty_three_candidate_pipelines():
pytest.skip(
"Assert r.candidate_count() == 23. Then unpack the (family, param, "
"make) triples from r.candidate_configs() and assert there are 11 "
"'ridge', 11 'lasso' and 1 'ols' entries. K is the number nobody "
"remembers -- count it before doing anything else with it."
)
def test_05_cross_validation_selects_the_winner_on_train_rows_only(split):
pytest.skip(
"Unpack (x_train, _x_test, y_train, _y_test) from the split fixture "
"and call r.select_best(x_train, y_train, seed=0). Assert the "
"returned (family, param) equals ('lasso', 1) and cv_rmse equals "
"53.8958. The winner was chosen on cross-validated train rows -- "
"the test rows have not been touched yet."
)
def test_06_the_gate_permits_exactly_one_test_evaluation(split):
pytest.skip(
"Fit the winner from r.select_best on the train rows, wrap "
"(x_test, y_test) in r.GatedTestSet, and assert the first "
"evaluation returns (56.5566, 0.3557, 45.2846) for (rmse, r2, mae) "
"and the counter becomes 1. Then assert a second evaluation raises "
"r.TestSetTouchedTwice mentioning 'validation score', and that the "
"counter did NOT advance on the refused attempt."
)
def test_07_the_margin_has_a_bootstrap_interval(split):
pytest.skip(
"Fit a DummyRegressor(strategy='mean') baseline and the winner from "
"r.select_best, both on x_train/y_train. Predict both on x_test. "
"Call r.margin_bootstrap_interval(y_test, pred_baseline, pred_model, "
"seed=0) and assert it returns (5.5852, 22.3324). Assert the point "
"margin -- baseline RMSE minus model RMSE -- rounds to 13.9071, and "
"that r.margin_distinguishable(lower, upper) is True: the margin "
"clears the interval's lower bound."
)
def test_08_the_residual_vs_fitted_diagnostic(split):
pytest.skip(
"Fit the winner and predict on x_test. Call r.residual_summary and "
"assert it returns (-3.6262, 56.4402). Call "
"r.heteroscedasticity_signal(pred, y_test) and assert it rounds to "
"0.2386. Call r.curvature_signal(pred, y_test) and assert it rounds "
"to -0.1278. Neither signal is dramatic here -- report both anyway."
)
def test_08b_the_normal_probability_check_and_the_largest_residuals(split):
pytest.skip(
"Fit the winner and predict on x_test. Call "
"r.normal_probability_correlation(y_test, pred) and assert it "
"rounds to 0.9901. Call r.largest_residuals(y_test, pred, n=5) and "
"assert the first two rows equal (60, 52.0, 209.3314, -157.3314) "
"and (65, 302.0, 153.8865, 148.1135). 0.9901 is close to a straight "
"Q-Q line -- these residuals are close to normal even on real data."
)
def test_09_error_by_target_level(split):
pytest.skip(
"Fit the winner and predict on x_test. Call "
"r.error_by_target_level(y_test, pred) and assert it returns "
"(55.2464, 57.8601, 1.0473). A ratio of 1.0473 means the model is "
"only 4.73 percent worse on the more-severe half of test targets -- "
"measure it before assuming a disease-progression model is fair or "
"unfair across severity."
)
def test_10_the_leaky_version_selects_by_peeking_at_the_test_set(split):
pytest.skip(
"Fit the honest winner and compute its test RMSE (round to 4 "
"places). Call r.leaky_selection_test_rmse(x_train, y_train, "
"x_test, y_test), which fits every one of the 23 candidates and "
"lets the test set itself pick the winner by lowest RMSE. Assert "
"the honest RMSE is 56.5566, the leaky RMSE is 55.5212, and "
"leaky_rmse <= honest_rmse -- lower RMSE is better, so the leak can "
"only match or beat the honest score, never lose to it."
)
def test_10b_the_leaky_gap_over_twenty_seeds(dataset):
pytest.skip(
"Call r.leaky_vs_honest_over_seeds(X, y, seeds=range(20)). Assert "
"20 rows come back. Compute the mean, sd, min and max of the gap "
"column and assert they round to 0.5279, 0.3686, 0.011 and 1.1451. "
"Then assert every single gap is non-negative -- across 20 "
"independent seeds, selecting by peeking at the test set never once "
"reported a worse (higher) RMSE than selecting honestly."
)
def test_11_prediction_interval_coverage(split):
pytest.skip(
"Fit the winner. Call r.prediction_interval_coverage(x_train, "
"y_train, x_test, y_test, fitted, seed=0) and assert it returns "
"(105.8797, 0.9459). The half-width comes from TRAINING residuals "
"only, never from the test residuals themselves -- sizing an "
"interval from the data you are about to check it against would be "
"circular. 0.9459 measured against a 0.95 nominal, on 111 test rows."
)
starter/test_regression_lib.py (2331 bytes)
"""Machinery checks: the helpers behave, before any claim is made.
These five tests are solved in both `starter/` and `examples/`. They exist
so that a broken helper reports itself as a broken helper rather than as a
surprising scientific result.
"""
import numpy as np
import pytest
import regression_lib as r
def test_the_dataset_loads_offline_and_matches_its_shape():
X, y, names = r.load_dataset()
assert X.shape == (442, 10)
assert y.shape == (442,)
assert names == ["age", "sex", "bmi", "bp", "s1", "s2", "s3", "s4", "s5", "s6"]
assert float(y.min()) == 25.0
assert float(y.max()) == 346.0
def test_candidate_configs_really_are_twenty_three_distinct_pipelines():
configs = r.candidate_configs()
assert len(configs) == 23
seen = set()
for family, param, make in configs:
pipe = make()
assert hasattr(pipe, "fit") and hasattr(pipe, "predict")
seen.add((family, param))
# No two configs share a (family, hyperparameter) pair.
assert len(seen) == 23
def test_the_gated_test_set_counts_and_refuses():
class AlwaysMean:
def predict(self, X):
return np.full(len(X), 150.0)
y = np.array([100.0, 200.0, 150.0, 150.0])
gate = r.GatedTestSet(np.zeros((4, 2)), y)
rmse, r2, mae = gate.evaluate(AlwaysMean())
assert rmse > 0
with pytest.raises(r.TestSetTouchedTwice):
gate.evaluate(AlwaysMean())
# A fresh gate is a fresh budget; the class holds no global state.
fresh_rmse, _r2, _mae = r.GatedTestSet(np.zeros((4, 2)), y).evaluate(AlwaysMean())
assert fresh_rmse == rmse
def test_the_normal_ppf_matches_known_reference_points():
# Well-known standard-normal quantiles, to a few decimals.
assert abs(r._normal_ppf(np.array([0.5]))[0] - 0.0) < 1e-6
assert abs(r._normal_ppf(np.array([0.975]))[0] - 1.959964) < 1e-4
assert abs(r._normal_ppf(np.array([0.025]))[0] - (-1.959964)) < 1e-4
def test_largest_residuals_returns_them_sorted_by_magnitude():
y_test = np.array([10.0, 20.0, 30.0, 40.0])
pred_test = np.array([10.0, 25.0, 10.0, 41.0])
rows = r.largest_residuals(y_test, pred_test, n=2)
assert len(rows) == 2
# Row 2 (residual +20) is the largest in magnitude; row 3 (residual -1) is smallest.
assert rows[0][0] == 2
assert rows[0][3] == 20.0
tests/run_tests.sh (11525 bytes)
#!/usr/bin/env bash
# Day 154 lab harness: "A Complete Regression Project"
#
# Prints "N checks, M failure(s)" and exits 0 only when M is zero.
set -u
LAB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$LAB_DIR"
PYTHON="${PYTHON:-.venv/bin/python3}"
PYTEST="${PYTEST:-.venv/bin/pytest}"
# Clear caches at the START so the final cleanliness check measures what
# THIS run left behind, not what a previous `pytest starter -q` left.
find . -path ./.venv -prune -o -type d -name '__pycache__' -exec rm -rf -- {} + 2>/dev/null
rm -rf .pytest_cache
CHECKS=0
FAILURES=0
ok() {
CHECKS=$((CHECKS + 1))
echo " ok: $1"
}
fail() {
CHECKS=$((CHECKS + 1))
FAILURES=$((FAILURES + 1))
echo " FAIL: $1"
}
if [ ! -x "$PYTHON" ]; then
echo "No lab .venv found at $PYTHON."
echo "Run: python3 -m venv .venv && .venv/bin/pip install -r requirements/requirements.txt"
exit 2
fi
echo "1. Installed versions match requirements/requirements.txt"
VERSION_CHECK=$("$PYTHON" - <<'PYEOF'
import numpy, sklearn, pytest
print("numpy", numpy.__version__)
print("scikit-learn", sklearn.__version__)
print("pytest", pytest.__version__)
PYEOF
)
echo "$VERSION_CHECK" | sed 's/^/ /'
while read -r pkg pin; do
pin_version="${pin#*==}"
installed=$(echo "$VERSION_CHECK" | awk -v p="$pkg" '$1==p {print $2}')
if [ "$installed" = "$pin_version" ]; then
ok "$pkg $installed matches the pin"
else
fail "$pkg installed=$installed pinned=$pin_version"
fi
done < <(sed 's/==/ ==/' requirements/requirements.txt)
echo ""
echo "2. Every published claim, reproduced directly (no pytest involved)"
DIRECT_CHECK=$("$PYTHON" - <<'PYEOF'
import sys
sys.path.insert(0, "examples")
import numpy as np
from sklearn.dummy import DummyRegressor
import regression_lib as r
errors = []
def expect(label, got, want):
if got != want:
errors.append(f"{label}: expected {want}, got {got}")
# 1. The dataset
X, y, names = r.load_dataset()
expect("shape", X.shape, (442, 10))
expect("y min", round(float(y.min()), 4), 25.0)
expect("y max", round(float(y.max()), 4), 346.0)
expect("y mean", round(float(y.mean()), 4), 152.1335)
# 2. The split and the baseline
x_train, x_test, y_train, y_test = r.split_once(X, y, seed=0)
expect("train shape", x_train.shape, (331, 10))
expect("test shape", x_test.shape, (111, 10))
base_rmse, base_r2 = r.baseline_metrics(x_train, y_train, x_test, y_test)
expect("baseline rmse", base_rmse, 70.4637)
expect("baseline r2", base_r2, -0.0001)
# 3. The sweep and selection
expect("K", r.candidate_count(), 23)
family, param, cv_rmse, fitted = r.select_best(x_train, y_train, seed=0)
expect("winner", (family, param), ("lasso", 1))
expect("cv_rmse", cv_rmse, 53.8958)
# 4. The gate: exactly one evaluation
gate = r.GatedTestSet(x_test, y_test)
test_rmse, test_r2, test_mae = gate.evaluate(fitted)
expect("test_rmse", test_rmse, 56.5566)
expect("test_r2", test_r2, 0.3557)
expect("test_mae", test_mae, 45.2846)
expect("evaluations after first look", gate.evaluations, 1)
try:
gate.evaluate(fitted)
errors.append("the gate permitted a second evaluation, which it must not")
except r.TestSetTouchedTwice as exc:
if "validation score" not in str(exc):
errors.append(f"the gate's message did not explain itself: {exc}")
if gate.evaluations != 1:
errors.append("the counter advanced on a refused evaluation")
# 5. The margin, with a bootstrap interval
baseline_model = DummyRegressor(strategy="mean").fit(x_train, y_train)
pred_baseline = baseline_model.predict(x_test)
pred_model = fitted.predict(x_test)
lower, upper = r.margin_bootstrap_interval(y_test, pred_baseline, pred_model, seed=0)
expect("margin interval", (lower, upper), (5.5852, 22.3324))
margin = round(base_rmse - test_rmse, 4)
expect("margin", margin, 13.9071)
expect("distinguishable", r.margin_distinguishable(lower, upper), True)
# 6. Residual diagnostics
resid_mean, resid_std = r.residual_summary(y_test, pred_model)
expect("resid_mean", resid_mean, -3.6262)
expect("resid_std", resid_std, 56.4402)
expect("heteroscedasticity", r.heteroscedasticity_signal(pred_model, y_test), 0.2386)
expect("curvature", r.curvature_signal(pred_model, y_test), -0.1278)
expect("qq correlation", r.normal_probability_correlation(y_test, pred_model), 0.9901)
top5 = r.largest_residuals(y_test, pred_model, n=5)
expect("largest residual row", top5[0], (60, 52.0, 209.3314, -157.3314))
# 7. Error by target level
rmse_low, rmse_high, ratio = r.error_by_target_level(y_test, pred_model)
expect("error by level", (rmse_low, rmse_high, ratio), (55.2464, 57.8601, 1.0473))
# 8. The leaky version
leaky_rmse = r.leaky_selection_test_rmse(x_train, y_train, x_test, y_test)
expect("leaky rmse", leaky_rmse, 55.5212)
if leaky_rmse > test_rmse:
errors.append(f"leaky rmse {leaky_rmse} was worse (higher) than the honest rmse {test_rmse}")
# 9. Prediction interval coverage
half_width, coverage = r.prediction_interval_coverage(x_train, y_train, x_test, y_test, fitted, seed=0)
expect("half width", half_width, 105.8797)
expect("coverage", coverage, 0.9459)
if errors:
for e in errors:
print("ERROR:", e)
sys.exit(1)
print("all direct checks passed")
PYEOF
)
if echo "$DIRECT_CHECK" | grep -q "all direct checks passed"; then
ok "exercises 1-11 reproduced directly against regression_lib, no pytest involved"
else
fail "direct library checks failed"
echo "$DIRECT_CHECK" | sed 's/^/ /'
fi
echo ""
echo "3. examples/ passes in full"
EXAMPLES_OUT=$("$PYTEST" examples -q 2>&1)
if echo "$EXAMPLES_OUT" | tail -1 | grep -qE "^19 passed"; then
ok "pytest examples -q -> 19 passed"
else
fail "pytest examples -q did not report 19 passed"
echo "$EXAMPLES_OUT" | tail -20 | sed 's/^/ /'
fi
echo ""
echo "4. starter/ is an untouched skeleton"
STARTER_OUT=$("$PYTEST" starter -q 2>&1)
if echo "$STARTER_OUT" | tail -1 | grep -qE "5 passed, 14 skipped"; then
ok "pytest starter -q -> 5 passed, 14 skipped (the machinery checks pass; the fourteen exercises are stubs)"
else
fail "pytest starter -q did not report 5 passed, 14 skipped"
echo "$STARTER_OUT" | tail -20 | sed 's/^/ /'
fi
echo ""
echo "5. pytest examples starter (one invocation) aborts on the module-name collision"
COMBINED_OUT=$("$PYTEST" examples starter 2>&1)
if echo "$COMBINED_OUT" | grep -q "import file mismatch"; then
ok "combined invocation reports import file mismatch, as documented -- never run starter and examples together"
else
fail "combined invocation did not fail with import file mismatch as expected"
fi
echo ""
echo "6. The report reproduces the captured table exactly"
REPORT_OUT=$("$PYTHON" examples/report_measurements.py 2>&1)
if [ "$REPORT_OUT" = "$(cat expected-output/measured-values.txt)" ]; then
ok "report_measurements.py output is byte-identical to expected-output/measured-values.txt"
else
fail "report_measurements.py drifted from expected-output/measured-values.txt"
echo "$REPORT_OUT" | diff - expected-output/measured-values.txt | head -20 | sed 's/^/ /'
fi
echo ""
echo "7. The test set is evaluated EXACTLY ONCE in the reference run"
GATE_CHECK=$("$PYTHON" - <<'PYEOF'
import sys
sys.path.insert(0, "examples")
import regression_lib as r
X, y, _names = r.load_dataset()
x_train, x_test, y_train, y_test = r.split_once(X, y, seed=0)
_family, _param, _cv, fitted = r.select_best(x_train, y_train, seed=0)
gate = r.GatedTestSet(x_test, y_test)
assert gate.evaluations == 0, "a fresh gate must start at zero evaluations"
gate.evaluate(fitted)
assert gate.evaluations == 1, "one evaluation must advance the counter to exactly one"
refused = 0
for _ in range(5):
try:
gate.evaluate(fitted)
except r.TestSetTouchedTwice:
refused += 1
assert refused == 5, "every repeated attempt after the first must be refused"
assert gate.evaluations == 1, "repeated refused attempts must not advance the counter"
print("test set touched exactly once, five further attempts refused, counter never moved")
PYEOF
)
if echo "$GATE_CHECK" | grep -q "touched exactly once"; then
ok "GatedTestSet enforces exactly one evaluation mechanically, not by convention"
else
fail "the one-evaluation guarantee did not hold"
echo "$GATE_CHECK" | sed 's/^/ /'
fi
echo ""
echo "8. Proof the harness can fail"
SCRATCH=$(mktemp -d "${TMPDIR:-/tmp}/d154-scratch.XXXXXX")
cp examples/*.py "$SCRATCH"/
SCRATCH_OUT=$("$PYTEST" "$SCRATCH" -q 2>&1)
if echo "$SCRATCH_OUT" | tail -1 | grep -qE "^19 passed"; then
ok "scratch copy of examples/ passes before it is broken"
else
fail "scratch copy did not pass before being broken: $(echo "$SCRATCH_OUT" | tail -3)"
fi
"$PYTHON" - "$SCRATCH/test_regression_claims.py" <<'PYEOF'
import sys
path = sys.argv[1]
text = open(path).read()
needle = "assert rows[0] == (60, 52.0, 209.3314, -157.3314)"
replacement = "assert rows[0] == (0, 0.0, 0.0, 0.0)"
assert needle in text, "could not find the assertion to break"
open(path, "w").write(text.replace(needle, replacement, 1))
PYEOF
BROKEN_OUT=$("$PYTEST" "$SCRATCH" -q 2>&1)
BROKEN_STATUS=$?
if [ "$BROKEN_STATUS" -ne 0 ] && echo "$BROKEN_OUT" | grep -q "test_08b_the_normal_probability_check_and_the_largest_residuals"; then
ok "breaking exercise 8b's assertion produces a non-zero exit and names the failing test"
else
fail "broken copy did not fail as expected (exit=$BROKEN_STATUS)"
fi
rm -rf "$SCRATCH"
echo ""
echo "9. The leaky-gap direction holds beyond the quoted seed range"
DIRECTION=$("$PYTHON" - <<'PYEOF'
import sys
sys.path.insert(0, "examples")
import numpy as np
import regression_lib as r
X, y, _names = r.load_dataset()
problems = []
# The leak never produces a worse (higher) RMSE, at seeds this lab does not quote.
rows = r.leaky_vs_honest_over_seeds(X, y, seeds=range(20, 25))
gaps = [g for _s, _h, _l, g in rows]
if not all(g >= 0 for g in gaps):
problems.append(f"a leaky gap went negative at an unquoted seed: {gaps}")
# Selecting is confined to train rows: refitting the winner never needs test.
x_train, x_test, y_train, y_test = r.split_once(X, y, seed=41)
_family, _param, cv_rmse, fitted = r.select_best(x_train, y_train, seed=41)
if not (30 < cv_rmse < 90):
problems.append(f"cv_rmse at an unquoted seed was out of range: {cv_rmse}")
if problems:
for p in problems:
print("ERROR:", p)
else:
print("every direction held")
PYEOF
)
if [ "$DIRECTION" = "every direction held" ]; then
ok "the leaky-gap direction and the selection mechanics hold at seeds this lab does not quote"
else
fail "a direction failed beyond the quoted seeds"
echo "$DIRECTION" | sed 's/^/ /'
fi
echo ""
echo "10. Offline, and nothing left behind"
if ! grep -rInE "https?://" examples/*.py starter/*.py > /dev/null 2>&1; then
ok "no URLs inside examples/ or starter/ source -- this lab reaches no network beyond the bundled dataset"
else
fail "found a URL inside examples/ or starter/"
fi
if [ -z "$(find . -path ./.venv -prune -o -type d -name '__pycache__' -print 2>/dev/null)" ]; then
ok "no __pycache__ left behind"
else
find . -path ./.venv -prune -o -type d -name '__pycache__' -exec rm -rf -- {} + 2>/dev/null
ok "no __pycache__ left behind (cleaned during this run)"
fi
if [ ! -d .pytest_cache ]; then
ok "no .pytest_cache left behind"
else
rm -rf .pytest_cache
ok "no .pytest_cache left behind (cleaned during this run)"
fi
echo ""
echo "---------------------------------------------------------------"
echo "$CHECKS checks, $FAILURES failure(s)"
if [ "$FAILURES" -ne 0 ]; then
exit 1
fi
exit 0
Troubleshooting
Troubleshooting
No lab .venv found at .venv/bin/python3
The harness will not run against whatever Python is on your PATH,
because every number here is pinned to exact package versions. Build the
environment first:
python3 -m venv .venv
.venv/bin/pip install -r requirements/requirements.txt
If you deliberately want a different interpreter, the harness honours
PYTHON and PYTEST:
PYTHON=/path/to/python3 PYTEST=/path/to/pytest bash tests/run_tests.sh
Expect version-check failures if those do not match the pins. That is the harness working, not the harness breaking.
import file mismatch when running pytest
You ran pytest examples starter in one invocation. Both directories
contain modules with the same names, so pytest cannot decide which
regression_lib a test meant. Run them separately:
.venv/bin/pytest examples -q
.venv/bin/pytest starter -q
Check 5 of the harness deliberately asserts that the combined invocation fails, so this is documented behaviour rather than a surprise.
My winning configuration is not Lasso(alpha=1)
Check expected-output/FIELDS.md before assuming this is a bug. The
winner at seed 0, under the pinned versions, is ('lasso', 1) at a
cross-validated RMSE of 53.8958 -- but Lasso(alpha=0.3), Ridge(alpha=10)
and plain OLS all score within two-tenths of a point of it. Day 145
already established that near-tied configurations trade places under
resampling. What must hold on any version: the winner's cross-validated
RMSE is comfortably below the 70.4637 baseline, and it was selected
without ever touching the test rows.
The margin's bootstrap interval doesn't match my by-hand run
margin_bootstrap_interval seeds its own np.random.default_rng(seed)
internally (default seed=0), so calling it with the same arguments and
the same seed reproduces the same 2000 resamples under this NumPy
version. If you changed n_boot or the seed, your bounds will differ --
that is expected. If you used the same arguments and still see different
bounds, check your NumPy version against the pin; NumPy's own
documentation states Generator gives no cross-version stream guarantee.
The residual diagnostics look different from the lesson's
If you re-split with a different seed or a different test_size, every
diagnostic in exercises 8, 8b and 9 will move -- they are all computed on
one seed's 111 test rows. What should hold at any seed: the Q-Q
correlation stays well above 0.9 (these residuals are close to normal),
and neither the heteroscedasticity nor the curvature signal is dramatic
(nothing near ±0.6 or higher). If yours is wildly different, check that
you selected the winner with select_best and predicted on the SAME
x_test the gate was built from.
The leaky RMSE is higher than the honest RMSE
This should not happen, and if it does on the pinned versions with seed
0, something is genuinely broken -- open an issue rather than adjusting
the assertion. leaky_selection_test_rmse searches all 23 candidates and
keeps the lowest test RMSE, which by construction includes the honestly
selected winner among its options; it cannot report a higher (worse)
number than the honest evaluation.
My leaky-gap numbers differ from the lesson's
Almost certainly fine, at the level of the decimals. What must hold across the full 20-seed sweep in exercise 10b: every single gap is non-negative. If a gap has gone negative at any seed, that is the one result that would falsify this lab's central mechanism -- investigate properly rather than adjusting the assertion.
Lasso or Ridge prints a convergence warning
max_iter=20000 is set on every Lasso in the sweep specifically to
avoid this at the smaller regularisation strengths. If you construct your
own Lasso with the default max_iter=1000 you may see a
ConvergenceWarning. Match the library's settings, or use its helpers
directly.
The harness takes a while
It does not, particularly -- the 20-seed leaky-gap comparison in exercise 10b, which cross-validates all 23 candidates 20 times over, took 2.9559 seconds on the capture machine, and the full harness completes in well under a minute. No timing is asserted anywhere, so a slower machine changes nothing about whether it passes.
Every number moves on my machine
Read expected-output/FIELDS.md. Every seeded split, cross-validation
fold and bootstrap resample in this lab depends on NumPy's default_rng
and scikit-learn's internal use of it, and NumPy's documentation is
explicit that Generator gives no stream-compatibility guarantee between
versions.
What must hold anywhere: cross-validation selects using train rows only, the leaky RMSE is never worse than the honest RMSE across the 20-seed sweep, and the test set is evaluated exactly once in the reference run. Harness check 9 confirms the leaky-gap direction and the selection mechanics survive at seeds this lab does not quote, so none of the directional claims rest on a single lucky seed.
Security notes
Security notes
What this lab touches
Nothing outside its own directory, and nothing outside your machine.
- Filesystem. The lab reads only files inside its own directory, plus
the diabetes dataset bundled inside your installed scikit-learn
package. The one write outside the lab directory is check 8 of the
harness, which creates a scratch directory with
mktemp -dunder$TMPDIR, copiesexamples/*.pyinto it, deliberately breaks one assertion to prove the harness can fail, and removes the directory again in the same run. Nothing is written to your home directory, nothing above the lab root is modified, and no system path is touched. - Network. After the one
pip install, this lab is completely offline. Check 10 asserts that no URL appears anywhere inexamples/orstarter/source. The dataset issklearn.datasets.load_diabetes(scaled=False), bundled inside the scikit-learn package itself; nothing is downloaded and no external dataset file is fetched.sklearn.datasets.fetch_california_housingis imported by name in exercise 1 only to inspect its signature (download_if_missingdefaults toTrue) -- it is never called, and this lab never fetches anything. - Credentials. There are none.
requires_api_keyisfalse, no account is needed, and nothing in this lab reads an environment variable that could hold a secret. - Privileges. Nothing here needs
sudo. If a step appears to ask for administrator rights, stop and re-read it -- it is not this lab. - Reversibility. Everything this lab creates is inside its own
directory and is removed by the cleanup commands in
metadata.yml.rm -rf .venvreturns the machine to exactly its prior state.
The one install step, and how to check it
pip install -r requirements/requirements.txt downloads three packages
from the Python Package Index into a lab-local virtual environment,
never into your system Python. Pinning exact versions is a security
control as well as a reproducibility one: an unpinned install resolves to
whatever is newest at the moment you run it, which is a moving target you
have not reviewed.
If you want to verify what you are installing before you install it, pip can check hashes for you:
.venv/bin/pip install --require-hashes -r requirements/requirements.txt
That requires a hash-annotated requirements file, which this lab does not
ship because the correct hashes differ per platform wheel. Generating one
for your own platform with pip-compile --generate-hashes is a
reasonable habit for any environment you care about.
The security idea in this lab
GatedTestSet here is the same access-control pattern Day 144 and Day
147 both used: it holds data, permits exactly one read, counts the reads,
and refuses the second with a message explaining what the refused answer
would actually have been. That is a budget enforced by the resource
itself rather than by the good intentions of whoever holds it, the same
shape as a one-time token, a single-use signed URL, or a rate limiter.
The design detail worth copying is that the counter does not advance on a refused attempt, which harness check 7 confirms with five repeated refused attempts in a row. A gate whose refusals consume budget can be drained by an attacker who never succeeds at anything.
The wider point, sharpened by this lab's leaky-selection exercise: a test
set spent by peeking rather than by an outright second .evaluate()
call is just as spent. leaky_selection_test_rmse fits every one of the
23 candidates and scores each one on the test rows directly to find the
lowest error -- no code anywhere calls evaluate twice, and the leak is
real anyway. A budget enforced only at one call site is not the same as
a budget enforced on the resource; this lab's leaky search deliberately
bypasses GatedTestSet to show that the gate protects only the path that
uses it.
What the code does that is worth understanding
- The dataset loader takes no seed and returns the same 442 rows every time, because it is bundled data, not sampled data. Every split, every cross-validation fold, and every bootstrap resample is separately seeded, and nothing is cached to disk or memoised across calls.
GatedTestSetholds no class-level state, so two gates are two independent budgets -- the same guarantee Day 144's and Day 147's versions made._normal_ppfis a closed-form rational approximation with no data dependence, no randomness, and no external call -- it evaluates a fixed polynomial on its input.- Nothing in this lab evaluates a string, imports dynamically, reads a path from data, or inspects the environment.
- The harness captures the exit status of
run_tests.shitself and never reads the status of a pipeline.cmd | tailreportstail's status, which is almost always zero -- an always-passing test suite is a security control that has quietly stopped working.
Reporting a problem
If you find something in this lab that writes outside its own directory, reaches a network it did not start, or asks for a credential, that is a bug. Nothing here is supposed to do any of those things.