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 38: Calibration and Post-Processing

Try the decision first

How large must the development set be before isotonic calibration beats the simpler Platt calibrator on new rows?

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

How large must the development set be before isotonic calibration beats the simpler Platt calibrator on new rows?

Calibration is fitted, so it can overfit like any model. A calibrator that looks best on its own development rows can be the worst choice on the leaderboard, and the size of the development set decides which one to trust.

Constructed data; the crossover size is a property of this generator, not a rule for every competition.

The experiment

A constructed binary task. A Gaussian naive Bayes base model is trained once on 3,000 rows of six correlated copies of one signal, so it double counts the evidence and is overconfident; the true log odds also bend, so no two-parameter curve fits them exactly. For each development-set size (the control), 30 independent development sets are drawn. On each, Platt scaling (logistic regression on the base log odds) and isotonic regression are fitted, then scored by log loss on their own development rows and on 20,000 assessment rows.

Calculation code
import numpy as np
from sklearn.isotonic import IsotonicRegression
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import log_loss, roc_auc_score
from sklearn.naive_bayes import GaussianNB

from kaggle_companion.activities._common import clean

SEED = 38
DRAWS = 30        # independent development sets per size; results are means over draws
EPS = 1e-3        # probabilities are kept inside [0.001, 0.999] so one confident miss cannot dominate log loss


def generate(n, rng):
    """Six noisy copies of one hidden signal plus two noise features. The true log odds bend upward for positive signal."""
    z = rng.normal(size=n)
    X = np.column_stack([z + rng.normal(0, 0.6, n) for _ in range(6)] + [rng.normal(size=n), rng.normal(size=n)])
    logit = 0.4 * z + 1.6 * np.maximum(z, 0) - 1.0
    y = (rng.random(n) < 1 / (1 + np.exp(-logit))).astype(int)
    return X, y


def to_logit(p):
    p = np.clip(p, EPS, 1 - EPS)
    return np.log(p / (1 - p))


def expected_calibration_error(y, p, bins=10):
    """The chapter's population-weighted ECE with equal-width bins; empty bins contribute nothing."""
    b = np.minimum((p * bins).astype(int), bins - 1)
    return float(sum((b == k).mean() * abs(y[b == k].mean() - p[b == k].mean()) for k in range(bins) if (b == k).any()))


def reliability(y, p, bins=10):
    b = np.minimum((p * bins).astype(int), bins - 1)
    return [[float(p[b == k].mean()), float(y[b == k].mean()), int((b == k).sum())] for k in range(bins) if (b == k).sum() >= 20]


def run(development_rows):
    rng = np.random.default_rng(SEED)
    X_base, y_base = generate(3000, rng)
    base = GaussianNB().fit(X_base, y_base)                    # frozen base model: never sees development or assessment labels
    X_a, y_a = generate(20000, rng)                            # assessment rows, used only for scoring
    p_a = np.clip(base.predict_proba(X_a)[:, 1], EPS, 1 - EPS)

    scores = {k: [] for k in ("none", "platt", "isotonic", "none_dev", "platt_dev", "isotonic_dev",
                              "auc_platt", "auc_isotonic")}
    for draw in range(DRAWS):
        X_d, y_d = generate(development_rows, rng)
        p_d = np.clip(base.predict_proba(X_d)[:, 1], EPS, 1 - EPS)
        platt = LogisticRegression(C=1e6).fit(to_logit(p_d)[:, None], y_d)
        iso = IsotonicRegression(out_of_bounds="clip", y_min=EPS, y_max=1 - EPS).fit(p_d, y_d)
        cal = {"platt": lambda p: platt.predict_proba(to_logit(p)[:, None])[:, 1], "isotonic": iso.predict}
        scores["none"].append(log_loss(y_a, p_a))
        scores["none_dev"].append(log_loss(y_d, p_d, labels=[0, 1]))
        for name, f in cal.items():
            scores[name].append(log_loss(y_a, f(p_a)))
            scores[name + "_dev"].append(log_loss(y_d, f(p_d), labels=[0, 1]))   # scored on the rows it was fitted to
            scores["auc_" + name].append(roc_auc_score(y_a, f(p_a)))
        if draw == 0:
            curves = {"none": reliability(y_a, p_a), "platt": reliability(y_a, cal["platt"](p_a)),
                      "isotonic": reliability(y_a, cal["isotonic"](p_a))}
            ece = {"none": expected_calibration_error(y_a, p_a),
                   "platt": expected_calibration_error(y_a, cal["platt"](p_a)),
                   "isotonic": expected_calibration_error(y_a, cal["isotonic"](p_a))}

    mean = {k: float(np.mean(v)) for k, v in scores.items()}
    return clean({
        "development_rows": development_rows,
        "assessment": {k: mean[k] for k in ("none", "platt", "isotonic")},
        "development": {k: mean[k + "_dev"] for k in ("none", "platt", "isotonic")},
        "isotonic_beats_platt": float(np.mean(np.array(scores["isotonic"]) < np.array(scores["platt"]))),
        "auc": {"none": roc_auc_score(y_a, p_a), "platt": mean["auc_platt"], "isotonic": mean["auc_isotonic"]},
        "ece_first_draw": ece, "reliability_first_draw": curves,
        "draws": DRAWS, "assessment_rows": len(y_a),
    })

