2.1 — Le workflow d’apprentissage automatique

Navigation : << 01-PythonForDataScience | 2.2-Descente-de-gradient >> | Index

Kernel : Python 3

Introduction

Ce notebook enseigne le workflow fondamental de l’apprentissage automatique supervisé : séparer les données (train/test), ajuster un modèle (fit), prédire (predict), puis évaluer. Il répond à une question centrale : pourquoi évalue-t-on sur des données que le modèle n’a pas vues ? Il montre aussi comment rendre le surapprentissage (overfitting) directement visible. C’est le prérequis de 2.2-Descente-de-gradient et de toute la série 02-ML-Cours.

Objectifs d’apprentissage

  1. Comprendre POURQUOI on sépare un jeu de données en entraînement et test.
  2. Exécuter le workflow fit / predict / evaluate avec scikit-learn.
  3. Rendre le surapprentissage visible grâce à un balayage de complexité.
  4. Distinguer les métriques de classification (accuracy) et de régression (MAE).

Prérequis

  • NumPy et Pandas (voir 01-PythonForDataScience).
  • Bases de Python (boucles, fonctions, indexation).

Référence. Mitchell, T. (1997), Machine Learning, McGraw-Hill. La distinction entraînement/généralisation et le surapprentissage (overfitting) y sont définis comme le concept central de l’apprentissage supervisé.

# Configuration : imports et graine aléatoire
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
from sklearn.linear_model import LinearRegression
from sklearn.datasets import make_classification, make_regression
from sklearn.metrics import accuracy_score, mean_absolute_error
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler

np.random.seed(42)
print("Configuration OK : 2.1 - Workflow ML")
Configuration OK : 2.1 - Workflow ML

Auto-évaluation

Ce carnet porte un dispositif d’auto-évaluation formatif : quatre questions, placées avant le parcours (diagnostic de prérequis), au milieu (vérification de la notion) et à la fin (transfert). Aucune note n’est stockée, aucune réponse n’est enregistrée.

Chaque cellule de question s’exécute sans erreur tant que vous n’avez pas répondu ; la correction s’affiche uniquement lorsque vous remplacez reponse=None par la lettre de votre choix, puis ré-exécutez la cellule.

# Dispositif d'auto-evaluation de la serie (module partage, issue #18207)
import pathlib
import sys

for _racine in (pathlib.Path.cwd(), *pathlib.Path.cwd().parents):
    if (_racine / "MyIA.AI.Notebooks").is_dir():
        sys.path.insert(0, str(_racine / "MyIA.AI.Notebooks" / "ML" / "DataScienceWithAgents"))
        break

from auto_evaluation import question  # noqa: E402

Avant de commencer — question de diagnostic

question(
    "Que mesure la performance d'un modèle calculée sur ses propres données d'entraînement ?",
    choix=[
        "Sa capacité à généraliser à des données nouvelles",
        "Sa capacité à restituer des données qu'il a déjà vues",
        "La qualité du jeu de données, indépendamment du modèle",
    ],
    bonne="B",
    explication="Un modèle évalué sur ce qu'il a vu mesure sa restitution, pas sa généralisation. C'est pourquoi on met des données de côté avant de conclure quoi que ce soit.",
    reponse=None,  # remplacez None par la lettre de votre choix, puis re-executez
    moment="avant",
)
Diagnostic de prérequis — Que mesure la performance d'un modèle calculée sur ses propres données d'entraînement ?

  A. Sa capacité à généraliser à des données nouvelles
  B. Sa capacité à restituer des données qu'il a déjà vues
  C. La qualité du jeu de données, indépendamment du modèle

Réponse non donnée. Remplacez `reponse=None` par la lettre de votre choix dans cette cellule, puis ré-exécutez-la pour afficher la correction.

1. Le jeu de données

Nous utilisons un jeu de données synthétique généré par make_classification. Le choix d’un jeu synthétique (plutôt qu’un CSV réel) est délibéré :

  • il est déterministe (même random_state → mêmes données), donc reproductible ;
  • on contrôle le niveau de bruit et le nombre de caractéristiques informatives, ce qui permet d’observer proprement le surapprentissage ;
  • aucune dépendance à un chemin de fichier fragile.

Paramètres utilisés :

  • n_samples=500 — 500 observations.
  • n_features=8 — 8 caractéristiques au total.
  • n_informative=5 — 5 caractéristiques réellement porteuses d’information.
  • n_redundant=2 — 2 caractéristiques redondantes (combinaisons linéaires des informatives).
  • flip_y=0.10 — 10 % des étiquettes sont inversées (bruit de label).
  • random_state=42 — pour la reproductibilité.
