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.
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()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.
- Raw against all rows: 0.338 / 0.056 = 6.0 (ratio of log losses).
- Correction, boosted: 0.0556 - 0.0563 = -0.0007.
- Calibration, boosted: 0.0554 - 0.0563 = -0.0009.
- Correction, logistic: 0.0580 - 0.0579 = +0.0001.
Apply this to your competition
- Downsample negatives only inside training, keep development and assessment rows at natural prevalence, and score with the competition metric.
- Never submit raw sampled probabilities to a probability metric; apply the correction or a development-fitted calibrator.
- Check the corrected probabilities: their mean on natural-prevalence rows should match the prevalence; if not, calibrate instead.
- Keep enough positives and negatives for the model to be calibrated on the sample; very aggressive keep rates also cost AUC.
Book location: Chapter 39, Negative Downsampling at Scale.
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.
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.
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.
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.
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.
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.
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.
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.