2.1.12 · D3Data Preprocessing & Feature Engineering

Worked examples — Train - validation - test splitting

2,586 words12 min readBack to topic

Before anything, six plain-word anchors so no symbol is unearned:


The scenario matrix

Every splitting problem you will meet lives in one of these cells. The last column names the worked example that covers it.

Cell Situation What breaks if you're naive Covered by
A Balanced, medium nothing — the baseline Example 1
B Imbalanced classes minority class vanishes from a split Example 2
C Rounding leftovers floors don't add up to Example 3
D Tiny (< 1000) splits too small to trust Example 4
E Huge (> 100k) wasting data on oversized test set Example 5
F Temporal / ordered data future leaks into the past Example 6
G Leakage via scaling test stats sneak into training Example 7
H Exam twist: two-step split ratios second split size is not the final ratio Example 8

Each example says which cell it hits. Together they fill the whole table.


Example 1 — Cell A: the balanced baseline

Forecast: guess the three counts per class before reading on. (Hint: symmetry should make spam and not-spam identical.)

  1. Find the target sizes of each split. Why this step? Fractions describe intent; models need whole counts. , clean.

  2. Split within each class using proportions. Here . Why this step? Stratifying = keeping identical in every bucket, so we allocate each class separately (see the parent's Step 2).

  3. Not-spam is identical by symmetry: 350 / 75 / 75.

Verify: Spam total ✓. Not-spam ✓. Grand total ✓. Every split is exactly 50 % spam — stratification holds perfectly.


Example 2 — Cell B: severe imbalance

Forecast: with only 100 diseased images, how many make it to the 150-image test set? Guess before computing.

  1. Split sizes: 700 / 150 / 150 (same as Example 1). Why? Split sizes depend only on , not on class balance.

  2. Allocate the minority class using : Why this step? Splitting within the 100 diseased rows guarantees each bucket gets its 10 % share — a plain shuffle could dump all 100 into training.

  3. Majority class fills the rest: , , healthy.

The figure below stacks each bucket as a bar: cyan = healthy, amber = diseased. What to look for: the amber slice sits at exactly 10 % of every bar's height — that constant amber fraction across all three bars is the whole visual point of stratification. The absolute amber counts (70, 15, 15) shrink with the bucket, but the ratio never moves.

Figure — Train - validation - test splitting

Verify: Diseased ✓, healthy ✓, total ✓. Each split is exactly 10 % diseased.


Example 3 — Cell C: the rounding leftover

Forecast: exactly — so surely nothing is left over? Guess, then watch the floors bite.

  1. Apply floors: Why this step? Each split independently rounds down, so each drops a fractional row.

  2. Count the assigned rows: . One row is homeless. Why this step? Three downward roundings almost always lose 1–2 rows even when the fractions sum to 1.

  3. Assign the remainder to the last split: . Why this step? We must place every row somewhere, and the standard convention (what scikit-learn's slicing does) is that the last split — test, the tail of the array — absorbs leftovers, since it is "everything after the val cut."

Verify: ✓. Ratios become — negligibly off, which is exactly why leftovers are harmless. The lesson: never assert your counts sum to without checking.


Example 4 — Cell D: tiny dataset

Forecast: how many samples land in the validation set — and would you trust a model comparison made on that many?

  1. Split sizes: , , . Why? Same floor arithmetic.

  2. Estimate the confidence-interval width for test accuracy. The parent's formula: for accuracy on test samples, the 95 % interval half-width is Why this tool and not another? Accuracy is a mean of 0/1 outcomes; the standard error of a proportion is , and is the number of standard errors covering 95 % of a normal curve. It answers "how fuzzy is my reported accuracy?"

  3. Plug in worst case , : Why worst case? is largest at , giving the widest (most pessimistic) interval.

Verify: . A test accuracy of "80 %" really means "somewhere between 57 % and 100 %" — useless. Conclusion: with , obey the parent's rule and use 2.1.13-Cross-validation instead of a single holdout.


Example 5 — Cell E: huge dataset

Forecast: how much narrower is the confidence interval for the giant split, and is the difference worth it?

  1. Test sizes: Why? We're comparing what fraction is "spent" on evaluation.

  2. CI half-width (worst case ) for each: Why this step? To see whether shrinking the test set actually hurt precision.

  3. Interpret: both are already tiny — vs . Meanwhile training rows jump from M to M. Why this step? is plenty for a final estimate, so the extra training rows are pure gain.

Verify: Training rescued rows ✓. Both intervals under ✓. Lesson: at huge , absolute test size (not percentage) is what matters — 20k is enough.


Example 6 — Cell F: temporal data (ordering trap)

Forecast: which month should be the first row of the test set?

  1. Diagnose the leak. Random shuffling scatters future months into training and past months into test. The model trains on Dec-Year2 to predict, say, Mar-Year1 — using the future to explain the past. Why this step? The whole point of a test set is to imitate deployment, where you only ever have the past. See 6.3.2-Data-leakage.

  2. Cut chronologically, no shuffle. Why floors again? val months.

The figure below draws the 24 months as a left-to-right timeline, coloured by bucket: cyan = train, amber = val, white = test. What to look for: the three colours never interleave — every cyan bar is strictly to the left of every amber bar, which is strictly left of every white bar. That "no colour ever jumps backward" property is exactly what stops the future from leaking into the past; a random split would speckle the three colours all across the timeline.

Figure — Train - validation - test splitting
  1. Count: train 16, val 3, test months. Leftovers land in test (Cell C convention).

Verify: ✓. Test months (20–24) are strictly later than every training month (1–16) — no future information reaches training. The ratio drifted to 66.7 / 12.5 / 20.8 because 24 is small; that is acceptable for time series, where order matters more than exact percentages.


Example 7 — Cell G: scaling leakage (the classic bug)

Forecast: does fitting on all-data make the test point look more or less extreme?

  1. Wrong way — fit on all 5 values. Mean Why this step? This is exactly what scaler.fit(X_all) does — the test value 100 pulls the mean up.

  2. Wrong-way std. Deviations from are , so Why this step? We need the leaky to finish the leaky standardization and see how tame the outlier becomes.

  3. Right way — fit on train only. Mean Deviations from are , so Why this tool? Standardization (see 2.1.11-Feature-scaling-normalization) needs a ; using train-only ones keeps the test set genuinely unseen.

  4. Standardize the test value 100 both ways with . Why this step? Putting the two -scores side by side is the punchline: the leak literally changes how anomalous the point appears.

Verify: , and ✓. ; ✓. Lesson: the correct pipeline flags 100 as a huge outlier (); the leaky one tames it to a mild , hiding the anomaly. Always fit_transform on train, transform on val/test.


Example 8 — Cell H: exam twist, two-step ratios

Forecast: the second split needs 70 : 10 — is test_size=0.10 correct? (It is a trap — guess before reading.)

  1. First split: hold out 20 % test. This leaves X_temp with of : Why this step? The test set must be carved off first and then never touched again, so all later splitting happens on the remaining 4000 rows.

  2. Work out what "10 % of " means as a fraction of X_temp. We want val to be of the original 5000 = 500 rows, but the second train_test_split only sees the 4000 temp rows, so: Why this step? train_test_split's test_size is always relative to the array it receives. Passing would take 10 % of 4000 = 400 rows (only 8 % of ) — the trap. We rescale via .

  3. Convert the fractions back into row counts to confirm we land on 70 / 10 / 20: Why this step? Turning the fraction back into counts is the final check that the trick actually produces the intended split, not just plausible-looking algebra.

Verify: ✓, ✓, ✓. And ✓. The trap — passing test_size=0.10 — would give val (8 %), silently wrong.


Recall Self-test

For , 70/15/15 with floors, how many test samples after leftovers? ::: 151 (702 + 150 + 150 = 1002, remaining 1 goes to test) Two-step split for final 70/10/20, second test_size? ::: 0.125, i.e. Correct standardized value of test point 100 given train ? ::: using Leaky standardized value of the same point (fit on all data)? ::: using — the outlier gets hidden Why is with an 18-sample test set untrustworthy? ::: 95% CI half-width — too wide; use cross-validation

Related: 3.2.1-Overfitting-underfitting (why the val set exists), 4.1.5-Early-stopping (a val-set consumer), 2.1.13-Cross-validation (the small- fix).