Aller au contenu
English

Seuils et calibration

  • Leçon 20
  • 75 min
  • Module 6 : Régler et finaliser le modèle
Leçon 20 sur 27 · Apprentissage automatique : du problème à un modèle fiableSeuils et calibration

Ce que vous saurez faire

Transformez les scores du modèle en signalements que l’équipe senior peut traiter. Vérifiez si les gens peuvent lire un score comme une probabilité. Faites ensuite passer au modèle son unique examen final sur les données de test. Le seuil par défaut ne signale presque rien. Un score juste l’an dernier peut être trop bas maintenant. Aucun de ces problèmes ne déclenche d’erreur.

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

Le carnet (notebook) Jupyter contient toutes les cellules de code, les tâches et les vérifications de cette leçon. Il contient aussi l’exercice du module et sa vérification, avec les consignes. Ses cellules de préparation installent pandas et scikit-learn. Elles téléchargent les données d’exercice, ticket_model.py et train.py. Vous écrivez evaluate.py dans le carnet. Il fonctionne donc sans le projet local. Utilisez l’explorateur et le graphique 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 75 minutes

Ce que vous devez savoir

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, scikit-learn 1.9.1, joblib 1.6.0 et matplotlib 3.11.2. Les commandes Windows n’ont pas été testées. Le carnet a été testé dans Jupyter.

Une petite clinique a un médecin disponible cet après-midi, avec du temps pour trois patients. Dix personnes attendent. Une infirmière examine chaque personne et note un score d’urgence. Le médecin voit ensuite les trois personnes aux scores les plus élevés. L’infirmière n’a pas besoin d’un seuil « correct » : le temps du médecin le détermine. Avec du temps pour trois personnes, le seuil correspond au score de la troisième personne la plus urgente.

L’équipe senior de Grace fonctionne de la même façon. Vous connaissez déjà les outils présentés dans Le compromis entre précision et rappel. Un score classe les billets de soutien. Un seuil transforme le score en signalement : signalez le billet si son score atteint ou dépasse le seuil. Un seuil plus bas augmente le rappel et diminue la précision. L’équipe de Grace peut examiner au plus 20 % des billets. Cette limite est sa capacité. Dans cette leçon, vous avez choisi le seuil de points le plus bas qui respecte cette limite : 3 points ou plus.

Une chose est nouvelle dans cette leçon : le score vient maintenant du modèle entraîné. C’est un nombre entre 0 et 1 qui peut prendre de nombreuses valeurs. Le seuil peut donc varier par petits pas, sans règle pour départager les égalités. La règle reste la même : le meilleur seuil est le plus bas qui respecte la capacité. Il détecte le plus de cas transférés à l’équipe senior que celle-ci peut examiner. Voici dix scores, avec une capacité de 3 billets sur 10 (30 %) :

Seuil Scores qui atteignent ou dépassent le seuil Signalés Respecte les 30 %?
0,43 0,81, 0,62, 0,55, 0,43 4 (40 %) Non
0,44 0,81, 0,62, 0,55 3 (30 %) Oui : le plus bas qui convient
0,56 0,81, 0,62 2 (20 %) Oui, mais il manque le billet à 0,55

choose_threshold() procède par pas de 0,01. Cette fonction se trouve dans le fichier train.py du cours, que vous téléchargerez plus loin dans cette leçon :

train.py (part)
def choose_threshold(scores, capacity):
"""The lowest threshold (in steps of 0.01) that flags no more than
`capacity` of the tickets. Lower thresholds flag more tickets and catch
more escalations, so the lowest one that fits catches the most."""
for step in range(1, 100):
threshold = step / 100
if (scores >= threshold).mean() <= capacity:
return threshold
return 1.0

Comme dans chaque carnet de ce cours, np désigne NumPy et pd désigne pandas. Les cellules de préparation du carnet les importent.

scores_10 = np.array([0.81, 0.62, 0.55, 0.43, 0.38, 0.30, 0.22, 0.15, 0.09, 0.04])
threshold_10 = choose_threshold(scores_10, 0.3)
print("threshold:", threshold_10, "| flagged:", (scores_10 >= threshold_10).sum())
Sortie
threshold: 0.44 | flagged: 3

Exemple détaillé : le seuil sur les données de validation

Section intitulée « Exemple détaillé : le seuil sur les données de validation »

Le seuil est un choix. On le choisit donc sur les données de validation, jamais sur les données de test. Priya entraîne le modèle final sur les données d’entraînement. Elle calcule les scores des 1 179 billets de mai et affiche les quatre comptes pour six seuils :

from sklearn.metrics import confusion_matrix
from ticket_model import FEATURES, TARGET, build_pipeline, load
train, valid = load("train"), load("valid")
pipeline = build_pipeline().fit(train[FEATURES], train[TARGET])
scores = pipeline.predict_proba(valid[FEATURES])[:, 1]
for threshold in [0.5, 0.3, 0.2, 0.19, 0.15, 0.1]:
flags = (scores >= threshold).astype(int)
tn, fp, fn, tp = confusion_matrix(valid[TARGET], flags).ravel()
print(f"{threshold:4}: flagged {tp + fp:3d} ({(tp + fp) / len(valid):5.1%}) TP {tp:3d} FP {fp:3d} FN {fn:3d} TN {tn:3d}"
f" precision {tp / (tp + fp):.3f} recall {tp / (tp + fn):.3f}")
Sortie
0.5: flagged 25 ( 2.1%) TP 17 FP 8 FN 166 TN 988 precision 0.680 recall 0.093
0.3: flagged 92 ( 7.8%) TP 46 FP 46 FN 137 TN 950 precision 0.500 recall 0.251
0.2: flagged 203 (17.2%) TP 81 FP 122 FN 102 TN 874 precision 0.399 recall 0.443
0.19: flagged 227 (19.3%) TP 86 FP 141 FN 97 TN 855 precision 0.379 recall 0.470
0.15: flagged 314 (26.6%) TP 106 FP 208 FN 77 TN 788 precision 0.338 recall 0.579
0.1: flagged 475 (40.3%) TP 128 FP 347 FN 55 TN 649 precision 0.269 recall 0.699

C’est le compromis du module 3, cette fois avec le vrai modèle. Chaque baisse détecte plus de cas transférés et entraîne plus d’examens inutiles. Les données ne peuvent pas décider quel seuil convient. Grace donne la réponse : au plus 20 % des billets.

threshold = choose_threshold(scores, 0.20)
flags = (scores >= threshold).astype(int)
print("threshold:", threshold, f"| flagged {flags.sum()} of {len(valid)} ({flags.mean():.1%})")
Sortie
threshold: 0.19 | flagged 227 of 1179 (19.3%)

Le seuil est de 0,19. À 0,18, le modèle signale 248 billets (21,0 %), ce qui dépasse la capacité. À 0,19, il détecte 86 des 183 cas transférés (rappel de 0,470), avec 227 examens. La règle de priorité 1 en détecte 61 avec 163 examens (rappel de 0,333). L’objectif d’amélioration de Définir une référence de base est donc atteint sur les données de validation. Il exige au plus 20 % de billets signalés et au moins 40 % des cas transférés détectés.

Chaque colonne représente la matrice de confusion du même modèle sur les mêmes 1 179 billets de mai, à un seuil. Seul le seuil change :

