Aller au contenu
English

Ajuster les transformations correctement

  • Leçon 15
  • 55 min
  • Module 5 : Préparer la modélisation sans risque

Ce que vous saurez faire

Préparez les caractéristiques d’un modèle sans laisser les données de validation ou de test contaminer l’entraînement. Mesurez ensuite l’effet d’une fuite sur un score pour reconnaître un score trop beau pour être vrai.

Carnet JupyterFaites cette leçon dans un carnet JupyterColab · Kaggle · GitHub · en anglais

Le carnet (notebook) Jupyter contient tous les scripts, tâches et vérifications de cette leçon, avec les consignes. Il reproduit aussi tous les chiffres sur les fuites présentés ici. Ses cellules de préparation installent pandas et scikit-learn, téléchargent les données d’exercice et recréent la séparation de la leçon 2. Il fonctionne donc sans le projet local. Lisez les explications ici et exécutez le code dans le carnet.

Le carnet est en anglais. Colab et Kaggle ouvrent votre propre copie. Il faut un compte Google ou Kaggle gratuit. Vous êtes bloqué ? Consultez le carnet de solution.

Avant de commencerCe qu’il faut savoir et avoir sous la mainEnviron 55 minutes

Ce que vous devez savoir

  • Les données d’entraînement, de validation et de test et la séparation temporelle, présentées dans Choisir une séparation.
  • Les colonnes qui causent des fuites et pourquoi, présentées dans Caractéristiques et moment de la cible.
  • Le sens d’une valeur manquante dans chaque colonne, présenté dans Traiter les valeurs désordonnées. order_value manque pour les billets de soutien liés au compte (aucune commande) et les lignes « unknown » (inconnues). customer_tenure_days manque pour les visiteurs et les clients supprimés.

Ce qu’il vous faut sur votre ordinateur (seulement pour le parcours local; le carnet n’en a pas besoin)

Testé sur macOS avec Python 3.14.6, pandas 3.0.6 et scikit-learn 1.9.1 (numpy 2.5.3). Les commandes Windows n’ont pas été testées. Le carnet a été testé dans Jupyter.

scikit-learn est la bibliothèque Python la plus courante pour l’apprentissage automatique classique. Ici, vous utilisez ses outils de prétraitement et un modèle simple. Installez la version utilisée dans le cours et ajoutez-la à requirements.txt :

Terminal
python -m pip install scikit-learn==1.9.1

Ouvrez ensuite requirements.txt et ajoutez une ligne pour qu’il contienne les deux paquets :

requirements.txt
pandas==3.0.6
scikit-learn==1.9.1

Vérifiez l’installation :

Terminal
python -c "import sklearn; print(sklearn.__version__)"
Sortie
1.9.1

Si vous voyez ModuleNotFoundError: No module named 'sklearn' (aucun module nommé « sklearn »), l’environnement n’est pas actif. Activez .venv et refaites l’installation.

Un modèle a besoin de nombres sans valeurs manquantes. Les caractéristiques passent donc d’abord par le prétraitement. Ces étapes remplissent les valeurs manquantes, placent les nombres sur une échelle semblable et convertissent les catégories en nombres. Chaque étape comprend deux parties :

  • L’ajustement (fit) apprend des nombres à partir des données. Un outil d’imputation par la médiane apprend la médiane de chaque colonne. (L’imputation remplace une valeur manquante par une autre valeur; vous l’avez choisie au module 4.) Un outil de mise à l’échelle apprend la moyenne et l’écart-type.
  • La transformation (transform) utilise les nombres appris pour modifier les données. L’outil d’imputation remplit chaque valeur manquante avec la médiane apprise. L’outil de mise à l’échelle calcule (valeur − moyenne) ÷ écart-type.

Voici un petit exemple : cinq valeurs d’entraînement de order_value, dont une manquante, et deux valeurs de test.

tiny_fit.py
import pandas as pd
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import StandardScaler
train = pd.DataFrame({"order_value": [20.0, 40.0, None, 60.0, 100.0]})
test = pd.DataFrame({"order_value": [None, 400.0]})
imputer = SimpleImputer(strategy="median")
imputer.fit(train)
print("median learned from train:", imputer.statistics_)
scaler = StandardScaler()
scaler.fit(imputer.transform(train))
print("mean and SD learned from train:", scaler.mean_.round(2), scaler.scale_.round(2))
print("test after transform:", scaler.transform(imputer.transform(test)).round(2).ravel())
Sortie
median learned from train: [50.]
mean and SD learned from train: [54.] [26.53]
test after transform: [-0.15 13.04]

