Module 10 — Chaînes de traitement reproductibles avec scikit-learn
Neuf modules de transformations. Appliquées à la main, dans un carnet, elles constituent une bombe à retardement : un ordre d'exécution qu'on oublie, une cellule relancée deux fois, une imputation ajustée sur l'ensemble des données. Ce module transforme tout ce qui précède en un objet unique, reproductible, et structurellement immunisé contre la fuite.
Le problème du prétraitement fait à la main
Trois défauts, qui se manifestent tous au pire moment.
D'abord, la fuite : fit_transform sur l'ensemble du jeu de données avant le découpage est l'erreur signalée à chaque module de ce cours. Ensuite, l'écart entre entraînement et production : les mêmes transformations, dans le même ordre, avec les mêmes paramètres appris, doivent être rejouées sur chaque nouvelle donnée — reproduire cela à la main dans un autre programme garantit des divergences. Enfin, l'irreproductibilité : un carnet dont les cellules ont été exécutées dans un ordre non linéaire n'est plus rejouable, même par son auteur.
Le Pipeline : une seule chose à ajuster, une seule à appliquer
Un Pipeline chaîne des transformations et un modèle final en un objet qui expose la même interface qu'un modèle simple.
from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
pipe = Pipeline([
("imputation", SimpleImputer(strategy="median")),
("echelle", StandardScaler()),
("modele", LogisticRegression()),
])
pipe.fit(X_train, y_train) # ajuste chaque etape sur l'entrainement seul
pipe.predict(X_test) # rejoue exactement les memes transformations
Le mécanisme est ce qui rend la fuite impossible. pipe.fit appelle fit_transform sur chaque étape avec les seules données d'entraînement, puis pipe.predict appelle transform — jamais fit — sur les nouvelles données. La médiane d'imputation et la moyenne de standardisation proviennent nécessairement de l'entraînement.
Le bénéfice devient décisif en validation croisée : le prétraitement est réajusté à l'intérieur de chaque pli, sur les données d'entraînement de ce pli uniquement. C'est la parade structurelle annoncée au cours 04 et rappelée à chaque module de celui-ci.
from sklearn.model_selection import cross_val_score, GridSearchCV
cross_val_score(pipe, X_train, y_train, cv=5) # aucune fuite possible
GridSearchCV(pipe, {
"imputation__strategy": ["median", "mean"],
"modele__C": [0.1, 1, 10],
}, cv=5) # le pretraitement lui-meme devient un hyperparametre
Cette dernière possibilité mérite d'être remarquée : la stratégie d'imputation ou le type de mise à l'échelle se règlent comme n'importe quel hyperparamètre, avec la syntaxe nom_etape__parametre. Les choix des modules 2 et 3 cessent d'être des intuitions pour devenir des décisions mesurées.
ColumnTransformer : des traitements différents par type de colonne
Un jeu de données réel est hétérogène : les colonnes numériques demandent imputation et mise à l'échelle, les catégorielles un encodage, le texte une vectorisation. ColumnTransformer applique un traitement distinct à chaque groupe et recolle le résultat.
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import OneHotEncoder
numeriques = ["age", "revenu", "anciennete"]
categorielles = ["ville", "type_contrat"]
pre = ColumnTransformer([
("num", Pipeline([
("imputation", SimpleImputer(strategy="median")),
("echelle", StandardScaler()),
]), numeriques),
("cat", Pipeline([
("imputation", SimpleImputer(strategy="constant", fill_value="Inconnu")),
("encodage", OneHotEncoder(handle_unknown="ignore")),
]), categorielles),
], remainder="drop")
pipe = Pipeline([("pre", pre), ("modele", LogisticRegression())])
Deux points de vigilance. remainder="drop" écarte explicitement les colonnes non listées ; c'est le comportement prudent, qui évite qu'un identifiant se glisse dans le modèle. Et handle_unknown="ignore" reste indispensable, une catégorie inconnue en production étant la règle plutôt que l'exception.
Industrialiser une transformation personnalisée
Les variables construites aux modules 5 et 7 — cycles, écarts au comportement habituel — n'existent pas dans scikit-learn. Pour qu'elles entrent dans la chaîne, il faut les emballer.
Le plus simple, quand la transformation n'apprend rien des données :
from sklearn.preprocessing import FunctionTransformer
import numpy as np
def encodage_cyclique(X):
return np.column_stack([np.sin(2 * np.pi * X / 12), np.cos(2 * np.pi * X / 12)])
FunctionTransformer(encodage_cyclique)
Quand la transformation doit apprendre quelque chose sur l'entraînement (des moyennes par client, un vocabulaire), il faut écrire une classe avec fit et transform en héritant de BaseEstimator et TransformerMixin. C'est le seul moyen de garantir que les statistiques apprises viennent de l'entraînement et sont rejouées à l'identique en production.
Sauvegarder et déployer
La chaîne complète se sérialise en un fichier, prétraitement et modèle ensemble :
import joblib
joblib.dump(pipe, "modele.joblib")
pipe = joblib.load("modele.joblib")
pipe.predict(nouvelles_donnees) # brutes, non pretraitees
C'est l'aboutissement du cours : le service de prédiction reçoit des données brutes et appelle predict. Aucune transformation à reproduire, donc aucune divergence possible entre entraînement et production.
Un Pipeline traite les lignes indépendamment. Les variables du module 7 qui exigent un historique ou un tri temporel — groupby, shift, rolling — ne s'y intègrent pas naturellement : elles se calculent en amont, dans une étape de préparation des données versionnée et testée, ou dans un magasin de variables (sujet du cours 33). Par ailleurs, la version des bibliothèques compte : un objet sérialisé avec une version de scikit-learn ne se recharge pas nécessairement avec une autre. Il faut figer les versions et conserver un fichier de dépendances aux côtés du modèle.
En résumé
- Le prétraitement manuel expose à la fuite, à l'écart entraînement-production et à l'irreproductibilité.
- Un
Pipelinen'ajuste qu'avec les données d'entraînement et rejoue les transformations à l'identique ; en validation croisée, le prétraitement est réajusté dans chaque pli. ColumnTransformerapplique des traitements distincts par type de colonne ;remainder="drop"ethandle_unknown="ignore"protègent la production.- Les transformations maison s'intègrent via
FunctionTransformerou une classefit/transform; la chaîne se sérialise entière, et les versions de bibliothèques doivent être figées.
Dernière étape : la récapitulation et l'examen de 40 questions qui valide l'ensemble du parcours.