Aller au contenu
English

L'exploration non supervisée

  • Leçon 24
  • 70 min
  • Module 7 : Comprendre et choisir le modèle
Leçon 24 sur 27 · Apprentissage automatique : du problème à un modèle fiableL'exploration non supervisée

Ce que vous saurez faire

Cherchez des groupes dans les données quand il n’y a aucune cible à prédire. Une méthode de regroupement renvoie toujours des groupes, même quand les données n’en contiennent pas. Cette leçon montre comment équilibrer les groupes (mise à l’échelle d’abord), les examiner (PCA) et choisir leur nombre honnêtement. Elle montre aussi pourquoi une personne qui connaît l’entreprise doit expliquer leur sens.

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

Le carnet (notebook) Jupyter contient toutes les cellules, tâches et vérifications de cette leçon et de l’exercice du module, avec les consignes. Ses cellules de préparation installent les paquets et téléchargent les données ainsi que le fichier ticket_model.py du cours. Aucune préparation locale n’est donc nécessaire. Lisez les explications 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 70 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 et matplotlib 3.11.2. Les commandes Windows n’ont pas été testées. Le carnet a été testé dans Jupyter.

Dans L’apprentissage non supervisé, vous avez trié une boîte de boutons mélangés sans étiquettes. Personne ne vous a indiqué les groupes, alors vous avez rassemblé les objets semblables. C’est l’apprentissage non supervisé : aucune cible, et la méthode cherche une structure dans les caractéristiques seules. Le regroupement en est le principal type. Il rassemble les lignes semblables dans des groupes. Les lignes d’un groupe sont plus proches entre elles que des lignes des autres groupes.

Les trois premières leçons de ce module utilisaient partout la cible, escalated_72h. Cette leçon la laisse de côté. Grace a une question de planification, pas de prédiction : « Nos clients se répartissent-ils en quelques types pour lesquels je pourrais planifier le soutien? » Aucune colonne n’indique le type d’un client. Il n’y a donc rien à prédire, ni de bonne réponse à laquelle comparer le résultat. Une personne doit juger si les groupes sont utiles.

Priya regroupe des clients, pas des billets. Grace peut agir sur un groupe de clients (par exemple, leur attribuer une personne-ressource), mais pas sur un groupe de billets de soutien.

Elle crée une ligne par client à partir des billets d’entraînement seulement. Les données de validation et de test restent à l’écart, comme pour le modèle. Les invités n’ont pas d’identifiant client, alors elle les exclut.

from ticket_model import FEATURES, TARGET, build_pipeline, load
train = load("train")
known = train[train["customer_id"].notna()]
customers = known.groupby("customer_id").agg(
tickets=("ticket_id", "size"), order_value=("order_value", "median"),
word_count=("word_count", "mean"), tenure_days=("customer_tenure_days", "max"))
print(len(customers), "customers;", train["customer_id"].isna().sum(), "guest tickets left out")
customers.describe().round(1)

Le résultat :

847 customers; 202 guest tickets left out
tickets order_value word_count tenure_days
count 847.0 837.0 847.0 847.0
mean 19.8 80.9 104.0 1272.9
std 28.4 42.7 27.9 741.1
min 1.0 10.4 0.0 2.0
25% 4.0 52.1 90.2 623.5
50% 10.0 72.2 102.9 1300.0
75% 23.0 104.1 115.3 1905.0
max 262.0 448.4 314.7 2607.0
Colonne Ce qu’elle signifie pour un client
tickets Ses billets de juillet 2024 à avril 2026 (la période d’entraînement).
order_value La valeur médiane des commandes liées à ses billets. Pour 10 clients, les billets concernent seulement le compte, donc cette valeur manque.
word_count La longueur moyenne de ses premiers messages.
tenure_days Son ancienneté comme client au moment de son dernier billet d’entraînement.

Regardez la ligne std (écart-type). L’écart-type de tenure_days est de 741,1 jours; celui de tickets est de 28,4. Gardez cela en tête pour la prochaine section.

Imaginez que vous devez placer 3 kiosques de crème glacée sur une longue plage pour réduire les déplacements des gens. Une méthode simple fonctionne bien. Placez les kiosques n’importe où. Chaque personne va au kiosque le plus proche. Déplacez ensuite chaque kiosque au milieu des personnes qui s’y sont rendues. Répétez jusqu’à ce qu’aucun kiosque ne bouge.

k-means applique cette méthode aux lignes de données. C’est la méthode de regroupement la plus courante. Vous choisissez k, le nombre de groupes (le nombre de kiosques). Puis elle répète deux étapes :

  1. Placez chaque ligne dans le groupe dont le centre est le plus proche (un point qui représente le groupe).
  2. Déplacez chaque centre vers la moyenne des lignes de son groupe.