Vérifiez à la main. Les valeurs d’entraînement connues sont 20, 40, 60 et 100. La médiane est donc (40 + 60) ÷ 2 = 50. Après l’imputation, la moyenne est (20 + 40 + 50 + 60 + 100) ÷ 5 = 54. La valeur de test manquante devient 50, et (50 − 54) ÷ 26,53 = −0,15. La valeur 400 devient (400 − 54) ÷ 26,53 = 13,04. Elle dépasse largement toutes les valeurs d’entraînement, et la valeur transformée le montre.

Voici maintenant l’erreur : ajuster sur les données d’entraînement et de test ensemble. Modifiez les deux appels à fit (ajuster) pour utiliser pd.concat([train, test]). Affichez ensuite les nombres appris et les valeurs d’entraînement transformées :

Sortie
median: [60.] mean and SD: [105.71] [122.22]
train after transform: [-0.7 -0.54 -0.37 -0.37 -0.05]

Une valeur de test a modifié tous les nombres appris. La médiane, la moyenne et l’écart-type contiennent maintenant de l’information des données de test, avant le test. C’est une fuite par le prétraitement. La règle : ajustez sur les données d’entraînement seulement. Transformez ensuite les données d’entraînement, de validation et de test avec les mêmes nombres appris.

train.csv, 927 billets de janvier à avril, passe à l'ajustement, qui apprend seulement des données d'entraînement, jamais de valid ou test. L'ajustement produit le prétraitement ajusté : médianes, moyennes, écarts-types et liste des catégories. Le prétraitement ajusté transforme train, valid (227 billets, mai) et test (215 billets, juin). Le modèle est ajusté sur les données d'entraînement transformées. Les données de validation transformées donnent le score de validation, utilisé pour choisir et améliorer. Les données de test transformées donnent le score de test, une seule fois, à la fin.
Seules les données d'entraînement passent à l'ajustement. Les données de validation et de test sont seulement transformées avec ce que l'ajustement a appris des données d'entraînement.
Description textuelle du diagramme

Le fichier d’entraînement, avec 927 billets de janvier à avril, est la seule entrée de l’ajustement. L’ajustement apprend les médianes, les moyennes, les écarts-types et la liste des catégories : c’est le prétraitement ajusté. Ce prétraitement transforme ensuite trois ensembles de données : train, valid (227 billets de mai) et test (215 billets de juin). Le modèle est ajusté sur les données d’entraînement transformées. Le modèle et les données de validation transformées donnent le score de validation, qui sert à choisir et à améliorer. Le modèle et les données de test transformées donnent le score de test, une seule fois, à la fin. Aucune flèche ne va de valid ou test vers l’ajustement.

Il est facile d’oublier la règle dans un long script. Un pipeline réunit les étapes de prétraitement et le modèle dans un seul objet. pipeline.fit(train) ajuste chaque étape sur les lignes d’entraînement. pipeline.predict_proba(valid) transforme seulement les lignes de validation et donne des scores. Le code fait respecter la règle; vous n’avez pas à vous en souvenir.

Les caractéristiques de Mei sont de deux types. Le prétraitement comporte donc deux branches. Un ColumnTransformer envoie chaque liste de colonnes à ses propres étapes :

Colonnes Étapes Ce que l’ajustement apprend
Catégories : channel, team, segment, region OneHotEncoder(handle_unknown="ignore") La liste des catégories dans chaque colonne.
Nombres : priority, order_value, word_count, customer_tenure_days, prior_tickets_90d, created_hour SimpleImputer(strategy="median", add_indicator=True), puis StandardScaler() La médiane et, après l’imputation, la moyenne et l’écart-type de chaque colonne.

L’encodage à chaud (one-hot encoding) transforme une colonne de catégories en plusieurs colonnes de 0 et de 1. Il crée une colonne pour chaque catégorie vue pendant l’ajustement. Un billet de clavardage reçoit 1 dans channel_chat et 0 dans les autres colonnes de canal.

