LangueFR
Français
English
Retour au sommaire du cours

Décision et action : du score à la décision

Un modèle produit un nombre ; une organisation prend une action. Entre les deux se trouve un espace que les cours de machine learning traversent en une phrase — « on seuille à 0,5 » — et où se perd l'essentiel de la valeur des projets. Ce module traite ce chaînon : coûts d'erreur, contraintes de capacité, effet réel d'une action, et optimisation sous contraintes.

1. Théorie de la décision et seuil optimal

Décider, c'est choisir l'action qui maximise l'utilité espérée. Pour une décision binaire, écrivons le coût attendu de chacune des deux options en fonction de la probabilité \(p\) que l'événement se produise :

le raisonnement, en quatre lignes
si J'AGIS (j'envoie un technicien) :       coût = C_FP × (1-p)     (intervention inutile)
si JE N'AGIS PAS :                          coût = C_FN × p         (panne subie)

agir est préférable quand    C_FP × (1-p)  <  C_FN × p
                    ⟺        C_FP          <  p × (C_FN + C_FP)
                    ⟺        p             >  C_FP / (C_FP + C_FN)
\[ p^\star = \frac{C_{FP}}{C_{FP}+C_{FN}} \]

La formule vaut mieux qu'un long discours : si rater une panne coûte cent fois plus qu'une inspection inutile, le seuil optimal est à 1 % — et non à 50 %. Le seuil par défaut de 0,5 n'est justifié que dans le cas très particulier où les deux erreurs coûtent exactement pareil, ce qui n'arrive presque jamais.

La conséquence pratique : obtenir de l'interlocuteur métier le rapport des coûts est une étape de modélisation à part entière. Elle n'exige pas des chiffres exacts — un ordre de grandeur suffit à déplacer le seuil de façon décisive, et la question « est-ce dix fois pire, ou cent fois pire ? » est presque toujours répondable, là où « combien exactement ? » ne l'est pas.

Python — seuil choisi sur le gain, pas sur le F1
import numpy as np

cout_fp, cout_fn = 50.0, 4_000.0      # inspection inutile / panne subie
p = modele.predict_proba(X_val)[:, 1]

seuils = np.linspace(0.001, 0.5, 500)
cout = [(((p >= s) & (y_val == 0)).sum() * cout_fp +
         ((p <  s) & (y_val == 1)).sum() * cout_fn) for s in seuils]

s_opt = seuils[int(np.argmin(cout))]
print(f"seuil optimal empirique = {s_opt:.3f}")
print(f"seuil théorique         = {cout_fp / (cout_fp + cout_fn):.3f}")
print(f"économie vs seuil 0,5   = {cout[np.argmin(np.abs(seuils - 0.5))] - min(cout):.0f} €")
Décider avec des probabilités exige qu'elles soient calibrées. Toute cette arithmétique suppose que « 0,012 » signifie réellement 1,2 % de chances. Or un gradient boosting ou un réseau de neurones produisent des scores mal calibrés (modules 8 et 14) : le seuil calculé serait alors faux, et d'autant plus faux qu'il est extrême. Calibrer (Platt, isotonique) sur un jeu dédié avant de raisonner en coûts, et vérifier par un diagramme de fiabilité — qui trace la fréquence observée en fonction de la probabilité annoncée, et doit suivre la diagonale.

2. Décider sous contrainte de capacité

Dans la réalité, le seuil n'est souvent même pas le bon objet : une équipe de maintenance peut traiter vingt interventions par semaine, un service client rappeler cent clients par jour. Peu importe alors que trois mille cas dépassent le seuil — on en traitera vingt. La décision devient un problème de sélection sous budget.

Quand chaque action a un coût et un gain variables, c'est un problème de sac à dos : on classe par ratio gain espéré sur coût plutôt que par score brut. Un cas à 80 % de risque coûtant trois jours d'intervention peut passer après deux cas à 50 % coûtant une heure chacun.

Et l'évaluation change de métrique : ni AUC ni F1, mais la courbe de gain cumulé — combien de valeur capturée en traitant les \(k\) premiers — et le lift aux rangs qui correspondent à la capacité réelle.