# Génération du jeu de données synthétique (exemple résolu)
X, y = make_classification(
    n_samples=500,
    n_features=8,
    n_informative=5,
    n_redundant=2,
    flip_y=0.10,
    random_state=42,
)

print("Forme de X :", X.shape)
print("Forme de y :", y.shape)
print("\nRépartition des classes :")
print(pd.Series(y).value_counts())

# Aperçu visuel : deux premières caractéristiques
plt.scatter(X[:, 0], X[:, 1], c=y, cmap='coolwarm', alpha=0.6, s=15)
plt.colorbar(label='classe')
plt.xlabel('caractéristique 0')
plt.ylabel('caractéristique 1')
plt.title('Jeu de données synthétique (2 features sur 8)')
plt.tight_layout()
plt.show()
Forme de X : (500, 8)
Forme de y : (500,)

Répartition des classes :
0    259
1    241
Name: count, dtype: int64

2. Pourquoi séparer train et test ?

Le concept-clé est le principe du holdout : on évalue un modèle sur des données qu’il n’a pas vues pendant l’entraînement. Pourquoi ? Parce qu’un modèle suffisamment flexible peut mémoriser les données d’entraînement (y compris leur bruit) sans pour autant généraliser à de nouvelles données. Mesurer la performance sur les données d’entraînement surestime donc la vraie capacité du modèle.

L’évaluation sur un ensemble de test distinct fournit une estimation honnête de la capacité de généralisation.

Référence. Stone, M. (1974), Cross-Validatory Choice and Assessment of Statistical Predictions, Journal of the Royal Statistical Society. Series B (Methodological) 36(2):111-147. La validation croisée et le principe d’évaluation sur des données non vues fondent la mesure honnête de la capacité de généralisation.

# Exemple résolu : découpage stratifié train/test (70% / 30%)
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.3, random_state=42, stratify=y
)

print("Taille entraînement :", X_train.shape, y_train.shape)
print("Taille test         :", X_test.shape, y_test.shape)
print()
# stratify=y conserve les proportions de classes entre train et test
print("Proportion classe 1 - train : {:.3f}".format((y_train == 1).mean()))
print("Proportion classe 1 - test  : {:.3f}".format((y_test == 1).mean()))
Taille entraînement : (350, 8) (350,)
Taille test         : (150, 8) (150,)

Proportion classe 1 - train : 0.483
Proportion classe 1 - test  : 0.480

Lecture du résultat : pourquoi ces deux nombres méritent votre attention

Les proportions de la classe 1 — 0,483 sur le train et 0,480 sur le test — sont presque identiques : c’est exactement l’effet recherché de stratify=y. Sans stratification, un tirage au hasard peut s’écarter de quelques points ; sur un jeu de test de 150 observations, deux points de pourcentage représentent trois exemples — peu ici. Mais sur une classe rare (1 % de fraudes, par exemple), un découpage non stratifié peut produire un jeu de test où la classe d’intérêt est quasi absente, et toute métrique calculée dessus devient un non-sens. La stratification est une assurance quasi gratuite contre ce risque d’échantillonnage : elle conserve dans chaque découpage la structure que le modèle doit apprendre ET celle sur laquelle il sera jugé.

Exercice 1 : comparer un découpage naïf au découpage stratifié

Contexte. Le paramètre stratify=y garantit que les proportions de classes sont conservées entre train et test. Que se passe-t-il si l’on ignore cette précaution ?

Objectif. Implémentez un découpage naïf : prenez les 70 % premières lignes du jeu de données comme entraînement (sans mélanger, sans stratifier), puis comparez la proportion de classe 1 dans ce train naïf à celle du train stratifié ci-dessus.

Indices.

  • Étape 1 : calculer n_train_naif = int(0.7 * len(X)).
  • Étape 2 : extraire X_train_naif = X[:n_train_naif] et y_train_naif = y[:n_train_naif].
  • Indice : comparez (y_train_naif == 1).mean() à (y_train == 1).mean().

La cellule de stub ci-dessous est à compléter.

# Exercice 1 : découpage naïf (70% premières lignes) vs stratifié
n_train_naif = int(0.7 * len(X))
# TODO etudiant : extraire X_train_naif, y_train_naif, X_test_naif, y_test_naif
X_train_naif = None  # TODO etudiant : remplacer
y_train_naif = None  # TODO etudiant : remplacer