add_indicator=True ajoute une colonne de 0 et de 1 pour chaque colonne numérique ayant des valeurs manquantes dans l’entraînement. Le 1 indique que la valeur manquait. Au module 4, vous avez vu qu’un order_value manquant a un sens (un billet lié au compte, aucune commande). L’indicateur conserve ce sens après le remplacement de la valeur manquante par la médiane.

Priya, la scientifique des données, demande à Mei une vérification rapide avant de commencer à travailler sur les modèles. La séparation est-elle utilisable? Les données contiennent-elles un signal? Mei écrit une courte vérification avec un modèle simple, une régression logistique. C’est une vérification des données, pas un produit. Le cours Apprentissage automatique : du problème à un modèle fiable entraîne et compare des modèles.

scripts/model_check.py
"""Check the prepared data with a simple model: fit on train only, score on
valid. A check for the data, not a model for Larkfield (C05 trains models)."""
import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler
CATEGORIES = ["channel", "team", "segment", "region"]
NUMBERS = ["priority", "order_value", "word_count", "customer_tenure_days", "prior_tickets_90d", "created_hour"]
TARGET = "escalated_72h"
def build_pipeline():
numbers = Pipeline([
("impute", SimpleImputer(strategy="median", add_indicator=True)),
("scale", StandardScaler()),
])
prepare = ColumnTransformer([
("categories", OneHotEncoder(handle_unknown="ignore"), CATEGORIES),
("numbers", numbers, NUMBERS),
])
return Pipeline([("prepare", prepare), ("model", LogisticRegression(max_iter=1000))])
def main():
train = pd.read_csv("data/model/train.csv")
valid = pd.read_csv("data/model/valid.csv")
pipeline = build_pipeline()
pipeline.fit(train[CATEGORIES + NUMBERS], train[TARGET])
learned = pipeline.named_steps["prepare"].named_transformers_["numbers"]
medians = learned["impute"].statistics_
print("Medians learned from train:", {name: float(m) for name, m in zip(NUMBERS, medians)})
channels = pipeline.named_steps["prepare"].named_transformers_["categories"].categories_[0]
print("Channels learned from train:", [str(c) for c in channels])
scores = pipeline.predict_proba(valid[CATEGORIES + NUMBERS])[:, 1]
print("ROC AUC on valid:", round(roc_auc_score(valid[TARGET], scores), 3))
if __name__ == "__main__":
main()
Lignes Pourquoi
CATEGORIES, NUMBERS, TARGET Des listes explicites. Les identifiants et les dates en sont exclus et ne peuvent donc pas devenir des caractéristiques par accident.
build_pipeline() Le prétraitement et le modèle dans un objet, créé par une fonction. Chaque utilisation reçoit les mêmes étapes.
pipeline.fit(train[...], train[TARGET]) Le seul fit du script, qui reçoit uniquement les lignes d’entraînement.
named_steps[...], named_transformers_[...] Inspectent le pipeline ajusté pour voir ce que l’ajustement a appris.
pipeline.predict_proba(valid[...])[:, 1] Transforme les lignes de validation avec les nombres appris à l’entraînement, puis donne à chaque billet un score entre 0 et 1.
roc_auc_score(...) Un nombre qui mesure à quel point les scores classent bien les billets.

L’aire sous la courbe ROC (ROC AUC) en une phrase : la probabilité qu’un billet transféré à un niveau supérieur, choisi au hasard, obtienne un score plus élevé qu’un autre billet choisi au hasard (0,5 correspond au hasard; 1,0, à un classement parfait). Le cours d’apprentissage automatique l’explique en détail.

Terminal
python scripts/model_check.py
Sortie
Medians learned from train: {'priority': 2.0, 'order_value': 86.21, 'word_count': 88.0, 'customer_tenure_days': 1488.5, 'prior_tickets_90d': 3.0, 'created_hour': 14.0}
Channels learned from train: ['chat', 'email', 'phone', 'unknown', 'web_form']
ROC AUC on valid: 0.673

0,673 est un score modeste : meilleur que le hasard, loin d’être parfait. C’est honnête. Le modèle apprend sur janvier à avril et est testé sur mai, après le changement de politique. Quand Priya a terminé ses choix, elle ajuste le même pipeline une fois sur les données d’entraînement et de validation ensemble. Elle évalue ensuite juin une seule fois : 0,761. Jusque-là, personne ne regarde les données de test.