Seuil de 0,5 (par défaut) Seuil de 0,19 (choisi) Seuil de 0,1
Détectés : transférés, signalés (VP) 17 86 128
Manqués : transférés, non signalés (FN) 166 97 55
Fausses alertes : non transférés, signalés (FP) 8 141 347
Correctement laissés : non transférés, non signalés (VN) 988 855 649
Signalés 25 (2,1 %) 227 (19,3 %) 475 (40,3 %)
Précision 0,680 0,379 0,269
Rappel 0,093 0,470 0,699
  • À 0,5, l’équipe reçoit 25 examens dans le mois, et 166 cas transférés attendent dans la file normale. La précision est élevée parce que seuls les cas les plus évidents passent.
  • À 0,1, l’équipe reçoit 475 examens, soit deux fois sa capacité. Parmi eux, 347 sont inutiles, soit presque trois sur quatre.
  • À 0,19, l’équipe atteint sa capacité et détecte presque la moitié des cas transférés. Environ 4 examens sur 10 trouvent un cas transféré, et un examen prend environ 10 minutes.

L’explorateur contient les scores du modèle final pour les 1 179 billets de mai. Il y a 996 billets non transférés (panneau supérieur) et 183 billets transférés (panneau inférieur). Ce sont les mêmes scores que dans le tableau, donc tous les comptes correspondent.

Explorer

Le seuil adapté à la capacité de l'équipe senior

01002003000–0,02 : 70 (0 signalé)0,02–0,04 : 225 (0 signalé)0,04–0,06 : 170 (0 signalé)0,06–0,08 : 106 (0 signalé)0,08–0,1 : 78 (0 signalé)0,1–0,12 : 64 (0 signalé)0,12–0,14 : 54 (0 signalé)0,14–0,16 : 40 (0 signalé)0,16–0,18 : 32 (0 signalé)0,18–0,2 : 35 (0 signalé)0,2–0,22 : 23 (0 signalé)0,22–0,24 : 15 (0 signalé)0,24–0,26 : 19 (0 signalé)0,26–0,28 : 12 (0 signalé)0,28–0,3 : 7 (0 signalé)0,3–0,32 : 7 (0 signalé)0,32–0,34 : 2 (0 signalé)0,34–0,36 : 4 (0 signalé)0,36–0,38 : 4 (0 signalé)0,38–0,4 : 8 (0 signalé)0,4–0,42 : 3 (0 signalé)0,42–0,44 : 6 (0 signalé)0,44–0,46 : 2 (0 signalé)0,56–0,58 : 2 (2 signalés)non signalés 988fausses alertes 8010200–0,02 : 3 (0 signalé)0,02–0,04 : 14 (0 signalé)0,04–0,06 : 11 (0 signalé)0,06–0,08 : 11 (0 signalé)0,08–0,1 : 16 (0 signalé)0,1–0,12 : 6 (0 signalé)0,12–0,14 : 6 (0 signalé)0,14–0,16 : 14 (0 signalé)0,16–0,18 : 11 (0 signalé)0,18–0,2 : 10 (0 signalé)0,2–0,22 : 6 (0 signalé)0,22–0,24 : 8 (0 signalé)0,24–0,26 : 14 (0 signalé)0,26–0,28 : 3 (0 signalé)0,28–0,3 : 4 (0 signalé)0,3–0,32 : 5 (0 signalé)0,34–0,36 : 3 (0 signalé)0,36–0,38 : 2 (0 signalé)0,38–0,4 : 4 (0 signalé)0,4–0,42 : 5 (0 signalé)0,42–0,44 : 4 (0 signalé)0,44–0,46 : 2 (0 signalé)0,46–0,48 : 2 (0 signalé)0,48–0,5 : 2 (0 signalé)0,5–0,52 : 2 (2 signalés)0,52–0,54 : 2 (2 signalés)0,54–0,56 : 1 (1 signalé)0,56–0,58 : 1 (1 signalé)0,58–0,6 : 2 (2 signalés)0,6–0,62 : 3 (3 signalés)0,62–0,64 : 4 (4 signalés)0,72–0,74 : 1 (1 signalé)0,8–0,82 : 1 (1 signalé)manqués 166détectés 1700,10,20,30,40,50,60,70,80,91Score donné par le modèleseuil 0,50Non transférés : 996Transférés : 183
01002003000–0,02 : 70 (0 signalé)0,02–0,04 : 225 (0 signalé)0,04–0,06 : 170 (0 signalé)0,06–0,08 : 106 (0 signalé)0,08–0,1 : 78 (0 signalé)0,1–0,12 : 64 (0 signalé)0,12–0,14 : 54 (0 signalé)0,14–0,16 : 40 (0 signalé)0,16–0,18 : 32 (0 signalé)0,18–0,2 : 35 (0 signalé)0,2–0,22 : 23 (0 signalé)0,22–0,24 : 15 (0 signalé)0,24–0,26 : 19 (0 signalé)0,26–0,28 : 12 (0 signalé)0,28–0,3 : 7 (0 signalé)0,3–0,32 : 7 (0 signalé)0,32–0,34 : 2 (0 signalé)0,34–0,36 : 4 (0 signalé)0,36–0,38 : 4 (0 signalé)0,38–0,4 : 8 (0 signalé)0,4–0,42 : 3 (0 signalé)0,42–0,44 : 6 (0 signalé)0,44–0,46 : 2 (0 signalé)0,56–0,58 : 2 (2 signalés)non signalés 988fausses alertes 8010200–0,02 : 3 (0 signalé)0,02–0,04 : 14 (0 signalé)0,04–0,06 : 11 (0 signalé)0,06–0,08 : 11 (0 signalé)0,08–0,1 : 16 (0 signalé)0,1–0,12 : 6 (0 signalé)0,12–0,14 : 6 (0 signalé)0,14–0,16 : 14 (0 signalé)0,16–0,18 : 11 (0 signalé)0,18–0,2 : 10 (0 signalé)0,2–0,22 : 6 (0 signalé)0,22–0,24 : 8 (0 signalé)0,24–0,26 : 14 (0 signalé)0,26–0,28 : 3 (0 signalé)0,28–0,3 : 4 (0 signalé)0,3–0,32 : 5 (0 signalé)0,34–0,36 : 3 (0 signalé)0,36–0,38 : 2 (0 signalé)0,38–0,4 : 4 (0 signalé)0,4–0,42 : 5 (0 signalé)0,42–0,44 : 4 (0 signalé)0,44–0,46 : 2 (0 signalé)0,46–0,48 : 2 (0 signalé)0,48–0,5 : 2 (0 signalé)0,5–0,52 : 2 (2 signalés)0,52–0,54 : 2 (2 signalés)0,54–0,56 : 1 (1 signalé)0,56–0,58 : 1 (1 signalé)0,58–0,6 : 2 (2 signalés)0,6–0,62 : 3 (3 signalés)0,62–0,64 : 4 (4 signalés)0,72–0,74 : 1 (1 signalé)0,8–0,82 : 1 (1 signalé)manqués 166détectés 1700,30,50,81Score donné par le modèleseuil 0,50Non transférés : 996Transférés : 183

Les barres pleines sont signalées (score égal ou supérieur au seuil). Les barres pâles ne sont pas signalées. Vous pouvez aussi faire glisser la poignée ronde sur la ligne, ou la sélectionner et appuyer sur les flèches (Maj pour des pas de 0,10). Les vérifications par semaine supposent 266 billets par semaine, avec la même répartition que ces 1 179.

Au seuil 0,50, le modèle en signale 25 : 17 de la classe « transférés » et 8 fausses alertes. Il manque 166 des 183 billets de la classe « transférés ».

Matrice de confusion au seuil 0,50
Classe réelleSignalésNon signalésTotal
Transférés17vrai positif166faux négatif (manqué)183
Non transférés8faux positif (fausse alerte)988vrai négatif996
Total251 1541 179
Signalés
25
sur 1 179 billets
Précision
68,0 %
17 sur 25 signalés
Rappel
9,3 %
17 sur 183
Taux de faux positifs
0,8 %
8 sur 996
Vérifications par semaine
6
environ 1 par jour
Scores du modèle final pour les 1 179 billets de mai des données de validation. Le seuil, la matrice de confusion et les examens par semaine sont aussi présentés. Mai comptait environ 266 billets par semaine. La capacité de 20 % correspond donc à environ 53 examens par semaine.
Description textuelle de l’explorateur