# TODO etudiant : calculer la proportion de classe 1 dans le train naïf vs le train stratifié
prop_naif = None  # TODO etudiant : remplacer (proportion de classe 1 dans y_train_naif)
prop_stratifie = None  # TODO etudiant : remplacer (proportion de classe 1 dans y_train)

print(f"Exercice 1 a completer : prop classe 1 (naif) = {prop_naif}, (stratifie) = {prop_stratifie}")
Exercice 1 a completer : prop classe 1 (naif) = None, (stratifie) = None

3. Premier modèle : un arbre de décision

Un arbre de décision (DecisionTreeClassifier) classe une observation en posant une suite de questions binaires sur les caractéristiques (« la feature 3 est-elle > 0.5 ? »), ce qui partitionne l’espace en régions. Le paramètre clé est max_depth : il contrôle la profondeur maximale de l’arbre, donc sa complexité. Un arbre peu profond sous-apprend (trop rigide) ; un arbre très profond surapprend (il mémorise).

Référence. Breiman, L., Friedman, J., Olshen, R. & Stone, C. (1984), Classification and Regression Trees, Wadsworth. Les arbres CART et la notion de profondeur (complexité du modèle) y sont introduits ; un arbre trop profond mémorise les données d’entraînement.

# Exemple résolu : arbre de décision de profondeur 3
arbre = DecisionTreeClassifier(max_depth=3, random_state=42)
arbre.fit(X_train, y_train)

# Prédictions sur train ET test pour comparer
y_pred_train = arbre.predict(X_train)
y_pred_test = arbre.predict(X_test)

acc_train = accuracy_score(y_train, y_pred_train)
acc_test = accuracy_score(y_test, y_pred_test)

# L'accuracy de train est en général >= à celle de test : c'est attendu
print("Accuracy - train : {:.3f}".format(acc_train))
print("Accuracy - test  : {:.3f}".format(acc_test))
Accuracy - train : 0.874
Accuracy - test  : 0.773

Vérification 1 — découper avant de transformer

question(
    "On normalise toutes les variables en calculant moyenne et écart-type sur le jeu complet, puis on sépare train et test. Qu'est-ce que cela change ?",
    choix=[
        "Rien : la normalisation est une transformation neutre",
        "Les statistiques ont vu le test : la performance mesurée est trop optimiste",
        "Le modèle devient moins précis, car il perd l'échelle d'origine",
    ],
    bonne="B",
    explication="C'est une fuite d'information : les statistiques de normalisation utilisées par l'entraînement ont été calculées sur des données de test. Les statistiques s'apprennent sur le train seul — le Pipeline (section 6) le fait pour vous.",
    reponse=None,  # remplacez None par la lettre de votre choix, puis re-executez
    moment="pendant",
)
Vérification — On normalise toutes les variables en calculant moyenne et écart-type sur le jeu complet, puis on sépare train et test. Qu'est-ce que cela change ?

  A. Rien : la normalisation est une transformation neutre
  B. Les statistiques ont vu le test : la performance mesurée est trop optimiste
  C. Le modèle devient moins précis, car il perd l'échelle d'origine

Réponse non donnée. Remplacez `reponse=None` par la lettre de votre choix dans cette cellule, puis ré-exécutez-la pour afficher la correction.

4. Rendre le surapprentissage VISIBLE

C’est le concept-phare de ce notebook. Plutôt qu’un seul point de mesure, nous balayons la complexité : pour chaque profondeur max_depth de 1 à 25, nous entraînons un arbre puis mesurons son accuracy sur train et sur test. On trace alors les deux courbes.

Que doit-on observer ?

  • L’accuracy d’entraînement monte vers 1.0 (l’arbre mémorise de plus en plus finement).
  • L’accuracy de test plafonne puis redescend : au-delà d’une certaine complexité, le modèle apprend du bruit.
  • L’écart de généralisation (train − test) s’élargit avec la profondeur : c’est la signature visible du surapprentissage.

Référence. Hastie, T., Tibshirani, R. & Friedman, J. (2009), The Elements of Statistical Learning, Springer (2e éd.), sections 2.9 et 7. Le compromis biais-variance et le surapprentissage y sont analysés : la complexité du modèle augmente la variance et réduit la capacité de généralisation.

# Exemple résolu : balayage de complexité (max_depth de 1 à 25)
depths = list(range(1, 26))
trains = []
tests = []

for d in depths:
    clf = DecisionTreeClassifier(max_depth=d, random_state=42)
    clf.fit(X_train, y_train)
    trains.append(accuracy_score(y_train, clf.predict(X_train)))
    tests.append(accuracy_score(y_test, clf.predict(X_test)))

