One split can be lucky
In b4 you picked the sweet-spot tree depth by watching a single test curve. That works, but it rests on one random split - shuffle the rows differently and the peak can wobble. Cross-validation gives you a steadier grade by averaging over several splits, and it is the tool that lets you tune hyperparameters honestly, because the test set stays sealed until the end. Think of it as taking the same exam five times with different question sets and reporting your average plus how much you varied.
cross_val_score, read the fold-to-fold spread, and let GridSearchCV re-discover b4's sweet-spot depth for you.
k-fold cross-validation 7 min live
Split the training data into k equal folds. Each fold takes a turn as the validation set while the other k-1 folds train the model; you do this k times, so every row is validated exactly once. Average the k scores for a steadier estimate, and look at the spread - a wide spread means the score depends heavily on which rows you happened to hold out. Crucially, all of this happens on the training data. You tune on these CV scores and never on the test set, which you spend exactly once, at the very end.
LiveWhat CV estimates3 min▶
Cross-validation does not build a better model - read that twice. It gives you a less-lucky estimate of out-of-sample performance: how the model is likely to do on data it has not seen. The mean is your best guess; the spread across folds is a second piece of information you should always report.
- The mean - your central estimate of generalisation.
- The spread (std) - how much the score swings depending on which rows you validate on. A wide spread is a warning that a single test split could easily mislead you.
Self-studyWhy you never tune on the test set2 min▶
The test set has exactly one job: to give you an honest, one-time estimate of performance on truly unseen data. The moment you use it to choose something - a depth, an alpha, a feature set - it stops being unseen. You have leaked information from it into your model choices, and the score it reports is now optimistic.
That is why tuning lives entirely on cross-validation (or a separate validation split). CV is your sandbox for trying settings; the test set is the sealed envelope you open once, at the very end, to report the number you will actually stand behind. Spend it early and you have nothing honest left to report.
Cross-validate and tune in Python 18 min · everyone builds
We stay on the b4 regression - predict order_value from prior_30d_spend with a decision tree - so the numbers line up. First a plain 5-fold CV, then GridSearchCV to pick max_depth, then one final look at the sealed test set.
Reload and carve out a sealed test set. Everything else happens on the training portion.
import numpy as np, pandas as pd
from sklearn.model_selection import train_test_split
df = pd.read_parquet("lumen_sessions.parquet")
buyers = df[df.converted == 1].copy()
X = buyers[["prior_30d_spend"]]
y = buyers["order_value"]
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.25, random_state=42) # test set: do not touch until the end
Run 5-fold cross-validation. Score a depth-4 tree and read both the mean and the spread.
from sklearn.tree import DecisionTreeRegressor
from sklearn.model_selection import cross_val_score
tree = DecisionTreeRegressor(max_depth=4, random_state=0)
scores = cross_val_score(tree, X_train, y_train, cv=5, scoring="r2")
print("fold scores:", scores.round(2)) # e.g. ~0.33 .. ~0.50
print("mean R2 = %.2f +/- %.2f" % (scores.mean(), scores.std()))
# -> mean ~ 0.40 +/- ~ 0.06
The mean lands around R² ~ 0.40 with a spread of about +/- 0.06, folds ranging roughly 0.33 to 0.50. That spread is the point: a single unlucky split could have handed you 0.33 and made a fine model look weak. The average is steadier, and the range is information you keep.
Tune max_depth with GridSearchCV. Let CV, not your eyeballs, pick the depth - and watch it re-find b4's sweet spot.
from sklearn.model_selection import GridSearchCV
grid = GridSearchCV(
DecisionTreeRegressor(random_state=0),
param_grid={"max_depth": range(1, 13)},
cv=5, scoring="r2")
grid.fit(X_train, y_train) # searches on TRAIN only, via CV
print("best params:", grid.best_params_) # -> max_depth around 3-4
print("best CV R2 : %.2f" % grid.best_score_) # -> ~ 0.40
# matches b4's U-curve peak at depth 3 - CV found it without touching the test set
GridSearchCV lands on max_depth around 3-4 - exactly the sweet spot of b4's U-shaped curve. No peeking at the test set, no guessing: cross-validation picked the complexity for you.
Now, and only now, open the test set. Score the tuned model once and report that number.
best = grid.best_estimator_
print("final held-out test R2 = %.2f" % best.score(X_test, y_test))
# report THIS number - it is the one you never tuned against
A team once tuned dozens of settings by watching the test score climb, then shipped the winning number to their VP. In production it fell flat - they had quietly overfit to the test set by choosing against it over and over. Cross-validation is the guardrail: tune on CV, keep the test set sealed, and the number you promise is the number you get.
This week ◐ 40 min total
- Run cv=10. Repeat the depth-4 CV with
cv=10and compare the mean and spread tocv=5. Does the mean move much? Does the spread tighten or widen, and why might more folds change it? - Grid a second hyperparameter. Add
min_samples_leafto theparam_gridalongsidemax_depthand let GridSearchCV search the combined space. What does it pick? - Confirm the sweet spot. Check that the CV-chosen depth matches b4's U-curve peak (depth ~3). If it differs, explain what the difference tells you.
- Optional: print
grid.cv_results_["mean_test_score"]and plot it against depth - you have just drawn b4's validation curve, this time built from cross-validation.
Three questions before you go 🎯 ◐ 90 seconds
1 · What does cross-validation give you?
CV estimates how the model will generalise by averaging over folds. It does not train a better model - it gives you a less-lucky estimate of the same model's performance.
2 · What must you never do?
The test set is spent once, at the very end. Tuning against it leaks information and produces an over-optimistic score. Tune on cross-validation or a validation split instead.
3 · In k-fold CV, how is each fold used?
Each of the k folds takes a turn as the validation set while the remaining k-1 folds train the model, so every row is validated exactly once across the k rounds.
What this session covers
This builder session covers k-fold cross-validation and hyperparameter tuning on the Lumen dataset, building directly on b4's overfitting curve and b5's alpha dial. Classification metrics in depth come next in b7.
Builder Session 6 cheat sheet · pin this
cross_val_score(model, X, y, cv=5, scoring="r2"). Returns one score per fold; take .mean() and .std().param_grid using CV, refits the best on all training data. Read best_params_ and best_score_.