The honest estimate
best_score_ is a winner’s score — and here the optimism is 0.0003
nested CVbest_score_selection optimismwinner’s cursetest set disciplineWatch it happen
Play it through, or step back and forth yourself.
That upward bias is what nested CV exists to measure.
GridSearchCV.best_score_ is the cross-validated score of the combination that scored best on those same folds. Picking the maximum of a set of noisy numbers gives you a number biased upwards. That's selection optimism.
The idea
GridSearchCV.best_score_ is the cross-validated score of the combination that scored best on those same folds. You picked the maximum of a set of noisy numbers, and the maximum of noisy numbers is biased upwards. That's selection optimism.
It's the same mechanism as lesson 3's golden rule, applied one level up: try enough candidates and one will look good by luck alone.
Nested cross-validation measures it
inner = GridSearchCV(pipe, grid, cv=3, scoring="roc_auc")
nested = cross_val_score(inner, X, y, cv=5, scoring="roc_auc")Put the whole search inside a cross-validation loop. The inner loop picks hyperparameters; the outer loop scores the entire procedure on data the inner loop never touched. The gap between best_score_ and the nested score is the optimism.
What ours actually says
grid size best_score_ nested optimism
4 candidates 0.8786 0.8789 −0.0003
40 candidates 0.8800 0.8797 +0.0003Essentially zero. Even at ten times the grid size.
That is not what most treatments of this topic imply, and it's worth saying plainly: on this problem, nested CV would have told us nothing we didn't already know. Understanding why is more useful than performing the ritual — because it tells you when you genuinely do need it.
What optimism scales with
It grows with:
- How many genuinely different things you tried. Four similar values of
Care nearly the same model; forest vs SVM vs boosting vs linear are not. - How noisy each estimate is — which means small data and high-variance models.
- How many times you have been round the loop. Twenty rounds of "let me try one more thing" is a search whose size you never counted.
And it shrinks with data. Ours has 900 rows and 180 per fold, so each fold's estimate is reasonably stable, and picking the best of eight stable numbers barely inflates anything.
Flip those around and it bites hard: 80 rows, sixteen per fold, forty candidate model families. There, best_score_ can be several points above the truth.
The cost
plain search 8 combos × 5 folds = 40 fits
nested (8 combos × 3 inner) × 5 outer = 120 fitsAnd note what nested CV does not give you: a model. Each outer fold picks its own hyperparameters and they may differ from each other. It estimates how well the procedure generalises. The model you ship comes from one final search on all the training data.
What to actually do
- Hold out a test set, before anything else.
- Search on the training data with cross-validation.
- Report the test score — never
best_score_. - Reach for nested CV when you have little data, many genuinely different candidates, or no test set to spare.
The held-out test set was already doing this job. That's why lesson 3 made such a point of keeping it sealed — it's your defence against every kind of selection optimism at once, and it costs nothing extra.
The failure this really guards against
Not the grid. The human loop. You try a model, check, adjust, check, add a feature, check. None of that appears in any cv_results_, and it is a search — usually a much bigger one than the grid you wrote down.
No amount of nesting protects you from it. Only an untouched test set does, which is the whole argument for the discipline.
See it run
The lesson's code, ready to run and to fiddle with.
Putting the kettle on…
Starting up…
Worked example
not gradedAlready written and ready to go — press Run to see what it does, then change a number, a column name, anything, and run it again.
tryshrinking the data to X.head(120) and re-running — the optimism appears.
Your turn
3 exercises. Write the code yourself, then press Check — a nudge and the answer are there if you want them.
Measure the optimism. Return [best_score_, nested_score] for a grid over clf__C = np.logspace(-2, 1, 4), both rounded to 4 places.
The number you should actually report. Search on the training split, then return [best_score_, test_auc], rounded to 4 places — and notice they are not the same.
Make the optimism appear. Repeat the nested comparison on just the first 120 rows and return [best_score_, nested], rounded to 4 places.
