Winning Kaggle the Reproducible Way

Interactive chapter

This page shows the chapter notebook with its saved results. Constructed examples do not measure competition gains.

Chapter 13: Hyperparameter Tuning with Optuna

Try the decision first

As the trial budget grows, how much of the search's reported improvement shows up on rows the search never used?

Write your expectation before opening the worked results. Use the chapter activity to compare supported inputs. To change the calculation itself, run the notebook on GitHub.

Your answers stay on this page until you download them. They are not sent to a server.

Open the chapter activity

Worked notebook and saved results

As the trial budget grows, how much of the search's reported improvement shows up on rows the search never used?

The chapter says an HPO winner's score is not an independently assessed improvement. The size of the difference, and how it grows with the budget, tells you how much of a tuning report to believe.

Constructed data and a stand-in learner and sampler. The reserved-row gain is small and noisy (it is below zero in some datasets, see the dots), and the optimism is a property of this 300-row generator, not a constant.

The experiment

A constructed binary task: 300 rows for the search and 4,000 rows reserved before it begins. A scikit-learn histogram gradient boosting model is tuned by random search (learning rate, leaves, leaf support, L2 and rounds) with 3-fold cross-validation, and the best configuration so far is refitted on the 300 rows and scored once on the reserved rows. Both are reported as gains over a fixed default configuration. The control is the number of trials; twelve independent datasets are averaged.

Calculation code
import numpy as np
from scipy.stats import rankdata
from sklearn.ensemble import HistGradientBoostingClassifier
from sklearn.model_selection import StratifiedKFold

from kaggle_companion.activities._common import clean

SEED = 13
REPLICATES = 12         # independent constructed datasets; every estimate is their mean
N_SEARCH, N_ASSESS = 300, 4000   # rows the search may use, and rows reserved before any search
BASELINE = dict(learning_rate=0.1, max_leaf_nodes=31, min_samples_leaf=20, l2_regularization=0.0, max_iter=100)


def generate(n, rng):
    """Ten numeric features: two main effects, an interaction, a bend and a threshold, plus noise."""
    X = rng.normal(size=(n, 10))
    logit = 0.9 * X[:, 0] - 0.7 * X[:, 1] + 0.6 * X[:, 2] * X[:, 3] + 0.5 * np.sin(2 * X[:, 4]) + 0.4 * (X[:, 5] > 0.5)
    y = (rng.random(n) < 1 / (1 + np.exp(-logit))).astype(int)
    return X, y


def sample_config(rng):
    """One random-search trial: learning rate, capacity, leaf support, regularization and rounds."""
    return dict(learning_rate=float(10 ** rng.uniform(-2, -0.3)), max_leaf_nodes=int(rng.integers(4, 32)),
                min_samples_leaf=int(rng.integers(3, 50)), l2_regularization=float(10 ** rng.uniform(-3, 1.5)),
                max_iter=int(rng.integers(10, 40)))


def roc_auc_score(y, score):
    """AUC as the rank (Mann-Whitney) statistic: the chance a random positive outscores a random negative. Same value as
    scikit-learn's roc_auc_score, computed directly because the search calls it hundreds of times."""
    ranks = rankdata(score)
    pos = y == 1
    n_pos, n_neg = pos.sum(), len(y) - pos.sum()
    return float((ranks[pos].sum() - n_pos * (n_pos + 1) / 2) / (n_pos * n_neg))


def model(config):
    return HistGradientBoostingClassifier(max_bins=32, random_state=0, **config)


def cv_auc(config, X, y, folds):
    """Development score of one configuration: mean AUC over the search folds."""
    return float(np.mean([roc_auc_score(y[v], model(config).fit(X[t], y[t]).predict_proba(X[v])[:, 1]) for t, v in folds]))


def assess(config, X, y, X_assess, y_assess):
    """Refit on all search rows and score once on the reserved rows."""
    return roc_auc_score(y_assess, model(config).fit(X, y).predict_proba(X_assess)[:, 1])


def run(trials):
    search_path, assess_path = [], []
    for rep in range(REPLICATES):
        rng = np.random.default_rng(SEED * 100 + rep)
        X, y = generate(N_SEARCH + N_ASSESS, rng)
        Xs, ys, Xa, ya = X[:N_SEARCH], y[:N_SEARCH], X[N_SEARCH:], y[N_SEARCH:]
        folds = list(StratifiedKFold(3, shuffle=True, random_state=0).split(Xs, ys))
        base_cv, base_assess = cv_auc(BASELINE, Xs, ys, folds), assess(BASELINE, Xs, ys, Xa, ya)
        best, gain_search, gain_assess = -1.0, [], []
        for _ in range(trials):
            config = sample_config(rng)
            score = cv_auc(config, Xs, ys, folds)
            if score > best:                       # a new best: freeze it and score it once on the reserved rows
                best, frozen = score, assess(config, Xs, ys, Xa, ya)
            gain_search.append(best - base_cv)      # what the search reports: best score so far minus the baseline's
            gain_assess.append(frozen - base_assess)  # what the frozen winner earns on reserved rows over the baseline
        search_path.append(gain_search)
        assess_path.append(gain_assess)
    s, a = np.array(search_path), np.array(assess_path)
    return clean({
        "trials": trials,
        "search_gain": float(s[:, -1].mean()), "assessment_gain": float(a[:, -1].mean()),
        "share_helped": float((a[:, -1] > 0).mean()),
        "mean_search_path": s.mean(axis=0), "mean_assessment_path": a.mean(axis=0),
        "assessment_path_per_replicate": a, "per_replicate_final": {"search": s[:, -1], "assessment": a[:, -1]},
        "replicates": REPLICATES, "search_rows": N_SEARCH, "assessment_rows": N_ASSESS,
    })