sweep = pd.DataFrame({
    'profondeur': depths,
    'accuracy_train': trains,
    'accuracy_test': tests,
})

plt.figure()
plt.plot(sweep['profondeur'], sweep['accuracy_train'], marker='o', label='accuracy train')
plt.plot(sweep['profondeur'], sweep['accuracy_test'], marker='s', label='accuracy test')
# Marquer la profondeur d'accuracy test maximale
meilleur = sweep.loc[sweep['accuracy_test'].idxmax(), 'profondeur']
plt.axvline(meilleur, color='gray', linestyle='--', alpha=0.7, label=f'max test (profondeur={meilleur})')
plt.xlabel("profondeur de l'arbre (max_depth)")
plt.ylabel('accuracy')
plt.title("Surapprentissage visible : train vs test selon la complexité")
plt.legend()
plt.tight_layout()
plt.show()

# Ligne d'accuracy test maximale
print(sweep.loc[sweep['accuracy_test'].idxmax()])

profondeur        4.000000
accuracy_train    0.914286
accuracy_test     0.826667
Name: 3, dtype: float64

Lecture du graphique : les ciseaux du surapprentissage

La courbe d’accuracy train monte continûment et finit par atteindre 1,0 : un arbre assez profond mémorise chaque observation, bruit compris. La courbe test suit la même trajectoire au début, culmine autour de la profondeur 4 (accuracy 0,827), puis plafonne voire régresse. L’écart qui s’ouvre entre les deux courbes — 0,914 contre 0,827 au sommet du test — est le surapprentissage rendu visible : le modèle gagne sur des exemples déjà vus sans gagner sur des exemples nouveaux.

Deux précautions pour la suite. D’abord, l’argmax du test (exercice 2) est une estimation fragile : sur 150 observations de test, un point d’accuracy représente 1,5 exemple, et le voisinage de l’optimum est plat — la validation croisée (notebook 2.5) remplacera ce jugement à décision unique par une moyenne sur plusieurs découpages. Ensuite, ce balayage a utilisé le jeu de test pour choisir l’hyperparamètre : en pratique, ce choix se fait sur un jeu de validation dédié, et le test ne sert qu’une seule fois, à la toute fin — sinon on optimise contre le juge.

Exercice 2 : trouver la profondeur optimale

Objectif. À partir du DataFrame sweep construit ci-dessus, extrayez la profondeur optimale : celle qui maximise l’accuracy de test. Reportez ensuite l’accuracy d’entraînement et de test à cette profondeur.

Indices.

  • Indice : sweep['accuracy_test'].idxmax() donne l’indice de la ligne où l’accuracy de test est maximale.
  • Étape 1 : calculer indice_optimal = sweep['accuracy_test'].idxmax().
  • Étape 2 : récupérer sweep.loc[indice_optimal, 'profondeur'], sweep.loc[indice_optimal, 'accuracy_train'] et sweep.loc[indice_optimal, 'accuracy_test'].
# Exercice 2 : profondeur optimale = argmax de l'accuracy test
# TODO etudiant : trouver l'indice de profondeur optimale puis la valeur
indice_optimal = None  # TODO etudiant : remplacer (idx de l'accuracy test maximale)
profondeur_optimale = None  # TODO etudiant : remplacer (sweep.loc[indice_optimal, 'profondeur'])
acc_train_opt = None  # TODO etudiant : remplacer
acc_test_opt = None  # TODO etudiant : remplacer
print(f"Exercice 2 a completer : profondeur optimale = {profondeur_optimale}, "
      f"train = {acc_train_opt}, test = {acc_test_opt}")
Exercice 2 a completer : profondeur optimale = None, train = None, test = None

5. Côté régression : une autre famille de métriques

Jusqu’ici nous faisions de la classification (étiquettes discrètes 0/1), mesurée par l’accuracy (proportion de prédictions correctes). En régression, la cible est un nombre continu : prédire « 2.51 » quand la vérité est « 2.49 » n’est ni totalement faux ni totalement juste. Il faut donc une métrique de distance entre prédiction et réalité.

La MAE (mean absolute error) est la moyenne des valeurs absolues des écarts :

\[ \mathrm{MAE} = \frac{1}{n} \sum_{i=1}^{n} |y_i - \hat{y}_i| \]

Plus la MAE est faible, meilleure est la régression (elle s’exprime dans la même unité que la cible).

Référence. La MAE (mean absolute error) est l’un des estimateurs d’erreur les plus robustes pour la régression ; voir Hastie, Tibshirani & Friedman (2009), The Elements of Statistical Learning, Springer (2e éd.), section 10.6 pour la comparaison des critères de perte (L1 vs L2).