Grace dit que Larkfield répondra bientôt aux messages sur les médias sociaux. Le premier billet avec channel = social arrivera, et aucun billet d’entraînement n’a ce canal. Que fait l’encodeur?

new_channel.py
import pandas as pd
from sklearn.preprocessing import OneHotEncoder
train = pd.read_csv("data/model/train.csv")
new_ticket = pd.DataFrame({"channel": ["social"]})
strict = OneHotEncoder().fit(train[["channel"]])
try:
strict.transform(new_ticket)
except ValueError as error:
print("ValueError:", error)
tolerant = OneHotEncoder(handle_unknown="ignore").fit(train[["channel"]])
print(tolerant.get_feature_names_out())
print(tolerant.transform(new_ticket).toarray())
print(tolerant.transform(pd.DataFrame({"channel": ["chat"]})).toarray())
Sortie
ValueError: Found unknown categories ['social'] in column 0 during transform
['channel_chat' 'channel_email' 'channel_phone' 'channel_unknown'
'channel_web_form']
[[0. 0. 0. 0. 0.]]
[[1. 0. 0. 0. 0.]]

Avec le réglage par défaut, une nouvelle catégorie arrête la transformation avec une erreur. En utilisation réelle, le service d’acheminement échouerait au premier billet provenant des médias sociaux. Avec handle_unknown="ignore", ce billet reçoit 0 dans chaque colonne de canal : « aucun des canaux que je connais ». Le modèle lui donne quand même un score à partir des autres caractéristiques. Ne « corrigez » pas cela en ajustant l’encodeur sur toutes les lignes. Cela couvre les catégories de votre fichier actuel, pas la prochaine nouvelle catégorie. Au module 6, vous ajoutez une vérification qui compte les catégories inconnues dans les nouvelles données.

« Ajuster le prétraitement sur toutes les données »

Section intitulée « « Ajuster le prétraitement sur toutes les données » »

Une erreur courante consiste à préparer toute la table, puis à la séparer. Les médianes, moyennes et écarts-types viennent alors de toutes les lignes, y compris celles de validation et de test. À quel point cela change-t-il le score ici? Mei le mesure. Elle ajuste le prétraitement sur les 1 369 lignes et le modèle sur l’entraînement seulement, comme le fait cette erreur :

fit_on_all.py
import sys
import pandas as pd
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score
sys.path.insert(0, "scripts")
from model_check import CATEGORIES, NUMBERS, TARGET, build_pipeline
train = pd.read_csv("data/model/train.csv")
valid = pd.read_csv("data/model/valid.csv")
test = pd.read_csv("data/model/test.csv")
everything = pd.concat([train, valid, test])
features = CATEGORIES + NUMBERS
# The mistake: fit the preprocessing on every row, then the model on train.
prepare = build_pipeline().named_steps["prepare"]
prepare.fit(everything[features])
model = LogisticRegression(max_iter=1000).fit(prepare.transform(train[features]), train[TARGET])
scores = model.predict_proba(prepare.transform(valid[features]))[:, 1]
print("Median prior_tickets_90d:", float(prepare.named_transformers_["numbers"]["impute"].statistics_[4]))
print("ROC AUC on valid:", round(roc_auc_score(valid[TARGET], scores), 4))
# The correct way, for comparison, with four decimals.
pipeline = build_pipeline().fit(train[features], train[TARGET])
print("Train only, ROC AUC on valid:", round(roc_auc_score(valid[TARGET], pipeline.predict_proba(valid[features])[:, 1]), 4))
Sortie
Median prior_tickets_90d: 5.0
ROC AUC on valid: 0.6726
Train only, ROC AUC on valid: 0.6728

La médiane apprise de prior_tickets_90d passe de 3 à 5, mais le score ne change qu’à la quatrième décimale. Soyez honnête à ce sujet : sur ces données, ajuster l’imputation par la médiane et la mise à l’échelle sur toutes les lignes change très peu le score. La médiane ou la moyenne d’une caractéristique contient très peu d’information sur la cible.

Corrigez quand même l’erreur. Son effet augmente avec les petits jeux de données, les catégories rares et les étapes qui apprennent de la cible. Un flux de travail ajusté sur toutes les lignes aujourd’hui finira un jour par ajuster quelque chose de plus puissant. Les fortes hausses viennent des caractéristiques et des agrégats qui causent des fuites. Le carnet de la leçon les mesure avec la même séparation et le même pipeline :

