Aller au contenu
English

Planifier les vérifications en production

  • Leçon 27
  • 75 min
  • Module 8 : Préparer la passation du modèle
Leçon 27 sur 27 · Apprentissage automatique : du problème à un modèle fiablePlanifier les vérifications en production

Ce que vous saurez faire

Planifiez les vérifications qui indiquent quand le modèle ne convient plus à sa tâche. Les vérifications des entrées bloquent les mauvaises données. Les signaux de surveillance montrent les changements. Une règle écrite indique quand réentraîner le modèle.

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

Le carnet (notebook) Jupyter ajoute les vérifications des entrées à predict.py et crée cinq problèmes dans les entrées. Ensuite, il calcule les scores des billets de soutien de juillet, tous les signaux de surveillance de cette page et joint les étiquettes. Il se termine par l’exercice du module : l’exécution à partir d’une nouvelle copie. Il ne nécessite aucune configuration locale, mais le projet local est l’approche principale de ce module.

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 75 minutes

Ce que vous devez savoir

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

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

Une voiture ne vous indique pas tout en permanence. Elle a quelques voyants d’alerte, et chacun surveille une chose. Un voyant d’huile rouge signifie « arrêtez maintenant » : continuer à rouler endommage le moteur. Un voyant de carburant orange signifie « agissez bientôt » : vous pouvez encore rouler, mais pas longtemps. Le carnet d’entretien indique « entretien tous les 15 000 km, ou plus tôt si un voyant reste allumé ». Les voyants ne conduisent pas la voiture. Ils avertissent la personne au volant d’un problème avant qu’il devienne coûteux.

Un modèle utilisé chaque jour a les mêmes besoins. Depuis le 1er juillet, le modèle de Larkfield calcule un score pour chaque nouveau billet. Personne ne peut lire 50 scores par jour et repérer un problème. Priya planifie donc quelques vérifications à l’avance. Certaines arrêtent l’exécution immédiatement, d’autres avertissent, et une règle écrite indique quand entretenir le modèle.

Que fait la première version de predict.py quand l’entrée ne correspond pas à ce que le modèle attend? Chaque jour, Mei, l’ingénieure de données, exporte les nouveaux billets vers predict.py dans un fichier CSV. Ce fichier contient les mêmes colonnes que july_tickets.csv. Les exportations changent, des collègues renomment des colonnes et de nouveaux types de billets apparaissent.

Prenez les trois premiers billets de juillet et modifiez-les de quatre façons. Ensuite, exécutez la première version sur les cinq fichiers. Deux des cinq produisent une erreur avec une longue trace d’appels que seule une personne qui programme en Python peut lire. Deux ne donnent aucun message : un billet provenant d’un nouveau canal et des valeurs de commande en cents plutôt qu’en dollars. L’erreur de cents fait passer le nombre de signalements de un à trois, sans avertir personne.

Fichier d’entrée Ce qui a changé La première version de predict.py
tiny_ok.csv Rien Scored 3 tickets with threshold 0.19: 1 flagged (33.3%).
tiny_social.csv Le billet 3 a channel = social Scored 3 tickets with threshold 0.19: 1 flagged (33.3%).
tiny_missing.csv La colonne prior_escalations_90d manque KeyError: "['prior_escalations_90d'] not in index"
tiny_text.csv Le billet 2 a priority = high ValueError: Cannot use median strategy with non-numeric data: could not convert string to float: 'high'
tiny_cents.csv order_value est en cents : 5438 au lieu de 54,38 Scored 3 tickets with threshold 0.19: 3 flagged (100.0%).

Les cas silencieux sont les plus dangereux. Un modèle calcule un score pour tout ce qu’il reçoit. Il ne dit jamais « cette entrée semble étrange ». Le programme qui l’entoure doit donc le dire.

Revenons au tableau de bord. Certains problèmes sont visibles immédiatement : un fichier endommagé, une colonne manquante. D’autres s’accumulent lentement au fil des jours. Un programme qui utilise un modèle a donc besoin de deux types de vérifications.

Les vérifications des entrées sont les voyants rouges. Elles s’exécutent avant le calcul des scores, sur chaque lot. Elles comparent le lot au schéma dans model_info.json. Ce schéma décrit les colonnes, les types et les valeurs attendus, présentés dans Empaqueter les artefacts. Un lot que le modèle ne peut pas traiter correctement arrête l’exécution, avec un message qui explique le problème.

La surveillance correspond aux voyants orange. Elle observe les résultats au fil du temps. Elle compare des nombres, comme la proportion de billets signalés, aux valeurs attendues. Elle repère les changements lents qu’aucun lot isolé ne révèle. Un changement des données au fil du temps s’appelle une dérive. Le nom vient d’un bateau que personne ne dirige. Chaque minute, le courant le déplace un peu. Après une heure, il est loin de son point de départ.

Les deux types de vérifications alimentent une décision écrite à l’avance : quand s’arrêter et réexaminer le modèle. Cette règle est le déclencheur de réentraînement, le carnet d’entretien du modèle. Le diagramme montre toute la boucle quotidienne.

De nouveaux billets arrivent chaque jour et passent par les vérifications des entrées. Si les vérifications échouent, l'exécution s'arrête avec le code de sortie 1. Aucun score n'est calculé, et Mei corrige l'exportation. Si elles réussissent, ou avertissent seulement d'une catégorie inconnue, le modèle écrit les scores et les signalements dans predictions.csv. Deux types de signaux suivent. Le jour même : le taux de signalement, le score moyen, la proportion de catégories inconnues et les moyennes des caractéristiques. 72 heures plus tard, quand les étiquettes arrivent : la précision et le rappel. Les deux mènent à une décision : une condition de déclenchement est-elle vraie? Sinon, gardez le modèle et vérifiez de nouveau demain. Si oui, des personnes l'examinent : elles trouvent la cause, puis corrigent les données, réentraînent le modèle ou mettent la fiche à jour.
La boucle quotidienne des vérifications. Les vérifications des entrées arrêtent un mauvais lot avant le calcul des scores. Les signaux sans étiquettes avertissent le jour même. La précision et le rappel suivent quand les étiquettes arrivent. Un déclencheur écrit lance un examen par des personnes.
Description textuelle du diagramme