Elle s’arrête quand aucune ligne ne change de groupe. « Le plus proche » désigne la distance en ligne droite, qui additionne les différences au carré de toutes les colonnes. Les colonnes doivent donc être comparables.

Voici six clients fictifs avec deux colonnes. A et E sont de petits clients avec peu de billets. C est un gros client.

tiny = pd.DataFrame({"tickets": [2, 3, 40, 45, 4, 50],
"tenure_days": [300, 2400, 350, 2300, 1300, 1250]}, index=list("ABCDEF"))
def distance(df, a, b):
"""The straight-line distance between two rows."""
return round(float(((df.loc[a] - df.loc[b]) ** 2).sum() ** 0.5), 2)
scaled_tiny = (tiny - tiny.mean()) / tiny.std(ddof=0)
print("as recorded: A to C", distance(tiny, "A", "C"), "| A to E", distance(tiny, "A", "E"))
print("scaled: A to C", distance(scaled_tiny, "A", "C"), "| A to E", distance(scaled_tiny, "A", "E"))

Le résultat :

as recorded: A to C 62.8 | A to E 1000.0
scaled: A to C 1.79 | A to E 1.21

Vérifiez la première ligne à la main. De A à C : √(38² + 50²) = √(1 444 + 2 500) = 62,8. De A à E : √(2² + 1 000²) = 1 000,0. Sans transformation, une différence de 1 000 jours masque une différence de 38 billets. A est alors « plus proche » du gros client C. Après la mise à l’échelle, chaque colonne compte en écarts-types ((valeur − moyenne) ÷ écart-type). A est alors plus proche de E, l’autre petit client. C’est le problème de Similarité et distance dans le cours de mathématiques, maintenant au sein d’une méthode de regroupement.

Exécutez k-means avec k = 2 sur les deux versions :

from sklearn.cluster import KMeans
for name, data in [("as recorded", tiny), ("scaled", scaled_tiny)]:
labels = KMeans(n_clusters=2, n_init=10, random_state=0).fit_predict(data)
groups = [list(tiny.index[labels == label]) for label in sorted(set(labels), key=list(labels).index)]
print(name, groups)

Le résultat :

as recorded [['A', 'C', 'E', 'F'], ['B', 'D']]
scaled [['A', 'B', 'E'], ['C', 'D', 'F']]

Sans transformation, les groupes sont « ancienneté courte » et « ancienneté longue » : seule tenure_days compte. Après la mise à l’échelle, ce sont « peu de billets » (A, B, E) et « beaucoup de billets » (C, D, F). Les deux sont des groupes de nombres valides. Seul le second résultat utilise les deux colonnes. Faites la mise à l’échelle avant le regroupement.

Priya place trois étapes dans un pipeline, comme au module 4. Il remplace les 10 valeurs de commande manquantes par la médiane, fait la mise à l’échelle, puis exécute k-means avec k = 3. n_init=10 exécute k-means à partir de 10 centres initiaux différents et garde le meilleur résultat. random_state=0 rend le résultat identique chaque fois. Avant de regarder, prédisez : quelle colonne séparera le plus les groupes?

from sklearn.impute import SimpleImputer
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
COLUMNS = ["tickets", "order_value", "word_count", "tenure_days"]
clusterer = make_pipeline(SimpleImputer(strategy="median"), StandardScaler(),
KMeans(n_clusters=3, n_init=10, random_state=0))
customers["cluster"] = clusterer.fit_predict(customers[COLUMNS])
profile = customers.groupby("cluster")[COLUMNS].mean().round(1)
profile["customers"] = customers["cluster"].value_counts()
profile

Le résultat :

tickets order_value word_count tenure_days customers
cluster
0 11.2 85.3 106.5 584.8 374
1 14.8 71.8 101.1 1858.2 407
2 98.5 111.8 107.2 1562.8 66

Ce tableau est le profil de chaque groupe : la moyenne de chaque colonne. Les nombres 0, 1 et 2 sont seulement des noms; k-means les attribue sans ordre particulier. Lisez-le par lignes :

  • Groupe 0, 374 clients : environ 11 billets, une ancienneté d’environ 585 jours (1,6 an).
  • Groupe 1, 407 clients : environ 15 billets, une ancienneté d’environ 1 858 jours (5 ans).
  • Groupe 2, 66 clients : environ 99 billets chacun, et des commandes plus importantes (111,8).

word_count est presque identique dans les trois groupes : cette colonne ne sépare pas les clients. Le regroupement n’a pas utilisé la cible. Priya l’examine maintenant, seulement pour décrire les groupes. Elle compte aussi les segments, une colonne que le regroupement n’a pas utilisée :

segment = known.groupby("customer_id")["segment"].first()
print(pd.crosstab(customers["cluster"], segment))
tickets = known.merge(customers["cluster"], left_on="customer_id", right_index=True)
print(tickets.groupby("cluster")[TARGET].agg(["size", "mean"]).round(3))

