Imbalanced data

Why accuracy is the wrong headline, resampling and class weights, synthetic oversampling done safely, threshold tuning, and evaluating with precision-recall.

Why accuracy lies

import numpy as np
from sklearn.dummy import DummyClassifier
from sklearn.metrics import accuracy_score, average_precision_score

print(np.bincount(y))                       # e.g. [9900, 100]

base = DummyClassifier(strategy="most_frequent").fit(X_train, y_train)
print("accuracy", accuracy_score(y_test, base.predict(X_test)))    # 0.99
print("average precision", average_precision_score(y_test, np.zeros_like(y_test)))
ImbalanceAccuracy isReport instead
Mild (20-40% positives)Still informativeAccuracy plus F1
Moderate (5-20%)OptimisticPrecision, recall, ROC-AUC
Severe (under 5%)MeaninglessAverage precision, recall at fixed alert volume
Extreme (under 0.1%)Actively misleadingPrecision at the top k ranked cases, lift over baseline

The class balance is a property of the problem, not a defect in the data. Removing negative rows to 'fix' it throws away information the model needs to locate the decision boundary.

Weights, resampling and synthesis

from imblearn.over_sampling import SMOTE, RandomOverSampler
from imblearn.pipeline import Pipeline as ImbPipeline
from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import StandardScaler

pipe = ImbPipeline([
    ("scale", StandardScaler()),
    ("smote", SMOTE(random_state=42, k_neighbors=5)),   # training folds only
    ("clf", LogisticRegression(max_iter=1000)),
])

# class weights need no synthetic rows and no extra library
weighted = LogisticRegression(max_iter=1000, class_weight="balanced")
  • class_weight="balanced" multiplies each class loss by its inverse frequency. It is the cheapest first thing to try.
  • Undersampling the majority class speeds training but discards real rows; use it only when the majority is genuinely redundant.
  • SMOTE interpolates between a positive row and its neighbours. It helps with continuous features and hurts with one-hot columns, where interpolation produces impossible categories.
  • imblearn.pipeline.Pipeline applies the sampler inside each fold. Resampling before cross-validation copies synthetic rows across folds and inflates the score.
  • Focal loss is available in gradient-boosting and neural libraries; it down-weights easy examples, which matters most at extreme imbalance.

Choose the operating point

from sklearn.metrics import precision_recall_curve

prob = pipe.predict_proba(X_test)[:, 1]
prec, rec, thr = precision_recall_curve(y_test, prob)

# if the team can review 200 cases a week, take the top 200 by probability
k = 200
order = np.argsort(-prob)[:k]
print("precision at k", y_test.values[order].mean())
print("recall at k", y_test.values[order].sum() / y_test.sum())
⚠️
Resampling changes the base rate the model sees, so its probabilities no longer match reality. If the outputs are used as probabilities rather than ranks, calibrate them afterwards — or fix the base rate explicitly in the downstream calculation.

FAQ

Should I balance the classes before training?
Try class weights first, evaluate with a threshold-free ranking metric, and only add resampling if it improves that metric on honest folds. Often the imbalance is not the real problem — the features are.
How do I know the gain is real?
Compare on identical folds using average precision or recall-at-fixed-volume, and check the spread across folds. A single lucky split on 100 positives proves nothing.

Classification metrics in depth Splitting data correctly

Last refreshed 2026-09-18.