De nouveaux billets passent par les vérifications des entrées chaque jour. Une vérification qui échoue arrête l’exécution avec le code de sortie 1, et aucun score n’est calculé. Un lot accepté, ou contenant seulement une catégorie inconnue, reçoit des scores et des signalements dans predictions.csv. Le jour même, quatre signaux ne nécessitent aucune étiquette : le taux de signalement, le score moyen, la proportion de catégories inconnues et la moyenne de chaque caractéristique numérique. Soixante-douze heures plus tard, les étiquettes arrivent et on peut calculer la précision et le rappel. Si une condition de déclenchement est vraie, Priya et Grace examinent le modèle. Elles trouvent la cause, puis corrigent les données, réentraînent le modèle ou mettent la fiche à jour. Sinon, le modèle reste en place, et les vérifications reprennent le lendemain.

Mei et Priya décident ce que chaque problème signifie et ce que le programme doit faire. Leur règle : arrêter si les scores seraient incorrects; avertir si les scores restent utilisables, mais que l’entrée a changé. Une colonne manquante ou du texte dans une colonne numérique rend le calcul des scores impossible. Un nombre très éloigné de sa plage habituelle indique presque toujours une erreur d’unité ou d’exportation. Les scores seraient donc incorrects, sans avertissement. Une catégorie que le modèle n’a jamais vue est différente : le pipeline peut encore calculer le score du billet, avec moins d’information.

Problème Exemple Action Pourquoi
Une colonne obligatoire manque prior_escalations_90d est absent du fichier Arrêter Le modèle ne peut pas calculer les scores sans cette colonne.
Une colonne numérique contient du texte priority = high Arrêter La valeur n’a aucun sens pour le modèle.
Un nombre sort de sa plage order_value = 5438 pour une commande de 54,38 $ Arrêter Une erreur d’unité ou d’exportation. Les scores seraient incorrects, sans avertissement.
Une catégorie inconnue channel = social Avertir et compter Le pipeline peut calculer son score (handle_unknown="ignore"). Un arrêt bloquerait l’acheminement de tous les billets. Mais plusieurs catégories inconnues indiquent que l’entrée a changé.

« Arrêter » signifie : ne calculer aucun score, afficher le problème et quitter avec le code 1. Un ordonnanceur ou un collègue voit l’échec immédiatement. Mais pourquoi ne pas calculer les scores des bonnes lignes seulement? Tomás le propose, et cela semble plus accommodant. Mais l’exécution semble alors réussie, les mauvaises lignes n’attendent dans aucune file, et personne ne remarque le problème avant une plainte. Un arrêt bien visible est le moyen le plus rapide d’obtenir une correction.

Modifiez predict.py à quatre endroits. D’abord, la chaîne de documentation décrit maintenant les vérifications :

predict.py (the docstring)
"""Score new tickets with the saved escalation model.
Nextia Learning, C05, Module 8 (Package artifacts; Plan production checks).
python predict.py data/july_tickets.csv predictions.csv
python predict.py --check reload the model and compare the fixed sample
Checks the input before it scores: the required columns, numbers that are
numbers, values in their allowed range, and categories that the model knows.
A missing column or a wrong type stops the run (exit code 1). An unknown
category is allowed (the model treats it as "none of the known ones") but is
counted and reported, because many of them mean that the input has changed.
"""

Ensuite, sous MODEL = Path("model"), ajoutez la plage autorisée de chaque nombre. Les plages sont volontairement larges : elles détectent les erreurs d’unité et les exportations défectueuses, pas les billets inhabituels. Un billet de 3 000 mots est inhabituel, mais réel; une commande de 543 800 $ indique une erreur d’exportation.

predict.py (under MODEL)
RANGES = {"priority": (1, 3), "word_count": (0, 10000), "order_value": (0, 5000), "customer_tenure_days": (0, 20000),
"prior_tickets_90d": (0, 500), "created_hour": (0, 23), "prior_escalations_90d": (0, 500)}

Puis, ajoutez check_input() après load_model(). Cette fonction lit le schéma dans model_info.json, pour que les vérifications correspondent toujours à l’artefact :

predict.py (after load_model)
def check_input(df, info):
"""Return a list of problems that stop the run, and a list of warnings."""
errors, warnings = [], []
missing = [c for c in FEATURES if c not in df.columns]
if missing:
return [f"missing columns: {', '.join(missing)}"], warnings
for name in info["features"]["numbers"]:
values = pd.to_numeric(df[name], errors="coerce")
bad = values.isna() & df[name].notna()
if bad.any():
errors.append(f"{name}: {bad.sum()} values are not numbers, for example {df.loc[bad, name].iloc[0]!r}")
continue
low, high = RANGES[name]
outside = values.notna() & ((values < low) | (values > high))
if outside.any():
errors.append(f"{name}: {outside.sum()} values outside {low} to {high}")
for name, known in info["features"]["categories"].items():
unknown = df[name].notna() & ~df[name].isin(known)
if unknown.any():
values = ", ".join(sorted(df.loc[unknown, name].astype(str).unique())[:5])
warnings.append(f"{name}: {unknown.sum()} rows have a category the model never saw ({values})")
return errors, warnings

Trois lignes méritent une explication :

Lignes Pourquoi
pd.to_numeric(..., errors="coerce") Le texte qui n’est pas un nombre devient une valeur manquante. Une valeur qui n’était pas manquante avant, mais l’est maintenant, était du texte. Les vraies valeurs manquantes (le order_value d’un billet account) restent autorisées : le pipeline les remplit.
RANGES[name] Une valeur hors de sa plage est une erreur, pas un avertissement.
~df[name].isin(known) known est la liste des catégories dans model_info.json, celles que l’encodeur a apprises.

Enfin, dans main(), vérifiez l’entrée après sa lecture et avant le calcul des scores. Voici la fonction main() complète :