Le résultat :

segment business home trade
cluster
0 19 259 96
1 15 320 72
2 25 1 40
size mean
cluster
0 4196 0.102
1 6040 0.094
2 6501 0.148

Le groupe 2 comprend presque uniquement des entreprises et des clients professionnels : 65 sur 66. Ses 66 clients ont créé 6 501 billets d’entraînement, soit 38,8 % des billets de clients connus. Ces billets sont plus souvent transférés au niveau supérieur (14,8 % contre environ 10 %). Les groupes 0 et 1 ont une répartition semblable des segments et des taux de transfert semblables (10,2 % et 9,4 %).

Tenez une théière devant une lampe et regardez son ombre sur le mur. De côté, l’ombre montre le bec et l’anse, et vous reconnaissez une théière. Vue d’en haut, ce n’est qu’un cercle. Un bon angle conserve l’essentiel de la forme en deux dimensions.

On ne peut pas représenter quatre colonnes sur une page plane. La PCA (analyse en composantes principales) trouve un bon angle pour l’ombre des données. Elle crée de nouveaux axes, appelés composantes principales. Chacun est un mélange pondéré des colonnes mises à l’échelle. La première composante conserve autant que possible la dispersion des données; la deuxième conserve autant que possible ce qui reste. Deux composantes donnent une image.

from sklearn.decomposition import PCA
scaled = clusterer[:-1].transform(customers[COLUMNS]) # the fitted imputer and scaler
pca = PCA(n_components=2).fit(scaled)
print("share of the spread kept:", pca.explained_variance_ratio_.round(3))
print(pd.DataFrame(pca.components_, index=["PC1", "PC2"], columns=COLUMNS).round(2))

Le résultat :

share of the spread kept: [0.309 0.267]
tickets order_value word_count tenure_days
PC1 0.75 0.49 0.12 0.42
PC2 0.03 -0.67 -0.00 0.74

clusterer[:-1] est le pipeline sans sa dernière étape : l’imputation et la mise à l’échelle, telles qu’ajustées. Les deux composantes conservent 30,9 % + 26,7 % = 57,5 % de la dispersion. L’image montre donc un peu plus de la moitié des différences entre clients. Le reste se trouve dans les deux dimensions non représentées. Lisez les poids : PC1 augmente avec les billets, la valeur des commandes et l’ancienneté (« un client plus gros et plus ancien »). PC2 augmente avec l’ancienneté et diminue avec la valeur des commandes.

  • Groupe 0 : clients plus récents
  • Groupe 1 : clients de longue date
  • Groupe 2 : grands comptes