Le panneau supérieur est un histogramme des scores des 996 billets non transférés. La plupart des scores sont bas : deux sur trois sont sous 0,10. La barre la plus haute couvre 0,02 à 0,04 (225 billets), et la médiane est de 0,065. Le score le plus élevé est de 0,91. Le panneau inférieur est un histogramme des 183 billets transférés. Leurs scores sont plus dispersés, de 0,02 à 0,80, avec une médiane de 0,18. Environ 3 billets transférés sur 10 ont un score inférieur à 0,10. Aucun seuil ne peut donc les détecter sans signaler la plupart des billets. Les deux panneaux se chevauchent sur la majeure partie de l’étendue. Une ligne verticale indique le seuil.

Au seuil initial de 0,50, le modèle signale 25 billets : 17 transférés et 8 non transférés. Il manque 166 cas transférés. La précision est de 68,0 %, le rappel de 9,3 %, et l’équipe reçoit environ 6 examens par semaine. À 0,30 : 92 signalés, rappel de 25,1 %. À 0,19 : 227 signalés (86 transférés, 141 non transférés), 97 manqués, précision de 37,9 %, rappel de 47,0 %. Cela donne environ 51 examens par semaine, soit environ 7 par jour. À 0,18, 248 billets sont signalés, ce qui dépasse 235. À 0,10 : 475 signalés, rappel de 69,9 %, environ 107 examens par semaine.

Abaissez le seuil de 0,50 à 0,19 et observez les examens par semaine. L’explorateur suppose 266 billets par semaine (1 179 billets de mai ÷ 31 jours × 7). La capacité de 20 % correspond à environ 53 examens par semaine. À 0,19, l’équipe en reçoit environ 51; à 0,18, environ 56, ce qui est trop.

Calibration : peut-on lire un score comme une probabilité?

Section intitulée « Calibration : peut-on lire un score comme une probabilité? »

Une météorologue annonce « 70 % de probabilité de pluie ». Comment savoir si ses prévisions sont bonnes? Rassemblez tous les jours où elle a annoncé 70 %. S’il a plu environ 7 de ces jours sur 10, son 70 % correspond à la réalité. S’il n’a plu que 4 jours sur 10, ses chiffres sont trop élevés. C’est vrai même si elle classe correctement les jours pluvieux devant les jours secs.

Grace a un projet semblable. Elle veut afficher « probabilité de transfert : 24 % » près d’un billet, pour chaque agent. Ce n’est honnête que si environ 24 % des billets au score proche de 0,24 sont réellement transférés. Un modèle qui a cette propriété est calibré : ses scores correspondent à la fréquence des événements. Dans Calibration et sous-groupes, dans le cours de mathématiques, vous avez lu un diagramme de calibration. Maintenant, vous vérifiez un vrai modèle, sur les données d’entraînement et sur le mois suivant le changement de politique.

Un nombre résume cela : le score de Brier, la moyenne de (score − résultat)². Le résultat vaut 1 pour un transfert et 0 sinon. Plus le score est bas, mieux c’est. Voici quatre billets :

Billet Score Transféré (score − résultat)²
A 0,9 1 (0,9 − 1)² = 0,01
B 0,2 0 0,04
C 0,6 0 0,36
D 0,1 0 0,01

Score de Brier = (0,01 + 0,04 + 0,36 + 0,01) ÷ 4 = 0,105. Le billet C contribue le plus : un score confiant, mais faux. Le score de Brier mélange deux choses : la qualité du classement des billets et la correspondance entre scores et taux. Pour voir uniquement la calibration, il faut donc aussi le graphique ci-dessous.

Pour le vrai modèle, Priya regroupe les scores par intervalles. Elle compare le score moyen au taux de transfert observé dans chaque intervalle :

train_scores = pipeline.predict_proba(train[FEATURES])[:, 1]
bins = [0, 0.05, 0.1, 0.2, 0.3, 0.5, 1]
def reliability(part, part_scores):
"""Tickets, mean score and observed escalation rate per score bin."""
table = pd.DataFrame({"score": part_scores, "escalated": part[TARGET].to_numpy()})
return table.groupby(pd.cut(part_scores, bins), observed=True).agg(
tickets=("escalated", "size"), mean_score=("score", "mean"), rate=("escalated", "mean")).round(3)
print("train\n", reliability(train, train_scores))
print("valid\n", reliability(valid, scores))
Sortie
train
tickets mean_score rate
(0.0, 0.05] 5576 0.031 0.030
(0.05, 0.1] 4713 0.072 0.072
(0.1, 0.2] 3843 0.142 0.140
(0.2, 0.3] 1493 0.244 0.249
(0.3, 0.5] 1025 0.376 0.389
(0.5, 1.0] 289 0.614 0.574
valid
tickets mean_score rate
(0.0, 0.05] 414 0.031 0.053
(0.05, 0.1] 290 0.071 0.114
(0.1, 0.2] 272 0.145 0.173
(0.2, 0.3] 111 0.241 0.315
(0.3, 0.5] 67 0.388 0.433
(0.5, 1.0] 25 0.625 0.680

Sur les données d’entraînement, le score moyen de chaque intervalle est proche de son taux. Les scores de 0,2 à 0,3 ont une moyenne de 0,244, et 24,9 % de ces billets ont été transférés. En mai, tous les taux dépassent le score moyen. Le même intervalle a une moyenne de 0,241, et 31,5 % des billets ont été transférés.

Un graphique de fiabilité est le diagramme de calibration que vous avez lu dans le cours de mathématiques. Il représente les mêmes intervalles : un point par intervalle, placé selon son score moyen et son taux observé. Un point sur la diagonale en pointillés signifie « score = taux ». C’est comme une météorologue dont les jours annoncés à 70 % sont pluvieux 7 fois sur 10.

  • Entraînement (juillet 2024 à avril 2026)
  • Validation (mai 2026)
  • Calibration parfaite (pointillés)
Graphique de fiabilité du modèle final : score moyen et taux de transfert observé dans six intervalles de scores. Il présente les données d'entraînement et de validation. L'entraînement reste près de la diagonale; mai se trouve au-dessus dans chaque intervalle.
Description textuelle du graphique

Un graphique carré. L’axe horizontal représente le score moyen d’un intervalle, et l’axe vertical, le taux de transfert observé. Les deux vont de 0 à 0,7. Une diagonale en pointillés indique une calibration parfaite. La courbe d’entraînement compte six points, tous proches de la diagonale : (0,031, 0,030), (0,072, 0,072), (0,142, 0,140), (0,244, 0,249), (0,376, 0,389) et (0,614, 0,574). Seul le dernier intervalle, avec 289 billets, est légèrement en dessous. La courbe de validation compte six points, tous au-dessus de la diagonale : (0,031, 0,053), (0,071, 0,114), (0,145, 0,173), (0,241, 0,315), (0,388, 0,433) et (0,625, 0,680). Un point marqué indique l’intervalle de 0,2 à 0,3 en mai : score moyen de 0,24, taux de 31,5 %. Le dernier intervalle de validation ne compte que 25 billets.

Le modèle est calibré sur les mois dont il a appris, et il sous-estime le risque en mai. Le score de Brier montre la même chose :