# Exemple résolu : régression linéaire et MAE
Xr, yr = make_regression(n_samples=300, n_features=5, noise=15.0, random_state=42)

# En régression, pas de stratify (la cible est continue)
Xr_train, Xr_test, yr_train, yr_test = train_test_split(Xr, yr, test_size=0.3, random_state=42)

modele_reg = LinearRegression()
modele_reg.fit(Xr_train, yr_train)
yr_pred = modele_reg.predict(Xr_test)

mae = mean_absolute_error(yr_test, yr_pred)
print("MAE (mean absolute error) : {:.3f}".format(mae))
MAE (mean absolute error) : 11.105

Exercice 3 : recalculer la MAE à la main

Objectif. Recalculez la MAE manuellement avec NumPy (moyenne des valeurs absolues des résidus) et comparez-la à la valeur renvoyée par mean_absolute_error de scikit-learn. Elles doivent être égales.

Indices.

  • Indice : np.mean(np.abs(yr_pred - yr_test)).
  • Étape 1 : calculer les résidus yr_pred - yr_test.
  • Étape 2 : prendre la valeur absolue puis la moyenne avec NumPy.
# Exercice 3 : MAE manuelle (moyenne des valeurs absolues des résidus)
# TODO etudiant : calculer la MAE à la main avec numpy
mae_manuelle = None  # TODO etudiant : remplacer
print(f"Exercice 3 a completer : MAE manuelle = {mae_manuelle}")
Exercice 3 a completer : MAE manuelle = None

Vérification 2 — l’écart qui mesure le surapprentissage

question(
    "Un arbre de décision devient de plus en plus profond. Que se passe-t-il ?",
    choix=[
        "L'accuracy d'entraînement monte et l'écart avec l'accuracy de test se creuse",
        "Les deux accuracies montent ensemble",
        "L'accuracy de test rejoint exactement l'accuracy d'entraînement",
    ],
    bonne="A",
    explication="Une profondeur plus grande réduit le biais et augmente la variance : l'arbre finit par mémoriser le train, et c'est l'écart train/test qui rend ce surapprentissage visible.",
    reponse=None,  # remplacez None par la lettre de votre choix, puis re-executez
    moment="pendant",
)
Vérification — Un arbre de décision devient de plus en plus profond. Que se passe-t-il ?

  A. L'accuracy d'entraînement monte et l'écart avec l'accuracy de test se creuse
  B. Les deux accuracies montent ensemble
  C. L'accuracy de test rejoint exactement l'accuracy d'entraînement

Réponse non donnée. Remplacez `reponse=None` par la lettre de votre choix dans cette cellule, puis ré-exécutez-la pour afficher la correction.

6. Bonne pratique : le Pipeline

Le prétraitement (mise à l’échelle, imputation des valeurs manquantes…) doit toujours vivre à l’intérieur d’un Pipeline scikit-learn. Pourquoi ? Pour éviter les fuites de données (data leakage) : si l’on calcule les paramètres d’un StandardScaler (moyenne, écart-type) sur tout le jeu de données — test compris — puis qu’on évalue sur le test, le modèle a indirectement « vu » des informations issues du test. Le Pipeline garantit que le scaler est ajusté uniquement sur le fragment d’entraînement, puis appliqué au test sans réutiliser ses propres statistiques.

# Exemple résolu : un Pipeline (scaler + arbre) sans fuite de données
pipeline = Pipeline([
    ('scaler', StandardScaler()),
    ('tree', DecisionTreeClassifier(max_depth=4, random_state=42)),
])

# Même workflow que sans pipeline, mais le scaler ne voit que le train
pipeline.fit(X_train, y_train)
y_pred_pipe = pipeline.predict(X_test)
acc_pipe = accuracy_score(y_test, y_pred_pipe)

print("Accuracy (Pipeline scaler + arbre) : {:.3f}".format(acc_pipe))
print("-> Le Pipeline applique le StandardScaler ajusté sur le train seulement :")
print("   aucune statistique du test ne fuite dans l'entraînement.")
Accuracy (Pipeline scaler + arbre) : 0.827
-> Le Pipeline applique le StandardScaler ajusté sur le train seulement :
   aucune statistique du test ne fuite dans l'entraînement.

Lecture : 0,827 — exactement l’accuracy de l’arbre nu à profondeur 4