predict.py (main)
def main():
parser = argparse.ArgumentParser(description="Score new tickets with the saved escalation model.")
parser.add_argument("input", nargs="?", help="CSV file with one row per new ticket")
parser.add_argument("output", nargs="?", default="predictions.csv")
parser.add_argument("--check", action="store_true", help="compare the fixed sample after reloading")
args = parser.parse_args()
pipeline, info = load_model()
if args.check:
check_sample(pipeline)
return
if not args.input:
parser.error("give an input CSV file, or --check")
df = read(args.input)
errors, warnings = check_input(df, info)
for warning in warnings:
print(f"Warning: {warning}")
if errors:
for error in errors:
print(f"Error: {error}")
sys.exit("Stopped: the input does not match the model's feature schema. Nothing was scored.")
scores = pipeline.predict_proba(df[FEATURES])[:, 1]
out = pd.DataFrame({"ticket_id": df["ticket_id"], "score": scores.round(4),
"flag": (scores >= info["threshold"]).astype(int)})
out.to_csv(args.output, index=False)
print(f"Scored {len(out)} tickets with threshold {info['threshold']}: {out['flag'].sum()} flagged "
f"({out['flag'].mean():.1%}). Wrote {args.output}.")

Votre fichier predict.py correspond maintenant à celui du cours, C05/project/predict.py. Exécutez-le de nouveau sur les cinq petits fichiers (le carnet les crée) :

Output: tiny_social.csv
Warning: channel: 1 rows have a category the model never saw (social)
Scored 3 tickets with threshold 0.19: 1 flagged (33.3%). Wrote tiny_out.csv.
Output: tiny_missing.csv (exit code 1)
Error: missing columns: prior_escalations_90d
Stopped: the input does not match the model's feature schema. Nothing was scored.
Output: tiny_text.csv (exit code 1)
Error: priority: 1 values are not numbers, for example 'high'
Stopped: the input does not match the model's feature schema. Nothing was scored.
Output: tiny_cents.csv (exit code 1)
Error: order_value: 3 values outside 0 to 5000
Stopped: the input does not match the model's feature schema. Nothing was scored.

tiny_ok.csv donne la même ligne qu’avant. Chaque problème produit maintenant un message qui permet à Mei d’agir. Les cas silencieux ne le sont plus. Ce sont les points de contrôle du contrat présenté dans Empaqueter les artefacts :

python train.py écrit le dossier model/, l'artefact : escalation_model.joblib, model_info.json et check_sample.csv. L'artefact et un fichier CSV de nouveaux billets passent aux vérifications des entrées, qui comparent les billets au schéma. Si les vérifications réussissent, le pipeline donne un score par billet. Il fait de même s'il trouve seulement une catégorie inconnue, qu'il signale par un avertissement et compte. Un billet dont le score atteint au moins 0,19 est signalé, et le résultat est predictions.csv. Si les vérifications échouent à cause d'une colonne manquante, d'un mauvais type ou d'une valeur hors plage, l'exécution s'arrête. Le code de sortie est 1 et aucun score n'est calculé. Séparément, predict.py --check recharge l'artefact dans un nouveau processus et compare 20 scores.
Les vérifications des entrées sont le point de contrôle entre les nouvelles données et le modèle. Un lot défectueux s'arrête avant le calcul des scores; une catégorie inconnue passe, avec un avertissement et un compte.
Description textuelle du diagramme

Les nouveaux billets atteignent le pipeline seulement par les vérifications des entrées, qui utilisent le schéma dans model_info.json. Une colonne manquante, un mauvais type ou une valeur hors plage arrête l’exécution avec le code de sortie 1. Aucun score n’est calculé. Une catégorie inconnue passe, avec un avertissement et un compte. Ensuite, le pipeline calcule le score de chaque billet. Un score d’au moins 0,19 active le signalement dans predictions.csv.

L’exportation de juillet contient 1 400 billets de soutien créés du 1 au 31 juillet 2026, après les données d’entraînement. Leurs cibles ne sont pas encore connues. Priya calcule leurs scores :

Terminal
python predict.py data/july_tickets.csv predictions.csv
Sortie
Warning: channel: 91 rows have a category the model never saw (social)
Scored 1400 tickets with threshold 0.19: 282 flagged (20.1%). Wrote predictions.csv.

Depuis le 15 juillet, Larkfield répond aux messages sur les réseaux sociaux. Parmi les billets, 91 viennent du nouveau canal social (réseaux sociaux). Le modèle les traite comme « aucun des canaux que je connais ». L’exécution continue, et l’avertissement les compte. Le taux de signalement de 20,1 % dépasse légèrement la capacité de 20 %. Mais un seul nombre pour le mois cache le moment du changement. La surveillance suit les nombres dans le temps.

Un signal de surveillance est un nombre que vous calculez pour chaque période et comparez à vos attentes. Les meilleurs signaux sont disponibles le jour même, car ils ne nécessitent aucune étiquette. Priya en choisit quatre. Pour chacun, elle connaît la valeur normale de mai, le mois de validation :

  • Le taux de signalement, comparé à la capacité de 20 %. C’est la contrainte de Grace. En mai, il était de 19,3 %.
  • Le score moyen. S’il change, les entrées ont changé, même si le taux de signalement n’a pas encore changé. En mai, il était de 0,120.
  • La proportion de catégories inconnues. Elle révèle de nouvelles entrées que le modèle n’a jamais apprises. En mai, elle était de 0.
  • La moyenne de chaque variable numérique, comparée aux données de validation. Elle révèle une dérive dans une entrée. Ici, la plus claire est prior_tickets_90d, avec une moyenne de 9,00 en mai.
