2.6.15 · D3Model Evaluation & Selection

Worked examples — Cross-validation pitfalls and nested CV

3,356 words15 min readBack to topic

This page is a drill hall. The parent note built the theory of why standard cross-validation lies and how nesting fixes it. Here we walk through every kind of situation the topic can throw at you, one worked example at a time, so that when you meet one in the wild — or in an exam — you have already seen its twin.

Before starting, three plain-language reminders so we never use a symbol we did not earn:

If 2.6.2-K-fold-cross-validation (splitting data into K equal chunks, training on K−1 and testing on 1, rotating) is fuzzy, read that first — everything below assumes it.


The scenario matrix

Every cross-validation mistake is one of a small number of shapes. This table lists every shape; the examples that follow each fill one or more cells.

Cell Scenario class The dangerous move Covered by
A Hyperparameter tuning, healthy data report min-over-λ CV score Ex 1
B Feature selection before CV rank features on all data Ex 2
C Preprocessing before CV (scaling) fit scaler on all data Ex 3
D Degenerate: only ONE λ tried () (no search → no selection bias) Ex 4
E Limiting case: tiny data / huge feature count noise correlates by chance Ex 5
F Reporting the wrong number after correct nested CV quote inner score Ex 6
G Real-world word problem (medical, grouped data) random split breaks groups Ex 7
H Exam twist: which number to deploy confuse estimate vs final model Ex 8

The "sign axis" of this whole page. Picture a single number line for the bias . Its origin (zero) is an honest report; everything to the right (positive) is optimistic — the report looks better than reality; everything to the left (negative) would be pessimistic. The rule that ties every cell together: leakage can only push you rightward (optimistic); it never pushes left. Cell D is exactly the origin — with there is no minimum-over-random-variables trick, so no selection bias arises. Keep that one-directional asymmetry in mind; it is the through-line of every example.

Figure — Cross-validation pitfalls and nested CV

Reading the figure (s01) — this is the map for the whole page. Alt-text / caption: "Two error curves versus number of configs searched; a flat teal honest curve, a dipping orange leaky curve, and a plum optimism band between them, touching at m=1." The horizontal axis is , how many configs you searched (also read as "how much you leaked"). The vertical axis is the reported error. The flat teal curve is the honest nested estimate — it does not improve just because you searched more. The dipping orange (red) curve is the leaky standard estimate — it sinks lower the more configs you try, because taking a minimum over more noisy numbers finds a luckier one. The vertical height of the shaded plum band between them is exactly , the optimism (positive bias) from the definition above. At the far left (, the black dot) the two curves touch — the band has zero height — and that is Cell D, the honest origin. Read every example below as "how far right on this band am I?"


Example 1 — Cell A: hyperparameter tuning done two ways

Forecast: guess before reading — which number is smaller, and by roughly how much?

  1. Standard CV number = the minimum over the configs. . This is the of standard CV. Why this step? Standard practice picks the best-scoring config and quotes that same score. Taking a minimum over noisy numbers systematically lands below their average — this is the selection bias the parent note derived.

  2. Nested CV number = the average of the outer-fold errors. This is our truth-proxy . Why this step? Each outer test fold ( for that fold) was never seen while its own α was chosen. Averaging (not minimising) over independent estimates removes the optimism.

  3. Optimism . Why this step? With our sign convention, taking nested () minus standard () gives — a positive, hence optimistic, gap. Report the standard 0.23 and you claim ~1.6 error-points better than reality.

Verify: the standard score must be ≤ the mean of the four CV scores (). Indeed ✓, and the optimism is positive (nested > standard) ✓ — bias sits to the right of zero (optimistic), matching Cell A's sign rule.


Example 2 — Cell B: feature selection inside vs outside the loop

