Module 4 of 6 · 35 minutes

Better models: overfitting, ensembles and validation

How do you know a model will work on tomorrow's data? This module covers the tools practitioners use to build models that generalise.

Learning objectives

  • Diagnose overfitting from training vs test scores
  • Explain random forests and gradient boosting
  • Compare models with k-fold cross-validation
  • Tune hyperparameters with grid search
  • Use pipelines to prevent leakage

4.1 Overfitting, again

A model that memorises its training data performs brilliantly on it and poorly on new data. Compare training and test scores: a big gap signals overfitting.

for depth in [2, 4, 8, 16, None]:
    t = DecisionTreeClassifier(max_depth=depth, random_state=0).fit(X_train, y_train)
    print(f"depth {str(depth):4}  train {t.score(X_train, y_train):.3f}   test {t.score(X_test, y_test):.3f}")

Unlimited depth reaches near-perfect training accuracy while test accuracy drops — classic overfitting.

124816no limittree depth (model complexity) →sweet spotgap = overfittingunderfittingtraining accuracytest accuracy
As a tree gets deeper, training accuracy keeps rising but test accuracy peaks and then falls. The widening gap is overfitting. (Illustrative values.)

4.2 Ensembles: many models are better than one

ModelIdeaStrengths
Random forestMany trees, each trained on a random sample of rows and features; they voteRobust, little tuning, resists overfitting
Gradient boostingTrees built one after another, each correcting the previous ones' errorsOften the most accurate on tabular data

For structured, tabular business data, tree ensembles are frequently the best-performing choice — often outperforming deep learning.

TrainingdataTree 1random sample AchurnTree 2random sample BstayTree 3random sample CchurnMajority votechurn (2 of 3)
A random forest: each tree sees a different random sample of rows and features, and the forest combines their votes. Gradient boosting instead builds trees in sequence, each fixing the last one's errors.

4.3 Cross-validation

A single train/test split can be lucky or unlucky. k-fold cross-validation splits the training data into k parts, trains on k−1 and validates on the remaining one, k times, and averages the scores. Use it to compare models and settings; keep the test set for the final check.

from sklearn.model_selection import cross_val_score
from sklearn.ensemble import RandomForestClassifier, HistGradientBoostingClassifier

models = {
    "logistic": LogisticRegression(max_iter=1000),
    "tree (depth 4)": DecisionTreeClassifier(max_depth=4, random_state=0),
    "random forest": RandomForestClassifier(n_estimators=300, random_state=0),
    "gradient boosting": HistGradientBoostingClassifier(random_state=0),
}
for name, m in models.items():
    scores = cross_val_score(m, X_train, y_train, cv=5, scoring="roc_auc")
    print(f"{name:18}  AUC {scores.mean():.3f} ± {scores.std():.3f}")

On this dataset the hidden pattern is close to linear, so logistic regression may do as well as the ensembles — a useful reminder that complex isn't always better.

Round 1validateRound 2validateRound 3validateRound 4validateRound 5validateTest setkept asideuntil the endtraining data split into 5 foldstrainvalidate→ average the five scores to compare models
5-fold cross-validation: every row is used for validation exactly once, giving a steadier estimate than a single split. The test set stays untouched.

4.4 Hyperparameter tuning

Hyperparameters are settings you choose before training (tree depth, number of trees, learning rate). Search over them with cross-validation:

from sklearn.model_selection import GridSearchCV

grid = GridSearchCV(RandomForestClassifier(random_state=0),
                    {"n_estimators": [100, 300], "max_depth": [4, 8, None], "min_samples_leaf": [1, 10]},
                    cv=5, scoring="roc_auc", n_jobs=-1)
grid.fit(X_train, y_train)
print(grid.best_params_, round(grid.best_score_, 3))
print("Test AUC:", round(roc_auc_score(y_test, grid.predict_proba(X_test)[:, 1]), 3))

4.5 Pipelines keep it honest

Put preprocessing (imputation, scaling, encoding) and the model in one pipeline, so cross-validation refits preprocessing within each fold — preventing leakage (see Data Quality, Module 5).

from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler

pipe = make_pipeline(StandardScaler(), LogisticRegression(max_iter=1000))
print(cross_val_score(pipe, X_train, y_train, cv=5, scoring="roc_auc").mean().round(3))

Key takeaways

  • A large train–test gap signals overfitting.
  • Random forests average many trees; boosting builds trees that correct errors.
  • Cross-validation gives more reliable comparisons than one split.
  • Tune hyperparameters with cross-validation; test once at the end.
  • Pipelines refit preprocessing within each fold.