from sklearn.metrics import brier_score_loss
print(f"train: model {brier_score_loss(train[TARGET], train_scores):.4f}, constant 0.117 {brier_score_loss(train[TARGET], np.full(len(train), 0.117)):.4f}")
print(f"valid: model {brier_score_loss(valid[TARGET], scores):.4f}, constant 0.117 {brier_score_loss(valid[TARGET], np.full(len(valid), 0.117)):.4f}")
print(f"valid: mean score {scores.mean():.3f}, escalation rate {valid[TARGET].mean():.3f}")
Sortie
train: model 0.0902, constant 0.117 0.1034
valid: model 0.1155, constant 0.117 0.1326
valid: mean score 0.120, escalation rate 0.155

Le modèle fait mieux qu’un score constant de 0,117 sur les deux jeux de données. Son score de Brier est moins bon sur les données de validation (0,1155 contre 0,0902). C’est en partie attendu : avec plus de transferts, tous les modèles ont un score de Brier plus élevé. Le reste vient de l’écart : le modèle annonce 12,0 % en moyenne, et 15,5 % des billets sont transférés.

Trouvez d’abord où se situe l’écart. Le changement de politique concernait les garanties, donc Priya compare les équipes :

by_team = valid.assign(score=scores).groupby("team").agg(
tickets=(TARGET, "size"), mean_score=("score", "mean"), rate=(TARGET, "mean"))
print(by_team.round(3))
Sortie
tickets mean_score rate
team
account 166 0.057 0.072
delivery 348 0.092 0.121
payment 206 0.214 0.233
returns 291 0.098 0.113
warranty 168 0.163 0.286

Les billets de garantie ont le plus grand écart : score moyen de 0,163 et taux de 0,286. Cela représente environ 12 points de pourcentage. Le modèle a appris le taux des garanties avant le changement de politique.

Demandez ensuite ce que l’écart compromet. La règle de capacité utilise seulement l’ordre des scores. Elle signale les billets les plus risqués jusqu’à atteindre la capacité, comme la liste de l’infirmière. Si tous les scores sont trop bas du même montant, l’ordre reste le même. Priya le vérifie : elle ajoute 0,035 à chaque score. La moyenne passe ainsi de 0,120 au taux réel.

from sklearn.metrics import average_precision_score
shifted = np.clip(scores + 0.035, 0, 1)
print(f"mean score: {scores.mean():.3f} -> {shifted.mean():.3f}")
print("average precision:", round(average_precision_score(valid[TARGET], scores), 3), "->",
round(average_precision_score(valid[TARGET], shifted), 3))
Sortie
mean score: 0.120 -> 0.155
average precision: 0.391 -> 0.391

L’ordre n’a pas changé, donc la précision moyenne n’a pas changé. Cela mène à trois conclusions :

  • Le seuil de 0,19 fonctionne toujours. Il signale les 19,3 % des billets de mai les plus risqués. Ce classement reste valable après le changement de politique.
  • Il ne faut pas présenter les scores comme « la probabilité de transfert ». Un billet de garantie au score de 0,16 est transféré presque deux fois plus souvent que 16 %. Un tableau de bord affichant « 16 % » induirait les agents de Grace en erreur.
  • La solution demande de nouvelles données, pas un nouveau seuil. Une méthode consiste à recalibrer les scores sur des billets récents étiquetés. Cela signifie apprendre une correspondance entre score et taux observé, par exemple avec CalibratedClassifierCV de scikit-learn. Vérifiez-la ensuite sur un mois ultérieur. L’autre méthode consiste à réentraîner le modèle en incluant mai et juin. Le module 8 en fait le plan de réentraînement.

Utilisez les sections 1 à 4 du carnet.

  1. Exécutez les cellules de préparation. Vous devriez voir la ligne Ready: (prêt).
  2. Exécutez la section 1. Vous devriez voir threshold: 0.44 | flagged: 3 (seuil : 0,44; signalés : 3).
  3. Exécutez le tableau des seuils dans la section 2. Vérifiez la ligne de 0,19 : 227 signalés, rappel de 0,470.
  4. Prédisez le seuil pour une capacité de 20 %, puis exécutez la cellule suivante. Vous devriez voir threshold: 0.19 (seuil : 0,19).
  5. Dans la section 4, exécutez le tableau de fiabilité et le graphique. La courbe de validation doit être au-dessus de la diagonale.
  6. Calculez brier_valid à la main : la moyenne de (score − résultat)², arrondie à 4 décimales.
  7. Exécutez la cellule de vérification. Elle affiche Check passed: brier_valid is right. (vérification réussie : brier_valid est correct) si votre valeur est correcte.
  8. Exécutez la cellule brier_score_loss. Votre valeur doit être égale à celle de la ligne de validation.
Afficher la réponse
brier_valid = round(float(np.mean((scores - valid[TARGET].to_numpy()) ** 2)), 4)
print(brier_valid)
Sortie
0.1155

Elle est égale à brier_score_loss sur les données de validation : 0,1155.

Changez une condition. Grace dit que, pendant une semaine chargée, son équipe peut examiner seulement 15 % des billets. Avant l’exécution, faites une prédiction : le seuil monte-t-il ou descend-il? Le rappel monte-t-il ou descend-il? Calculez ensuite le seuil avec choose_threshold(scores, 0.15) et comptez les billets de mai signalés (carnet, section 2).

Afficher la réponse
threshold_15 = choose_threshold(scores, 0.15)
flagged_15 = int((scores >= threshold_15).sum())
print(threshold_15, flagged_15)
Sortie
0.22 174

Le seuil monte à 0,22 : 174 billets signalés (14,8 %). Avec moins d’examens, le rappel descend à 0,410 (75 cas sur 183 détectés). La précision monte à 0,431, car seuls les cas les plus évidents passent. Le modèle détecte encore plus de cas transférés que la règle de priorité 1 (61 transferts). Le nombre d’examens est presque identique (174 contre 163).

Erreur courante

Le seuil par défaut de predict()

Symptôme : Tomás utilise pipeline.predict(), comme dans la plupart des tutoriels :

flags_default = pipeline.predict(valid[FEATURES])
print("predict() flags:", flags_default.sum(), f"({flags_default.mean():.1%})")
Sortie
predict() flags: 25 (2.1%)

L’équipe expérimentée reçoit 25 billets à examiner en un mois et a du temps libre. Pendant ce temps, 166 des 183 cas transférés attendent dans la file normale. Il n’y a ni erreur ni avertissement.

Cause : predict() utilise un seuil de 0,5. Seulement 25 billets de mai ont un score aussi élevé. En effet, seulement 15,5 % des billets sont transférés, et le modèle est prudent. La valeur 0,5 est un choix par défaut de la bibliothèque, pas une décision de l’entreprise.

Solution : utilisez predict_proba() et choisissez le seuil sur les données de validation selon la décision : choose_threshold(scores, 0.20) donne 0,19. Enregistrez le seuil avec le modèle pour que chaque utilisateur applique le même. train.py l’écrit dans model_info.json.

Conséquence dans le travail en IA : un modèle qui classe bien les cas (aire sous la courbe ROC de 0,752) ne fait presque rien. Il manque un seul nombre. Une équipe qui vérifie seulement que « le modèle est déployé » ne voit jamais ce problème. Vérifiez chaque semaine le taux de signalement par rapport à la capacité (module 8).

Cette partie est facultative. Voici les coûts approximatifs de Grace : un examen prend environ 10 minutes à un agent expérimenté. Supposons qu’un transfert manqué coûte environ 60 minutes de travail ultérieur (et un client mécontent). Le coût total à un seuil donné est donc 10 × billets signalés + 60 × transferts manqués, en minutes. Seuils et coûts, dans le cours de mathématiques, explique la courbe des coûts.

Explorer

Facultatif : quel serait le seuil le moins coûteux

