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

  1. 01Pourquoi un modèle est biaisé sur un dataset déséquilibré
  2. 02Le paramètre class_weight
  3. 03Random Oversampling et Undersampling
  4. 04SMOTE : la génération synthétique
  5. 05Les risques : surapprentissage et fuite de données
  6. 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 » :

95 %Accuracy
0 %Recall churn

Une accuracy élevée qui ne détecte aucun client à risque.

Matrice de confusion (10 000 clients)

Prédit 0Prédit 1
Réel 09 5000
Réel 15000

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.

À retenir : mesurer avec 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.

wc = nsamples / (nclasses × nc)

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

w00.526
w110.000

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
C'est souvent la première baseline à essayer avant tout rééchantillonnage.

03 · Rééchantillonnage

Random Oversampling / Undersampling

Fidèles
48
Churn
8
Fidèle Churn original Copie exacte Supprimé

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)

1

Choisir un point xi de la classe minoritaire

2

Trouver ses k plus proches voisins parmi la classe minoritaire (k = 5 par défaut)

3

Tirer au hasard un voisin xzi

4

Créer un point sur le segment qui les relie

5

Répéter jusqu'à atteindre le ratio voulu

xnew = xi + λ · (xzi − xi)   avec λ ~ U(0, 1)

04 · SMOTE · Démo interactive

La mécanique de génération, pas à pas

Cliquez sur Étape suivante pour démarrer.
xnew = xi + λ · (xzi − xi)
Fidèles 0
Churn réels 0
Synthétiques 0

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.

Le risque majeur : la fuite de données. Elle produit des scores excellents en validation, puis un effondrement en production.

05 · Risques

Fuite de données : l'ordre des opérations compte

✗ Incorrect

Dataset complet
↓
SMOTE
↓
train_test_split
↓
Train
Test ⚠

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

Dataset complet
↓
train_test_split (stratify=y)
↓
Train → SMOTE
Test intact
↓
Modèle entraîné

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)
Le 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 ?

TechniquePrincipePoint fortPoint faible
class_weightPondère la perteSimple, sans fuiteN'ajoute pas d'information
Random OverDuplique la minoritéAucune perte de donnéesSurapprentissage (copies)
Random UnderRéduit la majoritéRapidePerte d'information
SMOTEInterpole la minoritéPoints nouveaux et variésBruit, fuite si mal placé
Aucune méthode ne gagne toujours. On les compare en validation croisée, sur la même métrique orientée classe minoritaire.

Conclusion

Check-list des bonnes pratiques

Merci

Questions ?

Documentation : imbalanced-learn.org