Class imbalance is one of the most common real-world challenges in machine learning. Fraud detection, disease diagnosis, churn prediction — in all of these, the rare class (fraud, disease, churn) is what you care about most, but it makes up only 1-5% of the data. A naive classifier that predicts the majority class every time achieves 99% accuracy but is completely useless. This guide shows you how to actually solve the problem.
Understanding the Problem
Most ML algorithms optimise for overall accuracy, which is misleading with imbalanced data. If 1% of transactions are fraudulent, predicting “not fraud” for every transaction gives 99% accuracy but catches zero fraud cases. The real metrics you need are precision, recall, F1, and AUC-ROC — all of which measure how well the model handles the minority class.
from sklearn.metrics import classification_report, roc_auc_score
print(classification_report(y_test, y_pred))
print(f"AUC-ROC: {roc_auc_score(y_test, y_prob):.4f}")
Strategy 1 – Adjust Class Weights
The simplest and most effective first step: tell the model to penalise mistakes on the minority class more heavily. Most sklearn estimators support class_weight='balanced':
from sklearn.ensemble import RandomForestClassifier
from sklearn.linear_model import LogisticRegression
# Automatically compute weights inversely proportional to class frequency
rf = RandomForestClassifier(class_weight='balanced', n_estimators=100, random_state=42)
rf.fit(X_train, y_train)
# Manual weights
lr = LogisticRegression(class_weight={0: 1, 1: 10}, max_iter=1000)
lr.fit(X_train, y_train)
This is often enough to solve mild imbalance (10:1 ratio). Try this before any resampling.
Strategy 2 – Oversampling the Minority Class
SMOTE (Synthetic Minority Over-sampling Technique) creates synthetic minority class samples by interpolating between existing minority samples. This is better than simple duplication because it adds diversity.
pip install imbalanced-learn
from imblearn.over_sampling import SMOTE
from collections import Counter
print("Before:", Counter(y_train)) # {0: 9500, 1: 500}
smote = SMOTE(random_state=42, k_neighbors=5)
X_resampled, y_resampled = smote.fit_resample(X_train, y_train)
print("After:", Counter(y_resampled)) # {0: 9500, 1: 9500}
Critical: Only apply SMOTE to training data, never to test data. Apply it after the train/test split.
Strategy 3 – Undersampling the Majority Class
from imblearn.under_sampling import RandomUnderSampler, TomekLinks
# Random undersampling
rus = RandomUnderSampler(random_state=42)
X_resampled, y_resampled = rus.fit_resample(X_train, y_train)
# Tomek Links — removes majority class samples near the decision boundary
tl = TomekLinks()
X_clean, y_clean = tl.fit_resample(X_train, y_train)
Undersampling discards majority class data. Fine if you have lots of it; risky with smaller datasets.
Strategy 4 – Combine Over and Undersampling
from imblearn.combine import SMOTETomek, SMOTEENN
# SMOTE + Tomek Links (best general-purpose combination)
smt = SMOTETomek(random_state=42)
X_resampled, y_resampled = smt.fit_resample(X_train, y_train)
SMOTETomek first oversamples with SMOTE then cleans the boundary with Tomek Links. It’s one of the best general-purpose strategies for binary classification with imbalance.
Strategy 5 – Algorithm-Level Solutions
Some algorithms handle imbalance natively. Balanced Random Forest samples balanced bootstrap sets for each tree:
from imblearn.ensemble import BalancedRandomForestClassifier, EasyEnsembleClassifier
brf = BalancedRandomForestClassifier(n_estimators=100, random_state=42)
brf.fit(X_train, y_train)
# EasyEnsemble combines multiple undersampled classifiers
ee = EasyEnsembleClassifier(n_estimators=10, random_state=42)
ee.fit(X_train, y_train)
Strategy 6 – Adjust Classification Threshold
By default, classifiers use 0.5 as the decision threshold. For imbalanced data, lowering this to 0.2-0.3 increases recall at the cost of precision:
y_prob = model.predict_proba(X_test)[:, 1]
# Threshold sweep
from sklearn.metrics import precision_recall_curve
precisions, recalls, thresholds = precision_recall_curve(y_test, y_prob)
# Find threshold that maximises F1
f1_scores = 2 * (precisions * recalls) / (precisions + recalls + 1e-9)
optimal_threshold = thresholds[f1_scores.argmax()]
print(f"Optimal threshold: {optimal_threshold:.3f}")
y_pred_adjusted = (y_prob >= optimal_threshold).astype(int)
print(classification_report(y_test, y_pred_adjusted))
Which Strategy Should You Use?
Start with class_weight='balanced' — it’s zero cost and often solves the problem. If your imbalance ratio is extreme (>100:1), add SMOTE. For ensemble models, try BalancedRandomForest. Always tune the decision threshold at the end — it’s the highest-leverage, lowest-risk lever. And always evaluate with AUC-ROC and the PR curve, never just accuracy.
Conclusion
Imbalanced datasets are the rule, not the exception, in real-world machine learning. The solution is not a single silver bullet but a combination of strategies: appropriate metrics, class weights, SMOTE, and threshold tuning. Master these techniques and you’ll be able to build models that actually perform on the minority class — which is almost always the class that matters most.