01002003000–0,02 : 70 (0 signalé)0,02–0,04 : 225 (0 signalé)0,04–0,06 : 170 (0 signalé)0,06–0,08 : 106 (0 signalé)0,08–0,1 : 78 (0 signalé)0,1–0,12 : 64 (0 signalé)0,12–0,14 : 54 (0 signalé)0,14–0,16 : 40 (0 signalé)0,16–0,18 : 32 (0 signalé)0,18–0,2 : 35 (19 signalés)0,18–0,2 : 35 (19 signalés)0,2–0,22 : 23 (23 signalés)0,22–0,24 : 15 (15 signalés)0,24–0,26 : 19 (19 signalés)0,26–0,28 : 12 (12 signalés)0,28–0,3 : 7 (7 signalés)0,3–0,32 : 7 (7 signalés)0,32–0,34 : 2 (2 signalés)0,34–0,36 : 4 (4 signalés)0,36–0,38 : 4 (4 signalés)0,38–0,4 : 8 (8 signalés)0,4–0,42 : 3 (3 signalés)0,42–0,44 : 6 (6 signalés)0,44–0,46 : 2 (2 signalés)0,56–0,58 : 2 (2 signalés)non signalés 855fausses alertes 141010200–0,02 : 3 (0 signalé)0,02–0,04 : 14 (0 signalé)0,04–0,06 : 11 (0 signalé)0,06–0,08 : 11 (0 signalé)0,08–0,1 : 16 (0 signalé)0,1–0,12 : 6 (0 signalé)0,12–0,14 : 6 (0 signalé)0,14–0,16 : 14 (0 signalé)0,16–0,18 : 11 (0 signalé)0,18–0,2 : 10 (5 signalés)0,18–0,2 : 10 (5 signalés)0,2–0,22 : 6 (6 signalés)0,22–0,24 : 8 (8 signalés)0,24–0,26 : 14 (14 signalés)0,26–0,28 : 3 (3 signalés)0,28–0,3 : 4 (4 signalés)0,3–0,32 : 5 (5 signalés)0,34–0,36 : 3 (3 signalés)0,36–0,38 : 2 (2 signalés)0,38–0,4 : 4 (4 signalés)0,4–0,42 : 5 (5 signalés)0,42–0,44 : 4 (4 signalés)0,44–0,46 : 2 (2 signalés)0,46–0,48 : 2 (2 signalés)0,48–0,5 : 2 (2 signalés)0,5–0,52 : 2 (2 signalés)0,52–0,54 : 2 (2 signalés)0,54–0,56 : 1 (1 signalé)0,56–0,58 : 1 (1 signalé)0,58–0,6 : 2 (2 signalés)0,6–0,62 : 3 (3 signalés)0,62–0,64 : 4 (4 signalés)0,72–0,74 : 1 (1 signalé)0,8–0,82 : 1 (1 signalé)manqués 97détectés 8600,10,20,30,40,50,60,70,80,91Score donné par le modèleseuil 0,19Non transféré : 996Transféré : 183
01002003000–0,02 : 70 (0 signalé)0,02–0,04 : 225 (0 signalé)0,04–0,06 : 170 (0 signalé)0,06–0,08 : 106 (0 signalé)0,08–0,1 : 78 (0 signalé)0,1–0,12 : 64 (0 signalé)0,12–0,14 : 54 (0 signalé)0,14–0,16 : 40 (0 signalé)0,16–0,18 : 32 (0 signalé)0,18–0,2 : 35 (19 signalés)0,18–0,2 : 35 (19 signalés)0,2–0,22 : 23 (23 signalés)0,22–0,24 : 15 (15 signalés)0,24–0,26 : 19 (19 signalés)0,26–0,28 : 12 (12 signalés)0,28–0,3 : 7 (7 signalés)0,3–0,32 : 7 (7 signalés)0,32–0,34 : 2 (2 signalés)0,34–0,36 : 4 (4 signalés)0,36–0,38 : 4 (4 signalés)0,38–0,4 : 8 (8 signalés)0,4–0,42 : 3 (3 signalés)0,42–0,44 : 6 (6 signalés)0,44–0,46 : 2 (2 signalés)0,56–0,58 : 2 (2 signalés)non signalés 855fausses alertes 141010200–0,02 : 3 (0 signalé)0,02–0,04 : 14 (0 signalé)0,04–0,06 : 11 (0 signalé)0,06–0,08 : 11 (0 signalé)0,08–0,1 : 16 (0 signalé)0,1–0,12 : 6 (0 signalé)0,12–0,14 : 6 (0 signalé)0,14–0,16 : 14 (0 signalé)0,16–0,18 : 11 (0 signalé)0,18–0,2 : 10 (5 signalés)0,18–0,2 : 10 (5 signalés)0,2–0,22 : 6 (6 signalés)0,22–0,24 : 8 (8 signalés)0,24–0,26 : 14 (14 signalés)0,26–0,28 : 3 (3 signalés)0,28–0,3 : 4 (4 signalés)0,3–0,32 : 5 (5 signalés)0,34–0,36 : 3 (3 signalés)0,36–0,38 : 2 (2 signalés)0,38–0,4 : 4 (4 signalés)0,4–0,42 : 5 (5 signalés)0,42–0,44 : 4 (4 signalés)0,44–0,46 : 2 (2 signalés)0,46–0,48 : 2 (2 signalés)0,48–0,5 : 2 (2 signalés)0,5–0,52 : 2 (2 signalés)0,52–0,54 : 2 (2 signalés)0,54–0,56 : 1 (1 signalé)0,56–0,58 : 1 (1 signalé)0,58–0,6 : 2 (2 signalés)0,6–0,62 : 3 (3 signalés)0,62–0,64 : 4 (4 signalés)0,72–0,74 : 1 (1 signalé)0,8–0,82 : 1 (1 signalé)manqués 97détectés 8600,30,50,81Score donné par le modèleseuil 0,19Non transféré : 996Transféré : 183

Les barres pleines sont signalées (score égal ou supérieur au seuil). Les barres pâles ne sont pas signalées. Vous pouvez aussi faire glisser la poignée ronde sur la ligne, ou la sélectionner et appuyer sur les flèches (Maj pour des pas de 0,10).

Au seuil 0,19, le modèle en signale 227 : 86 de la classe « transféré » et 141 fausses alertes. Il manque 97 des 183 billets de la classe « transféré ». Le seuil le moins coûteux pour ces coûts est 0,14 (7 470).

Matrice de confusion au seuil 0,19
Classe réelleSignalésNon signalésTotal
Transféré86vrai positif97faux négatif (manqué)183
Non transféré141faux positif (fausse alerte)855vrai négatif996
Total2279521 179
Signalés
227
sur 1 179 billets
Précision
37,9 %
86 sur 227 signalés
Rappel
47,0 %
86 sur 183
Taux de faux positifs
14,2 %
141 sur 996
Coût total
8 090
minutes pour 1 179 billets
05 00010 00015 000Coût total (minutes)00,20,40,60,81Seuilmoins coûteux 0,14 : 7 470seuil 0,19 : 8 090
05 00010 00015 000Coût total (minutes)00,30,50,81Seuilmoins coûteux 0,14 : 7 470seuil 0,19 : 8 090
Les mêmes 1 179 billets de mai, avec le coût total en minutes à chaque seuil, selon les coûts saisis. La capacité de 20 % limite toujours le choix.
Description textuelle de l’explorateur

Le graphique du haut montre les deux mêmes panneaux de scores que le premier outil d’exploration, avec le seuil à 0,19. En dessous figurent la matrice de confusion, la précision, le rappel, le taux de faux positifs et le coût total. Deux champs numériques définissent les minutes pour un examen (valeur initiale : 10) et un transfert manqué (valeur initiale : 60).

