Imbalanced Classification Smote Class Weights Threshold Adjustment
# Imbalanced Classification: SMOTE, Class Weights & Threshold Adjustment
## Introduction & Motivation
Imbalanced datasets (rare positive class) bias models toward majority class. SMOTE (Synthetic Minority Oversampling) generates synthetic minority samples via k-NN interpolation. Class weights penalize misclassification of rare class. Threshold adjustment trades off precision-recall. Critical for fraud detection, disease diagnosis, anomaly detection.
Motivation: Standard accuracy misleading when class skew extreme (1% positive). Must optimize for recall/precision/F1.
Applications: Credit card fraud, medical diagnosis (rare disease), anomaly detection, click-through rate prediction.
---
## Core Concepts & Theory
### Imbalanced Learning
Naive classifier: always predict majority. No information gain; 99% accuracy but useless.
### SMOTE
Oversample minority: find k-NN in minority class; interpolate new samples.
### Class Weights
Penalize minority misclassification more: weight_pos > weight_neg.
### Cost-Sensitive Learning
Asymmetric loss: different cost for FP vs. FN.
---
## Mathematical Formulation
SMOTE sampling:
$$x_{ ext{syn}} = x_i + r \cdot (x_{nn_j} - x_i), \quad r \in [0,1]$$
Weighted cross-entropy:
$$\mathcal{L} = -\sum_i [y_i \log \hat{p}_i + (1-y_i) \log(1-\hat{p}_i)] \cdot w_{y_i}$$
Class weights:
$$w_0 = \frac{n}{2n_0}, \quad w_1 = \frac{n}{2n_1}$$
---
## Advanced Theory & Extensions
### SMOTE Variants
Borderline-SMOTE (focus on boundary samples); SVM-SMOTE (use SVM support vectors).
### Cost-Sensitive Thresholding
Adjust threshold P(y=1|x) > τ based on cost ratio.
### Ensemble Methods
RandomUnderSampler + RandomOverSampler in pipeline.
---
## Computational Considerations
SMOTE: O(k × m) where k = k-NN, m = minority samples.
Class weighting: O(1) in loss computation.
Threshold search: O(n) to compute metrics across thresholds.
---
## Practical Implementation Strategies
### Stratified CV
Maintain class ratio in train/validation splits.
### Pipeline Design
SMOTE on train only; prevent data leakage to test.
### Metric Selection
Recall: cost of missed positives; Precision: cost of false alarms; F1: balance.
---
## Benchmark Datasets & Evaluation
Fraud Detection: Kaggle Credit Card Fraud (284K samples, 0.17% positive).
Medical: UCI Breast Cancer (569 samples, 37% positive).
Metrics: Precision, Recall, F1, ROC-AUC, PR-AUC, cost-sensitive accuracy.
---
## Key Challenges & Limitations
### Overgeneralization
SMOTE may create unrealistic samples in high dimensions.
### Computational Cost
Large oversampling multiplies training time.
### Class Distribution Shift
Train distribution != test distribution (requires calibration).
---
## Hyperparameter Tuning
SMOTE k: 5-10; balance locality-diversity.
Class weight ratio: 2-100; heavier weights for rarer classes.
Threshold: 0.5-0.95; optimize for business metric.
---
## Real-World Applications & Case Studies
Stripe: Fraud detection via imbalanced learning + real-time thresholding.
Healthcare: Rare disease diagnosis via SMOTE + cost-sensitive trees.
Anomaly Detection: Unsupervised + imbalanced; few known anomalies.
---
## Integration with Other Methods
Imbalanced + Ensemble → balanced random forests, easy ensemble.
Imbalanced + Deep Learning → focal loss (down-weight easy examples).
---
## Summary & Key Takeaways
Imbalanced classification requires SMOTE oversampling, class weighting, and threshold adjustment to optimize for minority class performance.
Principles:
1. SMOTE generates synthetic minority samples via k-NN.
2. Class weights penalize minority misclassification.
3. Stratified CV maintains class ratio.
4. Threshold optimization balances precision-recall.
5. Evaluate with F1, recall, AUC; not accuracy.
---
---
## Appendix: Practical Labs
### Lab 1: SMOTE Oversampling
import numpy as np
from collections import Counter
def simple_smote(X, y, k=5, oversampling_factor=2):
"""Basic SMOTE implementation"""
X_minority = X[y == 1]
n_new = len(X_minority) * (oversampling_factor - 1)
X_syn = []
for _ in range(int(n_new)):
idx = np.random.randint(len(X_minority))
x_i = X_minority[idx]
# k-NN among minority
distances = np.linalg.norm(X_minority - x_i, axis=1)
knn_idx = np.argsort(distances)[1:k+1]
nn_idx = np.random.choice(knn_idx)
x_nn = X_minority[nn_idx]
# Interpolate
r = np.random.rand()
x_syn = x_i + r * (x_nn - x_i)
X_syn.append(x_syn)
X_syn = np.array(X_syn)
X_new = np.vstack([X, X_syn])
y_new = np.concatenate([y, np.ones(len(X_syn))])
return X_new, y_new
# Data
np.random.seed(42)
X = np.random.randn(100, 5)
y = np.concatenate([np.zeros(95), np.ones(5)])
X_bal, y_bal = simple_smote(X, y, k=3, oversampling_factor=3)
print(f"Original: {Counter(y)}, Balanced: {Counter(y_bal)}")
assert len(y_bal) > len(y), "Should oversample"
assert (y_bal == 1).sum() > (y == 1).sum(), "Should add minority"
print("✓ SMOTE oversampling working")
if __name__ == "__main__":
print("Lab 1: SMOTE - PASSED")### Lab 2: Class Weights
import numpy as np
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import f1_score
# Imbalanced data
np.random.seed(42)
X = np.random.randn(200, 5)
y = np.concatenate([np.zeros(190), np.ones(10)])
# Compute class weights
n_pos = (y == 1).sum()
n_neg = (y == 0).sum()
weight_pos = n_neg / (n_pos + 1e-8)
weight_neg = 1.0
print(f"Class weights: pos={weight_pos:.2f}, neg={weight_neg:.2f}")
# Train without weights
model_unweighted = LogisticRegression(max_iter=200, random_state=42)
model_unweighted.fit(X, y)
# Train with weights (sklearn balanced option)
model_weighted = LogisticRegression(max_iter=200, class_weight='balanced', random_state=42)
model_weighted.fit(X, y)
f1_unweighted = f1_score(y, model_unweighted.predict(X))
f1_weighted = f1_score(y, model_weighted.predict(X))
print(f"F1 unweighted: {f1_unweighted:.4f}, weighted: {f1_weighted:.4f}")
assert f1_weighted >= 0, "F1 should be non-negative"
print("✓ Class weighting working")
if __name__ == "__main__":
print("Lab 2: Class Weights - PASSED")### Lab 3: Threshold Optimization
import numpy as np
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import precision_recall_curve, f1_score
# Data
np.random.seed(42)
X = np.random.randn(200, 5)
y = np.concatenate([np.zeros(180), np.ones(20)])
model = LogisticRegression(max_iter=200, class_weight='balanced', random_state=42)
model.fit(X, y)
probs = model.predict_proba(X)[:, 1]
# Find optimal threshold
thresholds = np.linspace(0, 1, 50)
f1_scores = []
for threshold in thresholds:
y_pred = (probs >= threshold).astype(int)
if (y_pred == 1).sum() > 0:
f1 = f1_score(y, y_pred)
else:
f1 = 0
f1_scores.append(f1)
optimal_threshold = thresholds[np.argmax(f1_scores)]
print(f"Optimal threshold: {optimal_threshold:.3f}, max F1: {max(f1_scores):.4f}")
assert 0 <= optimal_threshold <= 1, "Threshold should be in [0,1]"
assert max(f1_scores) >= 0, "F1 should be non-negative"
print("✓ Threshold optimization working")
if __name__ == "__main__":
print("Lab 3: Threshold - PASSED")### Lab 4: Precision-Recall Trade-off
import numpy as np
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import precision_recall_curve
# Data
np.random.seed(42)
X = np.random.randn(300, 5)
y = np.concatenate([np.zeros(270), np.ones(30)])
model = LogisticRegression(max_iter=200, class_weight='balanced', random_state=42)
model.fit(X, y)
probs = model.predict_proba(X)[:, 1]
precision, recall, thresholds = precision_recall_curve(y, probs)
print(f"Precision at recall 0.5: {precision[np.argmin(np.abs(recall - 0.5))]:.3f}")
print(f"Number of thresholds: {len(thresholds)}")
assert len(precision) > 0, "Should have precision values"
assert len(recall) == len(precision), "Precision-recall should match"
assert all(0 <= p <= 1 for p in precision), "Precision in [0,1]"
print("✓ Precision-recall curve working")
if __name__ == "__main__":
print("Lab 4: PR Curve - PASSED")