400 des 847 clients (un échantillon aléatoire fixe), représentés sur les deux premières composantes principales. Leur couleur indique leur groupe k-means (k = 3, après mise à l'échelle). L'image conserve 57,5 % de la dispersion.
Description textuelle du graphique

Un nuage de points représentant 400 clients. L’axe horizontal est la première composante principale, qui augmente avec les billets, la valeur des commandes et l’ancienneté. Il va d’environ −2 à 7. L’axe vertical est la deuxième composante principale, qui augmente avec l’ancienneté et diminue avec la valeur des commandes. Il va d’environ −7 à 2. Le groupe 1, les clients de longue date (211 dans l’échantillon), forme un nuage dense en haut à gauche, autour de PC2 = 1. Le groupe 0, les clients plus récents (151), forme un nuage juste dessous, autour de PC2 = −0,5. Quelques clients de ce groupe ayant des commandes importantes forment un prolongement jusqu’à PC2 = −7. Les deux nuages se touchent : aucun espace ne les sépare. Le groupe 2, les grands comptes (38 dans l’échantillon), s’étend vers la droite, de PC1 = 1,5 à 7. Ses points sont peu nombreux et très espacés.

Ce que l’image montre :

  • Les groupes 0 et 1 se touchent. Aucun espace vide ne les sépare. k-means a coupé un nuage en deux parties : ancienneté courte et longue. La méthode coupe toujours; cela ne signifie pas qu’une frontière naturelle existe.
  • Le groupe 2 est dispersé. Les grands comptes diffèrent beaucoup entre eux. Ils forment un groupe surtout parce qu’ils sont loin de tous les autres.
  • Les couleurs viennent de k-means en quatre dimensions. La PCA ne fait que les représenter. Quelques points d’une couleur parmi ceux d’une autre couleur sont normaux. Dans les deux dimensions cachées, ces points sont plus proches de leur propre groupe.

Priya a choisi k = 3. Deux nombres peuvent aider à choisir k, mais aucun ne donne la réponse :

  • L’inertie est la somme des distances au carré entre chaque ligne et son centre. Une valeur plus petite indique des groupes plus serrés. Elle diminue toujours quand k augmente : avec un groupe par client, elle vaut 0.
  • La silhouette compare, pour chaque ligne, la distance moyenne à son propre groupe à celle du groupe voisin le plus proche. La moyenne va de −1 à 1. Près de 1, les groupes sont nets et séparés; près de 0, ils se chevauchent.
from sklearn.metrics import silhouette_score
for k in range(2, 9):
km = KMeans(n_clusters=k, n_init=10, random_state=0).fit(scaled)
print(f"k {k}: inertia {km.inertia_:.0f}, silhouette {silhouette_score(scaled, km.labels_):.3f}")

Le résultat :

k 2: inertia 2720, silhouette 0.231
k 3: inertia 2181, silhouette 0.262
k 4: inertia 1821, silhouette 0.260
k 5: inertia 1504, silhouette 0.285
k 6: inertia 1360, silhouette 0.273
k 7: inertia 1245, silhouette 0.247
k 8: inertia 1134, silhouette 0.255
  • L’inertie diminue de 539, 360, 317, 144, 115 et 111. On cherche une courbure (un « coude ») où la baisse ralentit. Ici, elle ralentit un peu après k = 5, mais il n’y a pas de coude net.
  • La silhouette est maximale à k = 5 (0,285), mais k = 3, 4 et 6 sont à moins de 0,025. Toutes les valeurs sont sous 0,3 : les groupes se chevauchent.

Le résumé honnête : ces clients ne se répartissent pas en groupes nets et séparés. k-means peut quand même donner des groupes utiles, mais les données ne décident pas de k. Les personnes qui utilisent les groupes le décident. Priya garde k = 3, parce que Grace peut nommer et utiliser trois groupes. La silhouette indique que k = 5 convient aussi bien; vous l’essayerez dans la variante.

Un groupe reste un ensemble de nombres jusqu’à ce qu’une personne qui connaît l’entreprise explique son sens. Priya montre à Grace le profil, les segments et les taux de transfert au niveau supérieur. Grace nomme chaque groupe et dit s’il est utile.

Groupe Nom donné par Grace Éléments à l’appui Utile?
0 Clients plus récents 374 clients, ancienneté d’environ 1,6 an, 11 billets Non, pas pour le soutien. Le taux de transfert (10,2 %) est le même que celui du groupe 1.
1 Clients de longue date 407 clients, ancienneté d’environ 5 ans, 15 billets Non, pour la même raison. C’est une division par ancienneté, qu’une ligne de pandas suffit à produire.
2 Grands comptes 66 clients (65 professionnels ou entreprises), 98,5 billets chacun, 38,8 % des billets des clients connus, 14,8 % transférés Oui. 66 clients créent presque 4 billets sur 10. Une personne-ressource attitrée pour chacun pourrait résoudre les problèmes avant qu’ils deviennent des billets.

Grace dit : « Le groupe 2 est réel. Je savais qu’on avait de gros clients professionnels, mais pas que 66 d’entre eux créent 39 % de nos billets. Les groupes 0 et 1 sont seulement “nouveaux” et “anciens”. Ils ne changent pas le travail de mon équipe, alors je ne les utiliserais pas. »

Trois leçons à en tirer :

  • k-means renvoie toujours k groupes. Il ne peut pas dire « il n’y a pas de groupes ». La séparation entre les groupes 0 et 1 coupe un seul nuage.
  • Seule la connaissance du domaine rend un groupe utile. Les nombres disaient « le groupe 2 a beaucoup de billets ». Grace en a fait une décision : une personne-ressource pour 66 clients.
  • Un groupe n’est pas une nouvelle vérité sur les clients. Un autre k, d’autres colonnes ou une autre mise à l’échelle donnent d’autres groupes. Notez les choix avec le résultat.

Le groupe 2 est-il utile au modèle de transfert au niveau supérieur? Probablement pas : le modèle a déjà prior_tickets_90d et prior_escalations_90d, qui contiennent presque les mêmes renseignements. L’exercice du module le vérifie.

  1. Exécutez les cellules de l’exemple détaillé pour que customers, profile et tickets existent.
  2. Trouvez le groupe avec la moyenne de tickets la plus élevée : big = profile["tickets"].idxmax(). Vous devriez voir 2.
  3. Calculez la part des billets d’entraînement des clients connus provenant de ce groupe : (tickets["cluster"] == big).mean(). Vous devriez voir environ 0,388.
  4. Comptez les clients de chaque segment dans chaque groupe avec pd.crosstab. Vous devriez voir 25 entreprises et 40 clients professionnels dans le groupe 2.
  5. Donnez un nom à chaque groupe et écrivez une phrase indiquant si Grace peut l’utiliser.
Afficher un exemple de réponse

Le groupe 2, « grands comptes », comprend 66 clients (7,8 % des 847), presque tous des professionnels ou des entreprises. Ils créent 38,8 % des billets d’entraînement des clients connus. Les groupes 0 et 1 sont les clients plus récents et les clients de longue date. Leurs taux de transfert sont semblables (10,2 % et 9,4 %), et seule leur ancienneté diffère. Vos noms peuvent être différents. Vérifiez que chaque nom vient des nombres du profil et que vous indiquez quels groupes ne mènent à aucune action.

Changez une condition : k = 5, où la silhouette était maximale. Créez le même pipeline avec n_clusters=5, puis créez le profil. Avant de l’exécuter, prédisez : le groupe des grands comptes subsiste-t-il? Selon quoi les nouveaux groupes se séparent-ils?

Afficher la réponse
tickets order_value word_count tenure_days
0 15.8 67.3 98.4 1931.1
1 16.8 145.1 105.3 994.2
2 10.3 60.6 89.6 592.7
3 7.0 65.8 154.4 936.1
4 100.7 110.2 106.9 1574.2

Les grands comptes subsistent presque sans changement : le groupe 4 a 63 clients et 100,7 billets chacun (contre 66 et 98,5). k-means divise les 784 autres clients en quatre plutôt qu’en deux :

  • ancienneté longue (groupe 0, 320 clients),
  • commandes importantes (groupe 1, 133 clients, valeur de commande de 145,1),
  • ancienneté courte (groupe 2, 236 clients),
  • messages longs (groupe 3, 95 clients, 154,4 mots).

Une silhouette plus élevée (0,285 contre 0,262) n’a pas créé une réponse plus utile. Grace pourrait trouver « commandes importantes » ou « messages longs » intéressants, mais aucun ne suggère encore une action de soutien. Un groupe présent pour chaque k, comme les grands comptes, est plus fiable qu’un groupe présent pour un seul k.

Erreur courante

Des groupes fondés sur une seule colonne

Symptôme : Tomás retire la mise à l’échelle du pipeline. k-means s’exécute sans avertissement et produit trois groupes. Priya montre les valeurs minimales et maximales dans chacun :

unscaled = make_pipeline(SimpleImputer(strategy="median"), KMeans(n_clusters=3, n_init=10, random_state=0))
raw = customers.assign(cluster=unscaled.fit_predict(customers[COLUMNS]))
raw.groupby("cluster")[["tickets", "tenure_days"]].agg(["min", "max"])
tickets tenure_days
min max min max
cluster
0 1 191 2.0 868.0
1 1 143 875.0 1737.0
2 1 262 1748.0 2607.0

Les groupes forment trois plages de tenure_days (ancienneté en jours) qui ne se chevauchent pas : jusqu’à 868 jours, de 875 à 1 737, et à partir de 1 748. Chaque groupe contient des clients avec 1 billet et des clients avec plus de 140 billets. Les grands comptes ont disparu.

Cause : k-means utilise des distances. tenure_days a un écart-type de 741 jours et tickets (billets), un écart-type de 28. Une différence d’ancienneté détermine donc chaque distance. Les trois autres colonnes comptent à peine. Le résultat ressemble à un regroupement, mais c’est un tri selon une seule colonne.

Correction : mettez d’abord les colonnes à l’échelle, dans le même pipeline : make_pipeline(SimpleImputer(...), StandardScaler(), KMeans(...)). Vérifiez ensuite le profil : si les groupes diffèrent sur une seule colonne, demandez-vous si cette colonne domine.

Conséquence dans le travail en IA : une équipe présente « trois types de clients » qui sont seulement trois plages d’ancienneté des comptes. Elle planifie ensuite des services pour eux. Rien dans la sortie n’indique que l’échelle était incorrecte. La même erreur touche toutes les méthodes utilisant des distances, comme la recherche de plus proches voisins.

Vérifiez votre compréhension

Pourquoi Priya a-t-elle regroupé les clients plutôt que les billets, et seulement à partir des billets d'entraînement?

Afficher la réponse

Les clients, parce que Grace peut agir sur un groupe de clients, par exemple en leur attribuant une personne-ressource. Elle ne peut pas agir sur un groupe de billets individuels. Seulement l’entraînement, parce que les mois de validation et de test doivent rester inconnus, comme pour le modèle. L’exercice du module utilise ensuite les groupes pour les billets de mai. C’est valable seulement si les données de mai n’ont pas servi à former les groupes.

Vérifiez votre compréhension

La silhouette est maximale à k = 5 (0,285). Tomás dit : les données contiennent donc cinq types de clients. Que répondez-vous?

Afficher la réponse

Les valeurs de silhouette sont toutes inférieures à 0,3. Pour k = 3 à 6, elles diffèrent d’au plus 0,025. Les groupes se chevauchent donc, et les données ne présentent pas un nombre clair de types. La silhouette et l’inertie sont des indices. k = 5 conserve les grands comptes et divise le reste en quatre parties qui se chevauchent. Choisissez k selon ce que les groupes permettent à Grace de faire. Indiquez dans le rapport que les groupes ne sont pas clairement séparés.

L’exercice du module réunit les leçons de ce module : examinez les billets mal classés et cherchez un motif utile dans les groupes. La vérification de réalisation demande au moins un problème de données et une limite du modèle, chacun avec des preuves.

Priya calcule les scores du mois de validation avec le modèle final au seuil de 0,19, comme dans L’analyse par sous-groupes. Elle attribue à chaque billet le groupe de son client, issu du regroupement des données d’entraînement. Les invités et les nouveaux clients de mai n’ont aucun groupe : ils reçoivent −1.

pipeline = build_pipeline().fit(train[FEATURES], train[TARGET])
valid = load("valid")
valid["score"] = pipeline.predict_proba(valid[FEATURES])[:, 1]
valid["flag"] = (valid["score"] >= 0.19).astype(int)
valid["cluster"] = valid["customer_id"].map(customers["cluster"]).fillna(-1).astype(int)
valid["caught"] = valid["flag"] * valid[TARGET]
by_cluster = valid.groupby("cluster").agg(tickets=(TARGET, "size"), escalated=(TARGET, "sum"),
caught=("caught", "sum"))
by_cluster["recall"] = (by_cluster["caught"] / by_cluster["escalated"]).round(3)
by_cluster

La sortie :

tickets escalated caught recall
cluster
-1 24 2 1 0.500
0 368 42 21 0.500
1 382 62 24 0.387
2 405 77 40 0.519

Priya la lit avec les habitudes acquises dans ce module :

  • Les nombres d’abord. Le groupe −1 compte 2 escalades : « trop peu pour juger ». Les autres en comptent de 42 à 77.
  • La différence est-elle réelle? Les clients de longue date (groupe 1) ont le rappel le plus faible : 0,387, contre 0,519 pour les grands comptes. L’erreur-type de chaque rappel est d’environ 0,06; celle de la différence est donc d’environ 0,08. La différence, 0,13, est inférieure à deux erreurs-types. Elle pourrait venir du hasard. Priya note « à surveiller », plutôt que d’en faire un constat.
  • Le motif aide-t-il le modèle? Les grands comptes font davantage l’objet d’escalades (77 sur 405, soit 19,0 %), et le modèle en détecte déjà davantage. Il les reconnaît grâce à prior_tickets_90d (billets antérieurs sur 90 jours) et prior_escalations_90d (escalades antérieures sur 90 jours). Le groupe ajoute donc peu d’information que le modèle n’a pas déjà. Il aide Grace à planifier, mais ne constitue pas une nouvelle caractéristique utile.

Faites cet exercice dans le carnet (notebook) Jupyter, à la section 9.

  1. Sélectionnez les 55 billets ayant fait l’objet d’une escalade qui ne sont pas signalés et dont le score est inférieur à 0,10.
  2. Comptez-les par priority (priorité), team (équipe) et cluster (groupe). Comparez chaque nombre aux escalades de ce sous-groupe.
  3. Examinez-en 10, ligne par ligne. Cherchez ce qu’un agent expérimenté verrait, mais qu’aucune colonne ne consigne.
  4. Écrivez vos constats : au moins un problème de données et une limite du modèle, chacun avec les chiffres qui le montrent. Utilisez les preuves des leçons de ce module.
Afficher la réponse modèle

Les nombres. Par priorité : 40 ont la priorité 3 et 15, la priorité 2. Par équipe : delivery (livraison) 17, returns (retours) 17, warranty (garantie) 10, account (compte) 7, payment (paiement) 4. Par groupe : 23 viennent de clients de longue date (groupe 1), 19 de grands comptes (groupe 2), 13 de clients plus récents (groupe 0). Comparés aux escalades de chaque groupe (62, 77 et 42), ces nombres représentent 37 %, 25 % et 31 %. Aucun motif clair ne se dégage par groupe.

Les lignes. La plupart semblent ordinaires : priorité 2 ou 3, livraison ou retours, message moyen, souvent aucune escalade dans les 90 derniers jours. Certains clients avaient déjà eu des escalades (T-117033 en avait eu 3). Le modèle les a vues, mais a accordé plus de poids aux autres colonnes. Rien dans les 11 colonnes ne distingue ces billets des nombreux billets ordinaires qui ne font pas l’objet d’une escalade.

Problèmes de données (un au choix, avec des preuves) :

  • Un problème d’étiquette après la politique de garantie. En mai, 28,6 % des billets de garantie font l’objet d’une escalade. Pourtant, le modèle entraîné avant le 1er mai leur attribue un score moyen de 0,163. Dans les données d’entraînement, le taux pour la garantie était de 15,9 %. La cible a changé de sens, et les données d’entraînement ne montrent pas ce nouveau sens.
  • Des entrées inconnues. En mai, 22 billets ont channel = unknown (canal inconnu) et 55 ont region = unknown (région inconnue). Au total, 90 billets ont au moins une entrée inconnue ou manquante. Ils sont trop peu nombreux pour juger (5 escalades pour le canal inconnu). Ils ne sont pas la cause principale des erreurs (14 sur 238).
  • Les invités n’ont aucun historique. En mai, 9 billets viennent d’invités : aucun identifiant client, aucune ancienneté, aucun billet antérieur et aucun groupe. Le modèle ne peut pas utiliser les caractéristiques d’historique pour eux. Et 9 billets ne suffisent pas à montrer s’il fonctionne pour eux.

Limites du modèle (une au choix, avec des preuves) :

  • Des escalades manquées avec un score faible. Parmi les 97 escalades manquées, 55 ont un score inférieur à 0,10. Aucun seuil respectant la capacité ne les détecte donc. Le rappel pour la priorité 3 est de 0,102 (6 sur 59), avec une erreur-type d’environ 0,04. C’est un écart fiable, pas le hasard.
  • Un élément qu’aucune colonne ne montre. Les données du cours sont accompagnées d’une description, leur fiche de données (dataset.md dans le dépôt des laboratoires). Elle indique qu’environ 1 billet sur 4 vient de clients réguliers ayant une tendance cachée à demander une escalade. Les lignes des billets manqués semblent ordinaires dans toutes les colonnes. Les contributions de T-117033 montrent comment le modèle voit un billet dont aucune colonne ne prédit l’escalade. Un meilleur modèle utilisant les mêmes colonnes ne peut pas corriger cela; seules de nouvelles informations le peuvent.
  • Les scores sous-estiment le risque après le changement de politique. Le score moyen de mai est de 0,120, contre un taux de 0,155 (Seuils et calibration). Le classement fonctionne encore, mais les scores ne doivent pas être présentés comme « la probabilité ».

Les groupes : un motif utile pour la planification : 66 grands comptes produisent 38,8 % des billets des clients connus. Aucun motif utile pour le modèle d’escalade : les différences de rappel entre groupes restent dans le bruit statistique. Le modèle possède déjà les colonnes d’historique.

Une réponse complète nomme le problème et donne les chiffres avec leurs nombres de cas. Elle indique leur provenance (validation, pas test) et le type de problème : données, étiquette ou modèle.

Vérifiez votre compréhension

Rappel des connaissances : nommez les quatre catégories d'erreurs du module et les deux questions à poser pour chaque erreur.

Afficher la réponse

Les quatre résultats d’une décision qui peut être correcte ou incorrecte : détecté (signalé et ayant fait l’objet d’une escalade), fausse alerte (signalé, sans escalade), manqué près du seuil (escalade, score de 0,10 à moins de 0,19) et manqué avec un score faible (escalade, score inférieur à 0,10). En mai : 86, 141, 42 et 55, plus 855 billets correctement non signalés. Les deux questions : est-ce un problème de données (une entrée est inconnue, manquante ou incorrecte)? Est-ce un problème d’étiquette (la cible est incorrecte ou a changé de sens)?

Vérifiez votre compréhension

Prédisez et expliquez : Tomás ajoute au tableau des clients une colonne en cents, order_value_cents (order_value × 100). Il refait le regroupement sans mise à l'échelle. Que deviennent les groupes? Et avec une mise à l'échelle?

Afficher la réponse

Sans mise à l’échelle, les valeurs des commandes en cents deviennent les plus grands nombres. Leur moyenne est d’environ 8 000 et leur écart-type, d’environ 4 000. Les groupes deviennent des plages de valeurs de commandes. L’ancienneté, qui déterminait auparavant les groupes, compte maintenant à peine. Avec la mise à l’échelle, order_value_cents (valeur de commande en cents) devient identique à order_value (valeur de commande), en écarts-types. Le résultat ressemble à un regroupement où order_value compte deux fois. La valeur de commande a donc plus de poids que les autres colonnes. Leçon : mettez les colonnes à l’échelle et n’ajoutez pas deux fois une colonne dans des unités différentes.

Vérifiez votre compréhension

Vérification de réalisation : à partir des preuves du module, nommez au moins un problème de données et une limite du modèle d'escalade de Larkfield. Donnez les chiffres, leurs nombres de cas et le jeu de données d'origine.

Afficher la réponse

Un problème de données (un problème d’étiquette) : après la politique de garantie du 1er mai, 28,6 % des billets de garantie font l’objet d’une escalade pendant le mois de validation (48 sur 168). Pourtant, le modèle leur attribue un score moyen de 0,163; dans les données d’entraînement, le taux était de 15,9 %. La cible a changé de sens après l’entraînement. Autres réponses valables : 90 billets de mai ont un canal ou une région inconnus, ou une valeur de commande manquante hors de l’équipe account; 9 billets d’invités n’ont aucun historique.

Une limite du modèle : parmi les 97 escalades manquées en mai, 55 ont un score inférieur à 0,10. Aucun seuil respectant la capacité de 20 % ne les détecte donc. Pour la priorité 3, le modèle détecte 6 escalades sur 59 (rappel de 0,102, erreur-type d’environ 0,04). Les billets manqués semblent ordinaires dans toutes les colonnes. La cause, comme une tendance cachée du client à demander une escalade, ne figure dans aucune colonne.

Tous les chiffres viennent du mois de validation. Une réponse complète donne chaque chiffre avec son nombre de cas. Elle distingue un problème de données ou d’étiquette d’une limite du modèle. Elle précise aussi qu’aucune décision n’a utilisé un sous-groupe de test.

Si votre dossier de projet ou votre carnet est dans un état étrange, revenez à un point de départ connu. Faites ces étapes dans un terminal ouvert dans ~/projects/ticket-model, avec .venv actif. Vous pouvez répéter chaque étape sans risque.

  1. Restaurez les données. Exécutez python get_data.py --reset. La dernière ligne est Ready: train 16939 rows (1983 escalated), valid 1179 (183), test 1002 (157). (Prêt : entraînement, 16 939 lignes dont 1 983 escalades; validation, 1 179 dont 183; test, 1 002 dont 157.)
  2. Restaurez ticket_model.py. Exécutez curl -fsSL -O https://raw.githubusercontent.com/Nextia-Intelligence/nextia-learning-labs/main/C05/project/ticket_model.py (curl.exe sous Windows).
  3. Vérifiez le modèle. Exécutez python -c "import ticket_model as t; print(len(t.FEATURES))". Vous devriez voir 11.
  4. Dans votre carnet, redémarrez le noyau et exécutez toutes les cellules depuis le début. Dans VS Code, sélectionnez Restart (redémarrer), puis Run All (tout exécuter).

Dans Colab ou Kaggle, vous n’avez rien à restaurer sur votre ordinateur. Supprimez le dossier ticket-model dans le panneau des fichiers. Redémarrez ensuite la session et exécutez toutes les cellules. Les cellules de configuration téléchargent tout de nouveau. Vos notes dans le carnet restent, à condition d’avoir enregistré une copie.

Récapitulatif

Dans ce module, vous avez examiné ce qui se cache derrière le score du modèle final. Vous avez utilisé le mois de validation pour chaque décision :

  • Erreurs et sous-groupes : en mai, le modèle détecte 86 escalades, produit 141 fausses alertes et manque 97 escalades. Parmi celles-ci, 55 ont un score inférieur à 0,10. Le rappel est de 0,771 pour payment, de 0,083 pour account et de 0,102 pour la priorité 3. Chaque taux s’accompagne de son nombre de cas. Un taux calculé sur 22 billets a un intervalle à 95 % allant de 5 % à 40 %.
  • Problèmes de données et d’étiquettes : warranty a un taux d’escalade de 28,6 %, mais un score moyen de 0,163. La politique du 1er mai a changé la cible. Au total, 90 billets ont une entrée inconnue ou manquante; ils expliquent seulement 14 des 238 erreurs.
  • Explications : les coefficients dépendent des autres caractéristiques (prior_tickets_90d : +0,18 seul, −0,29 dans le modèle final). L’importance par permutation place la priorité, les escalades antérieures et l’équipe en tête. Les contributions d’un billet s’additionnent pour donner exactement son score. Aucune n’est une raison ni une cause.
  • Un modèle simple par choix : la régression logistique et le boosting réglé détectent tous deux 86 escalades sur 183, à la capacité prévue. Larkfield conserve donc le modèle que les gens peuvent lire, vérifier et transmettre. Les règles du crédit, des hôpitaux et des assurances montrent l’importance de modèles lisibles quand une personne demande « pourquoi? ».
  • Groupes : après mise à l’échelle, k-means trouve 66 grands comptes qui produisent 38,8 % des billets des clients connus. Il trouve aussi une division selon l’ancienneté que Grace ne peut pas utiliser. L’inertie et les silhouettes, toutes inférieures à 0,3, sont des indices, pas des réponses.

Ensuite, dans le module 8, vous empaquetez le modèle et rédigez sa fiche de modèle. Elle consigne les sous-groupes, les limites et la raison du choix d’un modèle simple que vous avez trouvés ici. Vous planifiez ensuite les vérifications qui permettront de s’assurer qu’il reste fiable après le lancement.

Leçon suivante : Empaqueter les artefacts