Le graphique du bas montre le coût total des 1 179 billets de mai à chaque seuil. Avec les coûts initiaux, il atteint 10 980 minutes si aucun billet n’est signalé (183 transferts manqués × 60). À 0,19, il est de 8 090 minutes. Le seuil le moins coûteux est 0,14, avec 7 470 minutes. Il signale 345 billets (29,3 %), ce qui dépasse la capacité. Avec un transfert manqué de 30 minutes, le seuil le moins coûteux monte à 0,23 (163 billets signalés). Avec un transfert manqué de 120 minutes, il descend à 0,06 (686 billets signalés, soit 58,2 %).

Avec ces coûts, le seuil le moins coûteux est 0,14. Il signale 29,3 % des billets, mais l’équipe peut en examiner seulement 20 %. L’analyse des coûts ne change donc pas le seuil. Elle donne plutôt un argument à Grace : plus de temps d’agents expérimentés permettrait de détecter plus de transferts. Avec ces coûts approximatifs, ce temps supplémentaire serait rentable. Notez un autre point. Avec des scores calibrés, « signaler quand la probabilité dépasse 10 ÷ 60 = 0,17 » serait proche de la règle la moins coûteuse. Ici, le seuil le moins coûteux est plus bas, à 0,14, car les scores de mai sont trop faibles.

Dans Données d’entraînement, de validation et de test, les données de test étaient l’examen final. Les nouvelles questions étaient dans une enveloppe scellée, ouverte une seule fois à la fin. Les autres personnes font confiance au résultat de cet examen, car aucun réglage n’a été fait en fonction de lui. Si un élève voit l’examen, s’exerce dessus et le reprend, le deuxième résultat ne veut rien dire.

Tous les choix sont maintenant faits. La leçon 1 a choisi les variables, la leçon 2 a choisi le modèle et ses réglages. Cette leçon a choisi le seuil, 0,19. Il est donc temps d’ouvrir l’enveloppe : les 1 002 billets de juin 2026 qu’aucune étape n’a utilisés. Priya l’ouvre une seule fois, avec un script qui refuse de s’exécuter une deuxième fois. Tomás demande : « Pourquoi un script? Tu pourrais juste calculer les nombres dans le carnet. » Un script rend le test identique à chaque exécution, écrit le résultat dans un fichier et peut empêcher une deuxième consultation. Une cellule de carnet peut être exécutée de nouveau par accident.

D’abord, train.py ajuste le pipeline sur les données d’entraînement et choisit le seuil sur les données de validation avec choose_threshold(). Il enregistre ensuite le modèle et le seuil dans model/. Vous l’écrirez vous-même dans Empaqueter les artefacts, au module 8. Pour l’instant, téléchargez le fichier du cours dans votre dossier de projet, puis exécutez-le :

Terminal
curl -fsSL -O https://raw.githubusercontent.com/Nextia-Intelligence/nextia-learning-labs/main/C05/project/train.py
python train.py
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.

Si vous voyez ModuleNotFoundError: No module named 'ticket_model' (module introuvable : aucun module nommé « ticket_model »), le terminal n’est pas dans ~/projects/ticket-model. Allez dans ce dossier et exécutez la commande de nouveau.

Écrivez maintenant evaluate.py dans le dossier du projet, à côté de train.py :

evaluate.py
"""Report the saved model's results on the test set, once.
Nextia Learning, C05, Module 6, lesson 3 (Thresholds and calibration). Module 8
uses the report again.
python evaluate.py
Uses the threshold that train.py chose on valid. Writes
reports/test_report.json. If the report exists already, it stops: look at
the test set once, and do not change the model after you look.
"""
import json
import sys
from pathlib import Path
import joblib
from sklearn.metrics import average_precision_score, brier_score_loss, confusion_matrix, roc_auc_score
from ticket_model import FEATURES, TARGET, load
REPORT = Path("reports/test_report.json")
def main():
if REPORT.exists() and "--again" not in sys.argv:
sys.exit(f"{REPORT} exists: the test set was used already. Read the report. "
"Run with --again only to reproduce it, never to choose a change.")
pipeline = joblib.load("model/escalation_model.joblib")
info = json.loads(Path("model/model_info.json").read_text(encoding="utf-8"))
test = load("test")
scores = pipeline.predict_proba(test[FEATURES])[:, 1]
flags = (scores >= info["threshold"]).astype(int)
tn, fp, fn, tp = (int(v) for v in confusion_matrix(test[TARGET], flags, labels=[0, 1]).ravel())
rule = (test["priority"] == 1).astype(int)
rule_tp = int((rule & test[TARGET]).sum())
report = {
"rows": len(test), "escalated": int(test[TARGET].sum()), "threshold": info["threshold"],
"roc_auc": round(roc_auc_score(test[TARGET], scores), 3),
"average_precision": round(average_precision_score(test[TARGET], scores), 3),
"brier": round(brier_score_loss(test[TARGET], scores), 4),
"mean_score": round(float(scores.mean()), 3), "escalated_rate": round(float(test[TARGET].mean()), 3),
"confusion": {"tp": tp, "fp": fp, "fn": fn, "tn": tn},
"flag_rate": round((tp + fp) / len(test), 3),
"precision": round(tp / (tp + fp), 3), "recall": round(tp / (tp + fn), 3),
"baseline_priority_1": {"flagged": int(rule.sum()), "flag_rate": round(float(rule.mean()), 3),
"precision": round(rule_tp / rule.sum(), 3),
"recall": round(rule_tp / test[TARGET].sum(), 3)},
}
REPORT.parent.mkdir(exist_ok=True)
REPORT.write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8")
print(json.dumps(report, indent=2))
if __name__ == "__main__":
main()

Chaque partie a un rôle :

Lignes Pourquoi
if REPORT.exists() and "--again" not in sys.argv Le garde-fou. Une deuxième exécution s’arrête avec un message, pour éviter de refaire le test « juste une fois de plus » après un changement. --again reproduit le même rapport; cette option ne sert pas à choisir.
joblib.load(...), model_info.json Le modèle et le seuil viennent de train.py. evaluate.py ne choisit rien.
info["threshold"] Le seuil choisi sur les données de validation (0,19). Les données de test ne le définissent jamais.
confusion_matrix(..., labels=[0, 1]) Les quatre comptes dans un ordre fixe, même si l’un d’eux vaut 0.
baseline_priority_1 La règle sur les mêmes billets de test. Un nombre seul ne veut rien dire.
brier, mean_score, escalated_rate La calibration sur les données de test, comme dans cette leçon.
REPORT.write_text(...) Le rapport est un fichier dans reports/. Le résultat est donc conservé et peut être vérifié.

Lisez l’avertissement, puis exécutez le script.

Terminal
python evaluate.py
Sortie
{
"rows": 1002,
"escalated": 157,
"threshold": 0.19,
"roc_auc": 0.749,
"average_precision": 0.345,
"brier": 0.1207,
"mean_score": 0.123,
"escalated_rate": 0.157,
"confusion": {
"tp": 70,
"fp": 126,
"fn": 87,
"tn": 719
},
"flag_rate": 0.196,
"precision": 0.357,
"recall": 0.446,
"baseline_priority_1": {
"flagged": 150,
"flag_rate": 0.15,
"precision": 0.367,
"recall": 0.35
}
}

Sur les données de test, le modèle signale 196 des 1 002 billets (19,6 %, dans la limite de capacité). Il détecte 70 des 157 transferts : le rappel est de 0,446. La règle en détecte 55 : son rappel est de 0,350. L’objectif d’amélioration est donc aussi atteint sur les données de test : au plus 20 % signalés et au moins 40 % détectés. La précision moyenne est de 0,345, contre 0,391 sur les données de validation : un peu plus faible. C’est normal. Les données de validation ont aidé à choisir, donc leur résultat est un peu optimiste, et juin est un nouveau mois. L’écart de calibration reste aussi présent : score moyen de 0,123, taux de 0,157. Nous ne changeons rien après avoir regardé.