signals.py
import json
import pandas as pd
from ticket_model import read
info = json.load(open("model/model_info.json"))
july = read("data/july_tickets.csv").merge(pd.read_csv("predictions.csv"), on="ticket_id")
july["days"] = pd.cut(july["created_at"].dt.day, [0, 7, 14, 21, 28, 31],
labels=["1-7", "8-14", "15-21", "22-28", "29-31"])
july["unknown"] = ~july["channel"].isin(info["features"]["categories"]["channel"])
signals = july.groupby("days", observed=True).agg(
tickets=("flag", "size"), flag_rate=("flag", "mean"), mean_score=("score", "mean"),
unknown=("unknown", "mean"), prior_tickets=("prior_tickets_90d", "mean"))
print(signals.round(3))

La sortie :

Sortie
tickets flag_rate mean_score unknown prior_tickets
days
1-7 311 0.174 0.113 0.000 5.322
8-14 314 0.185 0.111 0.000 5.245
15-21 302 0.262 0.138 0.126 3.894
22-28 321 0.209 0.137 0.100 3.592
29-31 152 0.158 0.125 0.138 3.033

Comparez le tableau aux valeurs de mai. Les deux premières semaines de juillet, le taux de signalement est de 17,4 % et 18,5 %, sous la capacité. À partir du 15 juillet, tout change en même temps : 26,2 % de billets signalés, un score moyen de 0,138 et 12,6 % de canaux inconnus. La dernière ligne couvre seulement trois jours et 152 billets. Elle est donc moins fiable.

Les semaines donnent une vue peu détaillée. Le graphique montre le taux de signalement des 7 derniers jours, recalculé chaque jour à partir du 7 juillet :

  • Taux de signalement, 7 derniers jours
  • Capacité de 20 %
La proportion de billets signalés dans les 7 derniers jours dépasse la capacité de l'équipe expérimentée dès le 15 juillet, quand le canal social démarre, et reste au-dessus jusqu'au 30 juillet.
Description textuelle du graphique

Une ligne pour chaque jour du 7 au 31 juillet : la proportion des billets des 7 derniers jours signalés par le modèle. Du 7 au 14 juillet, elle se situe entre 15,7 % et 18,9 %, sous la ligne pointillée de capacité à 20 %. Le 15 juillet, elle atteint 21,0 %. Elle monte à 26,2 % le 21 juillet, puis reste entre 20,4 % et 22,4 % jusqu’au 30 juillet. Le 31 juillet, elle est de 18,6 %.

Qu’est-ce qui a changé? Priya examine les sous-groupes, comme dans L’analyse par sous-groupes :

  • Les 91 billets social ont un taux de signalement de 23,1 %. Les autres billets à partir du 15 juillet ont un taux de 21,8 %. Le nouveau canal n’est donc pas la seule cause.
  • La moyenne de prior_tickets_90d passe de 5,3 à 3,0 pendant le mois. En mai, elle était de 9,00. Mei vérifie l’exportation. Elle découvre que l’exportation de juillet compte seulement les billets du client avant le 1 juillet. Un billet du 25 juillet ne compte pas les billets du même client au début de juillet. Le calcul de cette variable diffère donc de celui de l’entraînement, et sa valeur diminue chaque jour.

Pourquoi une valeur plus faible de prior_tickets_90d augmente-t-elle les scores? Dans Expliquer les prédictions, vous avez vu son coefficient : −0,29. Moins de billets antérieurs signifie donc un score plus élevé. Priya peut le comprendre en une ligne, car le modèle est une régression logistique. C’est une des raisons présentées dans Quand un modèle simple est meilleur : un modèle simple est plus facile à déboguer quand ses entrées changent.

Voilà l’intérêt des signaux : ils montrent qu’un changement a eu lieu, et où chercher. Pour trouver la cause, il faut une personne qui connaît les données.

La cible escalated_72h d’un billet de juillet est connue 72 heures après la création du billet. En production, il est normal que les étiquettes arrivent après les prédictions. Mei les envoie dans july_labels.csv. Priya peut maintenant calculer les nombres les plus importants pour la décision de Grace. Rappel : la précision est la proportion des billets signalés qui ont été transférés à l’équipe expérimentée. Le rappel est la proportion des transferts qui ont été signalés.

labels.py
import pandas as pd
from ticket_model import read
july = read("data/july_tickets.csv").merge(pd.read_csv("predictions.csv"), on="ticket_id")
july = july.merge(pd.read_csv("data/july_labels.csv"), on="ticket_id")
day = july["created_at"].dt.day
for name, part in (("1-14 July", day < 15), ("15-31 July", day >= 15), ("All July", day > 0)):
d = july[part]
caught = (d["flag"] & d["escalated_72h"]).sum()
print(f"{name}: {len(d)} tickets, escalated {d['escalated_72h'].mean():.3f}, mean score {d['score'].mean():.3f},"
f" precision {caught / d['flag'].sum():.3f}, recall {caught / d['escalated_72h'].sum():.3f}")
Sortie
1-14 July: 625 tickets, escalated 0.149, mean score 0.112, precision 0.366, recall 0.441
15-31 July: 775 tickets, escalated 0.133, mean score 0.135, precision 0.312, recall 0.515
All July: 1400 tickets, escalated 0.140, mean score 0.125, precision 0.333, recall 0.480

Pour le mois : 14,0 % de billets transférés, une précision de 0,333 et un rappel de 0,480. Le modèle détecte encore plus de 40 % des transferts. Mais depuis le 15 juillet, il signale plus de billets alors que moins de billets sont transférés. La précision tombe à 0,312 : l’équipe expérimentée examine plus de billets qui n’en avaient pas besoin. Le score moyen (0,125) reste sous le taux réel (0,140), comme l’indique la fiche.

Mais pourquoi ne pas attendre les étiquettes et surveiller seulement la précision et le rappel? Omar pose cette bonne question : ce sont les nombres qui comptent. La réponse est le délai. Les étiquettes du 31 juillet arrivent le 3 août. Les signaux sans étiquettes ont changé le 15 juillet. Ce sont les voyants orange qui s’allument avant qu’on connaisse les dommages.