Ce n’est pas une coïncidence, et c’est l’occasion d’une observation honnête : un arbre de décision est insensible aux changements d’échelle. Ses seuils de coupure se déplacent avec la mise à l’échelle, mais les partitions de l’espace des caractéristiques restent les mêmes — le StandardScaler ne change donc rien pour ce modèle précis. La valeur du Pipeline n’est pas ici un gain d’accuracy : c’est la discipline. Dès que le modèle deviendra sensible aux échelles (k-NN, régression régularisée, SVM dans les notebooks suivants), ce même pipeline garantira que le scaler est ajusté sur le train seul ; écrit à la main, le réflexe « scaler puis découper » laisserait la moyenne et l’écart-type du test contaminer l’entraînement — une fuite silencieuse qui gonfle l’accuracy mesurée.

7. Reculer d’un pas : la grille « Data Science in Context »

Le workflow split -> fit -> predict -> evaluate est le geste technique central — mais un projet de science des données n’est pas que ce geste. Spector, Norvig, Wiggins et Wing (Data Science in Context, 2022 ; « More Than Just Algorithms », Communications of the ACM 66(8), 2023) proposent une grille en sept points pour interroger n’importe quel projet : données traitables, approche technique, fiabilité, intelligibilité, objectifs clairs, tolérance aux échecs, implications éthiques/juridiques/sociétales.

Appliquons-la au cas que ce notebook vient d’exécuter — non pas à la main, mais par une fonction : chaque ligne dira ce que le notebook couvre, ce qu’il tait, et quel notebook de la série y répond.

# Exemple résolu : grille Data Science in Context appliquée au cas de ce notebook
def grille_contexte(cas):
    """Rend la grille à sept points (Spector, Norvig, Wiggins & Wing) pour un cas donné.

    cas : dict {1..7: {"titre": str, "couvert": str, "tais": str, "ref": str}}
    """
    lignes = [
        {"point": f"{num}. {p['titre']}", "couvert ici": p["couvert"],
         "tenu silencieux": p["tais"], "où le voir": p["ref"]}
        for num, p in sorted(cas.items())
    ]
    return pd.DataFrame(lignes).set_index("point")

# Faits mesurés sur le cas RÉEL de ce notebook (aucune valeur écrite à la main)
prop_majoritaire = float(pd.Series(y).value_counts(normalize=True).max())
ecart_arbre = float(acc_train - acc_test)  # arbre de profondeur 3 (section 3)
chute_test = float(sweep["accuracy_test"].max() - sweep["accuracy_test"].iloc[-1])

cas_21 = {
    1: {"titre": "Données traitables",
        "couvert": f"Jeu synthétique déterministe (500 obs., 8 caractéristiques, classes quasi équilibrées : {prop_majoritaire:.2f} pour la majoritaire) — reproductible, sans dépendance de fichier.",
        "tais": "Le nettoyage de données réelles : valeurs manquantes, encodage, et surtout le déséquilibre de classes.",
        "ref": "2.12-Donnees-Desequilibrees.ipynb"},
    2: {"titre": "Approche technique",
        "couvert": "Arbre de décision et régression linéaire, workflow fit/predict/evaluate explicite, Pipeline anti-fuite (section 6).",
        "tais": "Le CHOIX de l'approche : pourquoi un arbre plutôt qu'une forêt ou un k-NN ? Aucune comparaison d'alternatives n'est exécutée.",
        "ref": "2.4-Arbres-Forets-Ensembles.ipynb"},
    3: {"titre": "Fiabilité",
        "couvert": f"Holdout stratifié + mise en garde explicite contre l'argmax fragile (section 4) ; l'écart de généralisation de l'arbre de profondeur 3 est mesuré : {ecart_arbre:.3f}.",
        "tais": "Une seule découpe train/test : ni validation croisée, ni intervalle de confiance sur la métrique.",
        "ref": "2.5-Biais-Variance-CV-ROC.ipynb ; 2.5b-Calibration-Probabilites.ipynb"},
    4: {"titre": "Intelligibilité",
        "couvert": "Métriques définies (accuracy, MAE) et courbes train/test lisibles ; mais fit() reste une boîte noire appelée, pas comprise.",
        "tais": "Pourquoi un arbre prédit ce qu'il prédit : importance des variables, SHAP, LIME, contrefactuels.",
        "ref": "2.14-Explicabilite-SHAP-LIME-Contrefactuels.ipynb"},
    5: {"titre": "Objectifs clairs",
        "couvert": "La tâche et la métrique sont fixées AVANT l'évaluation (classification -> accuracy, régression -> MAE) : pas de métrique choisie après coup.",
        "tais": "La dérivation du besoin métier vers la tâche ML : qui consomme la prédiction, pour quelle décision, à quel coût d'erreur ? Ce point vit au cadrage du projet, pas dans cette série technique.",
        "ref": "(hors série 02 : cadrage projet)"},
    6: {"titre": "Tolérance aux échecs",
        "couvert": f"Le balayage de complexité rend l'échec VISIBLE : l'accuracy test culmine puis perd {chute_test:.3f} entre son sommet et la profondeur 25.",
        "tais": "L'analyse systématique des erreurs : quels exemples sont mal classés, forment-ils des segments cohérents ?",
        "ref": "2.13-Analyse-Erreurs.ipynb"},
    7: {"titre": "Implications éthiques, juridiques, sociétales",
        "couvert": "Rien — et c'est un fait : données synthétiques, pas de personnes, pas de biais de collecte possible.",
        "tais": "Sur des données réelles, ce point serait le plus exigeant : équité entre sous-groupes, impact d'un faux négatif selon le profil.",
        "ref": "2.5c-Equite-Sous-Groupes.ipynb"},
}