Forecast: will putting feature selection inside the loop raise or lower the reported accuracy?

  1. Outside-loop selection uses all of , so features 2 and 4 (correlations 0.60, 0.55) were chosen partly because they matched the held-out rows too. Why this step? Let be the feature-picker (recall from the definitions: it is honest only if fed ). Here it was fed the whole , so it saw the rows it will later be graded on — textbook leakage.

  2. The honest pipeline is "select then evaluate on unseen". Inside the loop, for each fold the selector is fed only that fold's (the 90% training slice); the 10% rows had no vote. Why this step? We want to estimate performance of the whole pipeline " then " — first pick features on , then fit the model — on rows it never touched. Formally we compare (leaky, uses all of ) against the honest .

  3. Report the inside-loop 0.78, discard 0.92. In error terms (1 − accuracy) this is vs , so bias ; equivalently the accuracy drop . Why this step? The drop is the leakage you removed. Bigger drop ⇒ selection was fitting noise.

Verify: the two retained features are exactly those with correlation ({0.60, 0.55}) ✓; honest ≤ biased (0.78 ≤ 0.92) ✓; optimism direction = higher accuracy for the leaky method ✓ (accuracy leaks up, error leaks down — both land on the positive/optimistic side).


Example 3 — Cell C: scaling fit on the whole dataset