Le réentraînement consiste à entraîner le modèle de nouveau, avec des données plus récentes, pour créer un nouvel artefact. Quand Priya devrait-elle le faire? « Quand le modèle devient moins bon » est trop vague. Personne ne sait qui décide, quand, ni ce que « moins bon » signifie. Un déclencheur de réentraînement est une règle écrite à l’avance qui lance une révision. C’est comme le carnet d’entretien d’une voiture : un nombre de kilomètres, ou un voyant qui reste allumé. Priya l’écrit dans une nouvelle section de la fiche du modèle :

MODEL_CARD.md (new section)
## Monitoring and retraining
Every working day, compute over the last 7 days: the flag rate, the mean score, the share of
unknown categories, and the mean of each number feature. When the labels arrive (72 hours
later), compute precision and recall for each month.
Start a retraining review when one of these happens:
1. The flag rate is above 20% on 5 working days in a row.
2. Unknown categories are more than 2% of the tickets of the last 7 days.
3. Recall is below 0.40 on a month with labels.
The review is done by Priya and Grace within 5 working days. Until it ends, the senior team
reviews flagged tickets in score order, highest first, up to its capacity.

Chaque condition vient d’un élément que vous connaissez déjà. La condition 1 correspond à la capacité de Grace. Elle précise « 5 jours de suite » pour qu’une seule journée chargée ne déclenche pas de révision. La condition 2 fixe une petite proportion, car le modèle n’a rien appris sur une nouvelle catégorie. La condition 3 reprend l’objectif d’amélioration de Définir une référence de base : détecter au moins 40 % des transferts.

Mais pourquoi ne pas réentraîner automatiquement chaque mois? Tomás aime l’idée : aucune réunion, toujours des données récentes. Le déclencheur lance une révision, pas un réentraînement automatique, pour une raison : une personne vérifie d’abord la cause. Si la cause est une exportation défectueuse, réentraîner sur des données erronées enseigne l’erreur au modèle. Le cas de juillet le montre : un réentraînement automatique apprendrait la mauvaise valeur de prior_tickets_90d.

En juillet :

Condition Juillet Déclenchée?
1. Taux de signalement supérieur à 20 % pendant 5 jours ouvrables de suite Supérieur à 20 % les 15, 16, 17, 20 et 21 juillet Oui, le 21 juillet
2. Catégories inconnues supérieures à 2 % des 7 derniers jours 7 billets social sur 319 le 15 juillet (2,2 %) Oui, le 15 juillet
3. Rappel inférieur à 0,40 sur un mois étiqueté Rappel de 0,480 Non

La révision trouve deux causes : le nouveau canal et l’erreur d’exportation dans prior_tickets_90d. Grace et Priya décident :

  1. Mei corrige l’exportation de prior_tickets_90d. Elle ajoute une vérification qui compare la moyenne de chaque variable numérique à celle de mai.
  2. Priya réentraîne en incluant mai et juin. Les nouvelles données d’entraînement se terminent le 31 mai. Le modèle apprend donc à partir de billets créés après le changement de politique. Juin sert à choisir le seuil. Juillet, avec ses étiquettes, devient les nouvelles données de test, utilisées une seule fois.
  3. social reste une catégorie inconnue jusqu’à ce qu’il y ait assez de billets social étiquetés pour apprendre. L’avertissement continue, et la fiche du modèle le précise.
  4. Priya met à jour MODEL_CARD.md avec les nouveaux nombres, le nouveau SHA-256 de l’artefact et la nouvelle date de révision.

La deuxième étape aide aussi la calibration : le nouveau modèle apprend à partir de billets au taux de transfert plus élevé après le 1 mai.

Exécuter ces vérifications selon un horaire, stocker les prédictions, envoyer des alertes et revenir à un ancien artefact relève du déploiement. Déployer des modèles d’IA pour de vrais utilisateurs l’enseigne. Ce cours s’arrête au plan et aux scripts.

  1. Dans ~/projects/ticket-model, activez .venv.
  2. Modifiez predict.py aux quatre endroits indiqués dans Les vérifications des entrées dans predict.py.
  3. Exécutez python predict.py --check. Vous devriez voir Check passed (vérification réussie).
  4. Exécutez python predict.py data/july_tickets.csv predictions.csv. Vous devriez voir l’avertissement concernant 91 lignes social, puis 282 flagged (20.1%) (282 billets signalés, soit 20,1 %).
  5. Créez une copie défectueuse de l’entrée. Ouvrez data/july_tickets.csv dans un éditeur de texte et enregistrez-le sous broken.csv. Dans broken.csv, renommez la colonne priority en prio.
  6. Exécutez python predict.py broken.csv out.csv. Vous devriez voir Error: missing columns: priority (erreur : colonne manquante : priority), puis Stopped: (arrêté).
  7. Enregistrez signals.py et exécutez-le. Comparez votre tableau à la sortie ci-dessus.
  8. Enregistrez labels.py et exécutez-le. Vous devriez voir precision 0.333, recall 0.480 (précision de 0,333, rappel de 0,480) pour tout juillet.
  9. Ajoutez la section Surveillance et réentraînement à votre MODEL_CARD.md.
Si une étape échoue
  • L’étape 3 ou 4 affiche NameError: name 'RANGES' is not defined (le nom RANGES n’est pas défini) : ajoutez RANGES sous MODEL = Path("model").
  • L’étape 6 affiche une trace d’erreur KeyError (erreur de clé) au lieu de Error: (erreur) : main() n’appelle pas check_input() avant pipeline.predict_proba. Comparez main() au code ci-dessus.
  • L’étape 7 affiche FileNotFoundError: predictions.csv (fichier predictions.csv introuvable) : exécutez d’abord l’étape 4.

Changez une condition : nous sommes maintenant le 18 juillet, pas à la fin du mois. Les étiquettes du 1 au 14 juillet sont arrivées, 72 heures après le dernier de ces billets. Utilisez les sorties ci-dessus pour répondre :

  1. Lesquelles des trois conditions du déclencheur se sont déclenchées au 18 juillet?
  2. Quel est le rappel du 1 au 14 juillet? La condition 3 se déclenche-t-elle pour ces deux semaines?
  3. Priya et Grace devraient-elles lancer la révision le 18 juillet?