Caractéristiques (ajustement sur l’entraînement, évaluation sur mai) ROC AUC sur valid
Les 10 caractéristiques honnêtes 0,673
+ priority_now (fixé à 1 quand un billet est transféré à un niveau supérieur) 0,964
+ first_reply_minutes (connu après la première réponse) 0,856
+ le taux de transfert à un niveau supérieur de chaque client, calculé sur toutes les lignes 0,767
Caractéristiques honnêtes, prétraitement ajusté sur toutes les lignes 0,6726

La dernière fuite est la plus difficile à repérer. Le taux semble être un fait sur le client. Mais la valeur d’un billet de mai inclut sa propre cible et les cibles des billets ultérieurs. Le modèle y lit une partie de la réponse.

Le carnet de la leçon reproduit chaque nombre du tableau ci-dessus pour que vous voyiez chaque fuite se produire. L’encadré en haut de cette page contient les liens Colab et Kaggle. Le carnet réintègre par jointure les deux colonnes causant des fuites depuis les billets nettoyés, pour les expériences.

  1. Ouvrez le carnet dans Colab ou Kaggle. Pour l’exécuter sur votre ordinateur, consultez l’encadré sous les étapes.
  2. Exécutez les cellules de préparation. Vous devriez voir pandas 3.0.6 et scikit-learn 1.9.1, puis split.py: done (terminé).
  3. Dans la section 1, vérifiez à la main la médiane 50, la moyenne 54 et la valeur −0,15.
  4. Dans la section 4, le prétraitement est ajusté sur toutes les lignes. Prédisez d’abord l’effet. Vous devriez voir 0,6726 et 0,6728.
  5. Dans la section 5, écrivez votre prédiction pour priority_now avant d’exécuter la cellule. Vous devriez voir 0,964.
  6. Faites les tâches de la section 8 : exercice 1 et exercice 2. Chaque cellule de vérification affiche Check passed. (vérification réussie) quand votre réponse est correcte.
  7. Redémarrez et exécutez tout. Chaque cellule doit s’exécuter, et chaque cellule de vérification doit afficher Check passed.

À vous de jouer : la moyenne plutôt que la médiane

Section intitulée « À vous de jouer : la moyenne plutôt que la médiane »

Changez une chose dans scripts/model_check.py : mettez strategy="mean" dans le SimpleImputer. Avant de l’exécuter, prédisez : la valeur qui remplace un order_value manquant va-t-elle augmenter ou diminuer? Le score changera-t-il beaucoup? Exécutez ensuite le script. (La première ligne dit encore « Medians » (médianes), mais elle affiche maintenant les moyennes.) Remettez la stratégie à "median" à la fin.

Afficher la réponse
Sortie
Medians learned from train: {'priority': 2.348435814455232, 'order_value': 120.30397741530741, 'word_count': 107.33549083063646, 'customer_tenure_days': 1427.0087912087913, 'prior_tickets_90d': 5.858683926645091, 'created_hour': 13.627831715210355}
Channels learned from train: ['chat', 'email', 'phone', 'unknown', 'web_form']
ROC AUC on valid: 0.672

La valeur de remplacement pour order_value augmente, passant de 86,21 à 120,30. Quelques grosses commandes font monter la moyenne, mais pas la médiane. Le score change à peine (0,672 au lieu de 0,673). La colonne indicatrice marque encore chaque valeur remplacée, et le modèle peut en tirer de l’information. Le choix compte toujours pour le sens : un billet lié au compte « a » maintenant une commande de 120,30 $. Au module 4, vous avez décidé qu’une valeur de commande manquante n’est pas un nombre inconnu. La médiane avec un indicateur est donc le choix le plus honnête ici. Le score ne contredit pas ce choix.

Erreur courante

fit_transform sur les données de validation

Symptôme : Tomás écrit le prétraitement et le modèle comme des étapes séparées, sans pipeline. L’entraînement fonctionne. Il prépare ensuite les données de validation comme les données d’entraînement, et la prédiction échoue :

Tomás's code
prepare = build_pipeline().named_steps["prepare"]
train_x = prepare.fit_transform(train[features])
model = LogisticRegression(max_iter=1000).fit(train_x, train[TARGET])
valid_x = prepare.fit_transform(valid[features]) # fit again, on valid
model.predict_proba(valid_x)
Sortie
ValueError: X has 27 features, but LogisticRegression is expecting 28 features as input.

