Sélectionner les caractéristiques utiles
- Leçon 18
- 60 min
- Module 6 : Régler et finaliser le modèle
Leçon 18 sur 27 · Apprentissage automatique : du problème à un modèle fiableSélectionner les caractéristiques utiles
1. Comprendre l'apprentissage automatique
2. Cadrer le problème de prédiction
3. Évaluer les modèles supervisés
4. Créer les premiers modèles
5. Essayer d'autres familles de modèles
6. Régler et finaliser le modèle
7. Comprendre et choisir le modèle
8. Préparer la passation du modèle
Ce que vous saurez faire
Trouvez les colonnes qui améliorent le modèle et écartez celles qui donnent seulement cette impression. Une colonne qui révèle la réponse obtient le meilleur score à chaque vérification avant le lancement. Elle échoue seulement avec de vrais billets de soutien.
Carnet JupyterFaites cette leçon dans un carnet Jupyter
Le carnet (notebook) Jupyter contient toutes les cellules de code, tâches et vérifications de cette leçon, avec les consignes. Ses cellules de préparation installent les paquets, téléchargent les données d’exercice et écrivent ticket_model.py dans son état après le module 4. Vous le modifiez pour obtenir la version finale dans le carnet. Il fonctionne sans le projet local.
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 main
Ce que vous devez savoir
- La validation croisée sur les données d’entraînement, la moyenne ± l’écart-type des plis et pourquoi une différence inférieure à cette dispersion n’est pas réelle. Voir Données d’entraînement, de validation et de test : validation croisée.
- La précision moyenne, le score utilisé dans ce cours pour comparer les modèles. Voir Le compromis entre précision et rappel : la précision moyenne.
- Le journal d’expériences et ses fonctions utilitaires
log_experiment()etshow_log(). Voir L’apprentissage automatique est itératif : le journal d’expériences. Cette leçon ajoute les lignes E15 à E18. - Les courbes d’apprentissage, dans Contrôler la complexité.
- La fuite : une caractéristique inconnue au moment de la prédiction. Voir Caractéristiques et moment de la cible : ce que chaque colonne sait dans SQL et préparation des données pour l’IA.
Ce qu’il vous faut sur votre ordinateur (seulement pour le parcours local; le carnet n’en a pas besoin)
- Le dossier de projet
~/projects/ticket-modelavec son environnement.venvactif dans un terminal. Les carnets des leçons doivent être ouverts dans VS Code ou JupyterLab. Vous les avez créés dans Définir une référence de base : préparer votre projet. Sipython -c "import sklearn"échoue, activez de nouveau.venv(source .venv/bin/activatesur macOS et Linux,.venv\Scripts\Activate.ps1sur Windows). data/train.csv,data/valid.csv,data/test.csvetdata/extra_features.csv, obtenus avecpython get_data.pydans Définir une référence de base : préparer votre projet. Si un fichier manque ou a changé, exécutezpython get_data.py --reset.experiment_log.csvavec les lignes E01 à E14, provenant de Les méthodes d’ensemble. S’il manque, les cellules de préparation du carnet écrivent de nouveau les lignes.ticket_model.pyavec 6 nombres dansNUMBERS, provenant de Prétraiter avec un pipeline : écrire ticket_model.py. Cette leçon le modifie dans Modifier ticket_model.py. Si votre fichier ne fonctionne plus, recréez-le à partir de cette leçon. Vous pouvez aussi télécharger la version finale comme l’indique l’étape 8 de Modifier ticket_model.py.
Testé sur macOS avec Python 3.14.6, pandas 3.0.6 et scikit-learn 1.9.1. Les étapes pour Windows et Linux ne sont pas testées. Le carnet a été testé dans Jupyter.
Le corrigé sur le bureau
Section intitulée « Le corrigé sur le bureau »Une étudiante prépare un examen avec un livre d’exercices. Le corrigé se trouve à la fin du livre. Si elle lit la réponse avant de résoudre chaque exercice, elle réussit tous les exercices à la maison. Son score d’entraînement est parfait, mais il ne lui apprend rien. À l’examen, il n’y a pas de corrigé et son score baisse.
Un modèle peut faire la même chose. Si une de ses colonnes contient secrètement la réponse, le modèle apprend à lire cette colonne. Tous les scores calculés avec les mêmes données confirment ce résultat : entraînement, validation croisée, même le mois de validation. Seuls de vrais nouveaux billets révèlent la vérité. La colonne ne contient pas encore de réponse quand un nouveau billet arrive.
Le module 5 a essayé d’autres familles de modèles. Les arbres ont perdu contre la régression logistique. Les forêts et le boosting ont seulement égalé son résultat. Les courbes d’apprentissage ont montré que davantage de billets n’aideraient pas beaucoup non plus. La prochaine idée consiste donc à utiliser de meilleures informations : de nouvelles colonnes. Mei, l’ingénieure de données, en propose quatre. Chacune est une caractéristique candidate, une colonne qui pourrait devenir une caractéristique si elle réussit un test. Cette leçon présente ce test.
Vous avez déjà rencontré l’une des quatre. Le score à points du module 3 donne un point pour une escalade antérieure du client. Cette information vient de prior_escalations_90d. Le modèle ne l’a jamais utilisée. Elle doit maintenant justifier sa place dans le modèle, comme les trois autres.
Trois questions pour chaque candidate
Section intitulée « Trois questions pour chaque candidate »Une nouvelle colonne peut poser problème de trois façons. Elle peut contenir la réponse, comme le corrigé. Elle peut répéter une colonne déjà présente, comme une deuxième copie des mêmes notes. Ou elle peut ne rien ajouter de mesurable. Priya pose donc trois questions, dans cet ordre :
- Est-elle connue à la création du billet? Le modèle prédit à cet instant, le moment de la prédiction. Une valeur écrite plus tard n’est pas disponible à ce moment-là. Une colonne qui contient des informations postérieures au moment de la prédiction présente une fuite.
- Ajoute-t-elle des informations que le modèle n’a pas encore? Si elle répète une autre caractéristique, elle est redondante.
- La validation croisée sur les données d’entraînement montre-t-elle une amélioration supérieure au bruit? Sinon, écartez-la. Chaque colonne supplémentaire doit être recueillie, vérifiée et expliquée. Elle doit donc justifier sa place.
Chaque candidate appartient donc à l’un de trois types. Une candidate qui réussit les trois questions est utile. Celle qui échoue à la question 2 ou 3 est redondante : elle n’ajoute rien de mesurable. Généralement, elle répète une colonne que le modèle possède. Celle qui échoue à la question 1 présente une fuite : elle fait paraître le modèle meilleur qu’il ne peut l’être. On conserve seulement une candidate utile. Ce problème s’appelle une fuite : les caractéristiques contiennent des informations indisponibles pour le modèle au moment de la prédiction. Vous l’avez rencontré dans le cours de SQL, dans Caractéristiques et moment de la cible.
La question 1 vient en premier pour une raison. Les questions 2 et 3 utilisent des scores, et une fuite obtient toujours le meilleur score. Seule une question sur le moment permet de la repérer.
Huit billets, à la main
Section intitulée « Huit billets, à la main »Voici huit billets fictifs avec trois des candidates. priority est la priorité à la création du billet. Avant de lire les notes sous le tableau, essayez de répondre vous-même aux trois questions pour chaque candidate.
| Billet | priority | order_value | order_value_band | prior_escalations_90d | priority_now | escalated |
|---|---|---|---|---|---|---|
| T-01 | 2 | 35.00 | low | 1 | 1 | 1 |
| T-02 | 3 | 120.00 | mid | 0 | 3 | 0 |
| T-03 | 1 | 80.00 | mid | 2 | 1 | 1 |
| T-04 | 2 | 250.00 | high | 0 | 2 | 0 |
| T-05 | 3 | 45.00 | low | 1 | 1 | 1 |
| T-06 | 2 | (vide) | none | 0 | 2 | 0 |
| T-07 | 1 | 60.00 | mid | 0 | 1 | 0 |
| T-08 | 3 | 20.00 | low | 1 | 3 | 0 |
prior_escalations_90dcompte les escalades du client dans les 90 jours avant ce billet. Quatre billets en ont 1 ou plus (T-01, T-03, T-05, T-08), et 3 d’entre eux ont été escaladés. Quatre en ont 0, et aucun n’a été escaladé. Le compte utilise seulement les billets antérieurs. Il est donc connu à la création. Aucune autre colonne ne contient cette information. Utile.order_value_bandvautlow(faible) exactement quandorder_valueest inférieur à 50 (T-01, T-05, T-08). Elle vautnone(aucune) quand la valeur est vide, et ainsi de suite. Le modèle possède déjàorder_value. La tranche n’ajoute donc rien de nouveau. Redondante.priority_nowvaut 1 pour T-01, T-03, T-05 et T-07. Parmi ces 4 billets, 3 ont été escaladés. C’est aussi bon que la colonne d’historique. Les taux seuls ne montrent donc aucun problème. La question 1 le révèle. T-01 a été créé avec la priorité 2. Sa priorité est devenue 1 quand il a été escaladé. La colonne enregistre la réponse. Avec fuite.
La quatrième candidate, customer_escalation_rate, est la proportion des billets du client qui ont été escaladés. Son problème concerne les billets qu’elle compte. Vous le trouverez ci-dessous dans les données réelles.
L’illustration place chaque colonne sur une ligne du temps. Le modèle prédit à la création du billet, à la ligne bleue. Il peut utiliser seulement ce qui est écrit à gauche de cette ligne.
Description textuelle du diagramme
Une ligne du temps horizontale va de gauche à droite (de haut en bas sur un téléphone). Elle comporte quatre repères : 90 jours avant le billet, la création du billet, 72 heures plus tard et l’exportation des données. Une ligne bleue pointillée à « billet créé » indique que le modèle prédit à cet endroit. La zone de gauche porte la mention « Connu à la création du billet ». Celle de droite porte la mention « Écrit plus tard ».
La zone de gauche contient deux cases vertes. prior_escalations_90d se trouve au-dessus d’une accolade couvrant les 90 jours avant la création. Elle compte les escalades du client pendant cette période. priority se trouve au moment de la création. La zone de droite contient deux cases ambre. priority_now se trouve à « 72 heures plus tard ». Le service de soutien change la priorité quand il escalade un billet. customer_escalation_rate se trouve à « exportation des données ». Elle est calculée sur tous les billets étiquetés au moment de l’exportation.
Les quatre candidates dans les vrais billets
Section intitulée « Les quatre candidates dans les vrais billets »extra_features.csv contient les quatre candidates, avec une ligne par billet étiqueté. Le code de cette leçon commence avec les 10 caractéristiques de ticket_model.py, tel qu’écrit dans le module 4. Il nécessite aussi ces importations (la première cellule du carnet les contient toutes) :
import pandas as pdfrom sklearn.compose import ColumnTransformerfrom sklearn.impute import SimpleImputerfrom sklearn.linear_model import LogisticRegressionfrom sklearn.metrics import average_precision_score, roc_auc_scorefrom sklearn.model_selection import StratifiedKFold, cross_val_scorefrom sklearn.pipeline import Pipelinefrom sklearn.preprocessing import OneHotEncoder, StandardScaler
from ticket_model import CATEGORIES, NUMBERS, TARGET, build_pipeline, load
train, valid = load("train"), load("valid")cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=0)cv est la validation croisée du cours : 5 plis avec la même proportion d’escalades dans chaque pli. Le mélange fixe donne les mêmes plis à chaque exécution.
Joignez les candidates aux données d’entraînement et de validation par ticket_id. validate="one_to_one" arrête la fusion si un billet apparaît deux fois :
extra = pd.read_csv("data/extra_features.csv")candidates = train.merge(extra, on="ticket_id", how="left", validate="one_to_one")candidates_valid = valid.merge(extra, on="ticket_id", how="left", validate="one_to_one")print(len(extra), "rows in extra_features.csv; train after the merge:", len(candidates))print(candidates[["ticket_id", "prior_escalations_90d", "order_value_band", "customer_escalation_rate", "priority_now", TARGET]].head(4).to_string(index=False))La sortie :
19120 rows in extra_features.csv; train after the merge: 16939ticket_id prior_escalations_90d order_value_band customer_escalation_rate priority_now escalated_72h T-100001 0 mid 0.0000 3 0 T-100002 0 low 0.0541 1 0 T-100003 0 high 0.0333 3 0 T-100004 0 mid 0.2671 1 1Examinez maintenant chaque candidate avec une petite question.
Un historique plus chargé signifie-t-il un risque plus élevé? (5 signifie « 5 ou plus ».)
history = candidates["prior_escalations_90d"].clip(upper=5) # 5 means "5 or more"print(candidates.groupby(history)[TARGET].agg(["size", "mean"]).round(3)) size meanprior_escalations_90d0 9130 0.0911 3598 0.1192 1674 0.1443 902 0.1444 552 0.1525 1083 0.252Oui. Les billets des clients sans escalade récente sont escaladés dans 9,1 % des cas. Avec 5 escalades ou plus, ils sont escaladés dans 25,2 % des cas. La colonne compte seulement les billets antérieurs. Elle est donc connue à la création. Les invités ont toujours 0, car ils n’ont pas d’historique.
D’où viennent les tranches?
print(candidates.groupby("order_value_band")["order_value"].agg(["size", "min", "max"]))print("tickets with no order_value:", candidates["order_value"].isna().sum()) size min maxorder_value_bandhigh 2456 200.05 1450.00low 3791 4.50 49.98mid 8230 50.00 199.98none 2462 NaN NaNtickets with no order_value: 2462Les tranches ne se chevauchent pas : moins de 50, de 50 à 199,99, 200 et plus. La valeur none correspond aux 2 462 billets sans valeur de commande. La tranche correspond à order_value découpée en quatre parties.
Que dit priority_now sur les billets escaladés?
print(pd.crosstab(candidates["priority_now"], candidates[TARGET]))escalated_72h 0 1priority_now1 1831 19832 5140 03 7985 0Les 1 983 billets d’entraînement escaladés ont tous une valeur de 1 dans priority_now. Aucun billet avec 2 ou 3 n’a été escaladé. Le service de soutien fixe la priorité à 1 quand il escalade un billet, comme vous l’avez vu en SQL.
Sur quels billets calcule-t-on customer_escalation_rate? Vérifiez un client, C-00078 :
everything = pd.concat([load("train"), load("valid"), load("test")])customer = "C-00078"print("customer_escalation_rate in the file:", candidates.loc[candidates["customer_id"] == customer, "customer_escalation_rate"].iloc[0])print("rate over all labelled tickets: ", round(everything.loc[everything["customer_id"] == customer, TARGET].mean(), 4))print("rate over the train tickets only: ", round(train.loc[train["customer_id"] == customer, TARGET].mean(), 4))customer_escalation_rate in the file: 0.0959rate over all labelled tickets: 0.0959rate over the train tickets only: 0.0769La valeur du fichier est le taux calculé sur tous les billets étiquetés : 7 escalades sur 73 billets. Cela inclut mai et juin ainsi que la cible de chaque billet lui-même. Le 1er mai, Larkfield connaissait seulement les billets d’entraînement : 5 sur 65, soit 0,0769. La colonne contient donc le futur. C’est l’agrégat avec fuite de Ajuster les transformations correctement dans le cours de SQL, sous un nouveau nom.
Exemple détaillé : décider avec la validation croisée sur les données d’entraînement
Section intitulée « Exemple détaillé : décider avec la validation croisée sur les données d’entraînement »Tomás a une idée rapide : « Ajoutons les quatre colonnes et laissons le modèle décider lesquelles comptent. » Le problème, c’est que le modèle ne distingue pas une fuite d’une bonne caractéristique. Il voit seulement que priority_now prédit presque parfaitement la cible. Il fait donc surtout confiance à cette colonne. Le modèle choisira la fuite chaque fois. Les personnes doivent faire ce choix avec les trois questions.
Priya teste chaque candidate de la même façon : les 10 caractéristiques plus cette colonne, une régression logistique et les mêmes 5 plis d’entraînement. Deux fonctions utilitaires font le travail. pipeline_for() construit les mêmes étapes que build_pipeline(), pour d’autres listes de colonnes. cv_folds() renvoie la précision moyenne (AP) sur chaque pli :
def pipeline_for(categories, numbers): """The steps of build_pipeline(), for other lists of columns.""" number_steps = Pipeline([("impute", SimpleImputer(strategy="median", add_indicator=True)), ("scale", StandardScaler())]) prepare = ColumnTransformer([("categories", OneHotEncoder(handle_unknown="ignore"), categories), ("numbers", number_steps, numbers)]) return Pipeline([("prepare", prepare), ("model", LogisticRegression(max_iter=1000))])
def cv_folds(data, categories, numbers): """The AP on each of the 5 folds of train, for logistic regression with these columns.""" return cross_val_score(pipeline_for(categories, numbers), data[categories + numbers], data[TARGET], cv=cv, scoring="average_precision")order_value_band contient du texte. Elle va donc avec les catégories. Les trois autres sont des nombres :
tests = { "(nothing)": (CATEGORIES, NUMBERS), "prior_escalations_90d": (CATEGORIES, NUMBERS + ["prior_escalations_90d"]), "order_value_band": (CATEGORIES + ["order_value_band"], NUMBERS), "customer_escalation_rate": (CATEGORIES, NUMBERS + ["customer_escalation_rate"]), "priority_now": (CATEGORIES, NUMBERS + ["priority_now"]),}folds = {added: cv_folds(candidates, *columns) for added, columns in tests.items()}for added, fold_scores in folds.items(): gain = fold_scores - folds["(nothing)"] print(f"+ {added:25s} CV AP {fold_scores.mean():.3f} ± {fold_scores.std():.3f} gain per fold {gain.round(3)}")Avant de lire la sortie, faites une prédiction : quelle candidate apporte le plus? Quel résultat semble trop bon pour être vrai?
+ (nothing) CV AP 0.327 ± 0.012 gain per fold [0. 0. 0. 0. 0.]+ prior_escalations_90d CV AP 0.339 ± 0.016 gain per fold [0.01 0.01 0.005 0.025 0.013]+ order_value_band CV AP 0.328 ± 0.014 gain per fold [ 0.007 -0.005 0.004 0.001 0.001]+ customer_escalation_rate CV AP 0.445 ± 0.015 gain per fold [0.12 0.126 0.087 0.131 0.126]+ priority_now CV AP 0.851 ± 0.024 gain per fold [0.528 0.544 0.489 0.558 0.503]Priya lit les résultats en trois étapes.
D’abord, les candidates sans fuite. prior_escalations_90d ajoute 0,012 à la moyenne. C’est environ la taille de l’écart-type (0,012 à 0,016). Les moyennes seules ne permettent donc pas de conclure. Mais les cinq modèles utilisent les mêmes plis. Certains plis sont plus difficiles que d’autres pour tous les modèles. La majeure partie de la dispersion est donc commune. Une comparaison plus juste se fait pli par pli : le gain par pli. prior_escalations_90d aide sur les 5 plis, avec un gain de 0,005 à 0,025. Un gain sur chaque pli est une différence réelle. order_value_band aide sur quatre plis et nuit sur un, avec un gain moyen de 0,002. Ce n’est pas une différence réelle, comme on s’y attend d’une colonne qui répète order_value.
Ensuite, les bonds. Une colonne supplémentaire fait passer l’AP de 0,327 à 0,445, et une autre la fait passer à 0,851. Une caractéristique sans fuite l’a fait bouger d’environ 0,01. Dans tout le module 5, les arbres et les méthodes d’ensemble ne l’ont pas augmentée. « Trop bon pour être vrai » signale une fuite. C’est un signal, pas une preuve.
Enfin, la question 1 tranche. customer_escalation_rate utilise des cibles ultérieures. priority_now est définie par l’escalade elle-même. Aucune des deux n’est connue à la création d’un billet.
Le graphique montre les deux étapes. Le panneau supérieur place les cinq résultats sur une même échelle. Les deux fuites se trouvent donc loin des autres. Le panneau inférieur agrandit les deux candidates sans fuite et montre le gain sur chaque pli.
Description textuelle du graphique
Le panneau supérieur comporte cinq lignes sur un axe allant d’environ 0,3 à 0,9. Une ligne pointillée à 0,327 représente les 10 caractéristiques. Aucun ajout : 0,327 (0,315 à 0,339). Plus prior_escalations_90d : 0,339 (0,323 à 0,355). Plus order_value_band : 0,328 (0,314 à 0,342). Plus customer_escalation_rate : 0,445 (0,430 à 0,460). Plus priority_now : 0,851 (0,827 à 0,875). Les trois premières lignes se chevauchent près de la ligne pointillée. Les deux dernières, en orange, sont loin à droite.
Le panneau inférieur comporte deux lignes sur un axe allant d’environ −0,01 à +0,03, avec une ligne pointillée à zéro. Plus prior_escalations_90d : gain moyen de +0,013, de +0,005 à +0,025. Toute la barre est donc à droite de zéro. Plus order_value_band : gain moyen de +0,002, de −0,005 à +0,007. La barre traverse donc zéro.
Priya décide donc de conserver une candidate et d’en rejeter trois. Le tableau résume le type de chaque candidate et la raison de la décision :
| Ajout aux 10 caractéristiques | AP en validation croisée | Type | Décision |
|---|---|---|---|
| (aucun) | 0.327 ± 0.012 | ||
prior_escalations_90d |
0.339 ± 0.016 | Utile | conserver : connue à la création et utile sur chaque pli |
order_value_band |
0.328 ± 0.014 | Redondante | rejeter : elle répète order_value et le gain est inférieur au bruit |
customer_escalation_rate |
0.445 ± 0.015 | Avec fuite | rejeter : elle utilise des cibles ultérieures |
priority_now |
0.851 ± 0.024 | Avec fuite | rejeter : l’escalade la définit |
Rejeter une fuite n’est pas la même chose que rejeter une colonne redondante. Vous pourriez retester une colonne redondante un jour, par exemple avec un autre modèle. Une colonne avec fuite reste définitivement exclue. Elle ne peut jamais être connue au moment de la prédiction.
Confirmer sur le mois de validation
Section intitulée « Confirmer sur le mois de validation »La décision est prise, avec les données d’entraînement seulement. Priya la vérifie maintenant une fois sur mai, les données de validation :
for added, (categories, numbers) in tests.items(): pipeline = pipeline_for(categories, numbers).fit(candidates[categories + numbers], candidates[TARGET]) scores = pipeline.predict_proba(candidates_valid[categories + numbers])[:, 1] print(f"+ {added:25s} valid AP {average_precision_score(valid[TARGET], scores):.3f}" f" ROC AUC {roc_auc_score(valid[TARGET], scores):.3f}")+ (nothing) valid AP 0.353 ROC AUC 0.738+ prior_escalations_90d valid AP 0.391 ROC AUC 0.752+ order_value_band valid AP 0.358 ROC AUC 0.738+ customer_escalation_rate valid AP 0.485 ROC AUC 0.812+ priority_now valid AP 0.873 ROC AUC 0.981Omar demande : « Pourquoi ne pas choisir selon mai? C’est le mois le plus récent. » Deux raisons expliquent cet ordre : la validation croisée (CV) d’abord, puis mai :
- Mai doit rester une vérification indépendante. Si vous choisissez selon le score de mai, mai fait partie du choix. Essayez assez de candidats, et l’un d’eux gagnera sur mai par hasard. Il ne restera alors rien pour vous dire que c’était le hasard.
- Mai est petit. Il compte 183 escalades, donc son AP varie d’environ 0,03 par hasard. La CV utilise les 16 939 billets d’entraînement et montre la dispersion sur chaque partition.
Mai confirme le choix : prior_escalations_90d fait passer l’AP de validation de 0,353 à 0,391, et l’AUC ROC de 0,738 à 0,752. Mais regardez les fuites : elles semblent excellentes sur mai aussi, car l’information divulguée se trouve aussi dans les lignes de mai. Les données de validation ne peuvent pas détecter une fuite présente dans leurs propres colonnes. Seule la question « quand cette valeur est-elle écrite? » permet de la détecter.
L’écrire dans le journal d’expériences
Section intitulée « L’écrire dans le journal d’expériences »Dans L’apprentissage automatique est itératif, vous avez commencé un journal d’expériences, experiment_log.csv. Il contient une ligne par expérience, avec son score de CV, la décision et la raison. Le module 5 se terminait à E14, avec la régression logistique sur les 10 caractéristiques (E03) comme modèle à conserver. Un journal vous évite d’essayer deux fois la même idée. Il permet aussi à Grace ou à une personne qui révise le travail de comprendre les choix du modèle.
Une idée rejetée a aussi sa place dans le journal. Le mois prochain, quelqu’un proposera encore priority_now, et le journal explique déjà pourquoi la rejeter. Priya ajoute donc les quatre candidats, E15 à E18, avec les scores par partition de l’exemple détaillé. Elle remplit valid_ap parce qu’elle a vérifié chaque candidat une fois sur mai :
for row_id, added, valid_ap, decision, reason in [ ("E15", "prior_escalations_90d", 0.391, "keep", "Known at creation, and higher than E03 on all 5 folds."), ("E16", "order_value_band", 0.358, "reject", "It repeats order_value; the gain is smaller than the noise."), ("E17", "customer_escalation_rate", 0.485, "reject", "Leak: computed over later targets, so not known at creation."), ("E18", "priority_now", 0.873, "reject", "Leak: the help desk sets it to 1 when a ticket escalates."),]: change = f"E03 + {added}" + (" (11 features)" if row_id == "E15" else "") log_experiment(row_id, change, folds[added].mean(), folds[added].std(), valid_ap, decision, reason)show_log()Les dernières lignes de la sortie :
E13 Gradient boosting, default settings 0.316 0.010 0.369 rejectE14 Gradient boosting, max_depth=3, learning_rate=0.05 0.327 0.016 0.374 rejectE15 E03 + prior_escalations_90d (11 features) 0.339 0.016 0.391 keepE16 E03 + order_value_band 0.328 0.014 0.358 rejectE17 E03 + customer_escalation_rate 0.445 0.015 0.485 rejectE18 E03 + priority_now 0.851 0.024 0.873 rejectE15 est le nouveau modèle à conserver, à la place de E03. Le journal utilise les deux mêmes décisions qu’au module 5, keep (conserver) et reject (rejeter). La raison indique le type de candidat. Remarquez les deux fuites : elles ont les meilleurs scores du journal entier, et la décision est reject. La colonne des raisons permet au lecteur de comprendre ce choix. Si votre journal manque dans le carnet (notebook) Jupyter, ses cellules de préparation réécrivent E01 à E14.
Modifier ticket_model.py
Section intitulée « Modifier ticket_model.py »La décision est d’ajouter prior_escalations_90d. ticket_model.py nécessite deux changements : NUMBERS reçoit une septième entrée, et load() joint la colonne provenant de extra_features.csv. Ensuite, chaque leçon suivante, train.py, evaluate.py et predict.py obtiennent les 11 caractéristiques sans aucun autre changement. Voici le changement sous forme de diff : les lignes avec - disparaissent, et celles avec + arrivent.
"""The shared parts of Larkfield's escalation model: the columns, how to load a part of the data, and the preprocessing-plus-model pipeline.
Nextia Learning, C05, Module 4, lesson 3 (Preprocess with a pipeline).Nextia Learning, C05. You write this file in Module 4, lesson 3 (Preprocesswith a pipeline), and add prior_escalations_90d in Module 6, lesson 1 (Selectuseful features). train.py, evaluate.py and predict.py import it. """@@ NUMBERS = ["priority", "order_value", "word_count", "customer_tenure_days", "prior_tickets_90d", "created_hour"] "created_hour", "prior_escalations_90d"]@@ def load(part): """One part of the split: train, valid or test.""" return read(DATA / f"{part}.csv") """One part of the split (train, valid or test), with the extra feature prior_escalations_90d from extra_features.csv.""" df = read(DATA / f"{part}.csv") extra = pd.read_csv(DATA / "extra_features.csv", usecols=["ticket_id", "prior_escalations_90d"]) return df.merge(extra, on="ticket_id", how="left", validate="one_to_one")Chaque changement a une raison :
| Changement | Pourquoi |
|---|---|
"prior_escalations_90d" à la fin de NUMBERS |
C’est un compte. Il passe donc par l’imputation par la médiane et la mise à l’échelle, comme les autres nombres. FEATURES correspond à CATEGORIES + NUMBERS, donc il contient maintenant 11 entrées. |
usecols=["ticket_id", "prior_escalations_90d"] |
Lire seulement la colonne choisie. Les deux fuites n’entrent jamais dans les données que le modèle voit. |
how="left" |
Conserver chaque billet de la partie, même s’il n’a aucune ligne dans extra_features.csv. |
validate="one_to_one" |
Arrêter avec une erreur si un billet apparaît deux fois dans l’un des fichiers, plutôt que répéter des lignes sans avertissement. |
| La chaîne de documentation | Indique quelles leçons ont écrit le fichier. Le fichier correspond maintenant au ticket_model.py final du cours. |
Voici le fichier entier après le changement :
"""The shared parts of Larkfield's escalation model: the columns, how to loada part of the data, and the preprocessing-plus-model pipeline.
Nextia Learning, C05. You write this file in Module 4, lesson 3 (Preprocesswith a pipeline), and add prior_escalations_90d in Module 6, lesson 1 (Selectuseful features). train.py, evaluate.py and predict.py import it."""
from pathlib import Path
import pandas as pdfrom sklearn.compose import ColumnTransformerfrom sklearn.impute import SimpleImputerfrom sklearn.linear_model import LogisticRegressionfrom sklearn.pipeline import Pipelinefrom sklearn.preprocessing import OneHotEncoder, StandardScaler
DATA = Path("data")CATEGORIES = ["channel", "team", "segment", "region"]NUMBERS = ["priority", "order_value", "word_count", "customer_tenure_days", "prior_tickets_90d", "created_hour", "prior_escalations_90d"]FEATURES = CATEGORIES + NUMBERSTARGET = "escalated_72h"
def read(path): """Read a ticket CSV file. Only an empty cell is missing: the text "unknown" stays a category.""" return pd.read_csv(path, parse_dates=["created_at"], dtype={"customer_id": "string"}, keep_default_na=False, na_values=[""])
def load(part): """One part of the split (train, valid or test), with the extra feature prior_escalations_90d from extra_features.csv.""" df = read(DATA / f"{part}.csv") extra = pd.read_csv(DATA / "extra_features.csv", usecols=["ticket_id", "prior_escalations_90d"]) return df.merge(extra, on="ticket_id", how="left", validate="one_to_one")
def build_pipeline(model=None): """Preprocessing and model in one object. fit() learns the medians, the scaling and the categories from the rows it is given, and nothing else.""" 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", model if model is not None else LogisticRegression(max_iter=1000))])-
Ouvrez
ticket_model.pydans le dossier de votre projet (dans le carnet : la cellule%%writefile ticket_model.py). -
Ajoutez
"prior_escalations_90d"à la fin deNUMBERS, après"created_hour". -
Dans
load(), remplacez la lignereturnpar les trois lignes du diff. Mettez à jour les deux chaînes de documentation. -
Enregistrez le fichier.
-
Redémarrez le noyau du carnet pour que Python lise le nouveau fichier. (Le carnet utilise plutôt
importlib.reload(ticket_model).) -
Exécutez cette vérification :
from ticket_model import FEATURES, TARGET, build_pipeline, loadtrain, valid = load("train"), load("valid")pipeline = build_pipeline().fit(train[FEATURES], train[TARGET])scores = pipeline.predict_proba(valid[FEATURES])[:, 1]print(len(FEATURES), "features:", FEATURES)print("valid AP", round(average_precision_score(valid[TARGET], scores), 3)," ROC AUC", round(roc_auc_score(valid[TARGET], scores), 3))Vous devriez voir :
Sortie 11 features: ['channel', 'team', 'segment', 'region', 'priority', 'order_value', 'word_count', 'customer_tenure_days', 'prior_tickets_90d', 'created_hour', 'prior_escalations_90d']valid AP 0.391 ROC AUC 0.752 -
Si vous voyez 10 caractéristiques, le noyau utilise encore l’ancien fichier : redémarrez-le et relancez la vérification. Si vous voyez
MergeError(erreur de jointure), un billet apparaît deux fois : exécutezpython get_data.py --reset. -
Si votre fichier ne fonctionne toujours pas, remplacez-le par le fichier final du cours. Cela écrase votre version. Relancez ensuite la vérification.
Terminal curl -fsSL -O https://raw.githubusercontent.com/Nextia-Intelligence/nextia-learning-labs/main/C05/project/ticket_model.pyPowerShell curl.exe -fsSL -O https://raw.githubusercontent.com/Nextia-Intelligence/nextia-learning-labs/main/C05/project/ticket_model.py
Voici le modèle utilisé dans le reste du cours : une régression logistique avec les réglages par défaut, sur 11 caractéristiques. Il est entraîné sur les données d’entraînement seulement. AP de validation 0,391, AUC ROC 0,752. La prochaine leçon demande si d’autres réglages pourraient l’améliorer.
À vous de jouer : retirer plutôt une caractéristique
Section intitulée « À vous de jouer : retirer plutôt une caractéristique »Changez une condition : retirez une caractéristique au lieu d’en ajouter une. La sélection fonctionne dans les deux sens. Testez region et channel, en retirant chacune des 11 caractéristiques à tour de rôle, avec les mêmes partitions. Avant l’exécution, faites une prédiction : le modèle perdra-t-il quelque chose sans region? Sans channel?
all_folds = cv_folds(train, CATEGORIES, NUMBERS)print(f"all {len(FEATURES)} features CV AP {all_folds.mean():.3f} ± {all_folds.std():.3f}")for removed in ["region", "channel"]: fold_scores = cv_folds(train, [c for c in CATEGORIES if c != removed], [n for n in NUMBERS if n != removed]) change = fold_scores - all_folds print(f"without {removed:10s} CV AP {fold_scores.mean():.3f} ± {fold_scores.std():.3f} change per fold {change.round(3)}")Importez de nouveau CATEGORIES et NUMBERS après avoir modifié ticket_model.py, pour qu’ils contiennent les 11 caractéristiques.
Afficher la réponse
all 11 features CV AP 0.339 ± 0.016without region CV AP 0.340 ± 0.017 change per fold [-0.001 -0. -0.002 0.005 0.002]without channel CV AP 0.332 ± 0.015 change per fold [-0.001 -0.007 -0.011 -0.006 -0.01 ]- Sans
region: le changement monte et descend, de 0,001 en moyenne. Le modèle n’a pas besoin deregion. Selon la question 3, vous pouvez la retirer, et le modèle serait un peu plus simple. - Sans
channel: l’AP diminue sur chaque partition, de 0,007 en moyenne.channelapporte de l’information. Conservez-la.
Priya conserve quand même region et écrit pourquoi dans le journal. Sa collecte ne coûte rien, les rapports de Grace l’utilisent, et l’analyse par sous-groupes du module 7 compare les régions. La retirer ne change rien qu’elle puisse mesurer. Une décision de sélection comprend un nombre et une raison, tous deux consignés.
La fuite que la validation n’a pas pu détecter
Section intitulée « La fuite que la validation n’a pas pu détecter »Erreur courante
La fuite que la validation n'a pas pu détecter
Symptôme : Tomás ajoute priority_now et annonce le meilleur modèle du projet : AP de CV 0,851, AP de validation 0,873, AUC ROC 0,981. « Il fonctionne même sur mai, le mois qu’il n’a jamais vu. »
Cause : priority_now est la priorité au moment de l’exportation. Le service de soutien la règle à 1 lorsqu’il fait une escalade d’un billet. La colonne enregistre donc la cible. La fuite existe aussi dans les lignes de mai, donc mai ne peut pas la détecter. À la création d’un nouveau billet, son priority_now correspond à sa priorité. Priya évalue mai avec les valeurs qu’un nouveau billet possède réellement :
categories, numbers = tests["priority_now"] # the 10 features + priority_nowcolumns = categories + numbersleaky = pipeline_for(categories, numbers).fit(candidates[columns], candidates[TARGET])reported = leaky.predict_proba(candidates_valid[columns])[:, 1]at_creation = candidates_valid.assign(priority_now=candidates_valid["priority"]) # what a new ticket hasreal = leaky.predict_proba(at_creation[columns])[:, 1]print("valid AP as reported: ", round(average_precision_score(valid[TARGET], reported), 3))print("valid AP with the value at creation:", round(average_precision_score(valid[TARGET], real), 3))valid AP as reported: 0.873valid AP with the value at creation: 0.353Le rapport indique 0,873. En utilisation réelle, le même modèle obtient 0,353 : pas mieux que le modèle sans cette colonne. Le modèle a appris à faire confiance à priority_now. À la création, cette colonne ne contient aucune information sur le résultat. customer_escalation_rate fait la même chose à plus petite échelle. Son AP de validation annoncée est de 0,485. Elle passe à 0,396 quand les billets de mai reçoivent le taux connu de Larkfield au 1er mai.
Correction : pour chaque candidat, demandez quand sa valeur est écrite et comment elle a été calculée. Lisez la fiche des données : elle indique que les deux colonnes présentent des fuites. Traitez tout grand bond provenant d’une seule colonne comme une fuite, jusqu’à pouvoir prouver quand la valeur devient disponible. Excluez aussi les fuites du code : load() lit seulement prior_escalations_90d dans extra_features.csv.
Conséquence dans le travail en IA : une fuite donne le meilleur nombre dans chaque rapport : sur l’entraînement, en CV et sur la validation. Elle échoue seulement en production. Grace planifierait son équipe expérimentée selon un modèle qui semble détecter la plupart des escalades. En utilisation réelle, il n’en détecterait qu’une fraction. Un score trop beau pour être vrai est l’erreur la plus coûteuse de ce cours. Toutes les vérifications avant le lancement le confirment.
Vérification des connaissances
Section intitulée « Vérification des connaissances »Vérifiez votre compréhension
Priya a d'abord décidé avec la CV sur l'entraînement, puis examiné le mois de validation. Donnez deux raisons pour cet ordre.
Afficher la réponse
Premièrement, le mois de validation doit rester une vérification indépendante. Si vous choisissez selon ce mois, il fait partie du choix. Avec assez de candidats, l’un d’eux gagne sur mai par hasard. Deuxièmement, mai est petit : avec 183 escalades, son AP varie d’environ 0,03 par hasard. La CV utilise les 16 939 billets d’entraînement et montre la dispersion sur chaque partition.
Votre réponse contient-elle les points principaux de la nôtre ?
Connectez-vous et inscrivez-vous au cours pour enregistrer vos réponses. Chaque vérification compte pour le certificat.
Vérifiez votre compréhension
Un collègue propose minutes_to_first_reply. L'AP de CV passe de 0,339 à 0,62, et l'AP de validation augmente aussi. Que concluez-vous et que vérifiez-vous?
Afficher la réponse
Un bond de presque 0,3 provenant d’une seule colonne est trop beau pour être vrai : soupçonnez une fuite. Les caractéristiques sans fuite ajoutent environ 0,01. Vérifiez quand la valeur est écrite. La première réponse arrive après la création du billet. Les billets avec escalade reçoivent des réponses rapides d’agents expérimentés, donc la colonne contient une trace de la cible. Elle n’est pas connue au moment de la prédiction, donc ne l’utilisez jamais. La hausse sur mai ne prouve rien, car les lignes de mai contiennent la même fuite.
Votre réponse contient-elle les points principaux de la nôtre ?
Connectez-vous et inscrivez-vous au cours pour enregistrer vos réponses. Chaque vérification compte pour le certificat.
Vérifiez votre compréhension
Tomás dit : laissons le modèle choisir parmi les quatre candidats, car il donnera un poids presque nul à une colonne inutile. Pourquoi cela ne vous protège-t-il pas d'une fuite?
Afficher la réponse
Le modèle juge une colonne seulement selon sa capacité à prédire la cible dans les données reçues. Une fuite prédit très bien la cible, car elle contient la réponse. Le modèle lui donne donc un poids élevé, pas faible. Le modèle n’a aucun moyen de savoir quand une valeur a été écrite. Seule une personne qui demande « est-ce connu au moment de la prédiction? » peut retirer une fuite avant que le modèle la voie.
Votre réponse contient-elle les points principaux de la nôtre ?
Connectez-vous et inscrivez-vous au cours pour enregistrer vos réponses. Chaque vérification compte pour le certificat.
Récapitulatif
- Chaque caractéristique candidate doit répondre à trois questions, dans l’ordre : connue au moment de la prédiction, information nouvelle, gain de CV supérieur au bruit.
prior_escalations_90dest utile : l’AP de CV passe de 0,327 à 0,339, avec une hausse sur chaque partition.order_value_bandest redondante : elle répèteorder_value.customer_escalation_rateetpriority_nowprésentent des fuites. Conservez la caractéristique utile; rejetez les autres, en inscrivant la raison dans le journal.- Une fuite semble trop bonne dans chaque vérification, y compris sur le mois de validation. Demandez quand chaque valeur est écrite; le modèle ne peut pas poser cette question à votre place.
- Décidez avec la CV sur l’entraînement, puis confirmez une fois sur le mois de validation. Consignez chaque décision et sa raison dans le journal d’expériences.
ticket_model.pycontient maintenant 11 caractéristiques : AP de validation 0,391, AUC ROC 0,752.
Ensuite, vous gardez les mêmes caractéristiques et examinez les réglages du modèle : les réglages par défaut sont-ils les meilleurs?
Leçon suivante : Régler les hyperparamètres