Je traite une classification déséquilibrée avec des métriques adaptées, des coûts d’erreur, un bon seuil et parfois des modèles pondérés plutôt qu’un SMOTE automatique. Le vrai sujet, c’est rarement de rééquilibrer les lignes. C’est de prendre de meilleures décisions sur les cas rares.
Pourquoi l’accuracy ment ?
L’accuracy ment parce qu’un modèle peut prédire presque toujours la classe majoritaire et afficher un score flatteur tout en ratant les cas utiles.
Une classification déséquilibrée, c’est quand une classe écrase l’autre. Par exemple 98% de clients qui ne churnent pas contre 2% qui churnent. Ou un ratio 100:1, avec 100 transactions normales pour 1 fraude. Dans ce cas, un modèle qui répond “non fraude” tout le temps peut déjà afficher 99% d’accuracy. Sur le papier, c’est beau. En métier, c’est souvent nul.
Je le vois souvent sur des sujets comme la fraude, le churn, une maladie rare ou une panne critique. La classe minoritaire est justement celle qui compte. C’est elle qui coûte cher quand on la rate, ou qui rapporte le plus quand on la détecte à temps. Un client qui part, une fraude qui passe, une panne non anticipée, un patient à risque qu’on ne signale pas… Le problème n’est pas statistique seulement, il est économique.
Le vrai sujet, c’est le coût asymétrique des erreurs. Un faux positif peut gêner. Par exemple, on bloque une transaction légitime, on appelle un client qui n’allait pas partir, on déclenche une inspection inutile. C’est agaçant, parfois coûteux. Mais un faux négatif peut être bien pire. On laisse passer la fraude, on perd le client, on rate une alerte médicale, on subit l’arrêt machine.
Sur un projet fraude, j’ai déjà vu un modèle avec une très bonne accuracy être rejeté par les équipes métier. Il laissait passer trop de dossiers risqués. Techniquement, il scorait bien. Opérationnellement, il ne servait à rien. C’est là qu’on arrête de regarder uniquement l’accuracy et qu’on ouvre la matrice de confusion.
| Métrique | Ce qu’elle mesure | Quand je l’utilise |
| Précision | Parmi les alertes positives, combien sont vraiment positives. | Quand chaque alerte coûte du temps ou crée de la friction. |
| Rappel | Parmi les vrais positifs, combien le modèle retrouve. | Quand rater un cas coûte cher. |
| F1 | Un compromis entre précision et rappel. | Quand je veux une métrique simple, moins naïve que l’accuracy. |
| Matrice de confusion | Les vrais positifs, faux positifs, vrais négatifs et faux négatifs. | Tout le temps, pour comprendre les erreurs concrètement. |
| PR-AUC | La qualité du modèle sur le compromis précision-rappel. | Quand la classe positive est rare. |
| Average precision | Une synthèse de la courbe précision-rappel. | Quand je compare plusieurs modèles sur une classe rare. |
La ROC-AUC peut rester utile, parce qu’elle mesure la capacité globale du modèle à classer les positifs au-dessus des négatifs. Mais quand la classe positive est rare, la PR-AUC parle souvent mieux. Elle regarde ce qui intéresse vraiment le métier : Est-ce que mes alertes sont fiables, et est-ce que je retrouve assez de cas importants ?
Comment poser une baseline fiable ?
Je pose une baseline fiable en gardant le ratio de classes dans le split, en entraînant un modèle simple et en regardant les métriques de la classe minoritaire avant toute technique avancée.
C’est vraiment le point de départ. Pas SMOTE. Pas de modèle compliqué. Pas d’optimisation de seuil dès la première minute. Je veux d’abord savoir ce que vaut une prédiction bête, puis ce que vaut un modèle simple.
Une baseline naïve sert surtout à rendre visible le piège de l’accuracy. Sur un dataset avec 2% de positifs, un modèle qui prédit toujours “classe majoritaire” peut faire environ 98% d’accuracy. Ça a l’air excellent. En réalité, il ne détecte aucun cas intéressant. J’ai vu ça chez un client sur un sujet fraude, tout le monde était content du score global, jusqu’au moment où on a regardé le recall de la fraude. Il était à zéro.
Les métriques à regarder dès le début sont simples :
- Precision : Parmi les alertes prédites positives, combien sont vraiment positives.
- Recall : Parmi les vrais positifs, combien le modèle arrive à retrouver.
- F1 : Un compromis entre precision et recall.
- PR-AUC : Une mesure utile quand la classe positive est rare, basée sur la courbe precision-recall.
- Matrice de confusion : Le tableau brut des vrais/faux positifs et vrais/faux négatifs.
import numpy as np
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.dummy import DummyClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, average_precision_score, confusion_matrix, classification_report
# Dataset très déséquilibré, proche d’un cas fraude
X, y = make_classification(n_samples=20000, n_features=20, n_informative=8, n_redundant=4, n_clusters_per_class=2, weights=[0.98, 0.02], flip_y=0.01, random_state=42)
# Split stratifié pour conserver le même ratio minoritaire en train et test
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.30, stratify=y, random_state=42)
# Baseline naïve : prédit toujours la classe majoritaire
baseline = DummyClassifier(strategy='most_frequent')
baseline.fit(X_train, y_train)
y_pred_base = baseline.predict(X_test)
print('Accuracy baseline:', accuracy_score(y_test, y_pred_base))
print('Precision baseline:', precision_score(y_test, y_pred_base, zero_division=0))
print('Recall baseline:', recall_score(y_test, y_pred_base))
print('F1 baseline:', f1_score(y_test, y_pred_base))
print('Matrice baseline:\n', confusion_matrix(y_test, y_pred_base))
# Premier modèle simple, sans rééquilibrage
model = LogisticRegression(max_iter=1000)
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
y_score = model.predict_proba(X_test)[:, 1]
print('Accuracy:', accuracy_score(y_test, y_pred))
print('Precision:', precision_score(y_test, y_pred, zero_division=0))
print('Recall:', 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:\n', confusion_matrix(y_test, y_pred))
print(classification_report(y_test, y_pred, zero_division=0))
Le split stratifié est important parce qu’il garde à peu près les mêmes proportions de classes dans le train et le test. Sans ça, avec seulement 2% de positifs, je peux me retrouver avec un test mal représentatif, et mes scores deviennent instables.
Ce que je veux voir ici, ce n’est pas un modèle parfait. Je veux une base propre. Si la baseline est mal posée, toutes les comparaisons suivantes deviennent du bruit.
Pourquoi SMOTE ne suffit pas ?
SMOTE ne suffit pas parce qu’il fabrique des exemples synthétiques entre des points minoritaires. Ça peut aider quand la frontière entre les classes est assez simple. Mais ça peut aussi créer des observations qui n’existent pas vraiment.
Le principe est simple. SMOTE prend un point de la classe minoritaire, regarde ses voisins proches, puis interpole un nouveau point entre eux dans l’espace des variables. Si vos features sont propres, continues, bien normalisées, ça peut donner un coup de main. Des variantes existent, comme Borderline-SMOTE, ADASYN ou SMOTEENN. Elles essaient de mieux cibler les zones difficiles, ou de nettoyer certains exemples, mais le fond du sujet reste le même.
Le problème, c’est la vraie vie. Les données sont bruitées. La classe minoritaire est parfois hétérogène. Les variables catégorielles sont mal encodées. Une catégorie client, un pays ou un type de transaction transformé en nombre peut faire croire à SMOTE qu’il existe une distance logique entre “France” et “Espagne”, alors que ce n’est pas si simple.
En fraude, par exemple, créer de faux fraudeurs peut aider un modèle à apprendre un signal. Mais ça peut aussi lui apprendre des profils qui n’existent pas. J’ai déjà vu des scores offline meilleurs avec oversampling, puis une dégradation en production parce que le seuil de décision et le coût des erreurs n’avaient pas été travaillés. Le modèle “rappelait” plus de fraudes, oui, mais avec trop de faux positifs côté métier.
Il y a aussi un piège classique : appliquer SMOTE avant le split train/test. Là, on crée une fuite de données. Le modèle voit indirectement des informations issues du test pendant l’entraînement, donc les scores deviennent trop optimistes.
from imblearn.pipeline import Pipeline
from imblearn.over_sampling import SMOTE
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import StratifiedKFold, cross_validate
# SMOTE doit être dans le pipeline pour éviter de rééchantillonner le jeu de test
pipe_smote = Pipeline(steps=[
('smote', SMOTE(random_state=42)),
('model', LogisticRegression(max_iter=1000))
])
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
scores = cross_validate(
pipe_smote,
X_train,
y_train,
cv=cv,
scoring=['precision', 'recall', 'average_precision']
)
print('Precision moyenne:', scores['test_precision'].mean())
print('Recall moyen:', scores['test_recall'].mean())
print('PR-AUC moyenne:', scores['test_average_precision'].mean())
| SMOTE peut aider | Je l’éviterais |
| Quand les variables sont numériques, propres et bien normalisées. | Quand les données sont très bruitées ou pleines d’anomalies. |
| Quand la classe minoritaire forme des groupes assez cohérents. | Quand la classe minoritaire mélange plusieurs profils très différents. |
| Quand on l’utilise dans une validation croisée propre, uniquement sur le train. | Quand il est appliqué avant le split, avec fuite de données. |
| Quand l’objectif est d’améliorer le rappel sans exploser les faux positifs. | Quand le seuil, le coût d’erreur et l’impact business ne sont pas définis. |
Quelles méthodes marchent mieux ?
Les méthodes qui marchent souvent mieux que SMOTE sont celles qui changent la manière d’apprendre ou de décider, pas seulement la distribution des lignes. C’est souvent là que je vois la différence chez mes clients, surtout quand la classe positive est rare mais coûte cher à rater.
Avec class_weight=’balanced’ dans scikit-learn, je demande au modèle de pénaliser davantage les erreurs sur la classe minoritaire. Il ne crée pas de fausses lignes. Il apprend juste que rater un positif pèse plus lourd que rater un négatif.
Avec XGBoost ou LightGBM, l’idée proche s’appelle souvent scale_pos_weight. En pratique, je pars d’un poids approximatif égal au nombre de négatifs divisé par le nombre de positifs. Si j’ai 10 000 négatifs et 500 positifs, je teste autour de 20. Ce n’est pas magique, mais c’est un bon point de départ.
Il y a aussi les fonctions de perte adaptées, comme la focal loss. Une fonction de perte, c’est ce que le modèle cherche à minimiser pendant l’apprentissage. La focal loss donne plus d’importance aux exemples difficiles, ceux que le modèle classe mal ou avec peu de confiance. Je l’utilise surtout quand le modèle devient trop bon sur les cas faciles et oublie les vrais cas intéressants.
Les ensembles équilibrés vont plus loin. BalancedRandomForestClassifier et EasyEnsembleClassifier, dans imbalanced-learn, entraînent plusieurs modèles sur des sous-échantillons mieux équilibrés. C’est souvent plus robuste qu’un simple oversampling.
Quand les labels positifs sont vraiment trop rares ou peu fiables, je regarde parfois l’approche anomalie. IsolationForest ou One-Class SVM apprennent surtout ce qui est normal, puis signalent ce qui s’en écarte. Mais attention, ce n’est pas la même promesse qu’une classification supervisée. On détecte du bizarre, pas forcément du positif métier.
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import average_precision_score, precision_score, recall_score
# Si imbalanced-learn est installé, on peut tester une forêt vraiment équilibrée.
from imblearn.ensemble import BalancedRandomForestClassifier
models = {
'logistic_weighted': LogisticRegression(
max_iter=1000,
class_weight='balanced'
),
'random_forest_weighted': RandomForestClassifier(
n_estimators=300,
class_weight='balanced',
random_state=42,
n_jobs=-1
),
'balanced_random_forest': BalancedRandomForestClassifier(
n_estimators=300,
random_state=42,
n_jobs=-1
)
}
# On compare avec PR-AUC, précision et rappel.
# La PR-AUC est souvent plus utile que l’accuracy sur un dataset déséquilibré.
for name, clf in models.items():
clf.fit(X_train, y_train)
proba = clf.predict_proba(X_test)[:, 1]
# Le seuil 0.5 est un point de départ, pas forcément le meilleur seuil métier.
pred = (proba >= 0.5).astype(int)
print('\n', name)
print('PR-AUC:', average_precision_score(y_test, proba))
print('Precision:', precision_score(y_test, pred, zero_division=0))
print('Recall:', recall_score(y_test, pred))
| Méthode | Avantage | Limite | Quand je l’utilise |
| Class_weight | Simple, rapide, propre | Ne change pas la frontière si le signal est faible | Premier réflexe sur un modèle supervisé |
| Scale_pos_weight | Très efficace avec boosting | Demande un réglage propre | Avec XGBoost ou LightGBM |
| Focal loss | Insiste sur les cas difficiles | Moins standard à implémenter | Quand les faux négatifs coûtent cher |
| Ensembles équilibrés | Robuste sur gros déséquilibre | Plus lent, moins interprétable | Quand un Random Forest classique plafonne |
| Détection d’anomalies | Utile avec peu de positifs fiables | Ne prédit pas exactement une classe métier | Quand j’ai surtout des exemples normaux |
Comment construire un pipeline fraude ?
Je construis un pipeline fraude en séparant quatre choses : l’apprentissage, le score, le choix du seuil et la calibration de la décision. Le modèle ne “décide” pas vraiment. Il sort une probabilité, ou un score de risque. La décision finale dépend du seuil, du coût des faux positifs, du coût des faux négatifs, et surtout de votre capacité à traiter les alertes. Si votre équipe fraude peut regarder 200 dossiers par jour, ça ne sert à rien de sortir 5 000 alertes “optimisées”.
Le workflow propre ressemble à ça : split stratifié, preprocessing si nécessaire, modèle pondéré ou ensemble équilibré, validation croisée, mesure en PR-AUC, puis choix du seuil sur un vrai set de validation. Le test final, lui, reste fermé jusqu’à la fin. La PR-AUC, c’est l’aire sous la courbe précision-rappel. C’est souvent plus utile que l’accuracy quand la fraude représente 0,5% des cas.
Quand les probabilités doivent être utilisées comme de vraies probabilités, par exemple “ce client a 72% de risque”, je calibre le modèle avec CalibratedClassifierCV. La calibration sert à corriger les scores trop confiants ou pas assez confiants. J’ai vu ça chez un client : un modèle classait très bien les dossiers, mais ses probabilités étaient inutilisables pour prioriser les enquêtes.
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import precision_score, recall_score, f1_score
# On crée un vrai set de validation pour choisir le seuil sans toucher au test final
X_train_small, X_val, y_train_small, y_val = train_test_split(
X_train,
y_train,
test_size=0.25,
stratify=y_train,
random_state=42
)
# Le class_weight='balanced' donne plus de poids à la classe rare
clf = RandomForestClassifier(
n_estimators=300,
class_weight='balanced',
random_state=42,
n_jobs=-1
)
clf.fit(X_train_small, y_train_small)
# Le modèle sort un score de fraude, ici une probabilité estimée
val_scores = clf.predict_proba(X_val)[:, 1]
# On teste plusieurs seuils au lieu de garder le seuil par défaut à 0.50
thresholds = np.linspace(0.01, 0.99, 99)
rows = []
for threshold in thresholds:
pred = (val_scores >= threshold).astype(int)
rows.append({
'threshold': threshold,
'precision': precision_score(y_val, pred, zero_division=0),
'recall': recall_score(y_val, pred),
'f1': f1_score(y_val, pred, zero_division=0)
})
results = pd.DataFrame(rows)
# Exemple de règle métier : garder le meilleur rappel avec au moins 30% de précision
candidates = results[results['precision'] >= 0.30]
best = candidates.sort_values('recall', ascending=False).head(1)
print(best)
best_threshold = float(best['threshold'].iloc[0])
# On évalue une seule fois sur le test final, avec le seuil choisi sur validation
test_scores = clf.predict_proba(X_test)[:, 1]
test_pred = (test_scores >= best_threshold).astype(int)
print('Seuil retenu:', best_threshold)
print('Precision test:', precision_score(y_test, test_pred, zero_division=0))
print('Recall test:', recall_score(y_test, test_pred))
print('F1 test:', f1_score(y_test, test_pred, zero_division=0))
Dans un contexte business, je choisis souvent un seuil qui maximise le rappel avec une précision minimale acceptable. Ou alors je maximise un gain métier estimé : fraude évitée moins coût d’investigation.
- Si les labels sont fiables, je teste pondération et ensembles équilibrés.
- Si les faux négatifs coûtent cher, je travaille le rappel et le seuil.
- Si les positifs sont très rares ou mal labellisés, je regarde aussi les méthodes d’anomalie.
- Si le modèle part en production, je monitore la dérive, le taux d’alertes et la qualité des retours terrain.
Et si le vrai levier n’était pas SMOTE ?
Pour moi, une classification déséquilibrée se gagne d’abord à la mesure. L’accuracy rassure, mais elle cache souvent le problème. Je préfère poser une baseline propre, regarder précision, rappel et PR-AUC, puis tester des approches pondérées, des ensembles équilibrés, parfois SMOTE, parfois l’anomalie. Le seuil compte autant que le modèle, surtout quand une équipe doit traiter les alertes derrière. Le bon pipeline n’est pas celui qui affiche le plus beau score, c’est celui qui prend de meilleures décisions sur les cas rares. Le bénéfice pour vous est simple : moins de faux confort, plus d’impact business.
FAQ
- Pourquoi l’accuracy est mauvaise sur une classification déséquilibrée ? Parce qu’un modèle peut prédire presque toujours la classe majoritaire et obtenir un score très élevé. Avec 2% de fraude, un modèle qui prédit zéro fraude peut afficher environ 98% d’accuracy, tout en ratant 100% des cas importants.
- SMOTE est-il une mauvaise méthode ? Pas forcément. SMOTE peut aider quand la frontière entre classes est assez propre. Le problème, c’est de l’utiliser par réflexe. Sur des données bruitées, temporelles ou très hétérogènes, il peut créer des exemples synthétiques peu réalistes et améliorer les scores offline sans améliorer la décision réelle.
- Quelles métriques suivre pour une classe rare ? Je regarde surtout la précision, le rappel, le F1-score, la matrice de confusion et la PR-AUC. Le rappel dit combien de vrais cas rares on retrouve. La précision dit combien d’alertes positives sont réellement utiles. La PR-AUC résume mieux le comportement du modèle quand les positifs sont rares.
- Quelle alternative simple tester avant SMOTE ? Je commence souvent par la pondération des classes. Dans scikit-learn, class_weight=’balanced’ est rapide à tester. Avec XGBoost ou LightGBM, on peut ajuster le poids de la classe positive. C’est simple, reproductible, et ça évite de fabriquer artificiellement de nouvelles lignes.
- Pourquoi le réglage du seuil est si important ? Parce que le seuil transforme un score en décision. Le seuil 0.5 est rarement optimal sur une classe rare. En fraude, par exemple, on peut choisir un seuil qui garde un rappel élevé tout en limitant le volume d’alertes à traiter. C’est là que le modèle rejoint vraiment le business.
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, SEO et GEO. Avec mon agence webAnalyste et Formations Analytics, j’accompagne des équipes sur des sujets data très concrets : mesure, pipelines, qualité des données, scoring, automatisation et passage en production. J’ai travaillé avec des clients comme Logis Hôtel, Yelloh Village, BazarChic, la Fédération Française de Football ou Texdecor. Si vous voulez fiabiliser vos modèles, vos dashboards ou vos automatisations IA, contactez-moi, je peux vous aider.
⭐ 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.