Forecast: is the leak here large or subtle compared to Example 2?

  1. Leaky mean . Why this step? This mean was computed with the future values included — so the scaler is contaminated.

  2. Which std? Population vs sample — call it out. Deviations from the mean are , squares , summing to .

    • Population std (divide by , the convention scikit-learn's StandardScaler uses): variance , so .
    • Sample std (divide by , the default of numpy.std(ddof=1), pandas.std, and most stats courses): variance , so . Why this step? The number you get for the scale depends on this -vs- choice, and libraries disagree — this is a classic silent mismatch. For leakage the point is identical either way; we quote both so you are never surprised by which one a tool returns.
  3. Correct fix: inside each outer fold, fit the scaler on training rows () only, then transform the rows. The scaler must never fit on data it is about to be judged on. Why this step? Preprocessing is part of the pipeline; it must live inside the loop exactly like the model.

Verify: mean ✓; population std and sample std ✓. This is a subtle leak (unlike Ex 5): scaling rarely inflates scores much unless the feature distribution is wild — but it is still Cell C leakage.


Example 4 — Cell D: the ZERO case (only one hyperparameter, )

Forecast: does the parent note's "minimum over configs" argument even apply here?

  1. Count the configurations searched: (recall = number of hyperparameter settings tried). Why this step? The optimism came from taking a over several noisy CV scores. With one config there is nothing to minimise over.

  2. CV score = plain average . Why this step? This is already an (approximately) unbiased estimate of generalisation — no selection happened, so and the bias .

  3. Conclusion: nested CV is unnecessary for the estimate when and no feature selection / preprocessing decisions use the folds. Why this step? This is the degenerate cell: bias , sitting exactly at the origin of our sign axis. It is the honest baseline the other cells deviate from.

Verify: plain average ✓; with the "min over " equals the single value, so the selection-bias term vanishes ✓.


Example 5 — Cell E: limiting case, tiny n, huge p (Cawley–Talbot flavour)

Forecast: with 10× more features than samples, guess how much accuracy is illusory.

  1. Honest skill above chance . Why this step? Anything above 50% for balanced binary is genuine signal — the nested number is trustworthy, so it is our side (in accuracy terms).

  2. Illusory (leaked) accuracy . Why this step? With , many features correlate with by chance. Selecting on all of validates on the very rows those features were fit to. In error terms , a large positive/optimistic bias.

  3. Interpretation: almost as much of the biased headline is noise-fitting (18 points) as is real (23 points). This is the far-right end of the plum band in Figure s01. Why this step? This is why the topic exists — small-n/large-p is where naive CV fails catastrophically. Links to 2.7.3-Bias-variance-tradeoff: high-variance selection on tiny data.

Verify: honest lift ✓; leaked lift ✓; both non-negative and sum to the total lift above chance () ✓.


Example 6 — Cell F: correct nested run, wrong number reported

Forecast: is the inner 0.16 optimistic or pessimistic relative to the truth?

  1. The performance estimate is the OUTER average (from Ex 1). This is . Why this step? Inner scores exist only to choose λ; they are contaminated by selection within that fold, so they cannot serve as the honest performance number.

  2. The inner 0.16 is doubly optimistic, so setting is unjustified: it is (a) the winning config's own tuning score — already a minimum over the inner search — and (b) then hand-picked as the smallest across the five outer folds. Why this step? Two nested "pick the best" operations each drag the number below its honest average (the same min-over-noise mechanism as Cell A, applied twice), so 0.16 is far to the optimistic right of and must be rejected as a performance claim.

  3. Overstatement . Why this step? Reporting 0.16 claims your model is ~8.6 error-points better than the honest nested estimate — a large positive/optimistic gap.

Verify: outer average ✓; the cherry-picked inner value is less than every plausible honest number () ✓; overstatement ✓.


Example 7 — Cell G: real-world word problem, grouped data

Forecast: does random row shuffling help or hurt the honesty here?

  1. Random shuffling puts the same patient in both and . A patient's visit 3 can train while visit 4 tests. Why this step? The model memorises patient identity, not disease pattern — a classic leakage (see 2.6.16-Data-leakage).

  2. Group-wise split keeps all 5 visits of a patient together, so test patients are genuinely new. Why this step? The regulator's question is about new patients, so the evaluation unit must be the patient, not the row; the grouped 0.71 is the honest and the random 0.88 is the leaky .

  3. Answer the regulator with 0.71, the grouped number. Leakage inflated AUC by (positive/optimistic, since higher AUC = rosier report). Why this step? The question defines the correct split; matching the split to the deployment scenario is the whole game.

Verify: grouped ≤ random (0.71 ≤ 0.88) ✓; inflation ✓; direction optimistic (AUC leaks up) ✓ — same one-directional sign law as every other cell.


Example 8 — Cell H: exam twist, "which number to deploy?"

Forecast: are these two answers the same object or different objects? (They are different — that is the trap.)

  1. (a) Expected error = outer average = 0.246. This is the honest you quote. Why this step? Nested CV estimates the procedure's performance, not any one model. This is the number you report.

  2. (b) The shipped model: aggregate the outer folds' chosen α, then retrain on ALL 100 samples. The two standard aggregation rules are (i) majority vote — take the α that appears in the most outer folds — and (ii) the "refit" rule — ignore the per-fold winners and simply run one ordinary inner-CV hyperparameter search on the entire dataset. Both are accepted practice (scikit-learn's typical workflow uses (ii): the outer loop is only for scoring, and the deployed model comes from a final GridSearchCV.fit on all data). Here the per-fold winners are ; the majority-vote rule gives counts : 3 times, : 2 times ⇒ choose , then retrain on all 100 rows. Why this step? You want the strongest final artefact, so use every row — but its performance is still estimated by 0.246, not re-measured on the training data. Majority vote is the simplest defensible aggregator when the per-fold winners are stable (as here, 3 vs 2); if they disagreed wildly you would prefer rule (ii).

  3. Do NOT report a new CV score for the retrained-on-all model. That model has no held-out data left, so any number it produces would drop artificially below — re-introducing optimism. Why this step? The estimate (a) and the artefact (b) are separate deliverables. Confusing them re-introduces the very optimism nested CV removed. (Compare choosing when to stop in 3.4.5-Early-stopping: the stopping decision is tuning, its score is not the final estimate.)

Verify: majority-vote winner of is (count 3 > 2) ✓; deployment estimate stays 0.246, unchanged by retraining ✓.


Recall Quick self-test

Standard CV reports the ??? over the configs, nested reports the ??? over outer folds. ::: minimum ; average With our sign convention , does leakage give a positive or negative bias? ::: positive (optimistic) If you search only ONE hyperparameter config () and touch no folds for preprocessing, is your CV biased? ::: No — selection bias needs (Cell D, the origin) After nested CV you deploy the model by retraining on ??? data, using an ??? rule for α, and quote the ??? as its error. ::: all ; aggregation (majority vote or a final full-data search) ; outer average