Afficher la réponse
  1. La condition 2 s’est déclenchée le 15 juillet : 7 billets sur 319 des 7 derniers jours (2,2 %) venaient de social. La condition 1 ne s’est pas déclenchée. Au 17 juillet, dernier jour ouvrable avant le 18 juillet, le taux sur 7 jours dépassait 20 % depuis 3 jours ouvrables consécutifs. Les 15, 16 et 17 juillet, il était de 21,0 %, 21,5 % et 22,8 %. Il en faut 5. (Le 18 juillet, le taux est de 24,3 %, mais c’est un samedi.)
  2. Le rappel est de 0,441 du 1 au 14 juillet, au-dessus de 0,40. La condition 3 ne se déclenche pas. Elle est définie sur un mois étiqueté. Deux semaines avant le changement nous renseignent peu sur la période qui le suit.
  3. Oui. Une seule condition suffit à lancer une révision. La révision du 18 juillet permet déjà de demander à Mei ce qu’elle sait du nouveau canal. Elle n’attend pas que le taux de signalement dépasse la limite pendant 5 jours.

Erreur courante

Valeurs des commandes en cents, sans vérification de plage

Symptôme : en août, le collègue de Mei modifie l’exportation. order_value est maintenant en cents. La première version de predict.py traite le fichier de juillet sous cette forme sans erreur ni avertissement. Elle signale 83,6 % des billets, et le score moyen est de 0,799. L’équipe expérimentée reçoit plus de quatre fois sa capacité.

Cause : le modèle a appris qu’une grosse commande signifie plus de risque. Une commande de 54,38 $ vaut maintenant 5438, et chaque commande semble énorme. La colonne contient encore des nombres, donc une vérification des types seulement réussit. Dans le cours sur SQL, vous avez rencontré le même problème avec l’ancien formulaire Web, qui envoyait des cents : Traiter les valeurs désordonnées.

Correction : une plage pour chaque nombre, comme dans RANGES. La version finale de predict.py arrête l’exécution :

Output (exit code 1)
Warning: channel: 91 rows have a category the model never saw (social)
Error: order_value: 879 values outside 0 to 5000
Stopped: the input does not match the model's feature schema. Nothing was scored.

En cents, 879 des 1 400 valeurs de commandes dépassent 5 000 : toutes les commandes de plus de 50 $. Les autres commandes valent 50 $ ou moins, ou leur valeur manque. Elles restent donc dans la plage. Voilà pourquoi une vérification de plage détecte une erreur d’unité sur la plupart des lignes, pas sur toutes.

Conséquence pour le travail en IA : un modèle calcule un score pour tout ce qu’il reçoit. Il ne dit jamais « cette entrée semble étrange ». Le schéma et les plages sont votre seule protection contre un changement silencieux en amont. Gardez-les dans le programme de calcul des scores, à côté du modèle, pas dans un document.

Vérifiez votre compréhension

Pourquoi predict.py s'arrête-t-il pour une colonne manquante, mais avertit-il seulement pour une catégorie inconnue?

Afficher la réponse

Sans la colonne, le pipeline ne peut pas calculer les variables, ou il échoue avec une erreur qui n’explique pas le problème. Aucun score ne peut être calculé correctement. L’exécution s’arrête donc avec un message clair et le code de sortie 1. Une catégorie inconnue est différente. Le pipeline peut calculer son score, car OneHotEncoder(handle_unknown="ignore") lui donne 0 dans chaque colonne de canal. Arrêter bloquerait l’acheminement de tous les billets à cause de quelques-uns. Mais beaucoup de valeurs inconnues signifient que l’entrée a changé. Le programme les compte donc et les signale, et la surveillance prévoit un déclencheur pour leur proportion.

Vérifiez votre compréhension

En juillet, le taux de signalement mensuel est de 20,1 %, presque exactement la capacité. Tomás dit que la surveillance n'a rien trouvé. Utilisez les signaux sur 7 jours pour expliquer pourquoi il se trompe.

Afficher la réponse

Le mois cache le changement. Du 1 au 14 juillet, le taux de signalement est de 17,4 % et 18,5 % par semaine. À partir du 15 juillet, il est de 26,2 %, puis de 20,9 % : au-dessus de la capacité de 20 %. Le taux sur 7 jours reste supérieur à 20 % du 15 au 30 juillet. Le score moyen passe d’environ 0,112 à 0,138. Les canaux inconnus passent de 0 à 12,6 %, et la moyenne de prior_tickets_90d baisse. Deux déclencheurs s’activent : les catégories inconnues le 15 juillet, et le taux de signalement le 21 juillet. Quand les étiquettes arrivent, la précision à partir du 15 juillet est de 0,312, contre 0,366 les deux premières semaines.

Ce module a transformé un modèle de carnet (notebook) Jupyter en livrable : fichiers, commandes et documents qu’une autre personne peut exécuter et vérifier. L’exercice du module vous demande de livrer le classificateur et un rapport d’évaluation. Un collègue doit pouvoir reproduire chaque nombre avec les commandes seulement.

Livrez le dossier du projet avec :

  1. Le code : ticket_model.py, train.py, evaluate.py, predict.py, requirements.txt, get_data.py.
  2. MODEL_CARD.md, avec la section Surveillance et réentraînement.
  3. Un court rapport d’évaluation, reports/evaluation.md : le moment de prédiction et la cible, la comparaison à la référence de base, le seuil et sa méthode de sélection, les résultats du test (une seule fois), les principales limites, et les commandes exactes qui reproduisent chaque nombre du rapport, avec leurs sorties attendues.
  4. La preuve que tout fonctionne à partir de zéro : exécutez les commandes dans une nouvelle copie du projet, dans un nouveau terminal, avec un nouvel environnement .venv. L’artefact doit y donner des scores identiques.

N’envoyez pas .venv, data/ ni model/ : les commandes les recréent. C’est le but du test.

Afficher la réponse modèle

reports/evaluation.md :