Exécutez maintenant le script de nouveau, comme Tomás le ferait après « un petit changement » :

Terminal
python evaluate.py
Sortie
reports/test_report.json exists: the test set was used already. Read the report. Run with --again only to reproduce it, never to choose a change.

C’est le garde-fou. Le rapport reste dans reports/test_report.json. La vérification du module ci-dessous et le module 8 le réutilisent.

Le test va-t-il dans le journal des expériences? Non. Le journal consigne les expériences : des essais en concurrence pour devenir le modèle, chacun évalué avec la même validation croisée. Le seuil et le test ne choisissent aucun modèle, donc aucun des deux n’a de ligne. Le seuil est enregistré avec le modèle dans model/model_info.json, et le résultat du test dans reports/test_report.json. Le journal se termine à E21, avec E15 comme modèle à conserver. Une nouvelle ligne exige de nouvelles données, par exemple un réentraînement avec mai et juin (module 8).

Vérifiez votre compréhension

Pourquoi choisir le seuil sur les données de validation, et pourquoi choisir le seuil le plus bas qui respecte la capacité?

Afficher la réponse

Le seuil est un choix. Vous faites les choix sur les données de validation pour garder les données de test comme vérification finale fiable. Mai est aussi le mois qui suit le changement de politique. Il ressemble donc davantage à l’avenir que les données d’entraînement. Un seuil plus bas signale plus de billets et détecte donc au moins autant de transferts. Le seuil le plus bas respectant la capacité détecte donc le maximum de transferts que l’équipe peut examiner. Le seuil de 0,19 signale 227 billets (19,3 %); celui de 0,18 en signalerait 248 (21,0 %).

Vérifiez votre compréhension

Sur les données de validation, les billets dont les scores vont de 0,2 à 0,3 ont un score moyen de 0,241. Parmi eux, 31,5 % ont été transférés. Le modèle est-il trop confiant ou pas assez confiant dans cette plage? Enfreint-il la règle de capacité de 20 %?

Afficher la réponse

Pas assez confiant : le taux réel (31,5 %) dépasse le score (0,241), donc le modèle sous-estime le risque. Il n’enfreint pas la règle de capacité, car cette règle utilise seulement l’ordre des scores. Quand tous les scores sont trop faibles d’une valeur semblable, les mêmes billets restent les plus risqués. Ajouter 0,035 à chaque score laisse la précision moyenne à 0,391. Par contre, cela invalide toute utilisation du score comme probabilité, par exemple « probabilité de 24 % » sur un tableau de bord.

Vérifiez votre compréhension

Omar lit le rapport de test (précision moyenne de 0,345, rappel de 0,446 avec 19,6 % signalés). Il demande : les données de validation donnaient une précision moyenne de 0,391 et un rappel de 0,470. Devrait-on baisser le seuil pour retrouver 0,470 en juin? Que répondez-vous?

Afficher la réponse

Non. Le seuil a été choisi sur les données de validation. Le rapport de test donne une estimation fiable pour les nouveaux billets. Si nous changeons le seuil à cause du résultat du test, les données de test deviennent un deuxième jeu de validation. Leurs résultats ne sont alors plus fiables. Un résultat de test inférieur à celui de validation est normal : la validation a aidé à choisir, et juin est un nouveau mois. Le modèle atteint toujours l’objectif sur les données de test : 19,6 % signalés, rappel de 0,446 contre 0,350 pour la règle. Nous présentons ces nombres tels quels. Un changement exige de nouvelles données : des billets étiquetés datant d’après juin.

L’exercice du module vous demande de régler un petit espace de recherche et de choisir un seuil selon une capacité d’examen. Vous reprenezz tout le flux de travail de Régler les hyperparamètres et de cette leçon. Vous utilisez une nouvelle capacité, sans les données de test.

Exemple détaillé : le flux de travail de Priya pour 20 %

Section intitulée « Exemple détaillé : le flux de travail de Priya pour 20 % »
Étape Données Décision Preuve
Variables Validation croisée sur les données d’entraînement Ajouter prior_escalations_90d; jamais les deux fuites Précision moyenne en validation croisée de 0,327 à 0,339, plus élevée sur chaque pli
Métrique (la décision) Rappel à un taux de signalement d’au plus 20 %; précision moyenne pour comparer les modèles Capacité de Grace : 1 billet sur 5
Réglage Validation croisée sur les données d’entraînement Régression logistique, C = 1 Meilleur résultat : 0,3424 ± 0,0080; la régression logistique à 0,3393 est à moins d’une erreur-type
Seuil Données de validation (mai) 0,19 227 signalés (19,3 %), rappel de 0,470, précision de 0,379
Calibration Données d’entraînement et de validation Montrer les signalements aux agents, pas les scores Entraînement calibré; mai sous-estimé (0,120 contre 0,155); garantie : pire résultat
Test Données de test (juin), une seule fois Aucun changement ensuite Précision moyenne de 0,345, 19,6 % signalés, rappel de 0,446; règle : 0,350

Grace pourrait ajouter un agent expérimenté à temps partiel. L’équipe pourrait alors examiner 25 % des billets. Préparez la recommandation dans le carnet, section 10 :

  1. Réglez ce petit espace de recherche avec GridSearchCV, la validation croisée du cours et la précision moyenne : C à 0,1 ou 1, et class_weight à None ou "balanced". ("balanced" donne plus de poids aux billets transférés pendant l’ajustement.)
  2. Appliquez la règle d’une erreur-type et choisissez un réglage. Écrivez une phrase pour expliquer pourquoi.
  3. Choisissez le seuil pour une capacité de 25 % sur les données de validation avec choose_threshold().
  4. Écrivez le nombre de billets signalés, le taux de signalement, la précision et le rappel sur les données de validation.
  5. Écrivez ce que vous ne faites pas, et pourquoi.
Afficher la réponse modèle
Sortie
params mean_test_score std_test_score se rank_test_score
{'model__C': 0.1, 'model__class_weight': None} 0.3391 0.0154 0.0069 2
{'model__C': 0.1, 'model__class_weight': 'balanced'} 0.3364 0.0157 0.0070 4
{'model__C': 1, 'model__class_weight': None} 0.3393 0.0156 0.0070 1
{'model__C': 1, 'model__class_weight': 'balanced'} 0.3365 0.0162 0.0072 3

Réglage. Le meilleur est C = 1 sans pondération des classes (0,3393, erreur-type de 0,0070). La limite est 0,3393 − 0,0070 = 0,3323, et les quatre réglages la dépassent : ils sont à égalité. Gardez le plus simple, le modèle par défaut, qui est le pipeline que vous avez déjà. Ses scores restent aussi proches des taux, contrairement à ceux du modèle équilibré.

Sortie
default mean score 0.120 threshold 0.16 flagged 291 (24.7%) recall 0.557
balanced mean score 0.415 threshold 0.59 flagged 287 (24.3%) recall 0.552

Seuil. choose_threshold(scores, 0.25) donne 0,16 : 291 des 1 179 billets de mai signalés (**24,7 %). Il détecte 102 des 183 transferts (rappel de 0,557), avec une précision de 0,351. Par rapport au seuil de 0,19 à 20 % : 64 examens supplémentaires par mois et 16 transferts supplémentaires détectés. Le modèle équilibré exige un seuil différent (0,59) pour presque les mêmes billets. Cela le confirme : un seuil appartient à un modèle précis.