Measure it across the control

The control is development rows used to fit the calibrator. Each value below runs the whole experiment from the generator up.

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

VALUES = [50, 200, 1000, 5000]
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())
                                   50           200          1000           5000
No calibration, new rows        0.959         0.959         0.959          0.959
Platt, new rows                 0.587         0.566         0.561          0.560
Isotonic, new rows              0.694         0.589         0.557          0.554
Isotonic, own rows              0.480         0.509         0.542          0.550
Isotonic beats Platt      7% of draws  20% of draws  93% of draws  100% of draws

Look at it

The figure shows the default setting, development rows used to fit the calibrator = 200.

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

DEFAULT = 200
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

With 200 development rows, calibration cuts log loss on new rows from 0.959 to 0.566 with Platt (0.959 - 0.566 = 0.393) and 0.589 with isotonic, so Platt is better on new rows (isotonic won 20% of 30 draws). On its own development rows isotonic scores 0.509, 0.080 better than it does on new rows. AUC moves from 0.768 to 0.768 (Platt) and 0.760 (isotonic).

Apply this to your competition

Book location: Chapter 38, Fit Calibration on the Correct Predictions.

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.

Calibrate a classifier for a known test class prior

Conditions

  • training frequencies differ from a known balanced test set

Action

  • Proposed conditional application: Compare probability calibration for the test prior with class oversampling or weighted training.

Check

  • Proposed check: Compare prior calibration/oversampling/loss weighting on holdouts resampled to justified deployment priors.

Limits

  • Known balanced test prior is contest-specific; prior correction assumes appropriate conditional stability.

Find source links and value grades in the lesson atlas

Compare class-prior correction using an explicit target-prior assumption

Conditions

  • training class proportions differ from an asserted test prior
  • image classifier training and evaluation priors differ and many models are ensembled

Action

  • Proposed: compare probability recalibration using a documented test-prior assumption against uncalibrated outputs.
  • Compare explicit training-to-target prior correction with uncorrected probabilities, documenting target-prior evidence.

Check

  • Proposed: test prior correction on representative held-out data, inspect probability normalization and verify ordered output IDs.
  • Check calibration and predictive metric on independently labeled target-like examples.

Limits

  • Uniform test priors are assumed in source, not proven. The formula operates one-vs-rest and differs from standard multiclass prior-ratio renormalization; no before/after score values are shown.
  • Exact correction formula is linked elsewhere, not supplied here; target priors may be unknown in deployment.
  • Twelve-view TTA and ensemble runtime are separate implementation choices.

Find source links and value grades in the lesson atlas

Compare discrete output calibration against continuous predictions

Conditions

  • targets have discrete or tied distributions and base text models differ

Action

  • Compare target-distribution discretization with continuous outputs, fitting mappings on training folds.

Check

  • Measure held-out Spearman and sensitivity to target prevalence changes.

Limits

  • Precise discretization algorithm is absent; gains are author-reported local results.
  • BART diversity, linear blending and unsuccessful stacking/backtranslation are separate model experiments.

Find source links and value grades in the lesson atlas

Gate calibration stages using nested held-out gains

Conditions

  • OOF ensemble calibration can overfit the same predictions

Action

  • Proposed conditional application: Fit blend weights and class priors on four folds and score the fifth, disabling postprocessing stages with negative held-out gain.

Check

  • Proposed check: Repeat held-out gating across all folds and use a final untouched set after choosing stages.

Limits

  • Repeated stage choices can overfit meta-validation; language-mix agreement does not prove no shift.
  • The +0.0044 to +0.0024 numbers describe apparent versus nested held-out calibration gain; metric units and uncertainty are unspecified in the comparison.

Find source links and value grades in the lesson atlas

Calibrate predictive means and uncertainty while auditing model mismatch

Conditions

  • Bayesian predictions are systematically miscalibrated for an evaluation metric

Action

  • Proposed adaptation of source observation: Proposed adaptation: fit sensor-specific mean and uncertainty calibration on held-out predictions and investigate persistent structured residuals.

Check

  • Proposed verification: Compare calibration, coverage and competition metric with and without calibration on a separate split.

Limits

  • The author fit on the full training set; held-out fitting is proposed. Improved score does not prove a correct generative prior.

Find source links and value grades in the lesson atlas

Convert a strong external prediction into calibrated probabilities

Conditions

  • sports outcomes have published market spreads

Action

  • Proposed conditional application: Use available spreads or predicted margins as inputs to logistic probability mapping, then blend with efficiency-based probabilities.

Check

  • Proposed check: Fit the margin-to-probability map on past games, check reliability and log loss on later seasons, and enforce line availability timestamps.

Limits

  • Actual spreads existed for first-round games only; future games required predicted margins and the team acknowledges tournament luck.

Find source links and value grades in the lesson atlas