ML-2 : Préparation des données et ingénierie des features (Python / sklearn)

Navigation : Index | << ML-1 | Suivant >>

Objectifs d’apprentissage

A la fin de ce notebook, vous saurez : 1. Charger des données tabulaires avec pandas (DataFrame, équivalent Python de l’IDataView ML.NET) 2. Explorer et manipuler un DataFrame (types, valeurs manquantes, statistiques) 3. Appliquer des transformations : encodage one-hot, normalisation, imputation, concaténation de features 4. Construire un Pipeline sklearn complet de feature engineering (équivalent du pipeline ML.NET Transforms)

Parité ML.NET ⇄ Python

Ce notebook est le jumeau Python de ML-2-Data&Features.ipynb (ML.NET / C#). Il démontre les mêmes concepts (chargement, transformations, pipeline de features) avec l’écosystème pandas + scikit-learn au lieu de Microsoft.ML. La comparaison ML.NET ⇄ sklearn est l’objectif pédagogique de la série ML/ML.Net/ (#4956).

Concept ML.NET (C#) Équivalent Python (pandas / sklearn)
IDataView pandas.DataFrame
MLContext.Data.LoadFromTextFile pandas.read_csv
LoadColumn / ColumnName attributes Nommage de colonnes DataFrame
OneHotEncodingEstimator sklearn.preprocessing.OneHotEncoder
ReplaceMissingValues sklearn.impute.SimpleImputer
Concatenate("Features", ...) ColumnTransformer avec remainder='drop' (exclut le label)
mlContext.Transforms.* chain sklearn.pipeline.Pipeline

Prérequis

  • ML-1-Introduction-Python complété
  • Python 3.10+ avec pandas, scikit-learn, numpy

Durée estimée : 35-45 minutes


Préparation des Données et Ingénierie des Caractéristiques

En machine learning, la qualité des données détermine la qualité du modèle. Cette leçon couvre le feature engineering : l’art de transformer des données brutes (texte, catégories, valeurs manquantes) en une matrice numérique X qu’un modèle peut consommer.

Le dataset utilisé : taxi-fare (tarifs de taxi de NYC). Objectif : prédire fare_amount à partir de vendor_id, rate_code, passenger_count, trip_time_in_secs, trip_distance, payment_type.

Chargement des données avec pandas

Qu’est-ce qu’un DataFrame?

Un pandas.DataFrame est l’équivalent Python de l’IDataView ML.NET : une table en mémoire avec colonnes nommées, typage par colonne, et support des valeurs manquantes (NaN). C’est la structure standard pour la données tabulaires en Python ML.

Création d’un DataFrame

# Imports pandas / sklearn / numpy
import pandas as pd
import numpy as np
from sklearn.preprocessing import OneHotEncoder, StandardScaler
from sklearn.impute import SimpleImputer
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline

print("pandas:", pd.__version__)
print("sklearn charge avec succes.")
pandas: 3.0.2
sklearn charge avec succes.

Télécharger ou localiser les données

Pour ce jumeau Python, nous construisons le dataset in-memory (équivalent du inMemoryCollection ML.NET) afin de garantir une exécution reproductible sans dépendance réseau. En production, on utiliserait pd.read_csv("taxi-fare.csv").

# Construction in-memory du dataset taxi-fare (équivalent ML.NET ModelInput[])
# Schéma identique au notebook ML.NET : vendor_id, rate_code, passenger_count,
# trip_time_in_secs, trip_distance, payment_type, fare_amount
data = {
    "vendor_id":      ["CMT", "VST", "CMT", "VST", "CMT", "VST", "CMT", "VST"],
    "rate_code":      [1.0, np.nan, 1.0, 2.0, 1.0, 1.0, 3.0, 1.0],   # NaN = valeur manquante
    "passenger_count":[1.0, 1.0, 2.0, 1.0, 3.0, 2.0, 1.0, 4.0],
    "trip_time_in_secs":[1271, 474, 2100, 890, 1500, np.nan, 3200, 600],
    "trip_distance":  [3.8, 1.5, 7.2, 2.9, 5.1, 2.0, 11.0, 1.8],
    "payment_type":   ["CRD", "CSH", "CRD", "CSH", "CRD", "UNP", "CRD", "CSH"],
    "fare_amount":    [17.5, 8.0, 28.0, 12.5, 21.0, 9.0, 42.0, 7.5],
}
df = pd.DataFrame(data)
print("DataFrame charge :", df.shape, "lignes x colonnes")
df.head()
DataFrame charge : (8, 7) lignes x colonnes
vendor_id rate_code passenger_count trip_time_in_secs trip_distance payment_type fare_amount
0 CMT 1.0 1.0 1271.0 3.8 CRD 17.5
1 VST NaN 1.0 474.0 1.5 CSH 8.0
2 CMT 1.0 2.0 2100.0 7.2 CRD 28.0
3 VST 2.0 1.0 890.0 2.9 CSH 12.5
4 CMT 1.0 3.0 1500.0 5.1 CRD 21.0

Exploration du DataFrame

Contrairement à ML.NET où les types sont déclarés via attributs (LoadColumn, ColumnName), pandas infère automatiquement les types. On peut inspecter le résultat via df.dtypes et df.describe().

# Inspection des types et valeurs manquantes (équivalent ML.NET Schema)
print("=== Schema (dtypes) ===")
print(df.dtypes)
print()
print("=== Valeurs manquantes par colonne ===")
print(df.isna().sum())
print()
print("=== Statistiques descriptives (colonnes numeriques) ===")
df.describe()
=== Schema (dtypes) ===
vendor_id                str
rate_code            float64
passenger_count      float64
trip_time_in_secs    float64
trip_distance        float64
payment_type             str
fare_amount          float64
dtype: object

=== Valeurs manquantes par colonne ===
vendor_id            0
rate_code            1
passenger_count      0
trip_time_in_secs    1
trip_distance        0
payment_type         0
fare_amount          0
dtype: int64

=== Statistiques descriptives (colonnes numeriques) ===
rate_code passenger_count trip_time_in_secs trip_distance fare_amount
count 7.000000 8.000000 7.000000 8.000000 8.000000
mean 1.428571 1.875000 1433.571429 4.412500 18.187500
std 0.786796 1.125992 957.977706 3.282611 11.990882
min 1.000000 1.000000 474.000000 1.500000 7.500000
25% 1.000000 1.000000 745.000000 1.950000 8.750000
50% 1.000000 1.500000 1271.000000 3.350000 15.000000
75% 1.500000 2.250000 1800.000000 5.625000 22.750000
max 3.000000 4.000000 3200.000000 11.000000 42.000000

Lecture des statistiques

Le describe confirme l’intuition métier : trip_time_in_secs et trip_distance sont corrélés (moyennes 1 434 s et 4,4 km), mais la variance est forte (écart-type ~ 958 s (cellule describe plus haut)). fare_amount suit une distribution cohérente avec un tarif de base + taux kilométrique. Deux valeurs manquantes sont identifiées : rate_code (1 NaN) et trip_time_in_secs (1 NaN) — exactement celles que le pipeline d’imputation va traiter.

Notez que vendor_id et payment_type sont de type texte (str dans la sortie ci-dessus, object dans l’ancienne nomenclature pandas) : ils ne peuvent pas entrer directement dans un modèle numérique. C’est la justification du one-hot encoding qui suit.

Différence entre DataFrame et IDataView

Aspect IDataView (ML.NET) DataFrame (pandas)
Typage Schéma déclaré (attributs) Inféré automatiquement
Valeurs manquantes ReplaceMissingValues transform NaN natif, SimpleImputer
Lazy evaluation Oui (pipeline deferred) Non (matérialisé en mémoire)
Mutabilité Immuable (transform créée nouvelle vue) Mutable (in-place possible)

Le DataFrame est plus interactif (exploration immédiate) ; l’IDataView est plus scalable (lazy, schema strict). Les deux se prêtent au pipeline de transforms.

Transformations des données

Données catégorielles : One-Hot Encoding

Les colonnes vendor_id (CMT/VST) et payment_type (CRD/CSH/UNP) sont catégorielles : un modèle ne peut pas les consommer telles quelles. Le one-hot encoding crée une colonne binaire par modalité.

En ML.NET : mlContext.Transforms.Categorical.OneHotEncoding(...). En sklearn : OneHotEncoder (souvent intégré dans un ColumnTransformer).

Remplacer les valeurs manquantes

rate_code et trip_time_in_secs contiennent des NaN. En ML.NET : ReplaceMissingValues. En sklearn : SimpleImputer(strategy='mean') (ou median, most_frequent).

Construire le Pipeline sklearn

Le Pipeline sklearn est l’équivalent direct du chain ML.NET pipeline.Append(...) : il regroupe transforms + estimator dans un objet unique, fittable, sérialisable.

# Construction du ColumnTransformer (équivalent ML.NET OneHotEncoding + ReplaceMissingValues + Concatenate)
# - Colonnes catégorielles : OneHotEncoder (handle_unknown pour robustesse)
# - Colonnes numériques : SimpleImputer(mean) puis StandardScaler (normalisation)

categorical_cols = ["vendor_id", "payment_type"]
numeric_cols = ["rate_code", "passenger_count", "trip_time_in_secs", "trip_distance"]

categorical_transformer = OneHotEncoder(handle_unknown="ignore", sparse_output=False)
numeric_transformer = Pipeline(steps=[
    ("imputer", SimpleImputer(strategy="mean")),
    ("scaler", StandardScaler()),
])

# ColumnTransformer = équivalent de Concatenate("Features", ...) en ML.NET :
# il assemble les sorties des deux transformeurs en une matrice "Features" unique
preprocessor = ColumnTransformer(
    transformers=[
        ("cat", categorical_transformer, categorical_cols),
        ("num", numeric_transformer, numeric_cols),
    ],
    remainder="drop",  # on ignore fare_amount (label) pour la matrice de features
)

print("Pipeline (preprocessor) configure :")
print(preprocessor)
Pipeline (preprocessor) configure :
ColumnTransformer(transformers=[('cat',
                                 OneHotEncoder(handle_unknown='ignore',
                                               sparse_output=False),
                                 ['vendor_id', 'payment_type']),
                                ('num',
                                 Pipeline(steps=[('imputer', SimpleImputer()),
                                                 ('scaler', StandardScaler())]),
                                 ['rate_code', 'passenger_count',
                                  'trip_time_in_secs', 'trip_distance'])])

Fitter et transformer : produire la matrice Features

L’appel preprocessor.fit_transform(df) est l’équivalent ML.NET de pipeline.Fit(data).Transform(data) : il apprend les paramètres (moyennes, écarts-types, modalités) sur les données puis applique les transforms pour produire la matrice numérique finale.

# Fit + Transform : produit la matrice de features (équivalent ML.NET IDataView transformedData)
X = preprocessor.fit_transform(df)
print("Matrice de features X :", X.shape)
print("  (lignes, colonnes features apres one-hot + scaling)")
print()

# Récupérer les noms des features générées (one-hot expansion)
feature_names = preprocessor.get_feature_names_out()
print("Noms des features generees :")
for i, name in enumerate(feature_names):
    print(f"  [{i}] {name}")
print()

# Vérifier qu'une valeur manquante a bien été imputée (rate_code ligne 1 = NaN -> mean)
print("Apercu des 2 premieres lignes transformees :")
pd.DataFrame(X[:2], columns=feature_names)
Matrice de features X : (8, 9)
  (lignes, colonnes features apres one-hot + scaling)

Noms des features generees :
  [0] cat__vendor_id_CMT
  [1] cat__vendor_id_VST
  [2] cat__payment_type_CRD
  [3] cat__payment_type_CSH
  [4] cat__payment_type_UNP
  [5] num__rate_code
  [6] num__passenger_count
  [7] num__trip_time_in_secs
  [8] num__trip_distance

Apercu des 2 premieres lignes transformees :
cat__vendor_id_CMT cat__vendor_id_VST cat__payment_type_CRD cat__payment_type_CSH cat__payment_type_UNP num__rate_code num__passenger_count num__trip_time_in_secs num__trip_distance
0 1.0 0.0 1.0 0.0 0.0 -0.628971 -0.830747 -0.195956 -0.199472
1 0.0 1.0 0.0 1.0 0.0 0.000000 -0.830747 -1.156622 -0.948511

Lecture de la transformation : 7 colonnes -> 9 features

La matrice X passe de 7 colonnes brutes à 9 features : les 4 colonnes numériques (rate_code, passenger_count, trip_time_in_secs, trip_distance) sont conservées (imputées + normalisées), tandis que les 2 colonnes catégorielles explosent en 5 colonnes binaires (vendor_id → CMT/VST, payment_type → CRD/CSH/UNP). C’est l’expansion one-hot : chaque modalité devient une feature booléenne.

Vérifiez la ligne 1 (index 1) : vendor_id_VST = 1.0 confirme que le taxi est VST ; cat__payment_type_CRD = 0.0 et cat__payment_type_CSH = 1.0 indiquent un paiement en espèces. La colonne num__rate_code a perdu son NaN — remplacé par la moyenne (≈ 1,43), puis standardisée. Le pipeline a fait son travail : les données sont prêtes pour n’importe quel estimateur sklearn.

# Pipeline complet : preprocessing + regression (équivalent ML.NET pipeline.Append(RegressionTrainer))
from sklearn.linear_model import LinearRegression

model = Pipeline(steps=[
    ("preprocessor", preprocessor),
    ("regressor", LinearRegression()),
])

# X = features, y = label (fare_amount)
y = df["fare_amount"]

# Entraînement end-to-end (fit applique preprocessing PUIS regressor)
model.fit(df.drop(columns=["fare_amount"]), y)
print("Modele entraîne. Score R2 sur train :", round(model.score(df.drop(columns=["fare_amount"]), y), 3))
print()

# Prédiction sur les 2 premières lignes
pred = model.predict(df.drop(columns=["fare_amount"]).iloc[:2])
print("Prediction fare_amount (2 premieres lignes) :", [round(p, 2) for p in pred])
print("Valeurs reelles                       :", list(y.iloc[:2]))
Modele entraîne. Score R2 sur train : 1.0

Prediction fare_amount (2 premieres lignes) : [np.float64(17.49), np.float64(7.83)]
Valeurs reelles                       : [17.5, 8.0]

Lecture du R² = 1,0 : sur-apprentissage sur 8 lignes

Le score R² de 1,0 (parfait) est trompeur. Avec 8 lignes et 9 features, le modèle a plus de paramètres que d’exemples — il mémorise les données au lieu d’apprendre une règle générale. La preuve : les prédictions [17,49, 7,83] reproduisent exactement les valeurs réelles [17,5, 8,0] à 0,01 près. Ce n’est pas de la généralisation, c’est de la récitation.

En pratique, un tel score sur le train set exige une validation croisée ou un jeu de test indépendant pour être pris au sérieux. Ici, le dataset est volontairement minuscule pour la pédagogie — le R² parfait illustre le danger du sur-apprentissage sur trop peu de données.

Au-delà du score, notez l’écart de prédiction : 17,49 contre 17,5 et 7,83 contre 8,0 — des résidus de l’ordre du centième. Cette précision chimérique est le symptôme le plus net du surparamétrage : un modèle bien généralisé conserve une erreur de l’ordre du bruit des données, jamais zéro.

Exercices supplementaires

Exercice 1 : Featurisation de texte personnalisee

Objectif : créez un pipeline avec TfidfVectorizer (featurisation de texte) et comparez les paramètres par défaut vs personnalisés (n-grams).

# Exercice 1 : Featurisation de texte personnalisee
# TODO etudiant : Creez un pipeline avec TfidfVectorizer et comparez les parametres par defaut vs personnalises
# Indice : TfidfVectorizer(ngram_range=(1,2), max_features=...) pour configurer word/char n-grams
# Etape 1 : definissez une liste de documents texte (par ex. des avis produits)
# Etape 2 : creez un TfidfVectorizer par defaut et fit_transformez le corpus
# Etape 3 : creez un second TfidfVectorizer avec ngram_range=(1,2) et max_features=50
# Etape 4 : comparez la taille et le contenu des deux matrices

print("Exercice a completer : featurisation de texte personnalisee")
result_text_featurization = None  # TODO etudiant : remplacer par la matrice Tfidf
Exercice a completer : featurisation de texte personnalisee

Exercice 2 : Feature cross - combinaison de features categorielles

Objectif : combinez deux features catégorielles (vendor_id et payment_type) en une seule feature croisée, puis comparez les performances du modèle avec et sans cross-feature.

# Exercice 2 : Feature cross - combinaison de features categorielles
# TODO etudiant : Combinez deux features categorielles en une seule feature croisee
# Indice : concatenez vendor_id et payment_type en une colonne "vendor_payment" AVANT le one-hot encoding
# Etape 1 : ajoutez une colonne "vendor_payment" = vendor_id + "_" + payment_type
# Etape 2 : construisez un preprocessor qui one-hot encode UNIQUEMENT vendor_payment
# Etape 3 : entrainez un modele avec le cross-feature
# Etape 4 : comparez le R2 avec/sans cross-feature

print("Exercice a completer : feature cross")
result_feature_cross = None  # TODO etudiant : remplacer par le R2 avec cross-feature
Exercice a completer : feature cross

Exercice : Feature engineering pour la prédiction de prix immobiliers

Objectifs

  • Définir un DataFrame houses (quartier, surface, nb_chambres, prix)
  • Construire un Pipeline de preprocessing (one-hot sur quartier, scaling sur surface/chambres)
  • Entraîner un régresseur et prédire le prix

Indices - quartier est catégoriel ; surface, nb_chambres sont numériques ; prix est la cible - Réutilisez ColumnTransformer + Pipeline du notebook - 3-4 maisons suffisent pour la démo

# Exercice : Feature engineering pour maisons
# Completez les parties TODO pour creer un pipeline de preparation + regression

# TODO: Definir le DataFrame houses (3-4 maisons)
houses = None  # TODO etudiant : pd.DataFrame({"quartier":[...], "surface":[...], "nb_chambres":[...], "prix":[...]})

# TODO: Definir le ColumnTransformer (one-hot sur quartier, scaler sur surface/nb_chambres)
# TODO: Definir le Pipeline (preprocessor + LinearRegression)
# TODO: fit et predict

print("Exercice a completer : feature engineering pour maisons")
result_houses = None  # TODO etudiant : remplacer par le score R2
Exercice a completer : feature engineering pour maisons

Résumé

Dans ce notebook vous avez maîtrisé le feature engineering avec l’écosystème Python (pandas + scikit-learn), en miroir du notebook ML.NET :

  1. Chargement : pd.DataFrame in-memory (équivalent IDataView) avec schéma inféré automatiquement
  2. Exploration : df.dtypes, df.describe(), df.isna().sum() (gestion native des NaN)
  3. Transformations :
    • OneHotEncoder pour les catégorielles (équivalent OneHotEncodingEstimator ML.NET)
    • SimpleImputer pour les valeurs manquantes (équivalent ReplaceMissingValues)
    • StandardScaler pour la normalisation (souvent combiné avec l’imputer)
  4. Pipeline : ColumnTransformer + Pipeline sklearn = équivalent du chain mlContext.Transforms.*.Append(...) ML.NET, avec la matrice Features produite automatiquement

Parité conceptuelle clé

La séparation préoccupations est identique : ML.NET déclare les transforms (Transforms.Categorical.OneHotEncoding, Transforms.ReplaceMissingValues, Transforms.Concatenate) puis les chaîne dans un EstimatorChain ; sklearn déclare des transformeurs (OneHotEncoder, SimpleImputer, StandardScaler) puis les chaîne dans un ColumnTransformer / Pipeline. Les deux produisent une matrice Features numérique consommable par n’importe quel estimateur.

Prochaine étape

ML-3-Entrainement-Python.ipynb aborde l’entraînement et l’AutoML (équivalent mlContext.AutoML) en Python.

Références

Retour au sommet