Pourquoi le Feature Engineering reste la compétence ML la plus sous-estimée
Soyons honnêtes : en data science, on passe un temps fou à comparer des modèles et à peaufiner des hyperparamètres. Mais dans la vraie vie, c'est le feature engineering — cette étape de transformation des données brutes en variables exploitables — qui fait véritablement la différence. J'ai vu des projets passer de résultats médiocres à des performances impressionnantes, simplement en retravaillant les features. Sans toucher au modèle.
Bref, c'est la compétence qui a le plus d'impact, et pourtant elle reste souvent reléguée au second plan.
Dans ce guide, on va construire ensemble un pipeline de feature engineering robuste et reproductible, en combinant Pandas 3.0, Scikit-Learn 1.8 et la bibliothèque spécialisée feature_engine 1.9. Chaque technique est accompagnée de code fonctionnel que vous pouvez copier et adapter directement à vos propres projets. Alors, on y va.
Préparer l'environnement de travail
Avant toute chose, installez les bibliothèques nécessaires et vérifiez les versions :
pip install pandas==3.0.1 scikit-learn==1.8 feature-engine==1.9.4 numpy==2.4.0
import pandas as pd
import numpy as np
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer, make_column_selector
from sklearn.preprocessing import StandardScaler, OneHotEncoder, PolynomialFeatures
from sklearn.impute import SimpleImputer
from sklearn.model_selection import cross_val_score
from sklearn.ensemble import RandomForestClassifier
print(f"Pandas: {pd.__version__}")
print(f"NumPy: {np.__version__}")
Pour illustrer chaque technique, on va travailler sur un jeu de données fictif de clients. Le workflow reste identique quel que soit votre dataset tabulaire — c'est tout l'intérêt.
# Créer un dataset d'exemple
data = {
"age": [25, 34, 45, np.nan, 52, 29, 38, 41, 33, 47],
"revenu_annuel": [28000, 45000, 72000, 55000, 91000, 32000, 61000, np.nan, 48000, 83000],
"anciennete_mois": [6, 24, 60, 36, 120, 12, 48, 84, 18, 96],
"categorie_produit": ["A", "B", "A", "C", "B", "A", "C", "B", "A", "C"],
"ville": ["Paris", "Lyon", "Paris", "Marseille", "Lyon", "Paris", "Marseille", "Lyon", "Paris", "Marseille"],
"date_inscription": pd.to_datetime([
"2024-01-15", "2022-03-20", "2020-06-10", "2021-09-05", "2015-11-30",
"2023-07-22", "2020-12-01", "2018-04-18", "2022-10-08", "2016-08-14"
]),
"achats_6m": [2, 5, 12, 7, 20, 3, 9, 15, 4, 18],
"churned": [1, 0, 0, 0, 0, 1, 0, 0, 1, 0]
}
df = pd.DataFrame(data)
print(df.head())
Traitement des valeurs manquantes : la première étape incontournable
Les valeurs manquantes, c'est la réalité de quasiment tous les jeux de données. Avant de se lancer dans des transformations plus élaborées, il faut s'en occuper avec une stratégie adaptée au type de chaque variable.
Imputation numérique avec la médiane
Pour les variables numériques, la médiane est généralement un meilleur choix que la moyenne. Pourquoi ? Parce qu'elle résiste aux valeurs aberrantes — et croyez-moi, en production, il y a toujours des valeurs aberrantes.
from sklearn.impute import SimpleImputer
imputer_num = SimpleImputer(strategy="median")
cols_num = ["age", "revenu_annuel"]
df[cols_num] = imputer_num.fit_transform(df[cols_num])
print(df[cols_num].isnull().sum())
# age 0
# revenu_annuel 0
Créer un indicateur de valeur manquante
C'est une astuce qu'on oublie souvent : parfois, le simple fait qu'une valeur soit absente est en soi une information prédictive. Un client qui n'a pas renseigné son revenu, ça peut vouloir dire quelque chose. feature_engine rend cette création très simple :
from feature_engine.imputation import AddMissingIndicator
indicator = AddMissingIndicator(variables=["age", "revenu_annuel"])
df_with_indicator = indicator.fit_transform(df)
print(df_with_indicator[["age_na", "revenu_annuel_na"]].head())
Encodage des variables catégorielles
Les algorithmes de ML ne comprennent que les chiffres. Il faut donc transformer les variables catégorielles en valeurs numériques, mais attention : mal encoder une variable peut introduire des biais ou du bruit dans le modèle.
One-Hot Encoding avec Scikit-Learn
Le One-Hot Encoding reste le choix le plus classique : il crée une colonne binaire par catégorie. Depuis Scikit-Learn 1.8, pensez à utiliser sparse_output=False pour récupérer directement un array dense (c'est souvent plus pratique pour le debug) :
from sklearn.preprocessing import OneHotEncoder
ohe = OneHotEncoder(sparse_output=False, handle_unknown="ignore")
encoded = ohe.fit_transform(df[["categorie_produit", "ville"]])
feature_names = ohe.get_feature_names_out(["categorie_produit", "ville"])
df_encoded = pd.DataFrame(encoded, columns=feature_names, index=df.index)
print(df_encoded.head())
Target Encoding pour les variables à haute cardinalité
Quand votre variable catégorielle a des dizaines (voire des centaines) de modalités — pensez aux codes postaux, par exemple — le One-Hot Encoding fait littéralement exploser la dimensionnalité. Le Target Encoding est une alternative élégante : il remplace chaque catégorie par la moyenne de la variable cible pour cette catégorie.
from feature_engine.encoding import MeanEncoder
encoder = MeanEncoder(variables=["ville"])
encoder.fit(df, df["churned"])
df_target_enc = encoder.transform(df)
print(df_target_enc[["ville"]].head())
# ville contient maintenant des valeurs numériques
Attention importante : le Target Encoding doit impérativement être appliqué à l'intérieur d'une validation croisée. Sinon, vous risquez la fuite de données (data leakage), et vos scores en entraînement seront trompeusement bons. Intégrez-le toujours dans un Pipeline.
Création de features temporelles
Une date brute, en tant que telle, n'apporte pas grand-chose à un modèle. Mais une fois qu'on en extrait des composantes (mois, jour de la semaine, ancienneté…), on révèle des patterns saisonniers et des tendances qui peuvent être très prédictifs.
# Extraction manuelle avec Pandas 3.0
df["mois_inscription"] = df["date_inscription"].dt.month
df["jour_semaine"] = df["date_inscription"].dt.dayofweek
df["annee_inscription"] = df["date_inscription"].dt.year
# Ancienneté en jours depuis aujourd'hui
df["anciennete_jours"] = (pd.Timestamp.now() - df["date_inscription"]).dt.days
print(df[["date_inscription", "mois_inscription", "jour_semaine", "anciennete_jours"]].head())
Avec feature_engine pour un pipeline reproductible
Si vous voulez que cette extraction soit automatiquement reproductible dans un pipeline (et franchement, vous devriez), feature_engine propose un transformateur dédié :
from feature_engine.datetime import DatetimeFeatures
dt_transformer = DatetimeFeatures(
variables=["date_inscription"],
features_to_extract=["month", "day_of_week", "year"],
drop_original=True
)
df_dt = dt_transformer.fit_transform(df)
print(df_dt.head())
Features d'interaction et polynomiales
Souvent, ce sont les relations entre variables qui racontent l'histoire la plus intéressante. L'âge seul peut être utile, le revenu seul aussi — mais le ratio entre les deux ? C'est parfois là que la magie opère.
Interactions manuelles
# Ratio revenu par année d'ancienneté
df["revenu_par_anciennete"] = df["revenu_annuel"] / (df["anciennete_mois"] / 12 + 1)
# Intensité d'achat : achats par mois d'ancienneté
df["achats_par_mois"] = df["achats_6m"] / 6
# Interaction croisée
df["age_x_revenu"] = df["age"] * df["revenu_annuel"]
print(df[["revenu_par_anciennete", "achats_par_mois", "age_x_revenu"]].head())
Features polynomiales avec Scikit-Learn
PolynomialFeatures génère automatiquement tous les termes polynomiaux et d'interaction jusqu'au degré que vous spécifiez. C'est puissant, mais ça peut vite partir en vrille niveau dimensionnalité :
from sklearn.preprocessing import PolynomialFeatures
poly = PolynomialFeatures(degree=2, interaction_only=True, include_bias=False)
cols_interaction = ["age", "revenu_annuel", "achats_6m"]
poly_features = poly.fit_transform(df[cols_interaction])
poly_names = poly.get_feature_names_out(cols_interaction)
print(f"Nombre de features générées : {len(poly_names)}")
print(f"Noms : {poly_names}")
Conseil pratique : utilisez interaction_only=True pour limiter les dégâts — ça ne garde que les produits croisés, sans les termes au carré. Votre modèle vous remerciera.
Discrétisation (Binning) des variables numériques
Transformer une variable continue en catégories peut sembler contre-intuitif, mais ça aide certains modèles à capturer des relations non linéaires. Par exemple, la relation entre l'âge et le churn n'est pas forcément linéaire — il peut y avoir des seuils.
# Avec Pandas
df["tranche_age"] = pd.cut(
df["age"],
bins=[0, 30, 40, 50, 100],
labels=["junior", "confirmé", "senior", "expert"]
)
# Avec feature_engine pour un binning basé sur les quantiles
from feature_engine.discretisation import EqualFrequencyDiscretiser
discretiser = EqualFrequencyDiscretiser(q=4, variables=["revenu_annuel"])
df_disc = discretiser.fit_transform(df)
print(df_disc["revenu_annuel"].value_counts())
Construire un Pipeline complet avec ColumnTransformer
Bon, c'est là que tout prend son sens. Le vrai pouvoir du feature engineering, ce n'est pas telle ou telle technique isolée — c'est l'assemblage de toutes ces transformations en un pipeline unique, reproductible et sans fuite de données. Et pour ça, le ColumnTransformer de Scikit-Learn est votre meilleur allié.
Architecture du pipeline
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer, make_column_selector
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.impute import SimpleImputer
from sklearn.ensemble import GradientBoostingClassifier
# Sous-pipeline pour les colonnes numériques
numeric_pipeline = Pipeline(steps=[
("imputer", SimpleImputer(strategy="median")),
("scaler", StandardScaler())
])
# Sous-pipeline pour les colonnes catégorielles
categorical_pipeline = Pipeline(steps=[
("imputer", SimpleImputer(strategy="most_frequent")),
("encoder", OneHotEncoder(handle_unknown="ignore", sparse_output=False))
])
# Assemblage avec ColumnTransformer
preprocessor = ColumnTransformer(
transformers=[
("num", numeric_pipeline, ["age", "revenu_annuel", "anciennete_mois", "achats_6m"]),
("cat", categorical_pipeline, ["categorie_produit", "ville"])
],
remainder="drop"
)
# Pipeline complet : prétraitement + modèle
full_pipeline = Pipeline(steps=[
("preprocessor", preprocessor),
("classifier", GradientBoostingClassifier(n_estimators=100, random_state=42))
])
print(full_pipeline)
Entraînement et validation croisée
X = df[["age", "revenu_annuel", "anciennete_mois", "achats_6m", "categorie_produit", "ville"]]
y = df["churned"]
scores = cross_val_score(full_pipeline, X, y, cv=3, scoring="accuracy")
print(f"Accuracy moyenne : {scores.mean():.2f} (+/- {scores.std():.2f})")
L'avantage majeur ici : toutes les transformations sont apprises uniquement sur les données d'entraînement de chaque fold. Résultat ? Zéro fuite de données. C'est non négociable en production.
Détection automatique des types de colonnes
Avec make_column_selector, vous n'avez plus besoin de lister manuellement les colonnes — un vrai gain de temps quand votre dataset évolue :
preprocessor_auto = ColumnTransformer(
transformers=[
("num", numeric_pipeline, make_column_selector(dtype_include="number")),
("cat", categorical_pipeline, make_column_selector(dtype_include="object"))
],
remainder="drop",
verbose_feature_names_out=True
)
# Vérifier les noms des features générées
preprocessor_auto.fit(X)
print(preprocessor_auto.get_feature_names_out())
Optimisation des hyperparamètres à travers le pipeline
Et voici un truc que beaucoup de gens ignorent : avec un Pipeline Scikit-Learn, vous pouvez optimiser simultanément les paramètres de prétraitement et ceux du modèle. En une seule recherche.
from sklearn.model_selection import GridSearchCV
param_grid = {
"preprocessor__num__imputer__strategy": ["mean", "median"],
"classifier__n_estimators": [50, 100, 200],
"classifier__max_depth": [3, 5, 7]
}
grid_search = GridSearchCV(
full_pipeline,
param_grid,
cv=3,
scoring="f1",
n_jobs=-1
)
grid_search.fit(X, y)
print(f"Meilleurs paramètres : {grid_search.best_params_}")
print(f"Meilleur score F1 : {grid_search.best_score_:.2f}")
La notation avec double underscore (__) permet d'accéder aux paramètres de n'importe quel composant imbriqué. C'est un peu déroutant au début, mais une fois qu'on a compris la logique, c'est extrêmement puissant.
Feature Engineering avancé avec feature_engine
La bibliothèque feature_engine (version 1.9.4) étend Scikit-Learn avec des transformateurs spécialisés qui travaillent directement sur les DataFrames Pandas. Le gros avantage ? Elle préserve les noms de colonnes, ce qui rend le debug nettement plus agréable.
Transformation mathématique des variables
from feature_engine.transformation import LogTransformer, YeoJohnsonTransformer
# Log-transformation pour les distributions asymétriques
log_tf = LogTransformer(variables=["revenu_annuel"])
# Yeo-Johnson gère aussi les valeurs négatives et zéro
yj_tf = YeoJohnsonTransformer(variables=["revenu_annuel", "anciennete_mois"])
df_transformed = yj_tf.fit_transform(df)
print(df_transformed[["revenu_annuel", "anciennete_mois"]].describe())
Création automatique de features temporelles
from feature_engine.datetime import DatetimeFeatures
dt_features = DatetimeFeatures(
variables=["date_inscription"],
features_to_extract=["month", "day_of_week", "year", "quarter"],
drop_original=True
)
# S'intègre directement dans un Pipeline Scikit-Learn
pipeline_with_dates = Pipeline(steps=[
("dates", dt_features),
("preprocessor", preprocessor),
("classifier", GradientBoostingClassifier(random_state=42))
])
Sélection de features par importance
from feature_engine.selection import SelectByShuffling
selector = SelectByShuffling(
estimator=GradientBoostingClassifier(random_state=42),
scoring="f1",
cv=3,
random_state=42
)
# Identifie et supprime les features qui ne contribuent pas
# selector.fit(X_transformed, y)
# selected = selector.transform(X_transformed)
# print(f"Features conservées : {selector.get_feature_names_out()}")
Bonnes pratiques pour un feature engineering efficace en 2026
Après pas mal de projets (et quelques erreurs douloureuses), voici les règles que je m'impose systématiquement :
- Toujours utiliser un Pipeline : c'est la base. Encapsulez chaque transformation dans un Pipeline Scikit-Learn. Ça garantit la reproductibilité et ça élimine le risque de data leakage. Pas de négociation possible là-dessus.
- Commencer simple : avant de créer des features complexes, vérifiez la performance avec les features brutes. Si votre baseline est déjà correcte, chaque ajout doit prouver sa valeur par une amélioration mesurable.
- Exploiter le domaine métier : les meilleures features viennent rarement d'une recherche automatique. Parlez aux experts métier — ils savent souvent quelles variables comptent vraiment.
- Surveiller la dimensionnalité : le One-Hot Encoding et les features polynomiales peuvent faire exploser le nombre de colonnes. La sélection de features n'est pas optionnelle, c'est une étape nécessaire.
- Tester en validation croisée : une feature qui booste les scores sur le train set mais pas en cross-validation, c'est du surapprentissage déguisé. Supprimez-la sans hésiter.
- Activer
set_output(transform="pandas"): depuis Scikit-Learn 1.4, cette option conserve les noms de colonnes dans tout le pipeline. Honnêtement, ça change la vie pour le debug.
# Activer la sortie Pandas pour tout le pipeline
from sklearn import set_config
set_config(transform_output="pandas")
# Maintenant chaque transform() retourne un DataFrame avec les noms de colonnes
preprocessor.fit(X)
X_transformed = preprocessor.transform(X)
print(type(X_transformed)) #
print(X_transformed.columns.tolist())
FAQ
Quelle est la différence entre feature engineering et feature selection ?
Le feature engineering, c'est créer de nouvelles variables à partir de vos données brutes — encodage, interactions, transformations. La feature selection, c'est choisir parmi toutes ces features celles qui contribuent réellement à la performance du modèle. En pratique, les deux vont toujours ensemble : on crée d'abord, puis on élague.
Comment éviter la fuite de données (data leakage) lors du feature engineering ?
La règle d'or est simple : ne jamais apprendre les paramètres de transformation sur les données de test. Concrètement, utilisez un Pipeline Scikit-Learn qui applique fit_transform() uniquement sur le train set et transform() sur le test set. C'est particulièrement critique pour le Target Encoding et la normalisation — si vous ne le faites pas, vos métriques seront artificiellement gonflées.
Le feature engineering est-il encore nécessaire avec le deep learning ?
Oui, mais pas de la même façon. Les réseaux de neurones profonds apprennent certaines représentations automatiquement, c'est vrai. Mais pour les données tabulaires (et c'est encore la majorité des cas en entreprise), un bon gradient boosting — XGBoost, LightGBM — combiné à un feature engineering soigné surpasse régulièrement le deep learning. C'est encore largement vrai en 2026.
Quand utiliser feature_engine plutôt que Scikit-Learn seul ?
Dès que vous travaillez avec des DataFrames Pandas et que la lisibilité du code vous importe (et elle devrait). feature_engine offre aussi des transformateurs qui n'existent tout simplement pas dans Scikit-Learn natif : MeanEncoder, DatetimeFeatures, discrétiseurs par quantile… Et puisqu'il s'intègre parfaitement dans les Pipelines Scikit-Learn, il n'y a aucun compromis à faire.
Comment savoir si mes features améliorent réellement le modèle ?
Utilisez la validation croisée pour comparer les scores avant et après chaque ajout de feature. Complétez avec l'importance des features (feature_importances_ pour les modèles arborescents) ou la méthode de permutation (permutation_importance). Si une feature n'apporte pas d'amélioration significative en cross-validation, elle n'a rien à faire dans votre modèle.