Machine Learning · Classification
SMOTE
& gestion du déséquilibre des classes
Pourquoi un modèle ignore les clients qui résilient, et comment le corriger.
← → ou Espace pour naviguer · F pour plein écran
Objectif
Comprendre le problème, puis savoir le corriger
- 01Pourquoi un modèle est biaisé sur un dataset déséquilibré
- 02Le paramètre
class_weight - 03Random Oversampling et Undersampling
- 04SMOTE : la génération synthétique
- 05Les risques : surapprentissage et fuite de données
- 06Implémentation avec
imbalanced-learn
01 · Le problème
Le cas du churn : très peu de résiliations
Sur 100 clients d'un opérateur télécom, une poignée seulement résilie son abonnement chaque mois.
- Classe majoritaire : clients fidèles (
churn = 0) - Classe minoritaire : clients qui partent (
churn = 1) - Ratio typique : de 1:10 à 1:100
C'est justement la classe rare que l'on veut détecter, car un client perdu coûte cher.
01 · Le problème
Le paradoxe de l'accuracy
Un modèle « paresseux » qui prédit toujours « ne résilie pas » :
Une accuracy élevée qui ne détecte aucun client à risque.
Matrice de confusion (10 000 clients)
| Prédit 0 | Prédit 1 | |
|---|---|---|
| Réel 0 | 9 500 | 0 |
| Réel 1 | 500 | 0 |
Tous les churners finissent en faux négatifs.
01 · Le problème
Pourquoi le modèle est-il biaisé ?
La perte est dominée
La fonction de coût moyenne les erreurs sur tous les exemples. Avec 95 % de non-churners, minimiser la perte revient surtout à bien classer la majorité.
La frontière se décale
La frontière de décision est repoussée vers la classe minoritaire. Les probabilités prédites de churn restent sous le seuil de 0,5.
Trop peu d'exemples
Avec peu de churners, le modèle ne voit pas assez la diversité de leurs profils pour en apprendre des motifs généralisables.
recall, precision, F1 et PR-AUC de la classe minoritaire, jamais avec l'accuracy seule.
02 · Pondération
Le paramètre class_weight
Plutôt que de modifier les données, on modifie la fonction de coût : une erreur sur un churner pèse plus lourd.
C'est la formule utilisée par class_weight="balanced".
from sklearn.linear_model import LogisticRegression
# Pondération automatique
model = LogisticRegression(class_weight="balanced")
# Ou pondération manuelle
model = LogisticRegression(class_weight={0: 1, 1: 10})
Calculateur
Une erreur sur un churner coûte autant que 19 erreurs sur des clients fidèles.
02 · Pondération
class_weight : forces et limites
✓ Avantages
- Aucune donnée créée ni supprimée
- Aucun risque de fuite de données
- Coût de calcul nul, une seule ligne de code
- Disponible dans
LogisticRegression,SVC,RandomForest,DecisionTree…
✗ Limites
- N'ajoute aucune information sur la classe rare
- Peut dégrader la calibration des probabilités
- Tous les modèles ne l'acceptent pas (ex.
KNeighborsClassifier) - XGBoost / LightGBM utilisent
scale_pos_weight≈ nneg / npos
03 · Rééchantillonnage
Random Oversampling / Undersampling
03 · Rééchantillonnage
Deux approches, deux compromis
↑ Random Oversampling
On duplique au hasard des exemples de la classe minoritaire jusqu'à l'équilibre souhaité.
- Aucune perte d'information
- Copies exactes : le modèle mémorise les mêmes points → surapprentissage
- Dataset plus gros, entraînement plus long
↓ Random Undersampling
On supprime au hasard des exemples de la classe majoritaire.
- Entraînement plus rapide
- Perte d'information potentiellement utile
- Variance élevée si le dataset est petit
from imblearn.over_sampling import RandomOverSampler
from imblearn.under_sampling import RandomUnderSampler
X_res, y_res = RandomOverSampler(sampling_strategy=0.5, random_state=42).fit_resample(X_train, y_train)
04 · SMOTE
SMOTE : Synthetic Minority Oversampling Technique
Au lieu de copier les churners, on en fabrique de nouveaux, plausibles, entre des churners existants. (Chawla et al., 2002)
Choisir un point xi de la classe minoritaire
Trouver ses k plus proches voisins parmi la classe minoritaire (k = 5 par défaut)
Tirer au hasard un voisin xzi
Créer un point sur le segment qui les relie
Répéter jusqu'à atteindre le ratio voulu
04 · SMOTE · Démo interactive
La mécanique de génération, pas à pas
04 · SMOTE
Variantes utiles dans imbalanced-learn
SMOTENC
Gère les variables catégorielles (type de contrat, mode de paiement…). Indispensable pour un dataset de churn mixte.
BorderlineSMOTE
Ne génère qu'autour des points minoritaires proches de la frontière, là où le modèle hésite.
ADASYN
Génère davantage de points dans les zones où la classe minoritaire est difficile à apprendre.
SMOTETomek / SMOTEENN
SMOTE suivi d'un nettoyage des points ambigus qui se chevauchent entre classes.
05 · Risques
Les risques de SMOTE
Surapprentissage
Les points synthétiques sont des interpolations des mêmes churners. Le modèle peut apprendre des régions artificielles qui n'existent pas en production.
Bruit et chevauchement
Si un churner est un outlier ou est entouré de clients fidèles, SMOTE propage le bruit et brouille la frontière.
Données inadaptées
Interpoler des variables one-hot ou catégorielles donne des valeurs absurdes (ex. 0,37 « contrat mensuel »). En haute dimension, la notion de voisin se dégrade.
05 · Risques
Fuite de données : l'ordre des opérations compte
✗ Incorrect
Le test contient des points synthétiques fabriqués à partir des points du train, quasi-identiques à ceux vus pendant l'entraînement. Les scores sont gonflés.
✓ Correct
Le test reflète la distribution réelle. En validation croisée, SMOTE doit être appliqué dans chaque fold, sur la partie entraînement uniquement.
05 · Risques
En validation croisée : le piège classique
✗ Rééchantillonner avant la CV
X_res, y_res = SMOTE().fit_resample(X_train, y_train)
# Les folds de validation contiennent
# des points synthétiques → fuite
cross_val_score(model, X_res, y_res, cv=5)
✓ SMOTE dans un Pipeline imblearn
from imblearn.pipeline import Pipeline
pipe = Pipeline([
("smote", SMOTE(random_state=42)),
("model", model),
])
# SMOTE s'applique uniquement au fit,
# jamais au predict ni au fold de validation
cross_val_score(pipe, X_train, y_train, cv=5)
Pipeline d'imblearn (et non celui de sklearn) sait appeler fit_resample pendant l'entraînement seulement.06 · Implémentation
Exemple complet : churn avec imbalanced-learn
import pandas as pd
from sklearn.model_selection import train_test_split, StratifiedKFold, cross_validate
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from imblearn.over_sampling import SMOTE
from imblearn.pipeline import Pipeline
df = pd.read_csv("churn.csv")
X, y = df.drop(columns="churn"), df["churn"]
print(y.value_counts(normalize=True)) # ex. 0: 0.95 / 1: 0.05
# 1. Split stratifié AVANT tout rééchantillonnage
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, stratify=y, random_state=42)
# 2. Pipeline : scaling → SMOTE → modèle
pipe = Pipeline([
("scaler", StandardScaler()),
("smote", SMOTE(sampling_strategy=0.5, k_neighbors=5, random_state=42)),
("model", LogisticRegression(max_iter=1000)),
])
# 3. Validation croisée stratifiée, SMOTE refait dans chaque fold
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
scores = cross_validate(pipe, X_train, y_train, cv=cv,
scoring=["recall", "precision", "f1", "average_precision"])
06 · Implémentation
Optimiser et évaluer sur le test intact
from sklearn.model_selection import GridSearchCV
from sklearn.metrics import classification_report
param_grid = {
"smote__k_neighbors": [3, 5, 7],
"smote__sampling_strategy": [0.3, 0.5, 1.0],
"model__C": [0.1, 1, 10],
}
search = GridSearchCV(pipe, param_grid, cv=cv,
scoring="average_precision")
search.fit(X_train, y_train)
# Évaluation finale : test jamais rééchantillonné
y_pred = search.predict(X_test)
print(classification_report(y_test, y_pred))
Données mixtes (churn réel)
from imblearn.over_sampling import SMOTENC
# indices des colonnes catégorielles
cat_idx = [0, 3, 5] # contrat, paiement, région
smote = SMOTENC(categorical_features=cat_idx,
random_state=42)
- Choisir le score sur PR-AUC ou F1 de la classe 1
- Ajuster ensuite le seuil de décision selon le coût métier
Synthèse
Quelle technique choisir ?
| Technique | Principe | Point fort | Point faible |
|---|---|---|---|
class_weight | Pondère la perte | Simple, sans fuite | N'ajoute pas d'information |
| Random Over | Duplique la minorité | Aucune perte de données | Surapprentissage (copies) |
| Random Under | Réduit la majorité | Rapide | Perte d'information |
| SMOTE | Interpole la minorité | Points nouveaux et variés | Bruit, fuite si mal placé |
Conclusion
Check-list des bonnes pratiques
- Mesurer avec recall, precision, F1 et PR-AUC, pas avec l'accuracy
- Toujours faire un
train_test_splitstratifié avant tout rééchantillonnage - Commencer par la baseline
class_weight="balanced" - Placer SMOTE dans un
imblearn.pipeline.Pipelinepour la CV - Ne jamais rééchantillonner le jeu de test
- Utiliser
SMOTENCs'il y a des variables catégorielles - Ajuster le seuil de décision selon le coût métier d'un churn manqué
Merci
Questions ?
Documentation : imbalanced-learn.org