Data science en pratique : du problème au dataset
Dans un projet réel, le choix de l'algorithme occupe une fraction marginale du temps et explique une fraction marginale de la performance. Ce qui fait la différence, c'est le cadrage du problème, la qualité des données et la rigueur du protocole d'évaluation. Ce module couvre exactement cette partie-là — celle qu'on n'apprend pas dans les cours d'algorithmes, et qui décide pourtant du sort du projet.
📋 Au programme
1. Cycle de vie d'un projet
1. Cadrage métier définir la question, la métrique, l'usage du résultat ~10 % 2. Collecte / accès trouver, extraire, joindre les données ~20 % 3. Exploration comprendre les données, détecter les anomalies ~15 % 4. Préparation nettoyage, features, encodages ~25 % 5. Modélisation baseline, puis itérations ~10 % 6. Évaluation protocole, métriques, analyse d'erreurs ~10 % 7. Déploiement / suivi mise en production, monitoring, ré-entraînement ~10 %
Le cycle n'est pas linéaire : l'exploration révèle des problèmes de collecte, l'analyse d'erreurs renvoie au feature engineering, et le déploiement fait apparaître des contraintes de latence qui invalident un modèle. La seule erreur structurelle est de traiter ces étapes comme un tunnel à sens unique, avec une « livraison » à la fin.
La question à poser avant tout le reste
Quelle décision sera prise différemment grâce à ce modèle ? Si personne ne sait répondre, le projet produira un notebook et rien d'autre. Cette question détermine tout : la métrique (un coût d'erreur asymétrique change le seuil, module 24), la latence acceptable (temps réel ou lot nocturne, module 6), le besoin d'explicabilité (un refus de crédit doit être motivé, module 27), et même la faisabilité — certaines décisions ont besoin de causalité, pas de prédiction (module 11).
2. Cadrage : la cible et la baseline
Définir la cible est plus subtil qu'il n'y paraît. Prédire « le churn » exige de fixer une fenêtre (résiliation dans les 30 jours ? 90 ?), une date d'observation, et le sort des cas ambigus (un client inactif depuis six mois est-il parti ?). Deux définitions également raisonnables peuvent donner des taux de positifs allant du simple au triple, et donc des modèles incomparables. Écrire la définition en une phrase, et la faire valider par le métier, économise des semaines.
Fixer l'unité d'observation : une ligne = un client ? un client-mois ? une transaction ? Ce choix détermine la structure de dépendance des données, donc le schéma de validation (section 8), et il est très coûteux à changer en cours de route.
Établir une baseline, enfin, avant tout modèle. Sans elle, un R² de 0,7 ne veut rien dire — il peut être excellent ou ridicule selon le problème.
from sklearn.dummy import DummyRegressor, DummyClassifier # régression : prédire toujours la moyenne (ou la médiane) base = DummyRegressor(strategy="median").fit(X_tr, y_tr) # classification : prédire toujours la classe majoritaire base = DummyClassifier(strategy="most_frequent").fit(X_tr, y_tr) # série temporelle : la valeur de la veille, ou celle de l'an dernier (module 17) pred_naive = y[:-1] # et surtout : la RÈGLE MÉTIER existante, si elle existe pred_regle = np.where(df["anciennete"] < 3, 1, 0)
La baseline donne aussi le coût réel du projet : si une règle à trois conditions atteint 95 % de la performance du modèle, celui-ci ne vaut peut-être pas sa complexité opérationnelle.
3. Exploration : une checklist
Structure dimensions, types, mémoire, granularité (une ligne = quoi ?) Cible distribution, taux de positifs, extrêmes, évolution dans le temps Manquants taux par colonne, et STRUCTURE des manquants (section 5) Doublons lignes identiques ET doublons sur la clé métier Cardinalité colonnes constantes (inutiles), identifiants déguisés (dangereux) Distributions asymétrie, multimodalité, valeurs impossibles (âge = 200, prix < 0) Temps couverture, trous, saisonnalité, ruptures de collecte Relations corrélations entre variables, lien de chaque variable avec la cible Groupes déséquilibres entre segments, sous-populations sous-représentées
import polars as pl
df = pl.read_parquet("data/clients.parquet")
print(df.shape, df.estimated_size("mb"))
print(df.describe()) # stats par colonne
print(df.null_count()) # manquants par colonne
print(df.select(pl.all().n_unique())) # cardinalités
print(df.filter(pl.struct(df.columns).is_duplicated())) # doublons exacts
# valeurs impossibles : toujours confronter aux bornes MÉTIER
print(df.filter((pl.col("age") < 0) | (pl.col("age") > 120)))
# la cible dans le temps : LE graphique qui révèle les ruptures de collecte
print(df.group_by_dynamic("date", every="1w").agg(
taux=pl.col("cible").mean(), n=pl.len()))
# lien de chaque variable numérique avec la cible
print(df.select(pl.corr(pl.col(c), pl.col("cible")).alias(c)
for c in colonnes_numeriques))Les outils de profilage automatique (ydata-profiling, skrub) produisent en une ligne un rapport complet. Utiles pour débroussailler, ils ne remplacent pas le regard métier : seule une connaissance du domaine dit qu'un prix à 0,01 € correspond à un test interne, ou que la valeur « 9999 » est un code de saisie signifiant « inconnu ».
4. Visualisation utile
Trois bibliothèques couvrent tous les besoins : matplotlib (contrôle total, verbeux), seaborn (statistique, concis, excellents défauts), plotly (interactif, précieux pour explorer un nuage dense). Quelques principes qui changent réellement la lisibilité :
- Un histogramme cache autant qu'il montre : le nombre de classes change complètement l'allure d'une distribution. Doubler avec un violin plot ou afficher les points quand l'échantillon est petit.
- Sur des données asymétriques, passer en échelle log — sinon 99 % des points s'écrasent dans le premier centimètre du graphique.
- Contre la surimpression (des millions de points), utiliser la transparence, un échantillon, ou une carte de densité (hexbin).
- Toujours tracer la cible en fonction du temps : c'est le graphique qui révèle les ruptures de collecte, les changements de définition et la dérive (module 26).
- Se méfier des résumés : le quartet d'Anscombe et le « datasaurus » montrent que des jeux de données de moyennes, variances et corrélations identiques peuvent être radicalement différents. Regarder les distributions, pas seulement les statistiques.
import matplotlib.pyplot as plt, seaborn as sns
fig, ax = plt.subplots(2, 2, figsize=(12, 8))
sns.histplot(df["prix"], log_scale=True, ax=ax[0, 0]) # 1. la cible
ax[0, 0].set_title("distribution de la cible (échelle log)")
df.set_index("date")["cible"].resample("W").mean().plot(ax=ax[0, 1]) # 2. le temps
ax[0, 1].set_title("taux de positifs par semaine")
sns.heatmap(df[num].corr(), cmap="RdBu_r", center=0, ax=ax[1, 0]) # 3. corrélations
ax[1, 0].set_title("corrélations entre variables")
sns.boxplot(data=df, x="segment", y="prix", ax=ax[1, 1]) # 4. par groupe
ax[1, 1].set_yscale("log")
plt.tight_layout()5. Données manquantes
Avant de choisir une méthode, il faut comprendre pourquoi une valeur manque. La typologie de Rubin, qui n'est pas qu'une curiosité théorique :
- MCAR (missing completely at random) — le manque est indépendant de tout : un capteur tombé en panne au hasard. Supprimer les lignes ne biaise rien, on perd seulement de la puissance.
- MAR (missing at random) — le manque dépend des variables observées : les revenus manquent plus souvent chez les jeunes, et l'âge est connu. L'imputation conditionnelle est alors légitime, puisque l'information nécessaire est disponible.
- MNAR (missing not at random) — le manque dépend de la valeur elle-même : les hauts revenus refusent de répondre. Aucune imputation ne corrige cela ; il faut modéliser le mécanisme, ou au minimum documenter le biais.
# le taux de manquants dépend-il des autres variables ? (indice de MAR)
df["revenu_manquant"] = df["revenu"].isna()
print(df.groupby("revenu_manquant")[["age", "anciennete", "cible"]].mean())
# si les moyennes diffèrent nettement → le manque est INFORMATIF
# les manquants sont-ils groupés ? (plusieurs colonnes absentes ensemble)
import missingno as msno
msno.matrix(df); msno.heatmap(df) # visualise la co-occurrence des manquantsSupprimer les lignes acceptable si < 5 % et MCAR. Sinon on biaise l'échantillon.
Supprimer la colonne si > 60-70 % manquants ET aucun signal dans le fait de manquer.
Moyenne / médiane rapide ; réduit artificiellement la variance et écrase
les corrélations. Acceptable comme baseline.
KNN / itératif (MICE) impute à partir des autres colonnes ; coûteux, bien meilleur.
Valeur sentinelle -1, "inconnu" : parfait pour les modèles à base d'arbres,
qui peuvent isoler cette modalité.
Ne rien faire HistGradientBoosting, XGBoost et LightGBM gèrent nativement
les NaN et APPRENNENT la meilleure direction pour eux.
Indicateur de manque ajouter une colonne booléenne : le FAIT de manquer est
souvent informatif (un client qui ne renseigne pas
son revenu n'est pas un client au hasard).from sklearn.experimental import enable_iterative_imputer # noqa: F401 from sklearn.impute import IterativeImputer, SimpleImputer # imputation multivariée : chaque colonne est prédite à partir des autres, en boucle imp = IterativeImputer(max_iter=10, random_state=0) # conserver l'information "cette valeur manquait" : ajoute une colonne booléenne imp = SimpleImputer(strategy="median", add_indicator=True)
Pipeline (module 3), et rien d'autre.
6. Feature engineering
C'est l'étape au meilleur rendement sur données tabulaires : une bonne variable bat presque toujours un meilleur algorithme. L'idée générale est d'encoder la connaissance métier sous une forme que le modèle peut exploiter — un arbre ne « devinera » jamais qu'il faut diviser le prix par la surface.
Variables catégorielles
One-hot une colonne par modalité. Défaut sûr. Explose si cardinalité élevée.
Ordinal un entier par modalité. Uniquement si un ORDRE existe réellement
(S < M < L), ou pour des arbres, qui savent découper n'importe où.
Target encoding remplace la modalité par la moyenne de la cible. Très puissant sur
forte cardinalité (code postal, produit) — et très exposé à la fuite.
Hashing projette dans un espace de taille fixe. Utile en flux, perd la lisibilité.
Embeddings représentation dense apprise. Pour les réseaux (module 16).Le target encoding mérite un développement, car il est à la fois redoutablement efficace et dangereux. Pour une modalité \(c\) comptant \(n_c\) observations, on ne prend pas la moyenne brute — qui vaudrait 0 ou 1 pour une modalité vue une seule fois — mais une moyenne lissée vers la moyenne globale :
Le paramètre \(m\) contrôle la régularisation : une modalité vue trois fois est tirée vers la moyenne globale, une modalité vue mille fois garde sa propre moyenne. C'est exactement le regroupement partiel des modèles hiérarchiques (module 10), en version simplifiée.
Surtout, l'encodage doit être calculé en validation croisée interne : chaque pli encodé avec les statistiques des autres plis. Sans cela, chaque ligne contient un peu de sa propre cible — fuite garantie, avec un score de validation flatteur et un modèle inutilisable en production. sklearn.preprocessing.TargetEncoder fait cela correctement.
Variables numériques et temporelles
import numpy as np, polars as pl
df = df.with_columns(
# 1. transformations : log pour les queues lourdes (prix, revenus, durées)
log_prix=pl.col("prix").log1p(), # log1p gère la valeur 0
# 2. RATIOS et interactions : souvent l'apport décisif. Les arbres approchent
# très mal un ratio (il leur faut des dizaines de coupes), il faut le fournir.
prix_m2=pl.col("prix") / pl.col("surface"),
revenu_par_personne=pl.col("revenu") / pl.col("taille_foyer"),
intensite=pl.col("n_connexions") / pl.col("anciennete_jours"),
# 3. variables de calendrier
jour_semaine=pl.col("date").dt.weekday(),
mois=pl.col("date").dt.month(),
est_weekend=pl.col("date").dt.weekday() >= 6,
# 4. variables CYCLIQUES : pour que 23 h soit proche de 0 h
heure_sin=(2 * np.pi * pl.col("date").dt.hour() / 24).sin(),
heure_cos=(2 * np.pi * pl.col("date").dt.hour() / 24).cos(),
# 5. agrégats à fenêtre STRICTEMENT passée (le shift(1) est essentiel)
depense_moy_30j=pl.col("montant").shift(1).rolling_mean(30).over("client_id"),
n_achats_7j=pl.col("montant").shift(1).rolling_count(7).over("client_id"),
)Les agrégats à fenêtre sont le cœur du feature engineering sur données transactionnelles, et c'est exactement ce que font les fonctions de fenêtre SQL (module 4) — souvent plus efficacement, directement dans la base. La discrétisation (transformer l'âge en tranches) capture une non-linéarité pour un modèle linéaire, au prix d'une perte d'information ; elle est inutile pour un modèle à base d'arbres, qui la trouve tout seul.
Sélection de variables
Moins de variables, c'est moins de surapprentissage, moins de coût d'inférence et plus de robustesse. Par ordre de fiabilité croissante : filtres univariés (corrélation, information mutuelle — rapides mais aveugles aux interactions) ; importance par permutation sur un modèle ajusté (module 14) ; élimination récursive (RFE) ; et la régularisation \(L^1\), qui sélectionne en annulant des coefficients. Attention : les méthodes fondées sur la cible doivent, elles aussi, être évaluées à l'intérieur de la validation croisée, sinon la sélection elle-même devient une fuite.
7. Classes déséquilibrées
Fraude à 0,2 %, panne à 1 %, maladie rare : la classe qui intéresse est minuscule. Les réflexes, dans l'ordre de ce qui marche le mieux :
- Changer de métrique — l'accuracy n'a aucun sens ici ; PR-AUC, rappel à précision fixée, ou coût métier (module 8).
- Pondérer les classes —
class_weight="balanced"ouscale_pos_weight: le modèle paie plus cher une erreur sur la classe rare. Simple, sans distorsion des données. - Ajuster le seuil — un modèle produit une probabilité ; le seuil de décision est un choix métier qui se règle sur la courbe précision-rappel, jamais laissé à 0,5 par défaut (module 24).
- Rééchantillonner — sous-échantillonner la majorité (perte d'information) ou sur-échantillonner la minorité (risque de surapprentissage). SMOTE crée des exemples synthétiques par interpolation entre voisins : séduisant, mais il interpole aussi le bruit, fonctionne mal en grande dimension et avec des variables catégorielles, et la littérature récente montre qu'il apporte rarement mieux qu'une simple pondération.
from sklearn.metrics import precision_recall_curve
# 1 et 2 : métrique adaptée et pondération
modele = LGBMClassifier(scale_pos_weight=(y_tr == 0).sum() / (y_tr == 1).sum())
modele.fit(X_tr, y_tr, eval_metric="average_precision")
# 3 : choisir le seuil sur la courbe précision-rappel, pas à 0,5
p = modele.predict_proba(X_val)[:, 1]
prec, rap, seuils = precision_recall_curve(y_val, p)
# par exemple : le seuil qui garantit au moins 80 % de précision
i = np.argmax(prec[:-1] >= 0.80)
print(f"seuil = {seuils[i]:.3f} → précision {prec[i]:.1%}, rappel {rap[i]:.1%}")imblearn.pipeline.Pipeline. Et le jeu de test doit conserver la distribution réelle : c'est celle qu'on rencontrera en production.
8. Validation croisée : choisir le bon schéma
Un découpage unique train/test est bruité : le score dépend du tirage, et sur un petit jeu l'écart entre deux tirages peut dépasser l'écart entre deux modèles. La validation croisée à \(K\) plis moyenne \(K\) évaluations, chaque observation servant une fois au test. Son écart type entre plis est aussi informatif que la moyenne — un modèle à 0,82 ± 0,01 est préférable à 0,84 ± 0,09.
KFold (K=5) TimeSeriesSplit pli 1 [test][─── train ───] pli 1 [train][test]············· pli 2 [train][test][ train ] pli 2 [── train ──][test]······· pli 3 [── train ─][test][──] pli 3 [──── train ───][test]···· pli 4 [─── train ──][test][] pli 4 [────── train ────][test]· pli 5 [──── train ────][test] ↑ on n'entraîne JAMAIS sur le futur GroupKFold (groupe = client) client A ██ B ██ C ██ D ██ E ██ un client entier va d'un seul côté : pli 1 test train train train pas de client des deux côtés du découpage
KFold données i.i.d., régression K = 5 ou 10
StratifiedKFold classification : conserve les proportions le défaut en classif
GroupKFold plusieurs lignes par entité (patient, un groupe = un seul pli
client, bien immobilier)
StratifiedGroupKFold les deux contraintes à la fois
TimeSeriesSplit séries temporelles : entraîner sur le jamais de mélange
passé, tester sur le futur
LeaveOneOut très peu de données coûteux, variance élevée
Nested CV on optimise DES hyperparamètres ET on veut boucle interne = réglage
une estimation non biaisée boucle externe = évaluationfrom sklearn.model_selection import GroupKFold, cross_validate
# données transactionnelles : plusieurs lignes par client → GroupKFold
res = cross_validate(pipeline, X, y, groups=df["client_id"],
cv=GroupKFold(n_splits=5),
scoring=["roc_auc", "average_precision"],
return_train_score=True, n_jobs=-1)
# l'écart train/validation mesure directement le surapprentissage
print(f"train {res['train_roc_auc'].mean():.3f} | "
f"val {res['test_roc_auc'].mean():.3f} ± {res['test_roc_auc'].std():.3f}")Le protocole que je considère comme correct
- Mettre de côté un jeu de test dès le début, et ne l'ouvrir qu'une fois, à la fin.
- Travailler en validation croisée sur le reste : exploration, variables, réglages.
- Optimiser les hyperparamètres dans une boucle interne (nested CV) si l'estimation finale doit être non biaisée.
- Réentraîner sur toutes les données d'entraînement avec la configuration retenue.
- Évaluer une seule fois sur le test. Si le résultat déçoit et qu'on repart modifier le modèle, ce jeu de test est brûlé : il est devenu un jeu de validation, et son score n'est plus une estimation honnête.
9. Fuites de données : le catalogue
La fuite (data leakage) est l'erreur la plus coûteuse de la discipline, parce qu'elle est silencieuse : elle produit d'excellents scores en validation et un modèle qui s'effondre en production, quand plus personne ne sait d'où vient le problème.
Prétraitement global scaler, imputeur, PCA, sélection de variables ajustés sur TOUT
le jeu avant le découpage. → tout mettre dans un Pipeline
Fuite temporelle une variable utilise de l'information postérieure à la date
de prédiction : moyenne calculée sur toute la période,
statut final du dossier. → figer une date de référence
Duplication la même observation (ou quasi) des deux côtés du découpage :
doublons, utilisateur présent plusieurs fois. → GroupKFold
Cible déguisée une colonne qui n'existe QUE parce que l'événement a eu lieu :
"date de résiliation", "montant remboursé", "motif de panne".
Target encoding naïf moyenne de cible calculée en incluant la ligne courante.
→ encodage en validation croisée interne
Fuite par le seuil choisir le seuil de décision sur le jeu de test.
Sélection répétée essayer 200 modèles et garder le meilleur score de test :
le test devient un jeu d'entraînement déguisé.Comment la détecter
Un score anormalement élevé — une AUC de 0,99 sur un problème réputé difficile — doit déclencher une suspicion, pas une satisfaction. Trois gestes efficaces : regarder les importances de variables et se demander, pour la première d'entre elles, si elle serait réellement disponible au moment de la prédiction ; rejouer la chaîne complète sur une période postérieure (validation temporelle), où une fuite disparaît et fait s'effondrer le score ; et écrire pour chaque variable, noir sur blanc, à quel instant elle est connue. Mieux vaut découvrir cet effondrement en validation qu'en production.
Ce qu'il faut retenir du module
- Commencer par la décision que le modèle doit éclairer ; définir la cible et une baseline avant toute modélisation.
- Explorer méthodiquement : structure, cible, manquants, doublons, cardinalités, temps.
- Comprendre pourquoi une valeur manque avant de l'imputer ; garder l'indicateur de manque.
- Le feature engineering paie plus que le choix d'algorithme : ratios, agrégats à fenêtre passée, encodages adaptés à la cardinalité.
- Déséquilibre : changer de métrique et pondérer avant de songer à SMOTE ; ne rééchantillonner que le pli d'entraînement.
- Adapter le schéma de validation à la structure des données : groupes, temps, stratification.
- Tout prétraitement vit dans un pipeline. Un score trop beau est une fuite jusqu'à preuve du contraire.