Python — la seule courbe qui compte sous contrainte
ordre = np.argsort(-p)                       # du plus risqué au moins risqué
gain_cumule = np.cumsum(gain_reel[ordre])    # valeur réellement capturée
cout_cumule = np.cumsum(cout_action[ordre])

capacite = 20                                # ce qu'on peut traiter par semaine
print(f"gain sur les {capacite} premiers : {gain_cumule[capacite-1]:.0f} €")
print(f"lift@{capacite} = "
      f"{gain_cumule[capacite-1] / (capacite * gain_reel.mean()):.1f}× le hasard")

Un modèle avec une AUC inférieure peut très bien être meilleur sur les 2 % de cas que l'on traitera effectivement — et c'est cela qu'il faut optimiser, pas une métrique globale qui pondère également des cas qu'on ne regardera jamais.

3. Uplift : agir sur les bonnes personnes

Voici l'erreur de raisonnement la plus coûteuse du domaine. Un modèle de churn identifie les clients qui vont partir ; on leur envoie une offre de rétention. Mais certains partiront quoi qu'on fasse, d'autres restaient de toute façon, et certains — c'est documenté — partent parce que l'offre leur rappelle qu'ils peuvent partir. Cibler la probabilité d'événement, c'est confondre prédiction et effet d'une action (module 9).

L'uplift (ou effet de traitement individuel) estime la bonne quantité :

