Machine Learning 4 — Préparation des données
:::tip Notebook Kaggle Le code complet et exécutable de ce chapitre est sur Kaggle : Ouvrir →
Versions anglaise et chinoise disponibles depuis la page d'accueil. :::
Dans la vraie vie, les datasets sont rarement propres. Valeurs manquantes, variables catégorielles, classes déséquilibrées, fuites d'information... Ce chapitre couvre tout ce qu'il faut pour transformer un fichier brut en quelque chose qu'un modèle peut digérer.
Pourquoi ce chapitre ?
Vous y apprenez :
- à gérer les valeurs manquantes (suppression vs imputation) ;
- à encoder les variables catégorielles (One-Hot, ordinal, CatBoost natif) ;
- à utiliser
ColumnTransformer+Pipeline: le pattern industriel ; - à reconnaître et éviter le target leakage (fuite de données) ;
- à gérer les classes déséquilibrées (SMOTE,
class_weight, courbe PR) ; - à régler les hyperparamètres avec
GridSearchCV.
Valeurs manquantes
Premier réflexe : repérer où sont les NaN.
df.isna().sum() # nombre de NaN par colonne
df.isna().any(axis=1).sum() # nombre de lignes avec au moins un NaN
Deux stratégies principales :
1. Supprimer les lignes incomplètes (dropna). Simple, mais on perd des données et on peut introduire un biais si les valeurs manquantes ne sont pas aléatoires.
2. Imputer (remplacer par une valeur plausible). Pour les numériques : médiane (plus robuste que la moyenne aux outliers). Pour les catégorielles : mode (valeur la plus fréquente).
from sklearn.impute import SimpleImputer
num_imputer = SimpleImputer(strategy='median')
cat_imputer = SimpleImputer(strategy='most_frequent')
Encodage des variables catégorielles
Les algorithmes de ML attendent des nombres. Une colonne species = "Adelie" ou island = "Biscoe" doit être convertie.
One-Hot Encoding
Pour une variable à modalités, on crée colonnes binaires. pd.get_dummies est le plus simple :
df_encoded = pd.get_dummies(df, columns=['island'])
Mais en production, préférer OneHotEncoder de scikit-learn avec handle_unknown='ignore' : si une catégorie inédite arrive au test, le modèle ne plante pas.
:::warning Pas d'encodage entier sans réflexion
Encoder les modalités en 1, 2, 3 suggère un ordre ou une distance entre catégories. C'est faux pour des îles, des couleurs, etc. Toujours utiliser le One-Hot pour les variables nominales.
:::
ColumnTransformer + Pipeline
Le pattern industriel pour traiter automatiquement numériques et catégorielles dans le même Pipeline :
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import OneHotEncoder, StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer
num_features = X.select_dtypes(include='number').columns.tolist()
cat_features = X.select_dtypes(include=['object', 'category']).columns.tolist()
num_pipe = Pipeline([
('imputer', SimpleImputer(strategy='median')),
('scaler', StandardScaler()),
])
cat_pipe = Pipeline([
('imputer', SimpleImputer(strategy='most_frequent')),
('ohe', OneHotEncoder(handle_unknown='ignore', sparse_output=False)),
])
preprocessor = ColumnTransformer([
('num', num_pipe, num_features),
('cat', cat_pipe, cat_features),
])
model = Pipeline([
('prep', preprocessor),
('classifier', RandomForestClassifier()),
])
Avantages : pas de data leakage, gestion des catégories inédites, code propre, compatible cross_val_score et GridSearchCV.
Target leakage : le piège
Imaginons un dataset Student Performance qui contient G1 et G2 (notes des deux premiers trimestres) en variables explicatives, et G3 (note finale) comme cible.
Premier réflexe : balancer toutes les colonnes au modèle. R² obtenu : 0.95.
C'est trop beau pour être vrai. G1 et G2 sont presque la cible — connaître les notes des deux premiers trimestres prédit trivialement la note finale.
C'est un target leakage : on a inclus dans les features une variable qui n'aurait pas dû être disponible au moment de la prédiction réelle. Sans G1 et G2, le R² tombe à 0.2-0.3 — la vraie performance.
:::warning Le réflexe à avoir Pour chaque variable, vous demander : « Cette variable serait-elle disponible au moment où je veux faire la prédiction en vrai ? » Si non, retirez-la. :::
Classes déséquilibrées
Sur un dataset de fraude bancaire, 0.17 % seulement des transactions sont frauduleuses. Un modèle qui dit toujours « pas frauduleux » a 99.83 % d'accuracy et est inutile.
Trois stratégies
1. Sous-échantillonnage (undersampling) : réduire la classe majoritaire.
from imblearn.under_sampling import RandomUnderSampler
sampler = RandomUnderSampler()
X_res, y_res = sampler.fit_resample(X, y)
2. Sur-échantillonnage (oversampling) : SMOTE génère des points synthétiques par interpolation entre voisins.
from imblearn.over_sampling import SMOTE
sampler = SMOTE()
X_res, y_res = sampler.fit_resample(X, y)
3. Pondération : dire au modèle de pénaliser plus les erreurs sur la classe minoritaire.
from sklearn.ensemble import RandomForestClassifier
model = RandomForestClassifier(class_weight='balanced')
Pour XGBoost, c'est scale_pos_weight = n_negative / n_positive.
Métriques adaptées
L'accuracy est inutile en classes déséquilibrées. Préférez :
- Précision/Rappel/F1 par classe (cf.
classification_report) - PR-curve plutôt que ROC (la ROC est trop optimiste quand FP est dilué)
- Matrice de confusion explicite
Stratify dans le split
Pour garder la proportion de classes dans le train et le test :
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, stratify=y, random_state=42
)
GridSearchCV : régler les hyperparamètres
Plutôt que de fixer max_depth=5 au pifomètre, GridSearchCV essaie systématiquement toutes les combinaisons d'une grille et retient la meilleure par validation croisée :
from sklearn.model_selection import GridSearchCV
param_grid = {
'n_estimators': [100, 200],
'max_depth': [None, 5, 10],
'min_samples_split': [2, 5],
}
grid = GridSearchCV(
RandomForestClassifier(class_weight='balanced'),
param_grid,
scoring='f1', # F1 plutôt qu'accuracy en classes déséquilibrées
cv=5,
n_jobs=-1,
)
grid.fit(X_train, y_train)
print(grid.best_params_)
Pour des grilles très grandes, RandomizedSearchCV échantillonne au lieu d'énumérer.
CatBoost : le boosting natif catégoriel
Quand le dataset est dominé par des variables catégorielles (Mushrooms, Adult Census), CatBoost gère le catégoriel nativement, sans encodage One-Hot manuel.
from catboost import CatBoostClassifier
model = CatBoostClassifier(iterations=500, learning_rate=0.05, depth=6, verbose=False)
model.fit(X_train, y_train, cat_features=cat_indices)
Au lieu d'un One-Hot, CatBoost utilise des statistiques de cible régularisées par catégorie, calculées sur des ordres aléatoires pour éviter la fuite d'information.