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 39: Imbalanced Data

Try the decision first

How much log loss does negative downsampling cost, and at what keep rate does the analytic correction stop being as good as calibrating on development 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 much log loss does negative downsampling cost, and at what keep rate does the analytic correction stop being as good as calibrating on development rows?

Competitions with millions of negatives invite downsampling, and a probability metric then scores the sampled prevalence rather than the real one. The chapter gives a correction and a calibration route; this measures where each holds.

Constructed data. The correction ties with calibration at a 2% keep rate here; it fails only at far more aggressive rates and only for the boosted model.

The experiment

A constructed task with about 1.3% positives: 40,000 training rows, 30,000 development rows and 60,000 test rows, all at natural prevalence. Every positive and a fraction r (the control) of the negatives are kept for training. A gradient-boosted model and a logistic regression are fitted on the sample. Three outputs are scored by log loss and AUC on the test rows: raw sampled probabilities, the chapter's correction p r / (p r + 1 - p), and Platt scaling fitted on the development rows. The reference is the same model trained on every row. Results are means over 5 worlds.

Calculation code
import numpy as np
from sklearn.ensemble import HistGradientBoostingClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import log_loss, roc_auc_score

from kaggle_companion.activities._common import clean

SEED = 39
REPLICATES = 5          # independent worlds per keep rate; results are means over them
N_TRAIN, N_DEV, N_TEST = 40_000, 30_000, 60_000
EPS = 1e-6              # probabilities are kept inside [1e-6, 1 - 1e-6] so log loss stays finite


def generate(n, rng):
    """Eight features, a bent and interacting score, about 1.3% positives."""
    X = rng.normal(size=(n, 8))
    score = X[:, 0] + 0.8 * np.maximum(X[:, 1], 0) + 0.7 * X[:, 2] * X[:, 3] - 0.6 * np.abs(X[:, 4]) + 0.4 * X[:, 5]
    return X, (rng.random(n) < 1 / (1 + np.exp(-(1.2 * score - 5.6)))).astype(int)


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


def correct(p, keep):
    """The chapter's correction: natural odds equal sampled odds times the negative keep rate r."""
    return p * keep / (p * keep + 1 - p)


def one_world(keep, seed):
    rng = np.random.default_rng(seed)
    X_train, y_train = generate(N_TRAIN, rng)
    X_dev, y_dev = generate(N_DEV, rng)                       # natural prevalence: used only to fit the calibrator
    X_test, y_test = generate(N_TEST, rng)                    # natural prevalence: used only for scoring
    sampled = (y_train == 1) | (rng.random(N_TRAIN) < keep)   # keep every positive, each negative with probability `keep`
    out = {"training_rows": int(sampled.sum()), "prevalence": float(y_test.mean())}
    makers = {"gbm": lambda: HistGradientBoostingClassifier(max_iter=60, max_leaf_nodes=15, random_state=0),
              "logistic": lambda: LogisticRegression(max_iter=500)}
    for name, make in makers.items():
        model = make().fit(X_train[sampled], y_train[sampled])
        p_dev = np.clip(model.predict_proba(X_dev)[:, 1], EPS, 1 - EPS)
        p_test = np.clip(model.predict_proba(X_test)[:, 1], EPS, 1 - EPS)
        calibrator = LogisticRegression(C=1e6).fit(to_logit(p_dev)[:, None], y_dev)      # Platt scaling on development rows
        outputs = {"raw": p_test, "corrected": np.clip(correct(p_test, keep), EPS, 1 - EPS),
                   "calibrated": calibrator.predict_proba(to_logit(p_test)[:, None])[:, 1]}
        out[name] = {k: [log_loss(y_test, v), roc_auc_score(y_test, v), float(v.mean())] for k, v in outputs.items()}
    return out


def run(keep_rate):
    worlds = [one_world(keep_rate, SEED * 100 + r) for r in range(REPLICATES)]
    # Reference: the same models trained on every row (keep rate 1), same worlds.
    base = worlds if keep_rate == 1 else [one_world(1, SEED * 100 + r) for r in range(REPLICATES)]

    def mean(group, model, arm, j):
        return float(np.mean([w[model][arm][j] for w in group]))
    result = {"keep_rate": keep_rate, "training_rows": float(np.mean([w["training_rows"] for w in worlds])),
              "prevalence": float(np.mean([w["prevalence"] for w in worlds])), "replicates": REPLICATES}
    for model in ("gbm", "logistic"):
        result[model] = {arm: {"log_loss": mean(worlds, model, arm, 0), "auc": mean(worlds, model, arm, 1),
                               "mean_prediction": mean(worlds, model, arm, 2)} for arm in ("raw", "corrected", "calibrated")}
        result[model]["no_downsampling_log_loss"] = mean(base, model, "raw", 0)
        result[model]["no_downsampling_auc"] = mean(base, model, "raw", 1)
    result["calibrated_beats_corrected_gbm"] = float(np.mean([w["gbm"]["calibrated"][0] < w["gbm"]["corrected"][0] for w in worlds]))
    return clean(result)

Measure it across the control