Ce que vous ne faites pas. Vous ne réexécutez pas evaluate.py pour 25 % et ne regardez pas les données de test pour « vérifier » 0,16. Le rapport de test reste celui du seuil de 0,19. Si Grace adopte 25 %, les preuves fiables sont le résultat de validation ci-dessus, puis le prochain mois étiqueté (module 8).

Vérifiez votre compréhension

Rappel : nommez le rôle des données d'entraînement, de validation et de test au module 6, avec un nombre pour chacune.

Afficher la réponse

Les données d’entraînement servent à ajuster les modèles et à choisir les variables et les réglages par validation croisée stratifiée à 5 plis. La régression logistique avec C = 1 obtient une précision moyenne de 0,3393, à moins d’une erreur-type du meilleur résultat (0,3424). Les données de validation (mai) confirment les choix après le changement de politique et définissent le seuil. Il est de 0,19 pour la capacité de 20 % (227 signalés, rappel de 0,470). Les données de test (juin) donnent un seul rapport final, sans autre rôle : précision moyenne de 0,345, 19,6 % signalés, rappel de 0,446.

Vérifiez votre compréhension

Expliquer ou prédire : en juillet, le taux de transfert pour la garantie augmente encore, mais la part de chaque équipe reste identique. Le modèle et le seuil de 0,19 ne changent pas. Prédisez le taux de signalement et la précision. Dites lequel vous pouvez vérifier avant l'arrivée des étiquettes de juillet.

Afficher la réponse

Le taux de signalement reste à peu près identique (près de 19 %). Il dépend seulement des scores, qui dépendent seulement des variables, et celles-ci ne changent pas. La précision monte, car davantage de billets signalés sont transférés. Les scores du modèle sous-estiment maintenant encore plus le risque. Vous pouvez vérifier le taux de signalement le jour où les billets arrivent. La précision et le rappel exigent les étiquettes, qui arrivent 72 heures ou plus après. C’est pourquoi le module 8 surveille d’abord le taux de signalement.

Vérifiez votre compréhension

Vérification finale : rédigez le rapport sur les données réservées pour Grace en quatre ou cinq phrases. Utilisez le rapport de test et dites ce que vous n'en ferez pas.

Afficher la réponse

Une réponse modèle : « Le modèle de transfert est une régression logistique avec 11 variables, choisie par validation croisée à 5 plis sur les billets de juillet 2024 à avril 2026. Un modèle de boosting par gradient ne le dépassait pas de plus d’une erreur-type. Son seuil, 0,19, a été choisi sur mai 2026 pour votre capacité de 20 %. Sur les billets de juin, utilisés une seule fois, il signale 19,6 % des billets et détecte 44,6 % des transferts (70 sur 157), avec une précision de 0,357. La règle de priorité 1 en détecte 35,0 %. Ses scores sous-estiment le risque depuis la politique de garantie (score moyen de 0,123 contre un taux de 0,157 en juin). Montrez donc les signalements aux agents, pas les scores. Nous ne changerons ni le seuil ni le modèle à cause de ces nombres. Le prochain changement utilisera des billets nouvellement étiquetés. »

Vous pouvez maintenant régler et finaliser un modèle honnêtement :

  • Sélectionnez les caractéristiques avec trois questions. Connues au moment de la prédiction, information nouvelle, gain supérieur au bruit. prior_escalations_90d a satisfait ces critères (AP en validation croisée de 0,327 à 0,339); deux fuites semblaient trop bonnes (0,445 et 0,851) et n’ont jamais été utilisées.
  • Réglez les hyperparamètres par validation croisée sur les données d’entraînement. L’entraînement apprend les paramètres; vous choisissez les hyperparamètres avant. Utilisez une recherche par grille ou une recherche aléatoire, puis la règle d’une erreur-type. La régression logistique avec C = 1 fait jeu égal avec un modèle de boosting (amplification) supérieur de 0,0031.
  • Choisissez le seuil sur les données de validation, selon la décision. Le seuil le plus bas qui respecte la capacité : 0,19, avec 227 billets signalés et un rappel de 0,470. predict() utilise 0,5 et signale 2,1 %.
  • Distinguez le classement des probabilités. Le classement reste valable après le changement de politique; les scores, interprétés comme des probabilités, ne le sont plus.
  • Consultez les données de test une seule fois. evaluate.py a rapporté une AP de 0,345, 19,6 % de billets signalés et un rappel de 0,446. Faire des choix sur un jeu de données gonfle son score (0,411 contre 0,389 dans la simulation).
  • Notez chaque expérience dans le journal, avec ses résultats et sa raison, y compris les idées que vous avez rejetées. Le module 6 a ajouté E15 à E21; E15 est le modèle à conserver.

Si vos fichiers de projet ou vos résultats diffèrent de ceux du module, revenez à l’état du cours. Exécutez ces commandes dans ~/projects/ticket-model, avec .venv actif.

Terminal
python get_data.py --reset
for f in ticket_model.py train.py evaluate.py; do curl -fsSL -O https://raw.githubusercontent.com/Nextia-Intelligence/nextia-learning-labs/main/C05/project/$f; done
python train.py
python evaluate.py --again
  1. get_data.py --reset télécharge les données à nouveau et vérifie chaque fichier. La dernière ligne doit être Ready: train 16939 rows (1983 escalated), valid 1179 (183), test 1002 (157). (Prêt : entraînement, 16 939 lignes, dont 1 983 transférées au niveau supérieur; validation, 1 179, dont 183; test, 1 002, dont 157.)
  2. La ligne de téléchargement remplace vos trois fichiers de projet par ceux du cours.
  3. train.py doit afficher Trained on 16939 tickets. Threshold 0.19 for a review capacity of 20%. (Entraîné sur 16 939 billets. Seuil de 0,19 pour une capacité de révision de 20 %.)
  4. evaluate.py --again reproduit le rapport de test : précision moyenne de 0,345, rappel de 0,446. C’est permis ici, car aucun choix ne repose sur ce rapport.

Ne supprimez pas reports/test_report.json pour refaire le test « à neuf » après un changement. Une réinitialisation rétablit le modèle du cours; elle ne rend pas les données de test inconnues à nouveau. Dans un carnet (notebook) Jupyter, une nouvelle session (ou un nouveau dossier) part des fichiers téléchargés, sans réinitialisation nécessaire.

Récapitulatif

  • Le seuil opérationnel est choisi sur les données de validation selon la décision : le plus bas qui respecte la capacité. Pour 20 % : 0,19, avec 227 billets signalés et un rappel de 0,470. predict() utilise 0,5 et signale 2,1 %.
  • Un seuil appartient à un seul modèle. Un modèle équilibré nécessite 0,59 pour 25 %; le modèle par défaut nécessite 0,16.
  • Calibration : sur les données d’entraînement, les scores correspondent aux taux (Brier de 0,0902). En mai, le modèle sous-estime les probabilités (moyenne de 0,120, taux de 0,155, Brier de 0,1155), surtout pour les billets de garantie. Le classement reste valable, donc le seuil lié à la capacité fonctionne encore; ne présentez pas les scores comme des probabilités sans recalibration.
  • evaluate.py a ouvert les données de test une seule fois, après tous les choix : AP de 0,345, 19,6 % de billets signalés, rappel de 0,446 contre 0,350 pour la règle. Rien ne change après cette consultation.
  • Le module 6 a choisi les caractéristiques, les réglages et le seuil avec les données d’entraînement et de validation. Le journal des expériences (E15 à E21) consigne chaque expérience et sa raison.

Ensuite, le module 7 examine les erreurs : quels billets le modèle manque et pour qui il fonctionne moins bien. Il demande aussi quand un modèle simple est le meilleur choix.

Leçon suivante : L'analyse par sous-groupes