Les pipelines de scikit-learn
Dans la plupart des projets de Machine Learning, le jeu de données utilisé pour calibrer le modèle doit subir toute une série de transformations. Encodage de variables catégorielles, de la normalisation, du feature scaling et autres techniques spécifiques. Cependant, cette série de transformations doit être appliquée plus d'une fois.

Dans la plupart des projets de Machine Learning, le jeu de données utilisé pour calibrer le modèle doit subir toute une série de transformations. Encodage de variables catégorielles, de la normalisation, du feature scaling et autres techniques spécifiques.
Cependant, cette série de transformations doit être appliquée plus d'une fois. D'une part, au moment où il faut entraîner le modèle, et d'autre part lorsqu'il faut obtenir une prédiction pour de nouvelles données. Comment ne pas mélanger les différentes étapes et garantir un traitement consistant entre ces deux étapes ? C'est tout l'intérêt des pipelines de scikit-learn. Ces pipelines vont résoudre un certain nombre de problèmes.
- Toutes les opérations de transformations vont être centralisées dans le pipeline.
- Il sera plus facile d'intégrer ce pipelines à plusieurs endroits dans le code source.
- Il est modulable et peut ponctuellement être modifié.
- La gestion des versions du pipeline sera plus simple.
Pour illustrer les pipelines de scikit-learn, nous allons utiliser le jeu de données suivant.
import pandas as pd
dataset = pd.read_csv(
"https://dv495y1g0kef5.cloudfront.net/single_notebooks/data/car_acc.csv",
header=0,
names=["buying", "maint", "doors", "persons", "lug_boot", "safety", "eval"]
)
dataset.head()
Chaque ligne représente une voiture. Les variables explicatives sont :
buying(qualitative) : un prix d’achat (bas, moyen, haut et très haut).maint(qualitative) : entretien nécessaire pour le fonctionnement (bas, moyen, haut et très haut).doors(quantitative) : le nombre de portes.persons(quantitative) : le nombre de personnes.lug_boot(qualitative) : la taille du coffre (petit, moyen et grand).safety(qualitative) : le niveau de sécurité (faible, modéré et fort).
La variable que l’on souhaite modéliser (i.e. la variable réponse) est
nommée eval et représente le niveau de satisfaction de la voiture (unacc pour non satisfait ou acc pour satisfait).
Les Transformers
Un Transformer est un objet scikit-learn qui permet d'appliquer une transformation (encodage, normalisation, ...) sur un DataFrame. L'intérêt du Transformer est double.
- Pouvoir définir une méthode de transformation sur-mesure.
- Pouvoir appliquer cette méthode sur un DataFrame quelconque.
À lire aussi : découvrez notre formation MLOps
Nous allons commencer par encoder les colonnes lug_boot et safety en one-hot avec le OneHotEncoder.
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder
one_hot_encoder = Pipeline(
steps=[
('one_hot', OneHotEncoder(handle_unknown='ignore'))
]
)
ℹ️ Pas besoin de définir pour l'instant les colonnes qui vont subir cette transformation : cela sera effectué lorsque l'on assemblera les Transformers.
Nous pouvons également construire un Transformer sur mesure avec FunctionTransformer. En particulier, nous allons construire un Transformer pour certaines variables explicatives pour lesquelles nous considérons qu'il n'est pas nécessaire d'utiliser un OneHotEncoder. Les colonnes buying et maint prendront comme valeur 0, 1, 2 ou 3 (correspondant à low, med, high et vhigh).
from sklearn.preprocessing import FunctionTransformer
encoding = { "low": 0, "med": 1, "high": 2, "vhigh": 3 }
def grad_encoder(df):
for col in df.columns:
df[col] = df[col].apply(lambda x: encoding[x])
return df
eval_encoder = Pipeline(
steps=[
('grad', FunctionTransformer(grad_encoder))
]
)
Enfin, il y a quelques cas particuliers pour les colonnes doors et persons : la valeur 5more est présente dans la colonne doors s'il y a 5 portes ou plus, et de même dans la colonne persons lorsque le véhivule peut accueillir 6 passagers ou plus.
Dans ce cas, puisque nous souhaitons uniquement des valeurs numériques dans ces colonnes, nous allons construire un dernier Transformer pour encoder ces variables.
def num_encoder(df):
for col in df.columns:
df[col] = df[col].apply(lambda x: 5 if x == "5more" else x)
df[col] = df[col].apply(lambda x: 6 if x == "more" else x)
return df
num_encoder = Pipeline(
steps=[
('num', FunctionTransformer(num_encoder))
]
)
Une fois terminé, il ne reste plus qu'à combiner tous ces Transformers dans un ColumnTransformer : cela va permettre d'appliquer chaque Transformer sur un ensemble de colonnes sur-mesure. Plutôt que d'écrire un Transformer par colonne, cela permet de gagner du temps en appliquant la même méthode d'encodage sur plusieurs colonnes.
from sklearn.compose import ColumnTransformer
preprocessor = ColumnTransformer(
transformers=[
('categorical', one_hot_encoder, ['lug_boot', 'safety']),
('grad', eval_encoder, ['buying', 'maint']),
('num', num_encoder, ['doors', 'persons']),
]
)
Par ailleurs, la documentation de scikit-learn présente une liste exhaustive des Transformers disponibles par défaut.
Le Classifieur
Une fois que le pipeline de preprocessing est en place, nous pouvons ajouter la couche prédictive, qui ici est un Random Forest.
À lire aussi : découvrez notre formation MLOps
from sklearn.ensemble import RandomForestClassifier
rf = Pipeline(
steps=[
('preprocessor', preprocessor),
('classifier', RandomForestClassifier())
]
)
La particularité, c'est que la variable rf n'est pas uniquement le modèle, mais renferme également les étapes de preprocessing que nous appliquons aux données.
Ainsi, qu'il s'agisse d'un fit ou d'un predict, les données subiront les étapes du preprocessing au préalable.
Comme toujours, nous séparons le jeu de données en un ensemble d'entraînement et un ensemble de test.
from sklearn.model_selection import train_test_split
X = dataset.drop('eval', axis=1)
y = dataset['eval'].apply(lambda x: 0 if x == "unacc" else 1)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=24)
Puis nous calibrons directement le pipeline contenant le preprocessing et le modèle.
rf.fit(X_train, y_train)
Si maintenant, on souhaite calculer un score, même principe, la fonction predict appliquera toutes les transformations au DataFrame renseigné.
from sklearn.metrics import f1_score
y_pred = rf.predict(X_test)
print("Score : {:2.1f}%".format(f1_score(y_test, y_pred) * 100))
Modularité des pipelines
L'intérêt des pipelines, en plus de la reproductibilité, est la modularité offerte par la combinaison linéaire des étapes. Par exemple, plutôt que d'avoir un seul modèle, nous souhaitons tester plusieurs modèles : il suffit juste de modifier la couche de classification, sans modifier le preprocessing.
Par exemple, ici, nous allons calibrer trois modèles : un Random Forest, un Gradient Boosting et un modèle binomial. Tous les trois auront le même jeu de données en entrée, ce qui permettra de les comparer plsu efficacement.
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.linear_model import LogisticRegression
models_names = ["Random Forest", "Gradient Boosting", "Logistic Regression"]
# Contient les trois pipelines (une pour chaque modèle)
pipelines = [
Pipeline(steps=[('preprocessor', preprocessor), ('classifier', RandomForestClassifier())]),
Pipeline(steps=[('preprocessor', preprocessor), ('classifier', GradientBoostingClassifier())]),
Pipeline(steps=[('preprocessor', preprocessor), ('classifier', LogisticRegression())])
]
Nous pouvons ensuite appeler chaque pipeline.
for p, name in zip(pipelines, models_names):
p.fit(X_train, y_train)
y_pred = p.predict(X_test)
print("Score ({}) : {:2.1f}%".format(
name,
f1_score(y_test, y_pred) * 100
))
Nous vons directement ici que les deux premiers modèles sont bien meilleurs que le dernier. On pourrait ainsi imaginer la même technique si l'on effectuait une recherche par grille pour optimiser les hyper-paramètres.


