Le feature engineering est souvent plus déterminant que le choix de l’algorithme. Un bon feature engineering peut transformer un modèle médiocre en un excellent modèle ; un mauvais peut rendre inutile le meilleur des algorithmes.
Pipeline de prétraitement§
graph LR
raw[Données brutes] --> audit[Audit qualité]
audit --> clean[Nettoyage]
clean --> enc[Encodage]
enc --> scale[Mise à l'échelle]
scale --> feat[Feature Engineering]
feat --> sel[Sélection]
sel --> model[Modèle]
Audit et compréhension des données§
Avant tout traitement :
import pandas as pd
df = pd.read_csv("data.csv")
df.info() # Types, valeurs non-nulles
df.describe() # Statistiques descriptives
df.isnull().sum() # Valeurs manquantes par colonne
df.duplicated().sum() # Doublons
Questions clés :
- Quelle est la distribution de la variable cible ?
- Y a-t-il un déséquilibre de classes ?
- Quelles features ont des corrélations élevées entre elles ?
- Y a-t-il des outliers ?
Gestion des valeurs manquantes§
Stratégies§
| Méthode | Description | Quand l’utiliser |
|---|---|---|
| Suppression lignes | Supprimer si < 5% manquant | Peu de données manquantes |
| Suppression colonnes | Supprimer si > 50-70% manquant | Colonne quasi-vide |
| Imputation médiane | Remplacer par la médiane | Numériques avec outliers |
| Imputation moyenne | Remplacer par la moyenne | Distribution symétrique |
| Imputation mode | Remplacer par la valeur la plus fréquente | Catégorielles |
| Imputation par modèle | KNN, régression, MICE | Données MNAR |
| Indicateur de manquance | Ajouter colonne booléenne | Si le fait d’être absent est informatif |
from sklearn.impute import SimpleImputer, KNNImputer
# Imputation médiane
imputer = SimpleImputer(strategy='median')
X_filled = imputer.fit_transform(X)
# KNN imputation
knn_imputer = KNNImputer(n_neighbors=5)
X_knn = knn_imputer.fit_transform(X)
Types de manquance :
- MCAR (Missing Completely At Random) : manquance aléatoire, imputation simple possible
- MAR (Missing At Random) : conditionnel à d’autres variables, modélisation nécessaire
- MNAR (Missing Not At Random) : le fait d’être manquant est lié à la valeur elle-même (ex: revenus élevés non déclarés)
Gestion des outliers§
Détection§
# Méthode IQR (Interquartile Range)
Q1 = df['feature'].quantile(0.25)
Q3 = df['feature'].quantile(0.75)
IQR = Q3 - Q1
lower = Q1 - 1.5 * IQR
upper = Q3 + 1.5 * IQR
outliers = df[(df['feature'] < lower) | (df['feature'] > upper)]
# Z-score (>3 ou <-3 = outlier)
from scipy import stats
z_scores = stats.zscore(df['feature'])
outliers_z = df[abs(z_scores) > 3]
Traitement§
| Option | Description |
|---|---|
| Suppression | Supprimer si erreur de saisie évidente |
| Winsorisation | Plafonner à percentile 1% / 99% |
| Transformation log | Compresser les distributions longues |
| Traitement séparé | Modèle différent pour les valeurs extrêmes |
| Conservation | Si l’outlier est réel et informatif |
Encodage des variables catégorielles§
One-Hot Encoding§
Crée une colonne binaire par catégorie. Pour K catégories → K-1 colonnes (éviter la multicolinéarité).
pd.get_dummies(df['couleur'], drop_first=True)
# ou
from sklearn.preprocessing import OneHotEncoder
encoder = OneHotEncoder(drop='first', sparse=False)
Usage : variables nominales (pas d’ordre). Problème : explosion dimensionnelle si beaucoup de catégories.
Label Encoding§
Assigne un entier à chaque catégorie : rouge=0, vert=1, bleu=2.
Usage : variables ordinales (taille: S < M < L < XL) ou arbres de décision (insensibles à l’ordre arbitraire). Attention : induit un ordre artificiel, incompatible avec la régression logistique.
Target Encoding§
Remplace chaque catégorie par la moyenne de la variable cible pour cette catégorie.
# Moyenne de y pour chaque catégorie
target_mean = df.groupby('categorie')['target'].mean()
df['categorie_encoded'] = df['categorie'].map(target_mean)
Usage : haute cardinalité (pays, code postal, ID). Risque : data leakage si mal implémenté → utiliser avec K-Fold.
Frequency Encoding§
Remplace par la fréquence d’apparition. Préserve l’information de rareté.
Mise à l’échelle (Scaling)§
Indispensable pour les algorithmes sensibles à l’échelle : régression, SVM, KNN, réseaux de neurones. Non nécessaire pour les arbres de décision et forêts aléatoires.
graph TD
dist{Distribution ?}
dist -->|Gaussienne| std[Standardisation\nz = (x - μ) / σ]
dist -->|Non-gaussienne| minmax[Min-Max\nx' = (x - min)/(max - min)]
dist -->|Beaucoup d'outliers| robust[Robust Scaler\n(IQR)]
dist -->|Loi de puissance| log[Transformation log]
from sklearn.preprocessing import StandardScaler, MinMaxScaler, RobustScaler
# Standardisation : moyenne 0, écart-type 1
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X_train)
# Min-Max : [0, 1]
minmax = MinMaxScaler()
X_minmax = minmax.fit_transform(X_train)
# Robust : utilise médiane et IQR
robust = RobustScaler()
X_robust = robust.fit_transform(X_train)
Règle critique : fit uniquement sur le train set, transform sur train et test. Sinon data leakage.
Feature Engineering§
Variables numériques§
Transformations de distribution :
import numpy as np
df['log_revenu'] = np.log1p(df['revenu']) # log(1+x), gère x=0
df['sqrt_age'] = np.sqrt(df['age'])
df['revenu_carré'] = df['revenu'] ** 2
Interactions :
df['surface_par_piece'] = df['surface'] / df['nb_pieces']
df['age_x_revenu'] = df['age'] * df['revenu']
Discrétisation (Binning) :
df['age_bin'] = pd.cut(df['age'], bins=[0, 18, 35, 60, 100],
labels=['mineur', 'jeune', 'adulte', 'senior'])
Variables temporelles§
df['heure'] = df['timestamp'].dt.hour
df['jour_semaine'] = df['timestamp'].dt.dayofweek
df['mois'] = df['timestamp'].dt.month
df['est_weekend'] = df['jour_semaine'].isin([5, 6]).astype(int)
df['trimestre'] = df['timestamp'].dt.quarter
# Encodage cyclique (heure 23 est proche de heure 0)
import numpy as np
df['heure_sin'] = np.sin(2 * np.pi * df['heure'] / 24)
df['heure_cos'] = np.cos(2 * np.pi * df['heure'] / 24)
Variables textuelles§
# Longueur, complexité
df['nb_mots'] = df['texte'].apply(lambda x: len(x.split()))
df['nb_caracteres'] = df['texte'].str.len()
df['ratio_majuscules'] = df['texte'].apply(lambda x: sum(c.isupper() for c in x) / len(x))
df['nb_ponctuation'] = df['texte'].apply(lambda x: sum(c in '.,!?' for c in x))
Sélection de features§
Trop de features → surajustement, coût computationnel, bruit.
Méthodes de filtre (Filter Methods)§
Calculées indépendamment du modèle, rapides.
# Variance faible → feature peu informative
from sklearn.feature_selection import VarianceThreshold
sel = VarianceThreshold(threshold=0.01)
# Corrélation avec la cible
correlations = df.corr()['target'].abs().sort_values(ascending=False)
# Test χ² pour catégorielles
from sklearn.feature_selection import chi2, SelectKBest
selector = SelectKBest(chi2, k=10)
Méthodes wrapper (Wrapper Methods)§
Utilisent le modèle comme oracle, plus précises mais coûteuses.
from sklearn.feature_selection import RFE
from sklearn.ensemble import RandomForestClassifier
rfe = RFE(estimator=RandomForestClassifier(), n_features_to_select=10)
rfe.fit(X_train, y_train)
selected = X.columns[rfe.support_]
Méthodes embedded§
Sélection intégrée à l’entraînement :
# Importance des features (RandomForest, XGBoost)
importances = model.feature_importances_
top_features = pd.Series(importances, index=X.columns).sort_values(ascending=False).head(20)
# Lasso (L1) : annule les coefficients des features inutiles
from sklearn.linear_model import LassoCV
lasso = LassoCV(cv=5)
lasso.fit(X_train, y_train)
selected = X.columns[lasso.coef_ != 0]
Gestion du déséquilibre de classes§
Problème courant en détection de fraude, maladies rares, etc.
graph TD
imbclass{Déséquilibre ?}
imbclass -->|Léger < 1:10| weights[Pondération\nclass_weight='balanced']
imbclass -->|Modéré| smote[Oversampling\nSMOTE]
imbclass -->|Fort > 1:100| combine[SMOTE + Tomek Links\nou threshold tuning]
from imblearn.over_sampling import SMOTE
from imblearn.combine import SMOTETomek
# SMOTE : génère des exemples synthétiques de la classe minoritaire
smote = SMOTE(sampling_strategy=0.5, random_state=42)
X_resampled, y_resampled = smote.fit_resample(X_train, y_train)
# Pondération dans le modèle
from sklearn.ensemble import RandomForestClassifier
model = RandomForestClassifier(class_weight='balanced')
Attention : appliquer le resampling uniquement sur le train set, jamais sur le test set.
Erreurs courantes (Data Leakage)§
Le data leakage se produit lorsque des informations du futur contaminent l’entraînement.
| Type | Exemple | Solution |
|---|---|---|
| Leakage de target | Utiliser une feature calculée à partir de la cible | Vérifier les corrélations suspectes > 0.9 |
| Leakage temporel | Entraîner sur des données futures par rapport au test | Toujours faire un split temporel chronologique |
| Leakage de preprocessing | Scaler sur train + test avant de splitter | Toujours splitter avant de scaler |
| Feature identifiant | ID client → corrélé à la cible par hasard | Supprimer les identifiants |
Commentaires