The control is share of negatives kept for training. Each value below runs the whole experiment from the generator up.

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

VALUES = [1, 0.02, 0.005, 0.002]
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                       0.02                      0.005                      0.002
Training rows                                 40,000                      1,302                        684                        563
Raw log loss                                   0.056                      0.338                      0.971                      1.906
Corrected log loss                             0.056                      0.056                      0.063                      0.089
Calibrated log loss                            0.056                      0.055                      0.058                      0.059
Mean corrected prediction  0.012 vs prevalence 0.013  0.013 vs prevalence 0.013  0.024 vs prevalence 0.013  0.050 vs prevalence 0.013
AUC                                            0.851                      0.853                      0.834                      0.814

Look at it

The figure shows the default setting, share of negatives kept for training = 0.02.

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

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

Keeping 2% of negatives leaves 1,302 training rows. Raw boosted-model probabilities average 0.230 against a true prevalence of 0.013, so log loss is 0.338 against 0.056 when trained on all rows (0.338 / 0.056 = 6.0 times). The analytic correction brings it to 0.0556 (0.0556 - 0.0563 = -0.0007 against the all-rows model, mean prediction 0.013); calibrating on development rows gives 0.0554. The logistic model's corrected log loss is +0.0001 from its own all-rows reference. AUC moves +0.001 from 0.851.

Apply this to your competition

Book location: Chapter 39, Negative Downsampling at Scale.

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.

Balance class exposure through an epoch sampler

Conditions

  • image class counts are strongly imbalanced

Action

  • Proposed recipe: test oversampling so each epoch exposes comparable image counts per class.

Check

  • Proposed check: Compare natural and balanced sampling at equal optimization steps and report minority quality, calibration and repeated-example exposure.

Limits

  • Oversampling can memorize scarce examples and alter probability calibration; equal counts need not match the target metric.

Find source links and value grades in the lesson atlas

Audit near-all-negative subgroups before global modeling

Conditions

  • a large subgroup has exceptionally low positive prevalence

Action

  • Proposed adaptation of source observation: Compare a subgroup prior or explicit routing with fitting one global classifier.

Check

  • Proposed verification: Evaluate subgroup recall and overall metric on independent data.

Limits

  • Ignoring a group can miss rare positives; the decision is competition-specific, not a safe moderation default.

Find source links and value grades in the lesson atlas

Inspect class coverage alongside overall accuracy after resampling

Conditions

  • multiclass models ignore classes despite acceptable overall accuracy

Action

  • Compare class confusion matrices and prediction coverage before and after oversampling and other bundled training changes.

Check

  • Evaluate per-class recall and macro metrics on untouched data alongside overall accuracy.

Limits

  • The packet does not show isolated oversampling gain or modality; source public/private selection outcomes are retrospective.

Find source links and value grades in the lesson atlas

Select imbalanced classifiers using the competition macro metric

Conditions

  • bug severity classes have uneven frequencies

Action

  • Proposed conditional application: Compare class-balancing and ensemble alternatives on macro precision, including TF-IDF with a bagged Random Forest.

Check

  • Proposed check: Compare macro precision and per-class recall on report-group holdouts; fit vectorizer/resampling within folds.

Limits

  • Macro precision can hide low recall; SMOTE placement unspecified.

Find source links and value grades in the lesson atlas

Use error distribution to assess sampling diversity

Conditions

  • oversampling changes class errors without changing overall accuracy

Action

  • Proposed conditional application: Compare confusion matrices and ensemble models trained with and without oversampling when errors differ.

Check

  • Proposed check: Compare per-class and per-example error overlap on identical saved validation rows; test blend against each run.

Limits

  • Confusion changes alone do not ensure blend improvement; stochastic TTA varies scores.

Find source links and value grades in the lesson atlas

Use weighted sampling for an imbalanced recognition dataset

Conditions

  • recognition training is dominated by an others class

Action

  • Proposed adaptation of source observation: Compare weighted class sampling with natural sampling for minority recognition performance.

Check

  • Proposed verification: Evaluate per-class precision and recall on a validation set retaining deployment class frequencies.

Limits

  • The source gives no sampler weights, ablation or evidence that weighted sampling caused the reported F1.

Find source links and value grades in the lesson atlas

Retain cross-entropy as a benchmark for focal loss

Conditions

  • class imbalance suggests a specialized loss

Action

  • Proposed recipe: compare focal loss with ordinary cross-entropy before adopting it.

Check

  • Proposed check: Verify focal loss implementation on a known example and compare with cross-entropy at matched folds, budgets and gamma tuning.

Limits

  • Implementation doubt limits causal interpretation of the negative focal result.

Find source links and value grades in the lesson atlas

Tune sampling strength for severely imbalanced image classes

Conditions

  • plankton image classes have widely different counts

Action

  • Proposed recipe: test weighted sampling with adjustable sampling strength.

Check

  • Proposed check: Tune sampling strength on untouched macro-F1 folds with fixed optimization steps, measuring minority recall and calibration.

Limits

  • Rare classes may be repeatedly memorized; alpha0.25–0.5is a historical search range, not a universal default.

Find source links and value grades in the lesson atlas