reports/evaluation.md
# Evaluation report: ticket escalation model
Prediction time: when a ticket is created. Target: escalated_72h (escalated to a senior
agent within 72 hours). Decision: a flagged ticket goes straight to the senior team, which
can review at most 20% of new tickets.
Data: train 16,939 tickets (Jul 2024 to Apr 2026), validation 1,179 (May 2026),
test 1,002 (1 to 27 June 2026). Synthetic data, no real people.
Model: logistic regression in one pipeline (one-hot categories, median imputation with
indicators, scaling), 11 features, fitted on train only. Chosen over gradient boosting by
the one-standard-error rule (CV average precision 0.3393 against 0.3424, standard error
0.008). Threshold 0.19: the lowest that flags at most 20% of the validation set.
| At threshold 0.19 | Validation | Test (once) | Rule "priority 1" (test) |
|---|---|---|---|
| Flagged | 227 (19.3%) | 196 (19.6%) | 150 (15.0%) |
| Precision | 0.379 | 0.357 | 0.367 |
| Recall | 0.470 | 0.446 | 0.350 |
| Average precision | 0.391 | 0.345 | |
The model meets the target (at most 20% flagged, at least 40% caught) on validation and
test. The rule catches 35%. Nothing was changed after the test.
Limits: scores are too low since the policy change of 2026-05-01 (do not show them as a
chance); weak on account and priority 3 tickets; new channels are scored with less
information. See MODEL_CARD.md.
## Reproduce
python -m pip install -r requirements.txt
python get_data.py -> Ready: train 16939 rows (1983 escalated), valid 1179 (183), test 1002 (157).
python train.py -> Threshold 0.19 ... flagged 227 of 1179 (19.3%), precision 0.379, recall 0.47.
python predict.py --check -> Check passed: 20 scores are identical after reloading
python evaluate.py -> reports/test_report.json: precision 0.357, recall 0.446
Artifact SHA-256 (macOS, the versions in requirements.txt):
a4acd259a398e7c84517b98bf50d811197f90b0f2f1ea15f1f9e168ba3aff736

L’exécution dans une nouvelle copie. Créez un nouveau dossier, copiez-y seulement les fichiers livrés et exécutez les commandes :

New terminal
mkdir -p ~/projects/ticket-model-fresh/reports
cd ~/projects/ticket-model
cp ticket_model.py train.py evaluate.py predict.py requirements.txt get_data.py MODEL_CARD.md ~/projects/ticket-model-fresh/
cp reports/evaluation.md ~/projects/ticket-model-fresh/reports/
cd ~/projects/ticket-model-fresh
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements.txt
python get_data.py
python train.py
python predict.py --check
python evaluate.py
python predict.py data/july_tickets.csv predictions.csv
shasum -a 256 model/escalation_model.joblib predictions.csv ../ticket-model/predictions.csv

La sortie des cinq dernières commandes sur macOS (le JSON de evaluate.py est abrégé) :

Sortie
Trained on 16939 tickets. Threshold 0.19 for a review capacity of 20%.
Valid: ROC AUC 0.752, average precision 0.391, flagged 227 of 1179 (19.3%), precision 0.379, recall 0.47.
Saved model/escalation_model.joblib, model_info.json and check_sample.csv.
Check passed: 20 scores are identical after reloading (largest difference 1.1e-16).
{
"rows": 1002,
"escalated": 157,
"threshold": 0.19,
"roc_auc": 0.749,
"average_precision": 0.345,
...
"precision": 0.357,
"recall": 0.446,
...
}
Warning: channel: 91 rows have a category the model never saw (social)
Scored 1400 tickets with threshold 0.19: 282 flagged (20.1%). Wrote predictions.csv.
a4acd259a398e7c84517b98bf50d811197f90b0f2f1ea15f1f9e168ba3aff736 model/escalation_model.joblib
91936b4eff2c28a1b0c3382b8b88a05f2f7e677fc8395b2ab4486830a3dd9ff1 predictions.csv
91936b4eff2c28a1b0c3382b8b88a05f2f7e677fc8395b2ab4486830a3dd9ff1 ../ticket-model/predictions.csv

Les deux fichiers predictions.csv ont le même SHA-256. La nouvelle copie donne exactement le même score et le même signalement pour les 1 400 billets de juillet. evaluate.py s’exécute dans la nouvelle copie, car elle n’a pas encore de rapport. Cette exécution reproduit le même modèle; elle ne choisit rien. Ses nombres doivent être identiques, et ils le sont.

Sur Windows, pandas écrit les fichiers CSV avec d’autres fins de ligne. Les sommes de contrôle diffèrent donc de celles-ci. Comparez les deux fichiers du même ordinateur : ils doivent être identiques.

Un livrable complet comprend : chaque nombre du rapport reproduit par une commande indiquée; predict.py --check qui réussit dans la nouvelle copie; des prédictions identiques dans les deux copies; et une fiche et un rapport qui indiquent le même seuil et le même artefact.

Vérifiez votre compréhension

Rappel : énumérez le contenu d'une passation du modèle d'escalade et l'utilité de chaque élément.

Afficher la réponse
  • model/escalation_model.joblib : le pipeline entraîné complet (prétraitement et modèle), enregistré avec joblib.dump.
  • model/model_info.json : le schéma des variables, le seuil de 0,19, les versions des paquets, les sommes de contrôle des données et du fichier du modèle, et les résultats de validation.
  • model/check_sample.csv : 20 billets de soutien avec leurs scores enregistrés, pour predict.py --check.
  • requirements.txt : les versions exactes de pandas, scikit-learn, numpy, joblib et matplotlib.
  • train.py, predict.py (avec vérifications des entrées), evaluate.py et ticket_model.py : les commandes qui créent, vérifient et utilisent l’artefact.
  • MODEL_CARD.md : l’utilisation prévue, les utilisations exclues, les données, le choix du modèle, l’évaluation, les sous-groupes, les limites, les responsables, la date de révision, la surveillance et le déclencheur de réentraînement.
  • Un rapport d’évaluation avec les commandes qui reproduisent ses résultats.

