Classer une liste d'appels
- Étude de cas 4
- 120 min
- Facultative · 1 étude de cas requise pour le certificat
Étude de cas 4 sur 7 · Apprentissage automatique : du problème à un modèle fiableClasser une liste d'appels
1. Comprendre l'apprentissage automatique
2. Cadrer le problème de prédiction
3. Évaluer les modèles supervisés
4. Créer les premiers modèles
5. Essayer d'autres familles de modèles
6. Régler et finaliser le modèle
7. Comprendre et choisir le modèle
8. Préparer la passation du modèle
Ce que vous saurez faire
Classez une liste de clients pour qu’un centre d’appels joigne d’abord les acheteurs les plus probables. Il ne peut appeler que 10 % des clients. Vous trouverez une colonne qui semble parfaite, mais qu’on ne peut pas utiliser. Vous choisirez aussi un arbre que l’équipe peut lire. Ensuite, vous mesurerez son avantage sur le hasard et verrez pourquoi il faut le surveiller après son lancement.
Le carnet (notebook) Jupyter exécute chaque étape de cette page, avec le même code et les mêmes sorties. Il ajoute trois tâches avec des vérifications. Ses cellules de préparation installent pandas et scikit-learn et téléchargent les données (environ 0,3 Mo). Aucune préparation locale n’est donc nécessaire. Il s’exécute en environ une minute.
Avant de commencerCe qu’il faut savoir et avoir sous la main
Ce que vous devez savoir
Cette étude de cas utilise tout le cours. Ces leçons comptent le plus :
- La précision pour un nombre fixe de cas signalés, dans Le compromis entre précision et rappel, et la précision moyenne, dans sa section La précision moyenne.
- Comment un arbre divise les données et pourquoi un arbre sans limite mémorise, dans Les arbres de décision et Contrôler la complexité.
- Les fuites, dans Sélectionner les caractéristiques utiles.
- La recherche par grille et la règle d’une erreur-type, dans Régler les hyperparamètres.
- La surveillance et le réentraînement, dans Planifier les vérifications en production.
Ce qu’il vous faut sur votre ordinateur (seulement pour l’exécution locale; le carnet n’a besoin de rien de cela)
- Un dossier avec un environnement virtuel contenant pandas 3.0.6, scikit-learn 1.9.1 et matplotlib, comme dans Définir une référence de base : préparer votre projet. Le projet
ticket-modelconvient; cette étude de cas ne modifie pas ses fichiers. - Le carnet, téléchargé à partir du lien ci-dessus, dans VS Code ou JupyterLab. Sa cellule de préparation télécharge les données dans un nouveau dossier
call-list. Pour recommencer, supprimez ce dossier.
Testé sur macOS avec Python 3.14.6, pandas 3.0.6 et scikit-learn 1.9.1, dans Jupyter, à partir d’une session neuve. Windows et Linux n’ont pas été testés.
La situation
Section intitulée « La situation »Inês Duarte dirige le centre d’appels d’une banque portugaise. Ses agents vendent un dépôt à terme : le client place de l’argent à la banque pour une durée fixe et reçoit des intérêts. La liste de cette campagne compte des milliers de clients. Son équipe a le temps d’appeler seulement 1 client sur 10. Un appel qui se termine par « non » prend environ dix minutes du temps d’un agent. Un client qui aurait dit oui, mais qu’on n’appelle jamais, représente un dépôt que la banque n’obtient pas.
Inês n’a donc pas besoin d’une réponse oui/non pour chaque client. Elle veut la liste en ordre, avec les clients les plus susceptibles d’accepter en premier. Ainsi, les appels que son équipe peut faire obtiendront le plus de oui possible. Son analyste, Tiago, crée le modèle. Elle a un autre souhait : « Je veux comprendre pourquoi la liste est dans cet ordre. Mon équipe ne fera pas confiance à une boîte noire. »
Ce que vous allez créer
Section intitulée « Ce que vous allez créer »Vous créerez un arbre de décision qui attribue un score à chaque client de la liste. Vous l’évaluerez selon l’usage d’Inês : la proportion de oui dans les premiers 10 % de la liste. En cours de route, vous trouverez une fuite et comparerez des arbres de plusieurs tailles dans un journal d’expériences. Vous choisirez l’arbre le plus simple qui égale le meilleur à la capacité de l’équipe. Vous lirez ses règles et le testerez une seule fois. Ensuite, vous examinerez ce qui arrive quand l’économie change et planifierez la surveillance.
Les compétences viennent de tout le cours : cadrage (Traduire un besoin), références de base (Définir une référence de base), précision à une capacité donnée et arbres. Les fuites, le réglage, les sous-groupes (L’analyse par sous-groupes) et les vérifications en production entrent aussi en jeu.
Les données
Section intitulée « Les données »Les données proviennent du jeu de données Bank Marketing du UCI Machine Learning Repository, dans sa version élargie, bank-additional-full. Il contient 41 188 appels téléphoniques réels et anonymes d’une banque portugaise, de mai 2008 à novembre 2010. Il indique aussi si chaque client a souscrit.
La cellule de préparation du carnet télécharge le fichier et vérifie sa somme de contrôle. Tiago le charge :
raw = pd.read_csv("bank_additional_full.csv.gz")print(raw.shape)print(raw.columns.tolist())La sortie :
(41188, 21)['age', 'job', 'marital', 'education', 'default', 'housing', 'loan', 'contact', 'month', 'day_of_week', 'duration', 'campaign', 'pdays', 'previous', 'poutcome', 'emp.var.rate', 'cons.price.idx', 'cons.conf.idx', 'euribor3m', 'nr.employed', 'y']Ce que cela nous apprend. Une ligne représente le dernier appel à un client dans une campagne. Les colonnes décrivent le client, l’appel, les campagnes précédentes et l’économie le jour de l’appel. La cible y indique si le client a souscrit.
Le dictionnaire des données
Section intitulée « Le dictionnaire des données »Voici les colonnes utilisées dans cette étude de cas, en mots simples :
| Colonne | Signification | Usage |
|---|---|---|
age, job, marital, education |
Renseignements sur le client | Entrées |
default, housing, loan |
Le client a-t-il un crédit en défaut de paiement, un prêt immobilier, un prêt personnel? (oui, non, inconnu) | Entrées |
contact |
Téléphone mobile (cellular) ou fixe (telephone) |
Entrée |
month, day_of_week |
Moment de l’appel | Entrées |
duration |
Durée de l’appel, en secondes | Pas une entrée (voir ci-dessous) |
campaign |
Appels à ce client dans cette campagne, y compris celui-ci | Entrée |
pdays, previous, poutcome |
Jours depuis un appel d’une campagne précédente (999 = jamais), appels avant cette campagne et résultat précédent | Entrées (pdays remplacée, voir ci-dessous) |
emp.var.rate, cons.price.idx, cons.conf.idx, euribor3m, nr.employed |
Cinq chiffres sur l’économie portugaise : variation de l’emploi, prix à la consommation, confiance des consommateurs, taux Euribor à 3 mois et nombre de personnes en emploi | Entrées |
y |
Le client a-t-il souscrit? | Cible |
Premières vérifications
Section intitulée « Premières vérifications »print("missing values:", int(raw.isna().sum().sum()), "| exact repeats:", int(raw.duplicated().sum()))print("yes:", int((raw["y"] == "yes").sum()), f"({(raw['y'] == 'yes').mean():.1%})")text = raw.select_dtypes(include=["object", "string"]).columnsprint("unknown:", {c: int((raw[c] == "unknown").sum()) for c in text if (raw[c] == "unknown").any()})print("pdays = 999:", f"{(raw['pdays'] == 999).mean():.1%}")La sortie :
missing values: 0 | exact repeats: 12yes: 4640 (11.3%)unknown: {'job': 330, 'marital': 80, 'education': 1731, 'default': 8597, 'housing': 990, 'loan': 990}pdays = 999: 96.3%Ce que cela nous apprend. « Aucune valeur manquante » est faux. Les réponses manquantes sont écrites sous la forme « unknown » (inconnu). Pour default seulement, cela touche 8 597 clients, soit un cinquième du fichier. Tiago garde « unknown » comme catégorie distincte. En effet, « la banque ne sait pas » peut aussi révéler quelque chose sur un client. pdays = 999 signifie « jamais appelé auparavant » dans 96,3 % des lignes. Un modèle ne doit pas l’interpréter comme 999 jours. Tiago la remplace donc par une colonne oui/non, contacted_before. Les 12 doublons exacts sont supprimés. Et seulement 1 appel sur 9 se termine par oui.
Il n’y a pas d’identifiant de client. Un client appelé dans deux campagnes apparaît donc deux fois, sans lien entre les lignes. Le fichier est aussi en ordre chronologique, ce qui compte à la fin de cette étude de cas.
Cadrer le problème
Section intitulée « Cadrer le problème »- L’unité : un client sur la liste de la campagne.
- La cible :
subscribed, 1 si le client a dit oui. - Le moment de la prédiction : avant l’appel, quand la liste est triée.
- La décision : appeler les premiers 10 % de la liste. La mesure est donc le taux de réussite dans les premiers 10 % : la proportion de oui parmi les appels possibles. C’est la précision pour un nombre fixe de cas signalés, dans Le compromis entre précision et rappel. Le gain est le taux de réussite divisé par le taux de oui de toute la liste. Il indique combien de fois le résultat dépasse celui d’appels au hasard.
- Ce qui est inconnu au moment de la prédiction :
duration. La prochaine section montre pourquoi cela compte.
prepare() apporte les corrections, et les listes de colonnes précisent chaque rôle :
def prepare(raw): """Drop exact repeats, add the 0/1 target, and replace the code 999 of pdays with a yes/no column. The rows stay in the file's order, which is time order.""" calls = raw.drop_duplicates().reset_index(drop=True) calls["subscribed"] = (calls["y"] == "yes").astype(int) calls["contacted_before"] = (calls["pdays"] != 999).astype(int) return calls
calls = prepare(raw)CATEGORIES = ["job", "marital", "education", "default", "housing", "loan", "contact", "month", "day_of_week", "poutcome"]MACRO = ["emp.var.rate", "cons.price.idx", "cons.conf.idx", "euribor3m", "nr.employed"] # the economyNUMBERS = ["age", "campaign", "previous", "contacted_before"] + MACROFEATURES = CATEGORIES + NUMBERSTARGET = "subscribed"print(len(calls), "calls,", len(FEATURES), "features")La sortie :
41176 calls, 19 featuresCe que cela nous apprend. Il reste 41 176 appels, avec 19 entrées. duration n’en fait pas partie.
Explorer les besoins de la décision
Section intitulée « Explorer les besoins de la décision »La fuite
Section intitulée « La fuite »Le premier modèle rapide de Tiago utilisait toutes les colonnes et semblait excellent. Avant de le montrer à Inês, il examine le taux de oui selon la durée de l’appel :
duration_band = pd.cut(calls["duration"], [-1, 60, 120, 180, 300, 600, 5000], labels=["0-1 min", "1-2 min", "2-3 min", "3-5 min", "5-10 min", "10+ min"])print(calls.groupby(duration_band, observed=True)[TARGET].agg(calls="size", yes_rate="mean").round(3))La sortie :
calls yes_rateduration0-1 min 4284 0.0001-2 min 8628 0.0192-3 min 7784 0.0513-5 min 9278 0.1035-10 min 7738 0.18610+ min 3464 0.486Description textuelle du graphique
Un graphique à colonnes avec six colonnes pour la durée de l’appel en minutes. 0–1 (4 284 appels) : 0 %. 1–2 (8 628) : 2 %. 2–3 (7 784) : 5 %. 3–5 (9 278) : 10 %. 5–10 (7 738) : 19 %. 10 ou plus (3 464) : 49 %. Les colonnes montent régulièrement de gauche à droite.
Ce que cela nous apprend. Un long appel accompagne un oui, bien sûr : l’agent explique le produit et remplit les formulaires. Un appel de moins d’une minute se termine presque jamais par oui. Mais la durée est connue seulement après l’appel, quand la réponse est connue aussi. Un modèle qui l’utilise ne peut pas classer une liste avant que quelqu’un décroche le téléphone. C’est une fuite, comme dans Sélectionner les caractéristiques utiles. La documentation des données le signale. Pourtant, beaucoup de modèles publiés sur ces données l’utilisent encore.
Qui a déjà dit oui
Section intitulée « Qui a déjà dit oui »Un fait est connu avant l’appel : le résultat d’une campagne précédente.
print(calls.groupby("poutcome")[TARGET].agg(calls="size", yes_rate="mean").round(3))La sortie :
calls yes_ratepoutcomefailure 4252 0.142nonexistent 35551 0.088success 1373 0.651Ce que cela nous apprend. Les clients qui ont dit oui lors d’une campagne précédente disent encore oui dans 65,1 % des cas. « Appeler d’abord ceux qui ont déjà dit oui » est une règle évidente. C’est la référence de base. Mais seulement 1 373 clients (3,3 %) ont déjà dit oui, soit moins que les 10 % de l’équipe. La règle ne dit pas qui appeler ensuite.
Division et référence de base
Section intitulée « Division et référence de base »Le fichier est en ordre chronologique, mais il n’a pas de colonne de date. La question d’Inês est : « Classer le reste de la liste de cette campagne. » Les appels restants ressemblent à ceux déjà faits. La division principale est donc aléatoire et stratifiée : entraînement 60 %, validation 20 %, test 20 %. Une question plus difficile est : « Le classement fonctionnera-t-il encore pendant la période suivante? » Tiago prévoit maintenant une deuxième vérification, avant tout modèle. Il entraînera le modèle sur les premiers 80 % du fichier et le testera sur les derniers 20 %. Cette vérification arrive à la fin, comme épilogue.
hit_rate() trie par score et calcule la proportion de oui parmi les premiers 10 %. En cas d’égalité des scores, elle garde l’ordre de la liste.
from sklearn.model_selection import train_test_split
rest, test = train_test_split(calls, test_size=0.2, stratify=calls[TARGET], random_state=0)train, valid = train_test_split(rest, test_size=0.25, stratify=rest[TARGET], random_state=0)for name, part in [("train", train), ("valid", valid), ("test", test)]: print(f"{name:5s} {len(part):6d} calls, {part[TARGET].sum():5d} yes ({part[TARGET].mean():.1%})")
CAPACITY = 0.10
def hit_rate(y, scores, capacity=CAPACITY): """Share of yes among the calls with the highest scores, for a capacity share of the list. Ties keep the list order (the sort is stable).""" calls_made = int(round(capacity * len(y))) order = np.argsort(-np.asarray(scores, dtype=float), kind="stable") return float(np.asarray(y)[order[:calls_made]].mean())La sortie :
train 24705 calls, 2783 yes (11.3%)valid 8235 calls, 928 yes (11.3%)test 8236 calls, 928 yes (11.3%)Ce que cela nous apprend. Les trois ensembles ont le même taux de oui. Dans les données de validation, 10 % représente 824 appels.
Voici les références de base sur les données de validation : « toujours non », un ordre aléatoire et la règle.
y_valid = valid[TARGET].to_numpy()rule_valid = (valid["poutcome"] == "success").astype(int)print("calls the team can make:", int(round(CAPACITY * len(valid))))print("always no: accuracy", round(1 - y_valid.mean(), 3), "| catches", 0, "yes")print("random order: hit rate", round(y_valid.mean(), 3))print("rule, earlier success first: hit rate", round(hit_rate(y_valid, rule_valid), 3))La sortie :
calls the team can make: 824always no: accuracy 0.887 | catches 0 yesrandom order: hit rate 0.113rule, earlier success first: hit rate 0.273Ce que cela nous apprend. Un modèle qui dit toujours non atteint une exactitude de 88,7 %, mais n’obtient aucun dépôt. L’exactitude est donc inutile ici, comme l’indiquait Les métriques de classification. Un ordre aléatoire obtient un oui dans 11,3 % des appels. La règle en obtient 27,3 %. Elle appelle d’abord les 266 clients ayant déjà dit oui, puis les autres dans l’ordre de la liste. Le modèle doit dépasser 0,273.
Deux pipelines
Section intitulée « Deux pipelines »L’arbre reçoit les catégories avec un encodage un parmi plusieurs. Ses règles se lisent donc « contact est cellular », et non « contact ≤ 0,5 ». Il reçoit les nombres tels quels. Un arbre compare un nombre à un seuil et n’a pas besoin de mise à l’échelle (Les arbres de décision). La régression logistique, qui sert de vérification, reçoit les catégories avec le même encodage et les nombres mis à l’échelle.
def build_tree(max_depth=None, min_samples_leaf=1, numbers=NUMBERS): prepare_step = ColumnTransformer( [("categories", OneHotEncoder(handle_unknown="ignore", sparse_output=False), CATEGORIES)], remainder="passthrough", verbose_feature_names_out=False) return Pipeline([("prepare", prepare_step), ("model", DecisionTreeClassifier( max_depth=max_depth, min_samples_leaf=min_samples_leaf, random_state=0))])
def build_logistic(numbers=NUMBERS): prepare_step = ColumnTransformer([("categories", OneHotEncoder(handle_unknown="ignore"), CATEGORIES), ("numbers", StandardScaler(), numbers)]) return Pipeline([("prepare", prepare_step), ("model", LogisticRegression(max_iter=2000))])Le journal d’expériences
Section intitulée « Le journal d’expériences »Tiago évalue chaque expérience sur les mêmes 5 plis stratifiés des données d’entraînement, avec deux mesures. La précision moyenne (AP) examine tout le classement. top10 est le taux de réussite dans les premiers 10 %, qui correspond à la décision d’Inês. Le journal contient les colonnes de L’apprentissage automatique est itératif, plus cv_top10 et valid_top10. Le carnet définit cv_scores() et log_experiment().
Les premières lignes sont les références de base, puis un arbre avec duration, pour mesurer la fuite :
base = np.array([train[TARGET].iloc[rows].mean() for _, rows in cv.split(train, train[TARGET])])log_experiment("E01", "Baseline: random order", base, base, "baseline", "The floor: lift 1.")rule_ap, rule_top = [], []for _, rows in cv.split(train, train[TARGET]): part = train.iloc[rows] rule_ap.append(average_precision_score(part[TARGET], part["poutcome"] == "success")) rule_top.append(hit_rate(part[TARGET].to_numpy(), (part["poutcome"] == "success").astype(int)))log_experiment("E02", "Baseline: earlier success first", np.array(rule_ap), np.array(rule_top), "baseline", "What the team would do without a model.")leak = cv_scores(build_tree(None, 100, NUMBERS + ["duration"]), FEATURES + ["duration"])log_experiment("E03", "Tree, min leaf 100, WITH duration", leak["test_ap"], leak["test_top10"], "reject", "Leak: the length of a call is known only after the call.")La sortie :
E01 CV AP 0.113 ± 0.000 top 10% 0.113 baseline: Baseline: random orderE02 CV AP 0.215 ± 0.013 top 10% 0.284 baseline: Baseline: earlier success firstE03 CV AP 0.614 ± 0.014 top 10% 0.616 reject: Tree, min leaf 100, WITH durationCe que cela nous apprend. Avec la fuite, l’arbre obtient un oui dans 61,6 % de ses appels prioritaires, soit plus du double de la règle. Sur les données de validation, son aire sous la courbe ROC (ROC AUC) est de 0,944. Sans duration, le même type d’arbre atteint environ 0,80. Ce résultat ne peut jamais se produire en usage réel. La ligne reste dans le journal avec reject (rejeter), pour que personne ne réessaie.
Erreur courante
La fuite qui ressemble à un excellent modèle
Symptôme : un modèle atteint une ROC AUC de 0,94 sur des données réservées. Pour ce problème, tout le monde s’attend à environ 0,75 à 0,80. Sa variable la plus importante est duration.
Cause : la variable est mesurée après le résultat. Un long appel est une conséquence d’un oui, pas un signe annonciateur. Les données réservées ne détectent pas cela, car les lignes de test ont la même fuite.
Correction : pour chaque entrée, demandez-vous : « Est-ce que je connaîtrais cette valeur au moment de prendre la décision? » C’est la première question de Sélectionner les caractéristiques utiles. Retirez ce qui échoue à ce test et gardez le résultat avec fuite dans le journal comme avertissement.
Conséquence dans le travail en IA : en production, la colonne avec fuite est vide ou n’a aucun sens. La performance réelle du modèle est bien inférieure au rapport. Une équipe qui a promis 0,94 doit expliquer pourquoi la campagne a obtenu la moitié des oui attendus.
Des arbres de toutes les tailles
Section intitulée « Des arbres de toutes les tailles »Ensuite, un arbre sans limites et l’exploration des profondeurs de Contrôler la complexité :
full = cv_scores(build_tree())print("no limit: train AP", round(full["train_ap"].mean(), 3), "| CV AP", round(full["test_ap"].mean(), 3))log_experiment("E04", "Tree, no limits", full["test_ap"], full["test_top10"], "reject", "It memorises train.")sweep = []for depth in [1, 2, 3, 4, 5, 6, 8, 10, 12, 15]: r = cv_scores(build_tree(depth)) sweep.append({"depth": depth, "train_ap": r["train_ap"].mean(), "cv_ap": r["test_ap"].mean(), "cv_top10": r["test_top10"].mean()})sweep = pd.DataFrame(sweep).set_index("depth")print(sweep.round(3))La sortie :
no limit: train AP 0.999 | CV AP 0.178E04 CV AP 0.178 ± 0.009 top 10% 0.308 reject: Tree, no limits train_ap cv_ap cv_top10depth1 0.270 0.271 0.4472 0.348 0.348 0.4683 0.376 0.371 0.4874 0.413 0.395 0.4915 0.441 0.403 0.4846 0.472 0.405 0.4858 0.540 0.377 0.47410 0.610 0.337 0.45112 0.681 0.308 0.44115 0.784 0.261 0.401- AP d'entraînement
- AP en validation croisée
- Taux de réussite en validation croisée dans les premiers 10 % (pointillés)
Description textuelle du graphique
Un graphique linéaire avec max_depth de 1 à 15 sur l’axe horizontal et des scores de 0,2 à 0,8 sur l’axe vertical. L’AP d’entraînement augmente régulièrement : 0,27 à la profondeur 1, 0,38 à 3, 0,47 à 6, 0,78 à 15. L’AP en validation croisée monte aussi jusqu’à 0,405 à la profondeur 6, puis baisse : 0,377 à 8, 0,337 à 10, 0,261 à 15. L’écart entre les deux lignes se creuse après la profondeur 4. La ligne en pointillés du taux de réussite en validation croisée dans les premiers 10 % est plus haute et plus plate : 0,447 à la profondeur 1, 0,468 à 2, 0,487 à 3 (point marqué), 0,491 à 4, 0,484 à 0,485 à 5 et 6, puis elle descend à 0,401 à 15.
Ce que cela nous apprend. L’arbre sans limite mémorise chaque appel d’entraînement : AP d’entraînement de 0,999, AP en validation croisée de 0,178. Ses premiers 10 % dépassent à peine la règle. Une limite aide, mais les deux mesures ne s’accordent pas sur son niveau. L’AP, qui évalue toute la liste, favorise la profondeur 5 ou 6. Le taux de réussite dans les premiers 10 %, qui correspond à la décision, reste presque constant entre 3 et 6. La profondeur supplémentaire améliore l’ordre au bas de la liste, où personne n’appelle.
Une recherche par grille et l’arbre le plus simple qui suffit
Section intitulée « Une recherche par grille et l’arbre le plus simple qui suffit »Tiago explore ensemble la profondeur et la taille minimale des feuilles (Régler les hyperparamètres), avec les deux mesures :
from sklearn.model_selection import GridSearchCV
search = GridSearchCV(build_tree(), {"model__max_depth": [2, 3, 4, 5, 6, 8, None], "model__min_samples_leaf": [1, 20, 50, 100, 200]}, scoring={"ap": "average_precision", "top10": top10}, refit="top10", cv=cv)search.fit(train[FEATURES], train[TARGET])grid = pd.DataFrame(search.cv_results_)[["param_model__max_depth", "param_model__min_samples_leaf", "mean_test_ap", "mean_test_top10", "std_test_top10"]]grid.columns = ["max_depth", "min_leaf", "cv_ap", "cv_top10", "cv_top10_std"]print("best by AP:\n", grid.sort_values("cv_ap", ascending=False).head(3).round(3).to_string(index=False))print("best by top 10%:\n", grid.sort_values("cv_top10", ascending=False).head(3).round(3).to_string(index=False))La sortie :
best by AP: max_depth min_leaf cv_ap cv_top10 cv_top10_std None 100 0.433 0.483 0.017 8 100 0.428 0.484 0.017 None 50 0.423 0.487 0.014best by top 10%: max_depth min_leaf cv_ap cv_top10 cv_top10_std 8 50 0.420 0.496 0.023 5 50 0.412 0.494 0.023 4 20 0.397 0.493 0.023Ce que cela nous apprend. Le gagnant dépend de la mesure, et les différences sont petites. Tiago applique la règle d’une erreur-type à la mesure de la décision. Le meilleur taux de réussite des premiers 10 % est de 0,496. Son erreur-type est de 0,023 ÷ √5 = 0,010. Tout arbre atteignant 0,486 ou plus est donc aussi bon, compte tenu du bruit. Il choisit le plus simple.
Dans le carnet, vous calculez la limite et la plus petite profondeur qui l’atteint. La réponse est la profondeur 3, avec 0,487 : juste à l’intérieur de la limite. La profondeur 2 (0,468) ne l’atteint pas. Un arbre de profondeur 3 a au plus 8 feuilles, soit 8 règles que l’équipe d’Inês peut lire. Voici les candidats dans le journal, avec la régression logistique comme vérification :
E05 CV AP 0.433 ± 0.014 top 10% 0.483 reject: Tree, no depth limit, min leaf 100 (best AP)E06 CV AP 0.420 ± 0.015 top 10% 0.496 reject: Tree, max_depth 8, min leaf 50 (best top 10%)E07 CV AP 0.371 ± 0.015 top 10% 0.487 keep: Tree, max_depth 3E08 CV AP 0.448 ± 0.013 top 10% 0.489 reject: Logistic regression, 19 featuresCe que cela nous apprend. Les quatre candidats sont proches sur la mesure de la décision, de 0,483 à 0,496. L’arbre de profondeur 3 est le plus simple. La régression logistique a la meilleure AP. Mais à la capacité de l’équipe, elle obtient la même proportion de oui que l’arbre (0,489 contre 0,487). Cet écart reste bien à l’intérieur du bruit.
Rui, un autre analyste de la banque, demande : « Alors, pourquoi ne pas utiliser la régression logistique? » Parce qu’Inês a demandé des règles que son équipe peut lire. Un arbre de profondeur 3 tient sur une page. Des gens qui n’ont jamais entendu parler d’AP peuvent le vérifier. L’épilogue montrera une raison de garder la régression logistique en tête.
Tiago confirme le choix une seule fois sur les données de validation, pour l’arbre choisi et deux candidats :
candidates = {"E05": build_tree(None, 100), "E07": build_tree(3), "E08": build_logistic()}log = pd.read_csv(LOG)for id, pipe in candidates.items(): pipe.fit(train[FEATURES], train[TARGET]) s = pipe.predict_proba(valid[FEATURES])[:, 1] log.loc[log["id"] == id, ["valid_ap", "valid_top10"]] = [round(average_precision_score(y_valid, s), 3), round(hit_rate(y_valid, s), 3)] print(f"{id} ROC AUC {roc_auc_score(y_valid, s):.3f} AP {average_precision_score(y_valid, s):.3f}" f" top 10% {hit_rate(y_valid, s):.3f} lift {hit_rate(y_valid, s) / y_valid.mean():.2f}")log.to_csv(LOG, index=False)tree = candidates["E07"]scores = tree.predict_proba(valid[FEATURES])[:, 1]La sortie :
E05 ROC AUC 0.799 AP 0.469 top 10% 0.513 lift 4.56E07 ROC AUC 0.757 AP 0.389 top 10% 0.510 lift 4.52E08 ROC AUC 0.800 AP 0.474 top 10% 0.508 lift 4.51Ce que cela nous apprend. À la capacité de l’équipe, l’arbre à 8 feuilles obtient autant de oui que celui à 183 feuilles et la régression logistique. Cela représente environ 51 oui sur 100 appels, soit 4,5 fois le taux aléatoire. Sa ROC AUC et son AP sont plus faibles parce qu’il classe moins bien le bas de la liste. C’est le bon compromis pour cette décision. Le journal contient maintenant huit expériences, avec E07 comme expérience à conserver.
Lire l’arbre comme des règles
Section intitulée « Lire l’arbre comme des règles »export_text() affiche l’arbre. Les weights (poids) d’une feuille sont ses appels d’entraînement, sous la forme [non, oui] :
from sklearn.tree import export_text
names = list(tree.named_steps["prepare"].get_feature_names_out())print(export_text(tree.named_steps["model"], feature_names=names, show_weights=True, decimals=2))La sortie :
|--- nr.employed <= 5087.65| |--- contacted_before <= 0.50| | |--- contact_cellular <= 0.50| | | |--- weights: [276.00, 83.00] class: 0| | |--- contact_cellular > 0.50| | | |--- weights: [1164.00, 713.00] class: 0| |--- contacted_before > 0.50| | |--- day_of_week_mon <= 0.50| | | |--- weights: [161.00, 450.00] class: 1| | |--- day_of_week_mon > 0.50| | | |--- weights: [68.00, 82.00] class: 1|--- nr.employed > 5087.65| |--- cons.conf.idx <= -46.65| | |--- euribor3m <= 1.54| | | |--- weights: [1251.00, 269.00] class: 0| | |--- euribor3m > 1.54| | | |--- weights: [67.00, 62.00] class: 0| |--- cons.conf.idx > -46.65| | |--- month_oct <= 0.50| | | |--- weights: [18925.00, 1099.00] class: 0| | |--- month_oct > 0.50| | | |--- weights: [10.00, 25.00] class: 1Description textuelle du diagramme
L’arbre compte sept questions et huit feuilles. La première question sépare les appels selon le nombre de personnes en emploi au Portugal au moment de l’appel. Le seuil est de 5 087,65 milliers de personnes, soit une période de faible emploi. Pendant cette période, l’arbre demande si le client a déjà été contacté, puis le type de téléphone ou le jour. Dans l’autre période, il demande le niveau de confiance des consommateurs, puis le taux Euribor ou le mois. Les feuilles vont de R7, avec 20 024 appels et 5,5 % de oui, à R3, avec 611 appels et 73,6 % de oui.
Le même arbre sous forme de règles pour l’équipe d’Inês, dans l’ordre utilisé par la liste (taux de oui décroissant) :
| Règle | Quand elle s’applique | Appels d’entraînement | Taux de oui |
|---|---|---|---|
| R3 | Faible emploi, contact lors d’une campagne précédente, pas un lundi | 611 | 73,6 % |
| R8 | Emploi normal, confiance des consommateurs supérieure à −46,65, octobre | 35 | 71,4 % |
| R4 | Faible emploi, contact lors d’une campagne précédente, un lundi | 150 | 54,7 % |
| R6 | Emploi normal, faible confiance des consommateurs, Euribor supérieur à 1,54 | 129 | 48,1 % |
| R2 | Faible emploi, jamais contacté, téléphone cellulaire | 1 877 | 38,0 % |
| R1 | Faible emploi, jamais contacté, téléphone fixe | 359 | 23,1 % |
| R5 | Emploi normal, faible confiance des consommateurs, Euribor au plus égal à 1,54 | 1 520 | 17,7 % |
| R7 | Emploi normal, confiance des consommateurs supérieure à −46,65, autre mois qu’octobre | 20 024 | 5,5 % |
Ce que cela nous apprend. La première question ne porte pas sur le client. nr.employed est le nombre de personnes en emploi dans tout le pays, un chiffre publié tous les trois mois. Il sépare les appels en périodes économiques. Pendant la période de faible emploi, 44,3 % des appels ont donné un oui, contre 6,7 % autrement. Seules les questions suivantes portent sur les clients : un contact précédent et un téléphone cellulaire. L’arbre indique donc deux choses différentes à Inês :
- Quand une campagne fonctionne : dans ces données, quand l’emploi était faible et que les taux d’intérêt avaient baissé. Les gens préféraient alors un dépôt sûr. L’équipe ne peut pas choisir l’économie, mais elle peut adapter la taille d’une campagne à celle-ci.
- Qui appeler en premier : les clients contactés lors d’une campagne précédente, puis les clients avec un téléphone cellulaire.
Lisez les petites feuilles avec prudence. R8 compte seulement 35 appels d’entraînement et R6 en compte 129. Quelques appels de plus pourraient faire beaucoup varier leurs taux. La séparation selon le lundi dans R3 et R4 repose aussi sur seulement 150 appels. Un arbre présente chaque feuille avec la même assurance; le nombre d’appels indique à quel point on peut lui faire confiance. Rappelez-vous aussi la première question : elle compte dans l’épilogue.
Métrique et point de fonctionnement
Section intitulée « Métrique et point de fonctionnement »Tiago a choisi la métrique en cadrant le problème : le taux de réussite dans les premiers 10 % de la liste. L’équipe appelle seulement le haut de la liste. Le point de fonctionnement est donc une capacité, et non un seuil sur le score. L’équipe appelle les premiers 10 % de la liste, quels que soient leurs scores. Et si Inês obtient plus d’agents? Tiago affiche le taux de réussite, le gain relatif et la part de tous les oui trouvés, pour plusieurs capacités :
for capacity in [0.05, 0.10, 0.15, 0.20, 0.30]: rate = hit_rate(y_valid, scores, capacity) found = rate * round(capacity * len(y_valid)) / y_valid.sum() print(f"call {capacity:.0%}: hit rate {rate:.3f}, lift {rate / y_valid.mean():.2f}, finds {found:.1%} of all yes")La sortie :
call 5%: hit rate 0.590, lift 5.23, finds 26.2% of all yescall 10%: hit rate 0.510, lift 4.52, finds 45.3% of all yescall 15%: hit rate 0.414, lift 3.67, finds 55.1% of all yescall 20%: hit rate 0.347, lift 3.08, finds 61.6% of all yescall 30%: hit rate 0.247, lift 2.20, finds 65.8% of all yes- Arbre, profondeur 3
- Régression logistique (pointillés)
- Règle : réussites précédentes en premier
- Ordre aléatoire (pointillés)
Description textuelle du graphique
Un graphique carré. L’axe horizontal montre la part de la liste appelée, de 0 % à 100 %. L’axe vertical montre la part de tous les oui trouvés, de 0 % à 100 %. L’ordre aléatoire est une diagonale en pointillés. La règle monte rapidement pour les premiers 3 % de la liste (les oui précédents), puis suit une ligne proche de la diagonale : 24 % à 10 %, 33 % à 20 %, 59 % à 50 %. L’arbre monte rapidement à 45 % à 10 % et à 62 % à 20 %, puis s’aplatit : 66 % à 30 % et 76 % à 50 %. La régression logistique est presque identique à l’arbre jusqu’à 10 % (45 %), puis reste au-dessus : 64 % à 20 %, 72 % à 30 %, 82 % à 50 %.
Ce que cela nous apprend. À 10 %, l’arbre trouve 45,3 % de tous les oui de la liste avec un dixième des appels. Les premiers appels sont les meilleurs. Chaque tranche supplémentaire de 5 % de la liste ajoute moins de oui. Doubler l’équipe pour atteindre 20 % trouve 16 points de pourcentage de oui supplémentaires, avec un taux de réussite de 0,347. Au-delà de 20 %, la courbe de l’arbre s’aplatit. La plupart des appels restants se trouvent dans une grande feuille, R7, où chaque appel a le même score. L’arbre ne peut donc pas les classer. La régression logistique continue de les classer. Si la capacité d’Inês dépasse un jour 20 %, il faudra refaire la comparaison.
Dans la tâche « changer une chose » du carnet (notebook) Jupyter, une semaine chargée permet d’appeler seulement 5 % de la liste. Le taux de réussite monte à 0,590, parce que seules les meilleures feuilles sont appelées.
Erreurs et sous-groupes
Section intitulée « Erreurs et sous-groupes »Les oui que l’équipe n’appelle jamais
Section intitulée « Les oui que l’équipe n’appelle jamais »Chaque appel de validation arrive dans une feuille. Tiago compte les oui manqués (hors des premiers 10 %) par feuille :
order = np.argsort(-scores, kind="stable")called = np.zeros(len(valid), dtype=bool)called[order[: int(round(CAPACITY * len(valid)))]] = Trueleaf = tree.named_steps["model"].apply(tree.named_steps["prepare"].transform(valid[FEATURES]))errors = pd.DataFrame({"leaf": leaf, "score": scores.round(3), "called": called, "yes": y_valid, "missed_yes": (y_valid == 1) & ~called})print(errors.groupby(["leaf", "score"]).agg(calls=("yes", "size"), called=("called", "sum"), yes=("yes", "sum"), missed_yes=("missed_yes", "sum")))La sortie :
calls called yes missed_yesleaf score3 0.231 120 0 27 274 0.380 612 542 252 276 0.736 182 182 140 07 0.547 47 47 28 010 0.177 518 0 94 9411 0.481 36 36 17 013 0.055 6703 0 360 36014 0.714 17 17 10 0Ce que cela nous apprend. L’équipe appelle quatre feuilles au complet et la majeure partie d’une cinquième (542 des 612 appels au score de 0,38). Sur les 508 oui manqués, 360 se trouvent dans la grande feuille R7 (score de 0,055). Dans cette feuille, seulement 1 appel sur 19 réussit. L’arbre donne un seul score à ces 6 703 appels; il ne peut donc pas repérer les 360 parmi eux. C’est le prix de 8 règles. Un modèle plus fin pourrait classer les appels à l’intérieur de la feuille. Mais avec une capacité de 10 %, l’équipe ne les atteindrait pas de toute façon.
Qui se retrouve sur la liste
Section intitulée « Qui se retrouve sur la liste »Un classement décide qui reçoit un appel de vente. Tiago le vérifie donc par groupe d’âge, avec les nombres de clients :
age_band = pd.cut(valid["age"], [16, 25, 40, 60, 100], labels=["17-25", "26-40", "41-60", "61+"])frame = pd.DataFrame({"age band": age_band.to_numpy(), "called": called, "yes": y_valid})slices = frame.groupby("age band", observed=True).agg(clients=("yes", "size"), yes_rate=("yes", "mean"), share_called=("called", "mean"))slices["hit rate in calls"] = frame[frame["called"]].groupby("age band", observed=True)["yes"].mean()print(slices.round(3))La sortie :
clients yes_rate share_called hit rate in callsage band17-25 308 0.214 0.224 0.55126-40 4431 0.109 0.088 0.52141-60 3317 0.090 0.073 0.49061+ 179 0.441 0.693 0.492Ce que cela nous apprend. Les clients de 61 ans et plus représentent seulement 179 des 8 235 clients. Mais 69,3 % d’entre eux figurent sur la liste d’appels, contre 7 % à 9 % des groupes d’âge intermédiaires. L’arbre ne pose jamais de question sur l’âge. La raison est la période. La banque a fait 82 % de ses appels aux clients de plus de 60 ans pendant la période de faible emploi. L’arbre attribue un score élevé à cette période, et 75 % de ces clients sont à la retraite. Le script de calcul affiche les deux proportions. Pendant cette période, les clients plus âgés ont effectivement souvent dit oui (44,1 %); la liste est donc efficace.
Une décision humaine reste nécessaire. Une liste qui dirige la plupart des clients âgés et retraités vers un appel de vente demande de la prudence. Le produit doit leur convenir, l’agent ne doit pas les pousser à répondre, et le refus doit être facile. Un dépôt à terme présente peu de risques. Mais le même modèle pourrait classer une liste pour un produit plus risqué. Dans plusieurs pays, les règles de vente de produits financiers demandent aux entreprises de vérifier que le produit convient au client. Elles demandent aussi de traiter les clients vulnérables avec attention. Ce point concerne Inês et l’équipe de conformité de la banque. Ce n’est pas une raison de cacher l’âge dans l’analyse.
Le test final, une seule fois
Section intitulée « Le test final, une seule fois »L’arbre (E07) et la capacité (10 %) sont fixés. Tiago ouvre les données de test une seule fois. Il compare l’arbre à la règle et à un ordre aléatoire sur la même liste :
y_test = test[TARGET].to_numpy()test_scores = tree.predict_proba(test[FEATURES])[:, 1]calls_made = int(round(CAPACITY * len(test)))rule_test = (test["poutcome"] == "success").astype(int)print("calls:", calls_made, "| ROC AUC", round(roc_auc_score(y_test, test_scores), 3), "| AP", round(average_precision_score(y_test, test_scores), 3))for name, rate in [("tree, depth 3", hit_rate(y_test, test_scores)), ("rule", hit_rate(y_test, rule_test)), ("random (expected)", y_test.mean())]: print(f"{name:18s} hit rate {rate:.3f} lift {rate / y_test.mean():.2f} yes in {calls_made} calls: {rate * calls_made:.0f}")La sortie :
calls: 824 | ROC AUC 0.752 | AP 0.365tree, depth 3 hit rate 0.478 lift 4.24 yes in 824 calls: 394rule hit rate 0.263 lift 2.34 yes in 824 calls: 217random (expected) hit rate 0.113 lift 1.00 yes in 824 calls: 93Ce que cela nous apprend. Sur des appels qu’aucune étape n’a utilisés, 824 appels trouvent environ 394 dépôts avec l’arbre. La règle en trouve 217, et un ordre aléatoire, environ 93. Cela représente un gain relatif de 4,24. Le résultat du test est un peu inférieur à celui de la validation (0,478 contre 0,510). C’est attendu, puisque la validation a aidé à choisir. C’est le chiffre à communiquer. Il vaut seulement pour des listes de la même période, comme le montre la prochaine section.
Épilogue : la période suivante
Section intitulée « Épilogue : la période suivante »La séparation aléatoire a mélangé les appels de toutes les périodes. Chaque appel de test avait donc des voisins des mêmes semaines dans les données d’entraînement. Ce ne sera pas le cas pour la prochaine campagne d’Inês. Tiago fait la deuxième vérification prévue au départ. D’abord, comment le fichier change-t-il avec le temps?
tenth = np.arange(len(calls)) * 10 // len(calls) + 1by_tenth = calls.groupby(tenth).agg(yes_rate=(TARGET, "mean"), euribor3m=("euribor3m", "mean"), employed=("nr.employed", "mean"))print(by_tenth.round(3))La sortie :
yes_rate euribor3m employed1 0.028 4.857 5191.0002 0.035 4.859 5195.2703 0.042 4.941 5228.1004 0.066 4.961 5228.1005 0.062 4.965 5228.1006 0.055 4.845 5222.4847 0.102 3.370 5168.9368 0.120 1.354 5099.1009 0.157 1.260 5094.40110 0.460 0.802 5014.834Description textuelle du graphique
Un graphique en courbe avec les dix dixièmes du fichier sur l’axe horizontal et le taux de oui de 0 % à 50 % sur l’axe vertical. Le taux reste faible dans les six premiers dixièmes : 2,8 %, 3,5 %, 4,2 %, 6,6 %, 6,2 %, 5,5 %. Il monte ensuite : 10,2 % au septième, 12,0 % au huitième, 15,7 % au neuvième, puis bondit à 46,0 % au dixième.
Description textuelle du graphique
Un graphique en courbe avec les dix dixièmes du fichier sur l’axe horizontal et le taux Euribor moyen de 0 à 5,5 sur l’axe vertical. Le taux reste près de 4,9 pendant les six premiers dixièmes (4,86, 4,86, 4,94, 4,96, 4,97, 4,85), puis baisse : 3,37 au septième, 1,35 au huitième, 1,26 au neuvième et 0,80 au dixième.
Ce que cela nous apprend. L’économie a changé pendant la période des données, tout comme les réponses. Dans les six premiers dixièmes, moins de 7 appels sur 100 ont réussi; dans le dernier dixième, 46 ont réussi. Les taux d’intérêt ont baissé en même temps. Les cinq colonnes économiques sont des horloges : elles indiquent au modèle de quelle période vient un appel. C’est pourquoi la première question de l’arbre portait sur nr.employed.
Voici maintenant la vérification prévue : entraîner sur les premiers 80 % du fichier (appels anciens), puis attribuer des scores aux derniers 20 % (appels récents).
cut = int(len(calls) * 0.8)early, late = calls.iloc[:cut], calls.iloc[cut:]y_late = late[TARGET].to_numpy()print("yes rate: early", round(early[TARGET].mean(), 3), "| late", round(y_late.mean(), 3))for name, pipe in [("tree, depth 3", build_tree(3)), ("logistic regression", build_logistic())]: s = pipe.fit(early[FEATURES], early[TARGET]).predict_proba(late[FEATURES])[:, 1] print(f"{name:20s} ROC AUC {roc_auc_score(y_late, s):.3f} top 10% {hit_rate(y_late, s):.3f}" f" lift {hit_rate(y_late, s) / y_late.mean():.2f} mean score {s.mean():.3f}")print("late calls with a Euribor rate below anything in early:", f"{(late['euribor3m'] < early['euribor3m'].min()).mean():.1%}")La sortie :
yes rate: early 0.064 | late 0.308tree, depth 3 ROC AUC 0.638 top 10% 0.416 lift 1.35 mean score 0.125logistic regression ROC AUC 0.748 top 10% 0.570 lift 1.85 mean score 0.256late calls with a Euribor rate below anything in early: 98.5%Ce que cela nous apprend. L’arbre entraîné sur le passé classe beaucoup moins bien l’avenir : une aire sous la courbe ROC de 0,638, contre 0,752 au test aléatoire. Son gain relatif tombe à 1,35. Deux choses ont mal tourné en même temps :
- Les entrées sont sorties de la plage connue de l’arbre. Pour 98,5 % des appels récents, le taux Euribor est inférieur à celui de tous les appels anciens. Un arbre peut seulement répondre avec les feuilles qu’il a apprises. Tous ces appels arrivent donc dans quelques feuilles (les appels récents occupent seulement 6 des 8 feuilles), avec les anciens taux de oui. Le score moyen de l’arbre est de 0,125, alors que 30,8 % des appels donnent un oui.
- La relation a changé. Une règle comme « Euribor supérieur à 1,54 » a été apprise quand les taux étaient près de 5 %. Quand tous les taux sont inférieurs à 1,3, la règle ne sépare plus personne.
La régression logistique résiste mieux (aire sous la courbe ROC de 0,748). Elle suit la tendance d’un nombre au-delà de la plage observée. Elle peut donc dire « des taux encore plus bas, encore plus de oui ». Cela a aidé ici, mais ce n’est pas une garantie : une tendance peut aussi s’inverser. Aucun modèle n’a correctement estimé le niveau : les deux sous-estiment le taux de oui récent.
C’est une dérive, et aucun choix de modèle au départ ne l’aurait empêchée. La réponse est celle de Planifier les vérifications en production : surveiller et réentraîner.
- Surveillez les entrées, avant l’arrivée des réponses. Chaque semaine, comparez les colonnes économiques à la plage des données d’entraînement. Une alarme déclenchée quand plus de 5 % de la liste sort de cette plage aurait sonné immédiatement ici.
- Surveillez les résultats, quand les réponses arrivent. Chaque semaine, comparez le taux de réussite des appels faits au taux attendu par l’arbre pour ces feuilles. Un écart de 10 points de pourcentage ou plus pendant deux semaines signifie que le classement est dépassé.
- Réentraînez sur des appels récents, puis choisissez le prochain modèle avec une validation chronologique : entraînez sur les semaines anciennes, validez sur les plus récentes. Cette séparation correspond à la prochaine campagne. Avec cette validation, la comparaison entre l’arbre et la régression logistique pourrait donner un résultat différent. Refaites-la donc.
Recommandations
Section intitulée « Recommandations »Tiago écrit à Inês avec les mots qu’elle utilise :
- Utilisez l’arbre pour classer le reste de la liste de cette campagne. En appelant 10 % de la liste, il trouve environ 48 oui sur 100 appels. La règle « oui précédents en premier » en trouve 26, et un ordre aléatoire, 11. Cela représente environ 394 dépôts au lieu de 217 pour les mêmes 824 appels.
- Donnez les huit règles à l’équipe. Elles montrent qui appeler en premier : les clients déjà contactés, puis les numéros de cellulaire. Elles montrent aussi que la majeure partie de la réussite dépend de la période, et non du client.
- Si la capacité dépasse 20 %, demandez une nouvelle comparaison. Au-delà, l’arbre ne peut pas classer le grand groupe d’appels de la « période normale ».
- Faites attention aux clients plus âgés. Sept clients de plus de 60 ans sur dix se retrouvent sur la liste. Vérifiez que le produit leur convient et que le scénario d’appel leur permet de refuser facilement.
- Avant la prochaine campagne, réentraînez et vérifiez de nouveau. L’arbre a appris l’économie de 2008 à 2010. Surveillez les entrées et le taux de réussite hebdomadaire, comme indiqué plus haut.
Et voici à quoi ne pas l’utiliser :
- Pas pour une nouvelle campagne dans une économie différente, sans réentraînement ni vérification chronologique. L’épilogue montre le gain relatif passer de 4,2 à 1,4.
- Pas pour un autre produit. L’arbre a appris qui achète un dépôt à terme. Une carte de crédit ou un produit d’assurance attire d’autres acheteurs et présente d’autres risques pour le client.
- Pas pour affirmer quelque chose sur un client. « Score faible » signifie « peu de clients semblables ont dit oui pendant cette période », et non « ce client n’achètera jamais ».
Limites et prochaines étapes
Section intitulée « Limites et prochaines étapes »- Une banque, de 2008 à 2010. Les années d’une crise financière, avec des taux d’intérêt inhabituels. Les tendances pourraient ne pas tenir aujourd’hui ou ailleurs.
- La séparation principale mélange les périodes. Le test aléatoire répond à « classer le reste de cette liste ». Seule la vérification chronologique répond à « la prochaine campagne », et elle montre une forte baisse.
campaignest un peu suspecte. Cette variable compte les appels de cette campagne, y compris le dernier. Avant le premier appel, elle est inconnue; l’étude de cas la traite comme « appels jusqu’ici ». Une version plus stricte utiliserait seulement les appels précédents, que ces données ne consignent pas.- Aucun identifiant de client. Un client peut apparaître dans plusieurs campagnes, possiblement dans les données d’entraînement et de test.
- Le jour de l’appel.
month(mois) etday_of_week(jour de la semaine) décrivent quand l’appel a eu lieu. L’équipe connaît le mois d’une campagne avant son début, mais le jour de l’appel est fixé quand l’agent appelle. Lisez donc la séparation selon le lundi de R3 et R4 comme une piste pour planifier les appels. Ce n’est pas un fait sur le client. - Petites feuilles. R8 (35 appels) et R6 (129 appels) reposent sur peu de données.
- Réponses inconnues. Un cinquième des valeurs de
defaultest « unknown » (inconnu). Si la banque peut les compléter, les règles pourraient changer.
Faites-en un projet pour votre portfolio
Section intitulée « Faites-en un projet pour votre portfolio »Cette étude de cas montre trois choses que les employeurs recherchent. Vous avez trouvé une fuite que d’autres ne voient pas. Vous avez choisi un modèle pour la décision, pas pour un classement. Et vous avez testé ce qui se passe avec le temps. Personnalisez le projet avec un changement. Par exemple, utilisez une capacité de 5 %, le découpage temporel comme découpage principal, ou un autre jeu de données marketing ouvert. Puis publiez-le.
Quoi mettre dans le dépôt :
README.md(modèle ci-dessous).- Le carnet (notebook) Jupyter, exécuté du début à la fin, avec ses sorties.
experiment_log.csv, y compris E03 avec sa fuite et la raison de son inclusion.RULES.md: les huit règles sous forme de tableau, comme ci-dessus, avec les nombres. C’est la page qu’une équipe de centre d’appels lirait.MODEL_CARD.md, comme dans Documenter les limites d’utilisation : utilisation prévue (ordonner la liste d’une campagne avec une capacité de 10 %), utilisations exclues (nouvelles périodes sans réentraînement, autres produits), évaluation, résultat de la dérive, signaux de surveillance.- La source et la licence des données, avec le texte d’attribution de l’encadré ci-dessus.
Un modèle de README :
# Ranking a bank's call list under a 10% capacity
## ProblemA call centre can call 10% of a campaign's list. Order the list so those calls findas many term-deposit subscribers as possible.
## Data and licenceUCI "Bank Marketing" (Moro, Rita and Cortez, 2014), bank-additional-full,41,188 calls, Portugal 2008-2010, CC BY 4.0, https://doi.org/10.24432/C5K306.Changes: 12 exact repeats dropped; pdays=999 replaced by contacted_before.
## ApproachRemoved the duration leak (known only after the call). Random stratified split60/20/20, plus a planned time check (first 80% -> last 20%). Decision trees in agrid; chose the simplest within one standard error of the best top-10% hit rate:depth 3, 8 rules. Logistic regression as a check.
## Results (test set, used once)Top-10% hit rate 0.478 (lift 4.24): 394 yes in 824 calls vs 217 for the rule"earlier yes first" and about 93 at random.
## DriftTrained on the earlier 80%, the tree's ROC AUC on the later 20% falls to 0.64(logistic regression 0.75): interest rates left the training range.
## LimitsOne bank, crisis years; random split mixes periods; small leaves; no client ID.Comment le décrire en entrevue, en deux phrases : « J’ai classé la liste d’appels marketing d’une banque avec un arbre de décision à 8 règles, choisi pour une capacité d’appels de 10 %; il a trouvé 4,2 fois plus de souscripteurs par appel que des appels au hasard, et j’ai retiré une fuite liée à la durée d’appel qui donne aux modèles des résultats bien meilleurs que ce qu’ils peuvent réellement produire. Une vérification temporelle a montré que le classement se dégradait quand les taux d’intérêt sortaient de la plage d’entraînement, alors j’ai préparé des alertes sur les plages d’entrée et le taux de réussite hebdomadaire, ainsi qu’un plan de réentraînement. »
Pour aller plus loin :
- Faites de la validation temporelle la validation principale : entraînez sur les premiers 60 %, choisissez sur les 20 % suivants, testez sur les derniers 20 %. La profondeur 3 gagne-t-elle encore?
- Retirez les cinq colonnes économiques. Voyez quelles règles l’arbre trouve avec les seules données clients, et quel gain elles donnent.
- Comparez une forêt aléatoire ou du boosting (renforcement séquentiel) à la même capacité. Décidez si son gain vaut la perte des règles (Quand un modèle simple est meilleur).
- Écrivez la vérification de surveillance hebdomadaire sous forme d’un petit script. Il lit une nouvelle liste et affiche la proportion d’appels hors de la plage d’entraînement.
Vérification des connaissances
Section intitulée « Vérification des connaissances »Vérifiez votre compréhension
Le modèle d'un collègue sur ces données a une aire sous la courbe ROC de 0,94 et utilise la durée d'appel. Pourquoi ce modèle est-il inutile pour ordonner une liste d'appels, même si le calcul du score de test est honnête?
Afficher la réponse
La durée d’un appel n’est connue qu’après l’appel, quand la réponse est aussi connue. Pour ordonner une liste, le modèle doit attribuer un score aux clients avant qu’on les appelle. La colonne est donc vide à ce moment-là. Le score de test est calculé sur des lignes qui contiennent aussi la durée. Le test contient donc la même fuite et ne peut pas la détecter. Sans cette colonne, les arbres atteignent environ 0,76 à 0,80. Pour chaque entrée, la question est : connaîtrais-je cette valeur au moment de décider?
Votre réponse contient-elle les points principaux de la nôtre ?
Connectez-vous et inscrivez-vous au cours pour enregistrer vos réponses. Chaque vérification compte pour le certificat.
Vérifiez votre compréhension
La recherche par grille a trouvé un arbre avec une précision moyenne plus élevée (0,433) que l'arbre de profondeur 3 (0,371). Pourquoi Tiago a-t-il choisi la profondeur 3?
Afficher la réponse
La décision consiste à appeler les premiers 10 % de la liste. La mesure importante est donc le taux de réussite parmi ces premiers 10 %. Sur cette mesure, le meilleur arbre a atteint 0,496, avec une erreur-type d’environ 0,010. La profondeur 3 a atteint 0,487 : à moins d’une erreur-type, donc aussi bonne compte tenu du bruit. La précision moyenne (AP) plus élevée vient d’un meilleur ordre au bas de la liste, où personne n’appelle. La profondeur 3 donne 8 feuilles. L’équipe peut donc les lire comme 8 règles, ce qu’Inês avait demandé.
Votre réponse contient-elle les points principaux de la nôtre ?
Connectez-vous et inscrivez-vous au cours pour enregistrer vos réponses. Chaque vérification compte pour le certificat.
Vérifiez votre compréhension
Entraîné sur les premiers 80 % du fichier, l'arbre obtient une aire sous la courbe ROC de 0,638 sur les derniers 20 %. La régression logistique conserve 0,748. Expliquez la baisse et nommez deux signaux de surveillance qui avertiraient l'équipe.
Afficher la réponse
L’économie a changé. Parmi les appels tardifs, 98,5 % ont un taux Euribor inférieur à tous ceux vus par l’arbre. Le taux de réponses positives est passé de 6,4 % à 30,8 %. Un arbre peut seulement répondre avec les feuilles qu’il a apprises. Les appels tardifs aboutissent donc dans quelques feuilles aux anciens taux (score moyen de 0,125). Des règles comme « Euribor supérieur à 1,54 » ne séparent plus personne. La régression logistique prolonge la tendance d’un nombre au-delà de sa plage, ce qui a aidé ici. Deux signaux sont utiles. Avant tout appel : la proportion d’appels dont les entrées économiques sont hors de la plage d’entraînement. Quand les réponses arrivent : le taux de réussite hebdomadaire des appels effectués, comparé au taux prévu par le modèle.
Votre réponse contient-elle les points principaux de la nôtre ?
Connectez-vous et inscrivez-vous au cours pour enregistrer vos réponses. Chaque vérification compte pour le certificat.
Récapitulatif
- L’exactitude est inutile quand l’équipe appelle 10 % d’une liste : « toujours non » obtient 88,7 %. Le taux de réussite parmi les premiers 10 % et le gain correspondent à la décision.
- La durée d’appel semblait être la meilleure variable, mais c’est une fuite : elle n’est connue qu’après l’appel. L’aire sous la courbe ROC est de 0,944 avec elle, et d’environ 0,76 à 0,80 sans elle.
- Un arbre complet a mémorisé les données d’entraînement (AP de 0,999 contre 0,178 en validation croisée). Avec la règle d’une erreur-type appliquée à la mesure de décision, un arbre de profondeur 3 était aussi bon que le meilleur. Il offrait 8 règles lisibles.
- Sur les données de test, utilisées une seule fois : taux de réussite de 0,478, gain de 4,24, 394 réponses positives sur 824 appels contre 217 pour la règle.
- La première règle de l’arbre concernait l’économie, pas le client. Quand l’économie a changé, l’aire sous la courbe ROC de l’arbre est tombée à 0,638. Surveillez les plages d’entrée et le taux de réussite hebdomadaire, puis réentraînez avec une validation temporelle.
Ensuite, nous comparerons quatre familles de modèles selon un même protocole. La réponse honnête montrera les limites des prédictions possibles avec ces données.
Leçon suivante : Choisir un modèle pour les réadmissions à l'hôpital