Cause : fit_transform (ajuster et transformer) sur les données de validation ajuste de nouveau le prétraitement, sur les billets de mai. Les billets de mai n’ont aucun client du segment unknown (inconnu, pour les comptes supprimés). L’encodeur a donc appris une catégorie de moins, et la table contient 27 colonnes au lieu de 28. L’erreur est le cas favorable. Si mai avait les mêmes catégories, le code s’exécuterait sans erreur. Mais la mise à l’échelle utiliserait la moyenne et l’écart-type de mai pour chaque colonne. La même valeur aurait un sens différent dans l’entraînement et la validation. Le score serait faux, sans signe visible.

Correction : utilisez fit ou fit_transform seulement sur les données d’entraînement; utilisez seulement transform (transformer) sur toutes les autres données. Mieux encore, utilisez un seul pipeline, comme dans model_check.py. Il n’y a alors qu’un appel à fit, qui reçoit les lignes d’entraînement.

Conséquence en travail d’IA : la même erreur en production est pire. Un service qui réajuste son prétraitement sur chaque nouveau lot de billets donne un sens différent à la même entrée chaque jour. Personne ne voit d’erreur.

Vérifiez votre compréhension

Nommez ce que l'ajustement apprend à chacune des trois étapes de prétraitement, et les lignes qu'il peut utiliser.

Afficher la réponse

SimpleImputer(strategy="median") apprend la médiane de chaque colonne numérique. StandardScaler apprend la moyenne et l’écart-type de chaque colonne après l’imputation. OneHotEncoder apprend la liste des catégories de chaque colonne catégorielle. Les trois peuvent apprendre uniquement à partir des lignes d’entraînement. Les lignes de validation et de test sont seulement transformées avec ce qui a été appris.

Vérifiez votre compréhension

Tomás ajoute une colonne hours_open, soit les heures entre created_at et closed_at. L'aire sous la courbe ROC de validation passe de 0,673 à 0,709. Expliquez pourquoi, et dites ce que vous verriez dans le vrai service d'acheminement.

Afficher la réponse

closed_at est enregistré à la fermeture du billet de soutien. Donc, hours_open n’est pas connu au moment de la prédiction. C’est une fuite de données. Les billets transmis à un niveau supérieur restent ouverts plus longtemps. Dans les données d’entraînement, leur médiane est d’environ 53 heures, contre 27 heures pour les autres. La colonne contient donc de l’information sur la cible, et le score augmente. Dans le service d’acheminement, un nouveau billet n’a pas de closed_at. La colonne est donc vide pour chaque billet. Le classement réel du modèle est moins bon que ce qu’indiquait son score de validation. Une petite hausse reste une fuite. Ce n’est pas l’ampleur du changement qui compte, mais le moment où la valeur est enregistrée.

L’exercice du module vous demande de planifier avant d’ajuster un modèle. Un plan écrit rend chaque décision sur les fuites visible. Un collègue peut ainsi la vérifier avant qu’elle entraîne des coûts.

Exemple détaillé : le plan de Mei pour le modèle de transmission à un niveau supérieur

Section intitulée « Exemple détaillé : le plan de Mei pour le modèle de transmission à un niveau supérieur »

Mei écrit son plan sur une page, avec les nombres de ce module :

Question Décision Preuve
Moment de la prédiction À la création du billet. Larkfield achemine les billets à leur arrivée.
Cible escalated_72h : un événement escalated (transmis à un niveau supérieur) dans les 72 heures après created_at. 188 lignes sur 1 369 (13,7 %).
Maturité de la cible Seulement les billets créés au plus tard le 28 juin 2026 à 6 h (instantané − 72 heures). 29 billets retirés et comptés.
Caractéristiques channel, team, priority, segment, region, order_value, word_count, customer_tenure_days, prior_tickets_90d, created_hour. Toutes connues à la création. Historique tiré uniquement des billets antérieurs.
Jamais des caractéristiques priority_now, first_reply_minutes, closed_at, événements de résultat, csat, tout taux par client calculé sur toutes les lignes. Enregistrés après la création, ou contiennent des cibles.
Séparation Par date : entraînement avant mai, validation en mai, test du 1er au 27 juin. 927 / 227 / 215 lignes; taux de 11,7 % / 19,8 % / 16,3 %; 77 des 89 clients de test dans les données d’entraînement.
Prétraitement Un seul pipeline : encodage disjonctif avec handle_unknown="ignore"; imputation par la médiane avec indicateurs; mise à l’échelle. Ajustement sur les données d’entraînement seulement. Médianes des données d’entraînement, par exemple 86,21 pour order_value.
Vérifications avant tout modèle Les comptes de lignes concordent; les dates ne se chevauchent pas; aucune colonne avec fuite dans la sortie; une seule caractéristique très forte signale une fuite possible. build_summary.json, split_manifest.json.