Vérifiez votre compréhension

Prédiction : un nouvel export contient le canal whatsapp dans 40 billets sur 1 000 et les priorités P1, P2 et P3. Qu'affiche predict.py, et calcule-t-il les scores du lot?

Afficher la réponse

Il affiche un avertissement pour le canal : channel: 40 rows have a category the model never saw (whatsapp) (40 lignes contiennent une catégorie inconnue du modèle). Ensuite, il affiche une erreur pour la priorité : priority: 1000 values are not numbers, for example 'P1' (1 000 valeurs ne sont pas des nombres, par exemple « P1 », ou la première valeur du fichier). Puis il affiche Stopped: the input does not match the model's feature schema. Nothing was scored. (arrêt : les entrées ne respectent pas le schéma des variables du modèle; aucun score n’a été calculé), avec le code de sortie 1. Les scores du lot ne sont pas calculés, à cause de la priorité. Avec seulement le problème de canal, les scores seraient calculés, avec un avertissement. 40 sur 1 000 représente 4 %, au-dessus du seuil de déclenchement de 2 %. Cela lancerait donc aussi une révision du besoin de réentraînement.

Vérifiez votre compréhension

Vérification finale : chargez l'artefact dans un nouveau processus et vérifiez que les prédictions sont identiques sur un échantillon fixe. Donnez les commandes, la sortie qui le prouve et la raison d'utiliser un nouveau processus.

Afficher la réponse

Dans un nouveau terminal (ou une nouvelle copie du projet avec un nouvel environnement .venv), dans le dossier du projet :

Fenêtre de terminal
python predict.py --check
python predict.py data/july_tickets.csv predictions.csv

--check charge model/escalation_model.joblib et calcule les scores des 20 billets de model/check_sample.csv. Il les compare aux scores enregistrés pendant l’entraînement : Check passed: 20 scores are identical after reloading (largest difference 1.1e-16). (vérification réussie : les 20 scores sont identiques après rechargement; la plus grande différence est de 1,1 × 10⁻¹⁶). La différence est bien inférieure à la limite de 10⁻⁹; elle vient seulement de l’enregistrement des scores en texte. Pour un échantillon fixe plus grand, la somme SHA-256 de predictions.csv pour les 1 400 billets de juillet reste identique. C’est le cas après deux exécutions et dans une nouvelle copie (91936b4e… sur macOS).

Un nouveau processus est important parce qu’il démarre sans rien en mémoire : aucun pipeline entraîné, aucune variable, aucune importation du carnet. Si les scores y sont identiques, les fichiers seuls reproduisent le modèle. Une réponse complète montre la commande et la vérification réussie avec sa plus grande différence. Elle explique aussi pourquoi la vérification ne se fait pas dans le carnet d’entraînement.

Si votre projet ne fonctionne plus ou si vous voulez recommencer le module 8 :

  1. Supprimez les sorties : les dossiers model/ et reports/, et les fichiers predictions.csv, out.csv et broken.csv.
  2. Exécutez python get_data.py --reset. Vous devriez voir la ligne Ready: (prêt) avec 16 939, 1 983, 1 179, 183, 1 002 et 157.
  3. Si un fichier de code ne fonctionne plus, copiez la version du cours depuis C05/project : curl -fsSL -O https://raw.githubusercontent.com/Nextia-Intelligence/nextia-learning-labs/main/C05/project/<file> (curl.exe sur Windows).
  4. Exécutez python train.py, puis python predict.py --check. Vous devriez voir Threshold 0.19 (seuil de 0,19) et Check passed (vérification réussie).
  5. Exécutez python evaluate.py seulement si vous avez de nouveau besoin de reports/test_report.json. Il donne les mêmes résultats, parce que le modèle est le même.

Récapitulatif

Dans ce module, vous avez préparé la passation du modèle :

  • Empaqueter (Empaqueter les artefacts) : train.py enregistre le pipeline complet avec son schéma, son seuil de 0,19, ses versions, ses sommes de contrôle et un échantillon de vérification. requirements.txt fixe chaque version. predict.py --check prouve que les scores sont identiques dans un nouveau processus.
  • Documenter (Documenter les limites d’utilisation) : la fiche du modèle précise ses utilisations prévues et exclues, avec les preuves. Elle présente le rappel de test de 0,446 avec 19,6 % de billets signalés, contre 0,350 pour la règle. Elle décrit les sous-groupes faibles avec leurs effectifs, l’écart de calibration et les raisons du choix d’un modèle simple.
  • Vérifier (cette leçon) : predict.py s’arrête si une colonne manque, si un type est incorrect ou si une valeur dépasse les limites. Il compte aussi les catégories inconnues. La surveillance compare le taux de signalement, le score moyen, les catégories inconnues et les entrées aux valeurs attendues. En juillet, deux des trois conditions de déclenchement ont été remplies : le nouveau canal social (réseaux sociaux) et un taux de signalement sur 7 jours atteignant 26,2 %. Avec les étiquettes, le rappel de juillet est de 0,480 et la précision de 0,333.
  • Réentraîner selon une règle écrite et une révision, pas selon une impression. Les mécanismes de déploiement seront abordés dans le cours sur le déploiement.

Dans ce cours, vous avez appris ce qu’est l’apprentissage automatique et comment juger un modèle avant d’en créer un. Vous avez cadré un problème de prédiction et dépassé des références de base simples. Vous avez créé un pipeline, essayé des familles de modèles et consigné chaque expérience. Vous avez réglé le modèle sans toucher aux données de test et choisi un seuil adapté à la capacité. Vous avez expliqué les erreurs et choisi un modèle simple pour de bonnes raisons. Vous l’avez maintenant livré avec ses limites. Viennent ensuite les études de cas : des projets réels sur des données publiques, de la question à la recommandation. Réalisez-en au moins une pour obtenir le certificat. Le travail final vous demande ensuite de tout faire vous-même, de la référence de base au rapport d’évaluation.

Leçon suivante : Prévoir la demande de vélopartage