Atelier : régression complète
Pipeline, CV et test final sur données tabulaires.
Énoncé
À partir de data/colis_propres.csv, prédisez prix_eur depuis distance_km, poids_kg, urgence et transporteur. Comparez une baseline médiane à Ridge avec alpha dans [0.1, 1.0, 10.0]. Faites un court diagnostic exploratoire sur le train : distribution du prix, poids manquants et relation distance/prix. Choisissez le modèle avec validation croisée par client, puis mesurez une seule fois la MAE sur les clients de test. Terminez par un graphique résidus vs prédictions et une fiche de limites.
Contraintes
Séparez les clients avant toute imputation. commande_id, client_id, date_commande, delai_jours et retard ne sont pas des entrées de ce modèle. Imputation, échelle et encodage doivent être appris dans chaque pli du pipeline. Notez la graine, les versions des paquets, la moyenne et l'écart type des MAE de validation, puis une MAE finale sur test. L'écart type des plis n'est pas une marge d'incertitude du test.
Indices
GroupShuffleSplit réserve les clients de test ; GroupKFold construit les plis dans le train. La métrique neg_mean_absolute_error doit être changée de signe pour lire une erreur positive. ColumnTransformer applique l'imputation médiane et l'échelle aux nombres, l'imputation puis le one-hot aux catégories. Un nuage résidus vs prédictions qui forme une courbe signale une structure que Ridge ne capture pas.
Correction disponibleCherchez d’abord par vous-même.Voir la correction
Correction
Ce script fonctionne après l'atelier de nettoyage. Enregistrez-le sous regression.py et lancez python regression.py depuis le dossier contenant data :
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.compose import ColumnTransformer
from sklearn.dummy import DummyRegressor
from sklearn.impute import SimpleImputer
from sklearn.linear_model import Ridge
from sklearn.metrics import mean_absolute_error
from sklearn.model_selection import GroupKFold, GroupShuffleSplit, cross_val_score
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler
df = pd.read_csv("data/colis_propres.csv")
colonnes = ["distance_km", "poids_kg", "urgence", "transporteur"]
X, y, groupes = df[colonnes], df["prix_eur"], df["client_id"]
split = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42)
train_idx, test_idx = next(split.split(X, y, groupes))
X_train, X_test = X.iloc[train_idx], X.iloc[test_idx]
y_train, y_test = y.iloc[train_idx], y.iloc[test_idx]
g_train, g_test = groupes.iloc[train_idx], groupes.iloc[test_idx]
assert set(g_train).isdisjoint(g_test)
def preparer():
nombres = make_pipeline(SimpleImputer(strategy="median"), StandardScaler())
categories = make_pipeline(
SimpleImputer(strategy="most_frequent"),
OneHotEncoder(handle_unknown="ignore"),
)
return ColumnTransformer([
("nombres", nombres, ["distance_km", "poids_kg"]),
("categories", categories, ["urgence", "transporteur"]),
], sparse_threshold=0)
modeles = {"mediane": make_pipeline(preparer(), DummyRegressor(strategy="median"))}
for alpha in (0.1, 1.0, 10.0):
modeles[f"ridge_{alpha}"] = make_pipeline(preparer(), Ridge(alpha=alpha))
plis = list(GroupKFold(n_splits=5).split(X_train, y_train, g_train))
scores = {}
for nom, modele in modeles.items():
mae = -cross_val_score(
modele, X_train, y_train, cv=plis, scoring="neg_mean_absolute_error"
)
scores[nom] = mae.mean()
print(nom, "CV MAE moyenne", round(mae.mean(), 2), "écart type", round(mae.std(), 2))
gagnant = min(scores, key=scores.get)
final = modeles[gagnant].fit(X_train, y_train)
prediction = final.predict(X_test)
print("choix CV :", gagnant)
print("test MAE (euros) :", round(mean_absolute_error(y_test, prediction), 2))
residus = y_test.to_numpy() - prediction
plt.scatter(prediction, residus, alpha=0.6)
plt.axhline(0, color="black", linewidth=1)
plt.xlabel("prix prédit (euros)")
plt.ylabel("résidu : vrai moins prédit (euros)")
plt.tight_layout()
plt.savefig("data/residus_prix.png")
Comparez la MAE du gagnant à celle de la baseline sur les plis de validation avant de regarder le test. Le jeu est synthétique et le prix a été construit à partir de caractéristiques proches de celles fournies : un bon score ne prouve aucune utilité sur des colis réels. Si les résultats diffèrent, vérifiez d'abord le fichier, la graine et les versions, puis documentez l'écart au lieu de changer le modèle après le verdict test.