Grace a une nouvelle question. Quand un agent résout un billet, un modèle peut-il prédire si le client le rouvrira dans les 14 jours? Un chef d’équipe pourrait alors vérifier ces billets avant le retour du client. Copiez le tableau de Mei dans un fichier, reports/reopen-plan.md, et remplissez chaque ligne pour ce nouveau modèle. N’ajustez pas de modèle. Pour chaque décision, écrivez une phrase qui la justifie. Examinez bien le moment de la prédiction : ce n’est plus la création du billet.

Afficher la réponse modèle
Question Décision Justification
Moment de la prédiction Le moment de l’événement de résultat resolved (résolu), soit recorded_at. Le chef d’équipe agit quand le billet est résolu.
Cible reopened_14d : un événement reopened (rouvert) du même billet dans les 14 jours après cet événement resolved. Précis : quel événement, quelle fenêtre. Un billet résolu deux fois donne deux lignes, une par résolution.
Maturité de la cible Seulement les résolutions au plus tard le 1er juillet 2026 à 6 h − 14 jours = 17 juin 2026 à 6 h. Comptez les lignes retirées. Une résolution plus récente n’a pas encore ses 14 jours d’observation. Son résultat « aucune réouverture » n’est pas définitif.
Caractéristiques Les caractéristiques à la création, plus les faits connus à la résolution : first_reply_minutes, transmission à un niveau supérieur avant la résolution, resolution_code, heures entre la création et cette résolution, réouvertures antérieures de ce billet. Le moment de la prédiction a changé. first_reply_minutes causait une fuite pour le modèle de transmission à un niveau supérieur. Mais cette valeur est connue à la résolution, donc c’est une caractéristique valide ici. La fuite dépend du moment de la prédiction, pas de la colonne.
Jamais des caractéristiques csat (enregistré après la résolution), closed_at (pour un billet rouvert, c’est la fermeture finale, après la réouverture), priority_now (valeur à l’instantané, pas à la résolution), tout événement de résultat ultérieur, tout taux de réouverture par client calculé sur toutes les lignes. Chaque valeur est enregistrée après le moment de la prédiction ou contient des cibles.
Séparation Par date de résolution : par exemple, entraînement avant mai, validation en mai, test du 1er au 16 juin. Gardez toutes les résolutions d’un billet dans la même partie. Le modèle prédit les réouvertures futures à partir des réouvertures passées. Deux lignes d’un billet dans des parties différentes permettraient au modèle de voir son historique pendant l’entraînement.
Prétraitement Le même type de pipeline, ajusté sur les données d’entraînement seulement. resolution_code est une nouvelle colonne catégorielle, avec handle_unknown="ignore". Même règle; un nouveau code de résolution ne doit pas arrêter le service.
Vérifications avant tout modèle Lignes par partie et taux de réouverture par mois; aucun chevauchement de dates ou de billets entre les parties; pour chaque caractéristique, son enregistrement précède le recorded_at de la résolution. Le taux peut aussi changer après le 1er mai. La dernière vérification porte sur les fuites et se fait sur papier.

Votre réponse peut différer dans les détails, par exemple avec une autre fenêtre ou d’autres dates. Vérifiez trois choses : le moment de la prédiction est la résolution; csat et closed_at sont exclus; la maturité de la cible utilise le moment de la résolution, pas celui de la création.

Vérifiez votre compréhension

Rappel : donnez les trois questions qui permettent de choisir une séparation aléatoire, par date ou par groupe.