Measure it across the control

The control is search trials. Each value below runs the whole experiment from the generator up.

Calculation code
import pandas as pd
from kaggle_companion.activities.ch13 import explain

VALUES = [1, 4, 8, 16]
results = {value: run(value) for value in VALUES}
table = pd.DataFrame({str(v): explain(results[v], v)['metrics'] for v in VALUES})
print(table.to_string())
                            1       4       8      16
Trials                      1       4       8      16
Search-reported gain   -0.007  +0.018  +0.027  +0.032
Gain on reserved rows  +0.000  +0.008  +0.011  +0.008
Optimism               -0.007   0.010   0.016   0.024
Datasets helped           50%     75%     83%     75%

Look at it

The figure shows the default setting, search trials = 16.

Calculation code
import matplotlib.pyplot as plt
from kaggle_companion.activities.ch13 import draw, NCOLS, HEIGHT

DEFAULT = 16
fig, axes = plt.subplots(1, NCOLS, figsize=(6.6 if NCOLS == 1 else 10.4, HEIGHT), layout='constrained')
draw(axes, results[DEFAULT], DEFAULT)
plt.show()
Saved figure from this chapter calculation

What the result says

After 16 trials the search reports a gain of +0.032 AUC over the default; the frozen winner earns +0.008 on 4,000 reserved rows, so 0.032 - 0.008 = 0.024 of the reported gain is optimism. The winner beat the default on reserved rows in 75% of the 12 datasets.

Apply this to your competition

Book location: Chapter 13, The Optuna Pattern.

Technical corrections

Apply a lesson from this chapter

Select a lesson whose conditions match your project. State how you would test it and what would make you reject it.

Budget automated training per target

Conditions

  • Training one AutoGluon model for each of multiple regression targets.

Action

  • Proposed conditional application: Record the per-target time allowance and number of targets.
  • Proposed conditional application: Track the total training requirement; the source used1hour pertarget,3hours total.

Check

  • Proposed check: Record target count, parallelism and per-target allowances, measure actual total elapsed/GPUorCPUusage, and compare shorter/longer budgets on fixed holdouts.

Limits

  • Hardware and parallel execution are unspecified; allotted budgets need not equal elapsed time in another environment.

Find source links and value grades in the lesson atlas

Choose significance-based tree stopping on validation

Conditions

  • a custom tree otherwise grows until every information gain vanishes

Action

  • Proposed conditional application: Compare chi-square stopping confidence levels on a holdout and refit the selected level on full training data.

Check

  • Proposed check: Compare stopping levels on untouched validation, repeat splits, and keep final test evaluation separate from alpha selection.

Limits

  • Historical99%is not universal; source's statement that forests never overfit is not adopted.

Find source links and value grades in the lesson atlas

Prefer coarse hyperparameter changes under noisy validation

Conditions

  • small datasets yield volatile scores

Action

  • Proposed conditional application: Explore sizable hyperparameter increments and inspect response curves instead of tuning spurious decimal precision.

Check

  • Proposed check: Use coarse grid, repeated fixed folds and uncertainty bands before refining broad stable regions.

Limits

  • Some optima are narrow; coarse search alone does not prevent multiple-comparison overfit.

Find source links and value grades in the lesson atlas

Nest forward feature selection with nonlinear SVM tuning

Conditions

  • small numerical classification data may benefit from a compact nonlinear feature set

Action

  • Proposed adaptation of source observation: Proposed safe adaptation: fit outlier rules, scaling and forward selection within training folds, then tune the RBF SVM.

Check

  • Proposed verification: Compare original features, interactions and selected subsets on an untouched split.

Limits

  • The packet does not demonstrate fold-local transforms; outliers may be legitimate minority examples.

Find source links and value grades in the lesson atlas

Repeat similar wrapper implementations before ranking them

Conditions

  • two boosted-tree wrappers give close regression scores

Action

  • Proposed conditional application: Repeat comparisons under aligned data and random seeds before attributing small differences to the package.

Check

  • Proposed check: Align features, splits, parameters and paired seeds; compare RMSE distributions and wrapper defaults.

Limits

  • Defaults and stopping behavior may differ; randomness is author explanation.

Find source links and value grades in the lesson atlas

Use a cheap AutoML proxy for feature and dataset tuning

Conditions

  • high-quality AutoML makes each preprocessing trial expensive

Action

  • Proposed conditional application: Tune data configuration with five-minute good-quality runs, then compare final two-hour-per-property best-quality training with manual ensembles.

Check

  • Proposed check: Check proxy-versus-final configuration ranking on several trials, then compare final quality and total tuning cost on untouched folds.

Limits

  • Proxy ranking can change at full budget; the twentyfold estimate excludes substantial preprocessing tuning and is not a total-cost guarantee.

Find source links and value grades in the lesson atlas

Refine a broad randomized search with Bayesian tuning

Conditions

  • a credible baseline needs bounded hyperparameter tuning

Action

  • Proposed adaptation of source observation: Use a broad randomized stage followed by a narrower Bayesian search.

Check

  • Proposed verification: Reserve untouched validation and record total trial budget.

Limits

  • Search bounds and budget are unspecified.

Find source links and value grades in the lesson atlas