Je traite rarement une classification déséquilibrée avec SMOTE seul. Je pars des coûts d’erreur, des bonnes métriques, puis je teste pondération, pertes adaptées, ensembles équilibrés et seuils de décision. C’est moins automatique, mais beaucoup plus proche d’un vrai problème business.
Pourquoi l’accuracy ment-elle ?
L’accuracy ment dès qu’une classe écrase l’autre. C’est aussi simple que ça. Si 99 % de vos lignes sont “normales” et 1 % sont des fraudes, un modèle peut prédire “normal” tout le temps et afficher 99 % d’accuracy. Sur le papier, il a l’air brillant. Dans la vraie vie, il ne sert à rien.
La classification déséquilibrée, c’est quand une classe est beaucoup plus présente que l’autre. La classe majoritaire, c’est celle qu’on voit partout. La classe minoritaire, c’est celle qu’on cherche souvent vraiment. Une fraude bancaire. Un cancer sur une radio. Un client qui va résilier. Une panne machine. Une anomalie dans des logs.
Le ratio d’imbalance mesure cet écart. Par exemple, 98 % de non-fraudes et 2 % de fraudes, ça donne un ratio 98:2, ou 49:1. Et souvent, c’est cette petite minorité qui porte la valeur métier. Les 1 %, 2 % ou 5 % de cas rares sont justement ceux qui coûtent cher, qui déclenchent une action, qui demandent une décision humaine.
Le vrai sujet, c’est le coût asymétrique. Un faux positif, c’est une alerte inutile. C’est pénible, ça consomme du temps. Un faux négatif, c’est une fraude qui passe, un malade non détecté, une panne non anticipée. Là, le coût peut exploser. Optimiser une métrique globale comme l’accuracy revient souvent à optimiser le confort statistique du modèle, pas la décision business.
Je regarde donc les bonnes métriques dès le départ :
- La précision mesure parmi les alertes levées, combien sont vraiment positives.
- Le rappel mesure parmi les vrais cas positifs, combien le modèle retrouve.
- Le F1-score combine précision et rappel dans un seul score, utile quand il faut arbitrer.
- La matrice de confusion montre les vrais positifs, faux positifs, vrais négatifs et faux négatifs.
- La PR-AUC, l’aire sous la courbe précision-rappel, indique si le modèle retrouve les positifs sans noyer les équipes sous les fausses alertes.
La PR-AUC est souvent plus lisible que la ROC-AUC quand la classe positive est rare. La ROC-AUC regarde la séparation globale entre classes, mais elle peut rester flatteuse même quand les alertes utiles sont mauvaises. J’ai déjà vu des modèles à plus de 98 % d’accuracy totalement inutilisables, simplement parce qu’ils ne détectaient presque aucun cas important.
| Problème | Mauvaise lecture | Meilleure lecture |
| Classe rare à 1 % | 99 % d’accuracy semble excellent | Regarder rappel, précision et PR-AUC |
| Fraude ou diagnostic raté | Tous les erreurs valent pareil | Comparer le coût des faux négatifs et faux positifs |
| Trop d’alertes inutiles | Le modèle détecte beaucoup de cas | Mesurer la précision et la charge opérationnelle |
Comment créer un terrain de test fiable ?
Pour tester SMOTE ou n’importe quelle méthode plus avancée, je commence toujours par un terrain propre. Un dataset contrôlé, un split stratifié, puis une baseline simple. Pas pour gagner la compétition. Juste pour savoir d’où on part. Sur un cas de fraude avec 2 % de positifs, si le test set est mal construit, vos métriques racontent n’importe quoi.
J’installe les dépendances de base. XGBoost est là pour les essais suivants. LightGBM peut aussi être une très bonne alternative, surtout sur gros volume, mais je ne le mets pas en dépendance obligatoire ici.
pip install scikit-learn imbalanced-learn xgboost
Voici un exemple reproductible avec 20 000 lignes et environ 2 % de classe minoritaire. L’idée ressemble à un cas de fraude : peu de cas positifs, beaucoup de transactions normales, et un signal pas parfaitement propre.
from collections import Counter
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import (
classification_report,
confusion_matrix,
average_precision_score
)
# Création d'un dataset synthétique proche d'un problème de fraude
# Classe 0 = transaction normale
# Classe 1 = fraude
X, y = make_classification(
n_samples=20_000,
n_features=30,
n_informative=8,
n_redundant=6,
n_clusters_per_class=2,
weights=[0.98, 0.02],
class_sep=1.0,
flip_y=0.01,
random_state=42
)
print("Distribution complète :", Counter(y))
# Split stratifié obligatoire pour conserver le même ratio de classes
# Dans le train et dans le test
X_train, X_test, y_train, y_test = train_test_split(
X,
y,
test_size=0.25,
random_state=42,
stratify=y
)
print("Distribution train :", Counter(y_train))
print("Distribution test :", Counter(y_test))
# Baseline simple.
# Elle sert uniquement de point de comparaison avant SMOTE, XGBoost, etc.
model = LogisticRegression(
max_iter=1000,
class_weight=None,
solver="lbfgs"
)
model.fit(X_train, y_train)
# Probabilité d'appartenir à la classe fraude
y_proba = model.predict_proba(X_test)[:, 1]
# Seuil par défaut à 0.5
y_pred = (y_proba >= 0.5).astype(int)
print("Matrice de confusion :")
print(confusion_matrix(y_test, y_pred))
print("Rapport de classification :")
print(classification_report(y_test, y_pred, digits=4))
# PR-AUC = aire sous la courbe Precision-Recall
# Plus utile que la ROC-AUC quand la classe positive est rare
pr_auc = average_precision_score(y_test, y_proba)
print("PR-AUC :", round(pr_auc, 4))
Le point vraiment non négociable ici, c’est stratify=y. Sans ça, le train et le test peuvent avoir des ratios différents. Avec 2 % de positifs, ça va vite. Sur 5 000 lignes de test, on attend environ 100 fraudes. Mais selon le split, vous pouvez en avoir beaucoup moins, et là votre recall, votre précision, votre PR-AUC deviennent instables.
J’ai déjà vu un client conclure qu’un modèle était “excellent” juste parce que son test set contenait trop peu de fraudes difficiles. Le modèle n’était pas bon. Le terrain de test était mauvais.
Cette baseline n’est pas la solution finale. Elle donne juste une référence propre. Après seulement, on peut tester SMOTE, le sous-échantillonnage, les poids de classe, XGBoost, ou un réglage de seuil sérieux.
Pourquoi SMOTE déçoit-il souvent ?
SMOTE déçoit souvent parce qu’il invente des données. Pas n’importe comment, bien sûr, mais il fabrique des points synthétiques entre des exemples minoritaires proches. Sur le papier, c’est propre. Dans la vraie vie, ces nouveaux points ne correspondent pas toujours à de vrais cas métier.
SMOTE, pour Synthetic Minority Over-sampling Technique, crée de nouveaux exemples de la classe rare par interpolation. Il prend un point minoritaire, cherche ses voisins minoritaires, puis place un nouveau point entre les deux. Le problème, c’est que “entre deux fraudes” ne veut pas forcément dire “fraude réaliste”. J’ai déjà vu ça chez un client fraude paiement : le rappel montait, donc le modèle trouvait plus de fraudes, mais la précision s’écroulait. Résultat, trop d’alertes inutiles pour les analystes.
Les variantes essaient de corriger ça. Borderline-SMOTE se concentre sur les frontières de décision. ADASYN génère plus d’exemples là où le modèle galère. SMOTEENN et SMOTETomek ajoutent du nettoyage pour supprimer des voisins ambigus ou bruités. C’est utile, mais ce n’est pas magique. Sur des données bruitées, très rares ou avec beaucoup de variables, SMOTE peut ajouter de la confusion.
Le point non négociable : SMOTE s’applique uniquement sur le train. Jamais avant le split.
from sklearn.model_selection import train_test_split
from sklearn.metrics import precision_score, recall_score, f1_score, average_precision_score, confusion_matrix
from sklearn.ensemble import RandomForestClassifier
from imblearn.pipeline import Pipeline
from imblearn.over_sampling import SMOTE
# Séparation avant tout resampling pour éviter la fuite de données
X_train, X_test, y_train, y_test = train_test_split(
X, y,
test_size=0.2,
stratify=y,
random_state=42
)
# SMOTE est dans le pipeline, donc appliqué seulement sur X_train pendant fit
pipeline = Pipeline(steps=[
("smote", SMOTE(random_state=42, k_neighbors=5)),
("model", RandomForestClassifier(
n_estimators=300,
random_state=42,
class_weight=None
))
])
pipeline.fit(X_train, y_train)
y_pred = pipeline.predict(X_test)
y_score = pipeline.predict_proba(X_test)[:, 1]
print("Précision :", precision_score(y_test, y_pred))
print("Rappel :", recall_score(y_test, y_pred))
print("F1 :", f1_score(y_test, y_pred))
print("PR-AUC :", average_precision_score(y_test, y_score))
print("Matrice de confusion :")
print(confusion_matrix(y_test, y_pred))
L’erreur classique, c’est ça.
# À éviter absolument
X_resampled, y_resampled = SMOTE(random_state=42).fit_resample(X, y)
# Le split arrive trop tard
X_train, X_test, y_train, y_test = train_test_split(
X_resampled, y_resampled,
test_size=0.2,
random_state=42
)
Pourquoi c’est grave ? Parce que des points synthétiques très proches peuvent se retrouver à la fois dans le train et dans le test. Le modèle voit presque la réponse pendant l’entraînement. Les scores deviennent flatteurs, mais faux.
Je compare toujours SMOTE avec les mêmes métriques que la baseline : précision, rappel, F1, PR-AUC, c’est-à-dire l’aire sous la courbe précision-rappel, et matrice de confusion. SMOTE peut améliorer le rappel, oui. Mais si la précision tombe trop bas, vous venez peut-être de transformer un bon filtre en machine à spam opérationnel.
| Quand tester SMOTE | Quand l’éviter | Quoi surveiller |
| Classe minoritaire rare mais assez propre. | Données très bruitées ou labels douteux. | Hausse du rappel sans effondrement de la précision. |
| Modèle qui ignore presque tous les positifs. | Très haute dimension avec peu d’exemples minoritaires. | PR-AUC, pas seulement l’accuracy. |
| Besoin d’un benchmark rapide. | Cas métier où les faux positifs coûtent cher. | Matrice de confusion et volume d’alertes générées. |
Qu’est-ce qui marche mieux en pratique ?
Ce qui marche souvent mieux que SMOTE, c’est rarement une seule astuce. C’est plutôt une combinaison propre : pondérer les classes, choisir une fonction de perte adaptée, utiliser des ensembles équilibrés, puis régler le seuil de décision. J’ai vu pas mal de modèles gagner plus avec un bon seuil qu’avec trois variantes de sur-échantillonnage.
SMOTE agit sur les données. C’est utile parfois, mais ce n’est qu’un levier. On peut aussi agir sur l’algorithme, sur la fonction de perte, et sur la décision finale. Le seuil par défaut à 0,5 est souvent absurde en classification déséquilibrée, surtout quand on cherche à détecter une fraude, un churn rare, ou un incident critique.
L’apprentissage à coût sensible consiste à dire au modèle : “Une erreur sur la classe minoritaire coûte plus cher”. Dans scikit-learn, class_weight=’balanced’ ajuste automatiquement les poids selon la fréquence des classes.
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
# Modèle linéaire avec pénalisation renforcée de la classe rare
log_reg = LogisticRegression(class_weight="balanced", max_iter=1000)
log_reg.fit(X_train, y_train)
# Random Forest avec pondération automatique des classes
rf = RandomForestClassifier(class_weight="balanced", random_state=42)
rf.fit(X_train, y_train)
Les ensembles équilibrés vont plus loin. Avec imbalanced-learn, BalancedRandomForestClassifier ou EasyEnsembleClassifier construisent plusieurs modèles qui voient mieux la classe minoritaire, en contrôlant l’échantillonnage de la majorité.
from imblearn.ensemble import BalancedRandomForestClassifier
from sklearn.metrics import average_precision_score
# Forêt entraînée avec échantillonnage équilibré à chaque arbre
brf = BalancedRandomForestClassifier(random_state=42)
brf.fit(X_train, y_train)
# PR-AUC : métrique plus utile que l'accuracy quand les classes sont déséquilibrées
proba = brf.predict_proba(X_test)[:, 1]
pr_auc = average_precision_score(y_test, proba)
print(pr_auc)
Avec XGBoost, scale_pos_weight est souvent un très bon point de départ. On l’initialise simplement avec le ratio nombre de négatifs / nombre de positifs.
from xgboost import XGBClassifier
from sklearn.metrics import average_precision_score
# Calcul du poids de la classe positive
neg = (y_train == 0).sum()
pos = (y_train == 1).sum()
ratio = neg / pos
# Entraînement XGBoost sensible au déséquilibre
xgb = XGBClassifier(scale_pos_weight=ratio, eval_metric="logloss", random_state=42)
xgb.fit(X_train, y_train)
# Évaluation en PR-AUC
proba = xgb.predict_proba(X_test)[:, 1]
pr_auc = average_precision_score(y_test, proba)
print(pr_auc)
Les fonctions de perte modernes comme la focal loss ajoutent une autre idée : donner plus d’importance aux cas difficiles, et éviter que les exemples faciles dominent l’apprentissage. Ce n’est pas supporté nativement par scikit-learn. On la voit surtout en deep learning, ou dans certaines implémentations spécialisées.
| Méthode | Quand l’utiliser | Avantage | Risque |
| Pondération des classes | Premier réflexe simple | Facile, robuste | Peut augmenter les faux positifs |
| Ensembles équilibrés | Données tabulaires déséquilibrées | Bonne détection minoritaire | Temps d’entraînement plus long |
| Scale_pos_weight | XGBoost ou modèles boosting | Très efficace en pratique | Ratio à ajuster |
| Focal loss | Cas difficiles, deep learning | Apprend mieux les exemples ambigus | Implémentation moins standard |
| Réglage du seuil | Toujours après entraînement | Aligne le modèle avec le métier | Dépend du coût réel des erreurs |
Comment décider en production ?
En production, je ne garde pas le modèle qui a la meilleure accuracy. Je garde celui qui donne le meilleur compromis entre rappel, précision, coût métier et charge opérationnelle. Le seuil de décision est souvent aussi important que le modèle, surtout quand la classe positive est rare.
Un modèle sort une probabilité. Après, on choisit un seuil. À 0.5, on dit “fraude” si le score dépasse 50 %. Sauf que sur une fraude à 1 %, ce seuil est souvent absurde. Je préfère chercher un seuil qui respecte une contrainte métier claire. Par exemple, “Je veux au moins 80 % de rappel”, donc détecter au moins 80 % des fraudes connues.
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.metrics import average_precision_score, precision_recall_fscore_support, confusion_matrix
from sklearn.calibration import CalibratedClassifierCV
from sklearn.ensemble import RandomForestClassifier
# Données attendues : df contient vos variables et une colonne cible fraud
X = df.drop(columns=["fraud"])
y = df["fraud"]
# Split stratifié pour garder le même taux de fraude dans train et validation
X_train, X_valid, y_train, y_valid = train_test_split(
X, y, test_size=0.25, stratify=y, random_state=42
)
# Modèle pondéré : il pénalise plus les erreurs sur la classe rare
base_model = RandomForestClassifier(
n_estimators=300,
class_weight="balanced",
random_state=42,
n_jobs=-1
)
# Calibration : une proba à 0.8 doit vraiment ressembler à un risque de 80 %
model = CalibratedClassifierCV(
estimator=base_model,
method="isotonic",
cv=3
)
model.fit(X_train, y_train)
# Probabilités de fraude
probas = model.predict_proba(X_valid)[:, 1]
# PR-AUC : Plus utile que ROC-AUC quand la classe positive est rare
pr_auc = average_precision_score(y_valid, probas)
rows = []
for threshold in np.linspace(0.01, 0.99, 99):
preds = (probas >= threshold).astype(int)
precision, recall, f1, _ = precision_recall_fscore_support(
y_valid, preds, average="binary", zero_division=0
)
fp = ((preds == 1) & (y_valid == 0)).sum()
fn = ((preds == 0) & (y_valid == 1)).sum()
rows.append({
"threshold": threshold,
"precision": precision,
"recall": recall,
"f1": f1,
"false_positives": fp,
"false_negatives": fn
})
results = pd.DataFrame(rows)
# Exemple de règle métier : rappel minimum de 80 %, puis meilleur F1
candidates = results[results["recall"] >= 0.80]
best = candidates.sort_values("f1", ascending=False).iloc[0]
final_threshold = best["threshold"]
final_preds = (probas >= final_threshold).astype(int)
cm = confusion_matrix(y_valid, final_preds)
print("PR-AUC:", round(pr_auc, 4))
print("Seuil choisi:", round(final_threshold, 2))
print(best)
print(cm)
La calibration compte beaucoup. Un score mal calibré peut donner l’impression qu’un client est à 70 % de risque alors que, dans les faits, ce score correspond plutôt à 20 %. Et là, vos seuils deviennent trompeurs. J’ai déjà vu ça chez un client fraude : le modèle classait bien, mais les scores étaient inutilisables pour prioriser les analystes.
Mon framework reste simple :
- Commencer par une baseline propre, sans SMOTE au départ.
- Choisir les métriques business : PR-AUC, rappel, précision, coût des faux positifs et faux négatifs.
- Tester la pondération des classes et les modèles adaptés aux données déséquilibrées.
- Tester SMOTE proprement, uniquement dans le train, jamais avant le split.
- Ajuster le seuil selon une contrainte métier.
- Vérifier la stabilité sur validation.
- Surveiller la dérive après déploiement, parce que la fraude change vite.
Je compare toujours les résultats dans un tableau simple. Si un modèle détecte plus de fraudes mais multiplie les alertes par 10, ce n’est pas forcément un gain. Quelqu’un doit traiter ces alertes derrière.
| Modèle | PR-AUC | Précision | Rappel | Faux positifs | Faux négatifs | Commentaire métier |
| RandomForest pondéré | 0.42 | 0.31 | 0.82 | 940 | 36 | Bon rappel, charge analyste élevée |
| XGBoost pondéré | 0.48 | 0.44 | 0.79 | 510 | 42 | Meilleur compromis opérationnel |
Alors, on remplace SMOTE par quoi maintenant ?
Je ne jette pas SMOTE à la poubelle. Je le remets juste à sa place. Sur une classification déséquilibrée, je commence par les coûts d’erreur, les bonnes métriques et une baseline propre. Ensuite je teste la pondération, les modèles adaptés, les ensembles équilibrés, parfois SMOTE, puis surtout le seuil de décision. C’est moins séduisant qu’une méthode miracle, mais c’est ce qui tient mieux en production. Le bénéfice pour vous est simple : moins de faux bons modèles, plus de décisions utiles, et une performance alignée avec votre vrai problème business.
FAQ
- Pourquoi l’accuracy est-elle mauvaise en classification déséquilibrée ?
Parce qu’elle donne une vision globale qui masque la classe rare. Si 98 % des exemples sont négatifs, un modèle peut prédire négatif tout le temps et afficher 98 % d’accuracy. En pratique, il ne détecte rien d’utile. Je préfère regarder la précision, le rappel, la F1-score, la matrice de confusion et surtout la PR-AUC. - SMOTE est-il une mauvaise méthode ?
Pas forcément. SMOTE peut aider quand la frontière entre classes est assez propre et que les exemples minoritaires sont représentatifs. Le problème, c’est qu’il crée des points synthétiques. Sur des données bruitées, rares ou très complexes, ces points peuvent dégrader le modèle. Je le teste, mais jamais comme réflexe automatique. - Que tester avant SMOTE ?
Je teste d’abord une baseline propre avec split stratifié, puis des modèles avec pondération des classes comme class_weight=’balanced’. Ensuite je regarde les modèles adaptés à l’imbalance, comme Balanced Random Forest ou XGBoost avec scale_pos_weight. Ça donne souvent une comparaison plus saine que de modifier directement les données. - Quelle métrique choisir pour une classe minoritaire ?
Ça dépend du coût métier. Si rater un positif coûte cher, je surveille fortement le rappel. Si chaque alerte coûte du temps ou de l’argent, je surveille la précision. La PR-AUC est très utile parce qu’elle résume le compromis précision rappel sur une classe rare. Je garde toujours la matrice de confusion sous les yeux. - Pourquoi régler le seuil de décision ?
Parce que le seuil 0.5 est rarement le bon sur une classe rare. Le modèle sort souvent une probabilité, puis on décide à partir de quel score on déclenche une alerte. En ajustant ce seuil, on peut augmenter le rappel, réduire les faux positifs ou trouver un compromis plus réaliste pour les équipes métier.
A propos de l’auteur
Je suis Franck Scandolera, expert et formateur en tracking avancé server-side, Analytics Engineering, automatisation No/Low Code avec n8n, IA appliquée en entreprise et SEO/GEO. J’accompagne des équipes sur des sujets data très concrets : mesure fiable, pipelines, scoring, automatisation et passage en production. J’ai travaillé avec des références comme Logis Hôtel, Yelloh Village, BazarChic, la Fédération Française de Football ou Texdecor. Je dirige l’agence webAnalyste et l’organisme Formations Analytics. Si vous voulez cadrer ou industrialiser vos projets data et IA, contactez-moi.
⭐ Analytics engineer, Data Analyst et Automatisation IA indépendant ⭐
- Ref clients : Logis Hôtel, Yelloh Village, BazarChic, Fédération Football Français, Texdecor…
Mon terrain de jeu :
- Data Analyst & Analytics engineering : tracking avancé (GTM server, e-commerce, CAPI, RGPD), entrepôt de données (BigQuery, Snowflake, PostgreSQL, ClickHouse), modèles (Airflow, dbt, Dataform), dashboards décisionnels (Looker, Power BI, Metabase, SQL, Python).
- Automatisation IA des taches Data, Marketing, RH, compta etc : conception de workflows intelligents robustes (n8n, App Script, scraping) connectés aux API de vos outils et LLM (OpenAI, Mistral, Claude…).
- Engineering IA pour créer des applications et agent IA sur mesure : intégration de LLM (OpenAI, Mistral…), RAG, assistants métier, génération de documents complexes, APIs, backends Node.js/Python.