Afficher la réponse
  1. Le modèle prédira-t-il l’avenir à partir du passé? Alors, séparez par date.
  2. Le modèle rencontrera-t-il de nouveaux clients ou d’autres nouveaux groupes? Alors, gardez chaque groupe dans une seule partie.
  3. Les lignes sont-elles indépendantes, sans ordre temporel ni groupes répétés? Une séparation aléatoire constitue un test valide seulement dans ce cas.

Pour Larkfield, la réponse à la première question est oui. La séparation se fait donc par date.

Vérifiez votre compréhension

Prédisez et expliquez : Priya ajuste l'imputation et la mise à l'échelle sur les 1 369 lignes, plutôt que sur les données d'entraînement. Qu'arrive-t-il au score de validation sur ces données? Pourquoi vaut-il quand même la peine de corriger cela?

Afficher la réponse

Presque rien : 0,6726 au lieu de 0,6728. Une médiane, une moyenne et un écart-type contiennent très peu d’information sur la cible. Cela vaut quand même la peine de corriger le problème. Les nombres appris viennent du futur : la médiane de prior_tickets_90d passe de 3 à 5. L’effet augmente aussi avec peu de données, des catégories rares et des étapes qui apprennent à partir de la cible. Un pipeline ajusté sur les données d’entraînement ne coûte rien et évite tous ces problèmes.

Vérifiez votre compréhension

Vérification de l'exercice : dans l'exercice 2 du carnet Jupyter, une des trois nouvelles colonnes cause une fuite. Nommez-la, donnez les scores qui le montrent et expliquez comment elle a gonflé l'évaluation. Expliquez ensuite comment vous la trouveriez sans modèle.

Afficher la réponse

La colonne qui cause une fuite est reply_speed. Avec les caractéristiques sans fuite, l’aire sous la courbe ROC de validation est de 0,673. L’ajout de weekday donne 0,679 et celui de order_size, 0,687 : de petits changements. L’ajout de reply_speed donne 0,809.

Comment elle a gonflé l’évaluation : reply_speed vaut fast (rapide) quand first_reply_minutes est inférieur à 30. La première réponse arrive après la création du billet. Les agents expérimentés répondent rapidement aux billets transmis à un niveau supérieur. La colonne apporte donc une trace de la cible dans les lignes de validation. Le modèle classe ces billets plus haut à cause de cette information. Un nouveau billet n’a pas encore de première réponse. En utilisation réelle, la colonne n’a donc aucune valeur. Le classement réel est proche du score sans fuite de 0,673, et non de 0,809.

Sans modèle : demandez quand chaque valeur est enregistrée. reply_speed vient de first_reply_minutes, qui figure dans la liste des valeurs enregistrées après la création dans build_features.py. Un nouveau nom et une forme moins précise n’éliminent pas une fuite.

Une réponse complète nomme la colonne et compare son score au score sans fuite. Elle relie la fuite au moment où la valeur est enregistrée et décrit ce qui arrive en utilisation réelle.

Récapitulatif

Dans ce module, vous avez préparé les données pour un modèle sans fuites :

  • Chronologie : le moment de la prédiction est la création du billet. La cible escalated_72h est précise, et les 29 billets sans cible définitive sont retirés. Les caractéristiques viennent uniquement du passé. priority_now, first_reply_minutes, closed_at et les événements de résultat ne sont jamais des caractéristiques.
  • Séparation : par date, comme Larkfield utilisera le modèle. L’entraînement compte 927 lignes, la validation 227 et le test 215. Les taux de transmission à un niveau supérieur sont de 11,7 %, 19,8 % et 16,3 %. Une séparation aléatoire masque le changement de mai. Chaque séparation aléatoire est un seul tirage (de 0,665 à 0,827).
  • Ajustement : le prétraitement apprend uniquement des données d’entraînement, dans un seul pipeline. handle_unknown="ignore" empêche une nouvelle catégorie d’arrêter le modèle.
  • Preuves : les caractéristiques sans fuite donnent un score de 0,673. Les fuites le gonflent à 0,964 (priority_now), 0,856 (first_reply_minutes) et 0,767 (un taux par client sur toutes les lignes). L’ajustement du prétraitement sur toutes les lignes n’a changé que la quatrième décimale du score.

Ensuite, dans le module 6, vous écrirez les règles que chaque fichier livré doit respecter. La prochaine personne pourra alors faire confiance au jeu de données sans refaire vos vérifications.

Leçon suivante : Les règles de validation