\[ \tau(x) = \mathbb{E}\big[Y \mid T=1, X=x\big] - \mathbb{E}\big[Y \mid T=0, X=x\big] \]
les quatre populations
                          Reste si TRAITÉ          Part si TRAITÉ
  Reste si NON traité     SÛRS                     À NE PAS DÉRANGER
                          (gain nul, coût réel)    (l'action NUIT : uplift négatif)
  Part si NON traité      PERSUADABLES             CAUSES PERDUES
                          ← LA SEULE CIBLE UTILE   (gain nul, coût réel)

  Un modèle de churn classique cible "part si non traité", donc il mélange
  les PERSUADABLES et les CAUSES PERDUES — et ignore complètement le fait que
  les "à ne pas déranger" font perdre de l'argent DEUX fois.

Estimer \(\tau\) demande des données expérimentales — un A/B test où une partie de la population n'a pas été traitée (module 9) — ou des hypothèses causales fortes. Les approches usuelles : T-learner (un modèle par bras, différence des prédictions — simple, mais la différence de deux modèles bruités est très bruitée), S-learner (un seul modèle avec le traitement comme variable — risque de le sous-pondérer parmi cent autres), X-learner (meilleur quand les groupes sont déséquilibrés) et les forêts causales (econml, causalml), qui partitionnent l'espace pour maximiser l'hétérogénéité de l'effet plutôt que la pureté de la cible.

Python — estimer et évaluer un uplift
from causalml.inference.tree import UpliftRandomForestClassifier
from causalml.metrics import plot_qini, qini_score

modele = UpliftRandomForestClassifier(control_name="controle", n_estimators=300)
modele.fit(X_train, treatment=t_train, y=y_train)      # t = "traite" / "controle"

uplift = modele.predict(X_test)

# évaluation : on ordonne par uplift décroissant et on vérifie que le gain
# INCRÉMENTAL réel suit. Une courbe plate = le modèle ne trouve rien.
print(qini_score(df_test, outcome_col="y", treatment_col="t"))
plot_qini(df_test, outcome_col="y", treatment_col="t")

Le raisonnement vaut bien au-delà du marketing : traiter préventivement un équipement, envoyer un technicien, accorder une remise, prioriser un contrôle. Dans tous ces cas, la question utile n'est jamais « que va-t-il se passer ? » mais « qu'est-ce que mon action change ? ».

4. Bandits contextuels

Quand plusieurs actions sont possibles et que l'on observe le résultat immédiatement, on peut apprendre en agissant plutôt que de figer un test A/B pendant trois semaines. Un bandit contextuel choisit une action selon le contexte, observe la récompense, et met à jour sa politique — c'est le cas particulier du RL (module 20) sans transition d'état : l'action d'aujourd'hui ne change pas la situation de demain. D'où une méthode bien plus simple, plus stable et plus sûre.

stratégies et positionnement
ε-greedy          trivial à implémenter, exploration non ciblée
LinUCB            borne supérieure de confiance sur un modèle linéaire du contexte :
                  on essaie ce qui est soit bon, soit mal connu (module 19)
Thompson sampling échantillonner dans le postérieur (module 10) : excellent en pratique,
                  naturellement parallélisable, presque aucun réglage
Bandit + coût     intégrer le coût de chaque action dans la récompense

A/B test          quand il faut une MESURE causale propre à communiquer
Bandit            quand il faut MAXIMISER le résultat pendant l'apprentissage
                  (moins de trafic gaspillé sur les mauvaises variantes)

Deux précautions. Un bandit crée des données biaisées par sa propre politique — les bonnes actions sont sur-représentées — ce qui complique toute analyse a posteriori : il faut journaliser les probabilités de sélection pour permettre une évaluation hors ligne non biaisée par pondération inverse (même mécanique qu'au module 23). Et la non-stationnarité doit être traitée explicitement (oubli exponentiel des observations anciennes), sinon le bandit reste bloqué sur une action devenue mauvaise.

5. Décision sous incertitude : le vendeur de journaux

Le problème du newsvendor illustre parfaitement pourquoi une prévision ponctuelle ne suffit pas. On commande une quantité \(q\) avant de connaître la demande \(D\) ; le surstock coûte \(c_o\) par unité, la rupture \(c_u\).

le raisonnement marginal
Commander une unité de plus est rentable tant que :
   (probabilité qu'elle SERVE) × c_u   >   (probabilité qu'elle reste) × c_o
              P(D ≥ q) × c_u           >          P(D < q) × c_o
À l'optimum, égalité :  P(D < q*) = c_u / (c_u + c_o)
\[ q^\star = F_D^{-1}\!\left(\frac{c_u}{c_u + c_o}\right) \]

La quantité optimale n'est donc pas la demande moyenne, mais un quantile de sa distribution. Si une rupture coûte trois fois un surstock, il faut commander le quantile 75 %. C'est ce qui justifie la prévision probabiliste du module 17 : la distribution complète est l'entrée de la décision, la moyenne n'en est qu'un résumé souvent inadapté. Le même raisonnement gouverne le dimensionnement d'une marge de sécurité, d'une réserve d'énergie, d'un effectif de garde ou d'un stock de pièces de rechange.

6. Recherche opérationnelle

Beaucoup de décisions industrielles ne sont pas des prédictions mais des allocations sous contraintes : planifier une production, affecter des équipes, optimiser des tournées, choisir un plan de maintenance. Le ML estime des paramètres (durées, demandes, probabilités de panne) ; la recherche opérationnelle décide. Ignorer cette seconde moitié conduit à livrer des prédictions que personne ne sait transformer en planning.

Python — affectation sous contraintes avec OR-Tools
from ortools.linear_solver import pywraplp

s = pywraplp.Solver.CreateSolver("SCIP")               # solveur MILP

# VARIABLES DE DÉCISION : x[i,j] = 1 si l'équipe i traite l'intervention j
x = {(i, j): s.BoolVar(f"x_{i}_{j}") for i in equipes for j in interventions}

# CONTRAINTES
for j in interventions:                                # chaque intervention couverte
    s.Add(sum(x[i, j] for i in equipes) == 1)          #   exactement une fois
for i in equipes:                                      # capacité hebdomadaire
    s.Add(sum(duree[j] * x[i, j] for j in interventions) <= capacite[i])
for j in urgentes:                                     # les urgences sont prioritaires
    s.Add(sum(x[i, j] for i in equipes_qualifiees) == 1)

# OBJECTIF : maximiser le risque évité, estimé par le modèle de survie (module 17)
s.Maximize(sum(risque_evite[j] * x[i, j] for i in equipes for j in interventions))

statut = s.Solve()
if statut == pywraplp.Solver.OPTIMAL:
    print(f"valeur optimale : {s.Objective().Value():.0f}")
    plan = [(i, j) for (i, j), v in x.items() if v.solution_value() > 0.5]

Les outils : OR-Tools (Google — solveurs LP/MILP, programmation par contraintes, tournées de véhicules), PuLP ou Pyomo pour la modélisation, HiGHS comme solveur libre performant, Gurobi ou CPLEX en commercial.

Ce qu'il faut retenir du fonctionnement : un problème linéaire continu se résout en temps polynomial, mais l'ajout de variables entières le rend NP-difficile — d'où l'importance de la formulation. Une bonne formulation résout en secondes ce qu'une mauvaise ne résout jamais, parce qu'elle donne au solveur des bornes plus serrées pour élaguer son arbre de recherche. Et quand l'optimalité exacte n'est pas nécessaire, les métaheuristiques (recuit simulé, tabou, algorithmes génétiques) donnent de bonnes solutions rapidement.

7. Prédire puis optimiser : le piège

La démarche standard entraîne le modèle à minimiser une erreur de prédiction, puis optimise la décision à partir des prédictions. Le problème : ces deux objectifs ne sont pas alignés.

le même RMSE, deux conséquences opposées
Deux routes, coûts réels : A = 10, B = 12. Le modèle doit choisir la moins chère.

modèle 1 : prédit A = 11, B = 13     erreur = 1 sur chaque       → choisit A ✓ CORRECT
modèle 2 : prédit A = 11, B =  9     erreur = 1 sur chaque       → choisit B ✗ FAUX

Même RMSE, décisions opposées. Le RMSE ne voit pas que la seule chose qui
compte est l'ORDRE des deux prédictions, pas leur exactitude absolue.

L'apprentissage orienté décision (SPO+, optimisation différentiable) intègre le problème d'optimisation dans l'entraînement, de sorte que le modèle apprenne à bien prédire là où cela change la décision. C'est un domaine encore jeune, mais l'idée est immédiatement exploitable même sans ces méthodes : évaluer toute chaîne prédiction-décision sur le résultat final — coût total, risque évité, valeur capturée — et non sur la métrique intermédiaire du modèle. C'est le prolongement exact du principe du module 12 : la métrique doit venir de la décision.

8. Humain dans la boucle et effets de bord

  • Déléguer partiellement — la bonne architecture est souvent à trois zones : décision automatique quand le modèle est confiant et l'enjeu faible, transmission à un humain quand il hésite, procédure de référence quand l'entrée est hors domaine. Les ensembles conformes du module 10 formalisent exactement ce « je ne sais pas trancher » : quand l'ensemble prédit contient plusieurs classes, on transmet.
  • Mesurer le système, pas le modèle — ce qui compte est la performance de l'ensemble humain + modèle. Un modèle un peu moins performant mais dont les erreurs sont détectables par l'opérateur produit un meilleur système qu'un modèle opaque légèrement plus précis.
  • Anticiper les boucles de rétroaction (module 26) — un modèle qui déclenche des inspections ne verra plus jamais les pannes qu'il a évitées : ses futures données d'entraînement sont modifiées par ses propres décisions, et il finit par « apprendre » que les cas qu'il signale ne tombent jamais en panne. Conserver une petite fraction de cas traités aléatoirement — un groupe témoin permanent — est ce qui permet de continuer à mesurer honnêtement.
  • Prévoir le détournement — dès qu'une décision a des conséquences, les personnes concernées s'adaptent à la règle. Un critère devenu cible cesse d'être un bon critère ; les variables les plus faciles à manipuler doivent donc être écartées des modèles qui décident. C'est le même mécanisme que le détournement de récompense en RL (module 20).

Ce qu'il faut retenir du module

  • Le seuil optimal découle du rapport des coûts d'erreur, pas de la convention 0,5 — et suppose des probabilités calibrées.
  • Sous contrainte de capacité, évaluer par courbe de gain cumulé aux rangs réellement traités.
  • Cibler l'uplift, pas la probabilité d'événement : certaines actions nuisent à une partie de la population.
  • Un bandit contextuel apprend en agissant ; journaliser les propensions pour pouvoir évaluer après coup.
  • Une décision sous incertitude se prend sur un quantile, d'où la prévision probabiliste.
  • La moitié des problèmes industriels sont des problèmes d'allocation : la recherche opérationnelle complète le ML.
  • Évaluer la chaîne complète sur la valeur finale, pas le modèle sur sa métrique intermédiaire.

Coûts d'erreurUpliftBanditsNewsvendorMILPOR-Tools