with pd.option_context("display.max_colwidth", None):
    display(grille_contexte(cas_21))
couvert ici tenu silencieux où le voir
point
1. Données traitables Jeu synthétique déterministe (500 obs., 8 caractéristiques, classes quasi équilibrées : 0.52 pour la majoritaire) — reproductible, sans dépendance de fichier. Le nettoyage de données réelles : valeurs manquantes, encodage, et surtout le déséquilibre de classes. 2.12-Donnees-Desequilibrees.ipynb
2. Approche technique Arbre de décision et régression linéaire, workflow fit/predict/evaluate explicite, Pipeline anti-fuite (section 6). Le CHOIX de l'approche : pourquoi un arbre plutôt qu'une forêt ou un k-NN ? Aucune comparaison d'alternatives n'est exécutée. 2.4-Arbres-Forets-Ensembles.ipynb
3. Fiabilité Holdout stratifié + mise en garde explicite contre l'argmax fragile (section 4) ; l'écart de généralisation de l'arbre de profondeur 3 est mesuré : 0.101. Une seule découpe train/test : ni validation croisée, ni intervalle de confiance sur la métrique. 2.5-Biais-Variance-CV-ROC.ipynb ; 2.5b-Calibration-Probabilites.ipynb
4. Intelligibilité Métriques définies (accuracy, MAE) et courbes train/test lisibles ; mais fit() reste une boîte noire appelée, pas comprise. Pourquoi un arbre prédit ce qu'il prédit : importance des variables, SHAP, LIME, contrefactuels. 2.14-Explicabilite-SHAP-LIME-Contrefactuels.ipynb
5. Objectifs clairs La tâche et la métrique sont fixées AVANT l'évaluation (classification -> accuracy, régression -> MAE) : pas de métrique choisie après coup. La dérivation du besoin métier vers la tâche ML : qui consomme la prédiction, pour quelle décision, à quel coût d'erreur ? Ce point vit au cadrage du projet, pas dans cette série technique. (hors série 02 : cadrage projet)
6. Tolérance aux échecs Le balayage de complexité rend l'échec VISIBLE : l'accuracy test culmine puis perd 0.053 entre son sommet et la profondeur 25. L'analyse systématique des erreurs : quels exemples sont mal classés, forment-ils des segments cohérents ? 2.13-Analyse-Erreurs.ipynb
7. Implications éthiques, juridiques, sociétales Rien — et c'est un fait : données synthétiques, pas de personnes, pas de biais de collecte possible. Sur des données réelles, ce point serait le plus exigeant : équité entre sous-groupes, impact d'un faux négatif selon le profil. 2.5c-Equite-Sous-Groupes.ipynb

Lecture de la grille : les trous sont le résultat

Deux lignes parlent plus que les autres. Le point 7 est vide — et c’est honnête : sur des données synthétiques, il n’y a personne à léser ; mais le réflexe doit être pris AVANT les données réelles, où ce point devient le plus exigeant (2.5c-Equite-Sous-Groupes). Le point 5 n’est couvert qu’à moitié : fixer la métrique avant l’évaluation est une discipline que ce notebook tient ; la dérivation du besoin métier vers la tâche ML ne vit dans aucun notebook de la série — c’est un cadrage de projet, pas un geste technique.

La grille ne dit pas que ce notebook est incomplet : elle dit où continuer — 2.12 pour les données réelles déséquilibrées, 2.5 pour la fiabilité multi-découpes, 2.13 pour l’analyse d’erreurs, 2.14 pour l’intelligibilité.

Exercice 4 : la grille sur le second cas de ce notebook

Contexte. La section 5 a exécuté un second cas : la régression (make_regression, LinearRegression, MAE). La grille s’applique à tout projet — y compris à ce cas-là.

Objectif. Construisez cas_regression : le dictionnaire des sept points appliqués au cas régression. Deux lignes au minimum doivent changer réellement par rapport à cas_21 (la métrique n’est plus l’accuracy ; il n’y a pas de stratify sur une cible continue). Rendez ensuite la grille avec grille_contexte(cas_regression).

Indices.

  • Étape 1 : partir de la structure de cas_21 (mêmes clés 1..7, mêmes champs titre/couvert/tais/ref).
  • Indice : pour le point 1, mesurez par exemple l’étendue de la cible : float(yr.max() - yr.min()).
  • Indice : pour le point 3, le holdout de la régression n’est PAS stratifié — pourquoi ?
# Exercice 4 : grille appliquée au cas régression de la section 5
# TODO etudiant : construire le dict des 7 points pour le cas (Xr, yr, modele_reg, mae)
cas_regression = None  # TODO etudiant : remplacer (dict {1..7: {"titre", "couvert", "tais", "ref"}})

if cas_regression is None:
    print("Exercice 4 a completer : appliquer la grille au cas regression (metrique, donnees, fiabilite...)")
else:
    print(grille_contexte(cas_regression))
Exercice 4 a completer : appliquer la grille au cas regression (metrique, donnees, fiabilite...)

Après le parcours — question de transfert

question(
    "Vous estimez une performance par validation croisée, avec une standardisation des variables. Où placer le `StandardScaler` ?",
    choix=[
        "Sur le jeu complet, avant la boucle de validation",
        "Dans le `Pipeline`, pour que chaque pli apprenne ses propres statistiques",
        "Peu importe : le score obtenu est le même",
    ],
    bonne="B",
    explication="Hors du Pipeline, les statistiques voient les plis de validation et le score remonte artificiellement. Dans le Pipeline, la transformation est réapprise à chaque pli, et le score mesure ce qu'il prétend mesurer.",
    reponse=None,  # remplacez None par la lettre de votre choix, puis re-executez
    moment="apres",
)
Transfert — Vous estimez une performance par validation croisée, avec une standardisation des variables. Où placer le `StandardScaler` ?

  A. Sur le jeu complet, avant la boucle de validation
  B. Dans le `Pipeline`, pour que chaque pli apprenne ses propres statistiques
  C. Peu importe : le score obtenu est le même

Réponse non donnée. Remplacez `reponse=None` par la lettre de votre choix dans cette cellule, puis ré-exécutez-la pour afficher la correction.

Conclusion et transition

Récapitulatif.

  • Le workflow canonique : train_test_split → fit → predict → evaluate.
  • On sépare train et test pour estimer honnêtement la généralisation.
  • Le surapprentissage devient visible quand on balaye la complexité : l’écart train/test s’élargit.
  • Le choix de la métrique dépend de la tâche : accuracy pour la classification, MAE pour la régression.
  • Le Pipeline protège contre les fuites de données.

Transition. Le prochain notebook (2.2-Descente-de-gradient) ouvre la boîte noire : au lieu d’appeler fit(), nous implémenterons nous-mêmes l’optimisation qui fait apprendre un modèle.

References

  1. Mitchell, T. (1997). Machine Learning. McGraw-Hill. — Le surapprentissage et la distinction entraînement/généralisation comme concept central de l’apprentissage supervisé.
  2. Stone, M. (1974). Cross-Validatory Choice and Assessment of Statistical Predictions. Journal of the Royal Statistical Society. Series B (Methodological) 36(2):111-147. — Le principe d’évaluation sur des données non vues.
  3. Breiman, L., Friedman, J., Olshen, R. & Stone, C. (1984). Classification and Regression Trees. Wadsworth. — Les arbres CART et la profondeur comme contrôle de complexité.
  4. Hastie, T., Tibshirani, R. & Friedman, J. (2009). The Elements of Statistical Learning. Springer (2e éd.). — Compromis biais-variance, surapprentissage, critères de perte pour la régression.
  5. Pedregosa, F. et al. (2011). Scikit-learn: Machine Learning in Python. Journal of Machine Learning Research 12:2825-2830. — La bibliothèque scikit-learn utilisée dans tout ce notebook (train_test_split, DecisionTreeClassifier, LinearRegression, métriques, Pipeline).
Retour au sommet