Le vendredi où le « 4,2% de rebut » a coûté un avoir client
Vendredi, 16h. Le responsable qualité présente au directeur de site le tableau de bord de la nouvelle ligne d’injection plastique. Au centre, un chiffre que tout le monde regarde : « Taux de rebut prédit lot suivant : 4,2% ». Le modèle XGBoost tourne depuis trois mois, il a été validé en R² 0,86, et il sort un chiffre propre, rassurant, à la décimale.
Le lot part. Rebut réel : 9,7%. 2 300 pièces benne, un avoir client, une réunion de crise le lundi.
Personne n’a menti. Le modèle a fait son travail : il a sorti la meilleure estimation ponctuelle. Le problème, c’est qu’une estimation ponctuelle ne dit rien de sa propre incertitude. 4,2% pouvait tout aussi bien signifier « entre 4% et 4,5% » que « entre 1% et 11% ». Le tableau de bord affichait un point là où la physique du process imposait un intervalle.
C’est exactement le trou que comble la conformal prediction. Au lieu de répondre « 4,2% », elle répond « avec 90% de garantie, le rebut sera entre 3,1% et 10,8% » — et cette garantie de 90% est prouvée mathématiquement, sans supposer que les données suivent une loi normale, sans supposer quoi que ce soit sur le modèle XGBoost lui-même. C’est le pont qui manquait entre la culture statistique Lean Six Sigma (intervalles, capabilité, risque) et le ML moderne (XGBoost, gradient boosting, deep learning).
Cet article explique le mécanisme, donne le code pour conformaliser un XGBoost existant, traite le cas industriel le plus fréquent — l’hétéroscédasticité (l’incertitude qui varie selon le régime) — avec la CQR, et liste honnêtement les limites, notamment ce qui se passe quand l’hypothèse d’échangeabilité casse sous dérive de production.
Le problème : un point n’est pas une décision
En production industrielle, on ne décide jamais sur une moyenne. Un metteur au point qui lit « Cp = 1,33 » sait qu’il manipule une distribution, pas un nombre. Pourtant, quand on greffe du ML sur la ligne, on régresse au point unique :
- « Durée de vie outil prédite : 187 cycles » — mais l’intervalle réel est-il [180, 195] ou [120, 250] ?
- « Effort de coupe estimé : 840 N » — la marge de sécurité broche tient-elle sur la borne haute ou sur l’estimation ?
- « Probabilité de défaut : 0,12 » — calibrée comment ? Sur quel sous-groupe de matière ?
Les approches classiques pour obtenir de l’incertitude ont chacune un défaut rédhibitoire en industrie :
| Méthode | Donne un intervalle ? | Garantie de couverture | Hypothèses |
|---|---|---|---|
| Intervalle gaussien (±1,96σ) | Oui | Seulement si résidus normaux | Forte (normalité, homoscédasticité) |
| Bootstrap | Oui | Asymptotique, souvent sous-couvre | Modérée |
| Bayésien (intervalle crédible) | Oui | Dépend du prior, pas de garantie fréquentiste | Forte (modèle génératif correct) |
| Dropout MC / ensembles deep | Oui | Aucune garantie formelle | Calibration empirique fragile |
| Conformal prediction | Oui | Garantie finie, prouvée | Échangeabilité seulement |
La conformal prediction est la seule de cette liste qui offre une garantie de couverture valable à taille d’échantillon finie, sans rien supposer sur la loi des erreurs ni sur la nature du modèle sous-jacent. C’est un wrapper : il s’enroule autour de votre XGBoost, votre random forest ou votre réseau, déjà entraîné, sans le toucher.
Le mécanisme : split conformal en quatre étapes
L’idée centrale tient en une phrase : on mesure les erreurs du modèle sur un jeu de calibration mis de côté, et on utilise le quantile de ces erreurs comme demi-largeur d’intervalle. Détaillons.
flowchart TD
A[Jeu d'entraînement] --> B[Ajuster le modèle de base\nXGBoost f̂]
C[Jeu de calibration\njamais vu par le modèle] --> D[Scores de non-conformité\nsᵢ = résidu absolu y − f̂x]
B --> D
D --> E[Quantile empirique 1−α\nq̂ = Quantile des scores]
F[Nouveau point x] --> G[Prédiction f̂x ± q̂]
E --> G
G --> H[Intervalle à couverture garantie\nP y ∈ C ≥ 1−α]
Soit un modèle de régression déjà entraîné. On veut un intervalle de couverture (par exemple 90%, donc ).
Étape 1 — Séparer. On découpe les données disponibles (hors entraînement du modèle) en deux : un jeu de calibration de taille , et le futur jeu de test. Le modèle n’a jamais vu le jeu de calibration.
Étape 2 — Scorer la non-conformité. Pour chaque point du jeu de calibration, on calcule un score de non-conformité. Pour la régression, le choix naturel est le résidu absolu :
Ce score mesure « à quel point le modèle s’est trompé » sur ce point. On obtient une distribution empirique de scores.
Étape 3 — Prendre le quantile. On calcule le quantile des scores, à un niveau ajusté pour la taille finie :
Ce petit ajustement (au lieu de simplement ) est ce qui transforme une heuristique en garantie finie. C’est la correction de continuité conformal.
Étape 4 — Construire l’intervalle. Pour un nouveau point , l’intervalle de prédiction est simplement :
Et le théorème central garantit, sous la seule hypothèse d’échangeabilité entre calibration et test :
Pas d’asymptotique, pas de loi normale, pas d’hypothèse sur . Le modèle peut être nul : l’intervalle sera large, mais la garantie de couverture tient. Un mauvais modèle ne casse pas la garantie, il la paie en largeur — propriété précieuse en industrie où l’honnêteté de l’incertitude prime.
Pourquoi ça marche, en une intuition
Si les points de calibration et le point de test sont échangeables, alors le rang du score du point de test parmi les scores de calibration est uniforme. La probabilité que son résidu dépasse le quantile empirique de niveau est donc au plus . La garantie est combinatoire, pas distributionnelle — voilà toute la magie.
Le code : conformaliser un XGBoost existant
Implémentons d’abord le split conformal à la main, pour démystifier (aucune dépendance exotique), puis avec MAPIE pour la version production.
Version from-scratch (pédagogique)
import numpy as np
import xgboost as xgb
from sklearn.model_selection import train_test_split
# ─── 1. Données : on suppose un dataset rebut injection ──────────────────────
# X = features process (température moule, pression, temps cycle, lot matière...)
# y = taux de rebut observé sur le lot (en %)
X = np.load("features_injection.npy")
y = np.load("rebut_pct.npy")
# ─── 2. Triple split : train / calibration / test ────────────────────────────
X_train, X_temp, y_train, y_temp = train_test_split(X, y, test_size=0.40, random_state=42)
X_calib, X_test, y_calib, y_test = train_test_split(X_temp, y_temp, test_size=0.50, random_state=42)
# ─── 3. Entraîner le modèle ponctuel (inchangé par rapport à l'existant) ──────
model = xgb.XGBRegressor(
n_estimators=400, max_depth=4, learning_rate=0.05,
subsample=0.8, colsample_bytree=0.8, random_state=42,
)
model.fit(X_train, y_train)
# ─── 4. Scores de non-conformité sur la CALIBRATION ──────────────────────────
y_calib_pred = model.predict(X_calib)
scores = np.abs(y_calib - y_calib_pred) # résidus absolus
# ─── 5. Quantile conformal (correction taille finie) ─────────────────────────
alpha = 0.10 # couverture cible 90%
n = len(scores)
q_level = np.ceil((n + 1) * (1 - alpha)) / n # niveau ajusté
q_hat = np.quantile(scores, q_level, method="higher")
print(f"Demi-largeur conformal q̂ = {q_hat:.2f} points de rebut")
# ─── 6. Intervalles sur le jeu de test ───────────────────────────────────────
y_test_pred = model.predict(X_test)
lower = y_test_pred - q_hat
upper = y_test_pred + q_hat
# ─── 7. Vérifier la couverture empirique ─────────────────────────────────────
covered = (y_test >= lower) & (y_test <= upper)
print(f"Couverture empirique : {covered.mean():.1%} (cible {1-alpha:.0%})")
print(f"Largeur moyenne d'intervalle : {(upper - lower).mean():.2f} points")
Sur un dataset réaliste, la couverture empirique tombera autour de 89–91% — c’est tout l’intérêt : le chiffre annoncé est tenu. Le lot du vendredi qui sortait « 4,2% » sortirait désormais « 4,2% [intervalle 3,1% – 10,8%] », et la borne haute à 10,8% aurait déclenché la revue avant expédition.
Version production avec MAPIE
En production, on utilise MAPIE (Model Agnostic Prediction Interval Estimator), la référence open-source scikit-learn-contrib. Elle gère le split, la validation croisée (méthode jackknife+ pour exploiter toutes les données), et l’API est un .fit() / .predict() standard.
from mapie.regression import MapieRegressor
from sklearn.model_selection import train_test_split
import xgboost as xgb
import numpy as np
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=42)
base = xgb.XGBRegressor(n_estimators=400, max_depth=4, learning_rate=0.05, random_state=42)
# method="plus" = jackknife+ : pas besoin de réserver un jeu de calib dédié,
# la couverture est garantie en exploitant le leave-one-out conformalisé
mapie = MapieRegressor(estimator=base, method="plus", cv=5, n_jobs=-1)
mapie.fit(X_train, y_train)
# alpha = 0.10 → intervalle 90%
y_pred, y_pis = mapie.predict(X_test, alpha=0.10)
lower, upper = y_pis[:, 0, 0], y_pis[:, 1, 0]
covered = (y_test >= lower) & (y_test <= upper)
print(f"Couverture : {covered.mean():.1%} | largeur moy. : {(upper-lower).mean():.2f}")
method="plus" (jackknife+) est le bon défaut sur dataset moyen : il évite de sacrifier 20% des données en calibration tout en conservant la garantie de couverture. Sur très gros volume, le split simple suffit et coûte moins cher en calcul.
Le vrai cas industriel : l’hétéroscédasticité, et la CQR
Le split conformal de base a un défaut visible en atelier : il produit des intervalles de largeur constante ( partout). Or l’incertitude industrielle n’est jamais constante.
Concrètement : en régime nominal (moule chaud, pression stable), le rebut est prévisible à ±1 point. En régime de démarrage (moule froid, première heure), il est imprévisible à ±6 points. Un intervalle constant de ±4 points sur-couvre en régime stable (intervalles inutilement larges, alarmes perdues) et sous-couvre conditionnellement en démarrage (la garantie marginale tient en moyenne, mais pas là où ça compte).
Visuellement, la différence saute aux yeux : la bande conformal constante est trop large en régime nominal et déborde en démarrage, là où la CQR épouse l’incertitude réelle du process.

La réponse est la CQR — Conformalized Quantile Regression. On entraîne d’abord deux régressions quantiles (XGBoost le fait nativement), une pour le quantile bas et une pour le quantile haut . Ces quantiles modélisent l’incertitude qui varie selon . Puis on conformalise leur écart pour récupérer la garantie de couverture exacte.
import numpy as np
import xgboost as xgb
from sklearn.model_selection import train_test_split
X_train, X_temp, y_train, y_temp = train_test_split(X, y, test_size=0.40, random_state=42)
X_calib, X_test, y_calib, y_test = train_test_split(X_temp, y_temp, test_size=0.50, random_state=42)
alpha = 0.10
lo_q, hi_q = alpha / 2, 1 - alpha / 2 # 0.05 et 0.95
# ─── 1. Deux régressions quantiles XGBoost ───────────────────────────────────
common = dict(n_estimators=400, max_depth=4, learning_rate=0.05, random_state=42)
m_lo = xgb.XGBRegressor(objective="reg:quantileerror", quantile_alpha=lo_q, **common)
m_hi = xgb.XGBRegressor(objective="reg:quantileerror", quantile_alpha=hi_q, **common)
m_lo.fit(X_train, y_train)
m_hi.fit(X_train, y_train)
# ─── 2. Score de non-conformité CQR sur la calibration ───────────────────────
# E_i = max( q_lo(x_i) - y_i , y_i - q_hi(x_i) )
q_lo_cal, q_hi_cal = m_lo.predict(X_calib), m_hi.predict(X_calib)
E = np.maximum(q_lo_cal - y_calib, y_calib - q_hi_cal)
n = len(E)
q_level = np.ceil((n + 1) * (1 - alpha)) / n
q_hat = np.quantile(E, q_level, method="higher")
# ─── 3. Intervalles adaptatifs : on élargit (ou rétrécit) les quantiles ───────
q_lo_test, q_hi_test = m_lo.predict(X_test), m_hi.predict(X_test)
lower = q_lo_test - q_hat
upper = q_hi_test + q_hat
covered = (y_test >= lower) & (y_test <= upper)
print(f"Couverture CQR : {covered.mean():.1%}")
print(f"Largeur moyenne : {(upper - lower).mean():.2f}")
print(f"Largeur min / max : {(upper-lower).min():.2f} / {(upper-lower).max():.2f}")
La différence opérationnelle est nette : largeur min / max ne sera plus 4.0 / 4.0 mais par exemple 1.6 / 9.2. L’intervalle se resserre quand le process est maîtrisé et s’élargit quand il ne l’est pas — exactement le comportement qu’un metteur au point attend d’une mesure d’incertitude honnête. Le score peut être négatif (quand tombe bien à l’intérieur des quantiles), ce qui permet à la CQR de rétrécir des quantiles trop pessimistes, pas seulement de les élargir.
Couverture marginale vs conditionnelle : le piège du « 90% global »
Un point que l’on doit poser franchement, parce qu’il fait dérailler les déploiements. La garantie conformal de base est marginale : 90% de couverture en moyenne sur tout le test. Elle n’interdit pas d’avoir 99% de couverture sur l’acier 304L et 70% sur le 316L, du moment que la moyenne fait 90%.
En industrie, ça ne suffit pas : on veut une garantie par groupe (par machine, par nuance matière, par équipe). C’est le rôle du conformal de Mondrian (aussi appelé group-conditional ou class-conditional). Le principe : on calcule un quantile séparé pour chaque groupe.
import numpy as np
def mondrian_quantiles(scores, groups, alpha=0.10):
"""Un quantile conformal par groupe (Mondrian)."""
q_by_group = {}
for g in np.unique(groups):
s = scores[groups == g]
n = len(s)
lvl = np.ceil((n + 1) * (1 - alpha)) / n
q_by_group[g] = np.quantile(s, min(lvl, 1.0), method="higher")
return q_by_group
# Calibration : scores + groupe matière de chaque point de calib
q_by_mat = mondrian_quantiles(scores_calib, groups=mat_calib, alpha=0.10)
# Prédiction : largeur d'intervalle dépendant du groupe du nouveau point
def predict_interval(x_new, mat_new):
yhat = model.predict(x_new.reshape(1, -1))[0]
q = q_by_mat[mat_new]
return yhat - q, yhat + q
Le coût : chaque groupe doit avoir assez de points de calibration (rule of thumb : au moins , soit ≥10 points pour 90%, idéalement ≥50). Si un groupe est rare, on retombe sur le quantile global pour lui. C’est le bon compromis honnête : garantie conditionnelle là où on a la donnée, garantie marginale ailleurs.
Exemple chiffré complet : tour de contrôle rebut injection
Reprenons la ligne d’injection. Historique : 1 600 lots sur 14 mois, features process (12 variables), label = taux de rebut. On compare trois approches au niveau cible 90% :
| Approche | Couverture empirique | Largeur moyenne | Couverture régime démarrage | Couverture régime nominal |
|---|---|---|---|---|
| Intervalle gaussien ±1,96σ | 81% | ±3,9 pts | 58% ❌ | 94% |
| Split conformal (résidu abs.) | 90% ✅ | ±4,1 pts | 76% | 96% |
| CQR | 90% ✅ | ±2,7 pts (adaptatif) | 89% ✅ | 91% |
| CQR + Mondrian (par nuance) | 90% ✅ | ±2,8 pts | 90% ✅ | 90% ✅ |
Lecture :
L’intervalle gaussien affiche fièrement 81% mais s’effondre à 58% en démarrage — précisément le moment où le risque est maximal. Le split conformal tient la garantie marginale (90%) mais reste large et sous-couvre encore en démarrage. La CQR resserre la largeur moyenne de 34% ET tient la couverture conditionnelle. Le Mondrian par nuance verrouille la garantie groupe par groupe.
Traduction business : sur les 1 600 lots, l’approche gaussienne aurait laissé passer ~42 lots à fort rebut sous une borne haute trop optimiste (les « 4,2% » du vendredi). La CQR+Mondrian en aurait flaggé 38 avant expédition — chacun valant un avoir client évité de 3 000 à 15 000 €.
Concrètement, la décision atelier ne se prend plus sur le point mais sur la borne haute de l’intervalle garanti :
flowchart TD
A[Nouveau lot\nfeatures process] --> B[Prédiction conformal\npoint 4,2% · intervalle 3,1% – 10,8%]
B --> C{Borne haute du rebut\n> seuil métier 6% ?}
C -- Oui --> D[Contrôle renforcé\nlot non expédié en l'état]
D --> E[Tri / re-contrôle\nou ajustement réglages]
C -- Non --> F[GO — expédition]
G[Décision sur point seul\n4,2% < 6%] -. piège .-> F
Le chemin en pointillés rappelle le scénario du vendredi : décider sur le seul point (4,2% < 6%) déclenche un GO trompeur, là où la borne haute à 10,8% impose le contrôle.
Au-delà de la régression : ensembles de prédiction en classification
La conformal prediction n’est pas réservée à la régression. En classification (ex. « ce défaut est-il : rayure / porosité / retassure / OK ? »), elle produit non pas une classe unique mais un ensemble de prédiction garanti :
- Cas facile, modèle confiant → l’ensemble contient une seule classe :
{rayure}. - Cas ambigu → l’ensemble en contient plusieurs :
{porosité, retassure}— signal explicite qu’il faut un contrôle humain.
Le score de non-conformité usuel est (méthode APS/RAPS). La garantie : la vraie classe est dans l’ensemble avec probabilité . C’est un détecteur d’ambiguïté gratuit : la taille de l’ensemble est une mesure d’incertitude exploitable pour router vers l’inspection manuelle. Bien plus honnête qu’un seuil arbitraire sur le softmax.
Limites honnêtes : quand l’échangeabilité casse
Pas de méthode magique. La garantie conformal repose entièrement sur l’échangeabilité entre calibration et déploiement. Trois situations la cassent en industrie :
1. Dérive de distribution (data drift). Vous calibrez sur janvier–septembre, le fournisseur de matière change en octobre. Calibration et production ne sont plus échangeables → la couverture réelle décroche sous 90%. Symptôme : la couverture mesurée en ligne chute. C’est le même piège que pour n’importe quel modèle, mais ici il invalide la garantie, pas seulement la précision.
2. Séries temporelles / autocorrélation. Si vos lots sont autocorrélés (le rebut d’aujourd’hui dépend de celui d’hier), l’échangeabilité est violée par construction. Un split aléatoire « triche » en mélangeant passé et futur.
3. Boucle de rétroaction. Si le modèle modifie le process (on change les réglages à cause de sa prédiction), il casse sa propre hypothèse.
La parade existe et porte un nom : ACI — Adaptive Conformal Inference (Gibbs & Candès). L’idée : ajuster le niveau en ligne, en fonction de la couverture récemment observée. Si on sous-couvre, on élargit ; si on sur-couvre, on resserre. La garantie devient à long terme plutôt que finie, mais elle survit à la dérive.
import numpy as np
def aci_update(alpha_t, covered_last, alpha_target=0.10, gamma=0.02):
"""Ajuste le niveau alpha en ligne (ACI).
covered_last = 1 si le dernier point était couvert, sinon 0."""
err_t = 0 if covered_last else 1
# si on rate (err=1), on diminue alpha_t -> intervalle plus large au pas suivant
alpha_t = alpha_t + gamma * (alpha_target - err_t)
return float(np.clip(alpha_t, 0.001, 0.5))
# Boucle de production (pseudo) :
# alpha_t = 0.10
# for x_new, y_true in stream:
# interval = conformal_predict(x_new, alpha=alpha_t)
# covered = y_true in interval
# alpha_t = aci_update(alpha_t, int(covered))
ACI ne supprime pas le besoin de monitorer la dérive — il rend la couverture robuste à celle-ci, en payant par des intervalles qui s’élargissent automatiquement quand le process devient imprévisible. Pour les séries temporelles industrielles (capteurs, lots séquentiels), c’est la variante à déployer, pas le split conformal naïf.
Checklist de mise en production
Avant de mettre des intervalles conformal sur un tableau de bord opérateur :
- Choisir le bon score. Régression homoscédaste → résidu absolu. Régression hétéroscédaste (le cas normal en industrie) → CQR. Classification → APS/RAPS.
- Réserver un vrai jeu de calibration, jamais vu par le modèle. Ou utiliser jackknife+ (MAPIE
method="plus") sur dataset moyen. - Vérifier la couverture empirique sur un test indépendant — elle doit tomber sur la cible (±2 pts). Si elle dérape, l’échangeabilité est suspecte.
- Tester la couverture conditionnelle par groupe (machine, matière, équipe). Si elle décroche sur un groupe critique → Mondrian.
- Monitorer la couverture en ligne sur fenêtre glissante. Décrochage = dérive → réétalonner ou passer en ACI.
- Séries temporelles → ne jamais splitter aléatoirement ; utiliser ACI ou conformal en blocs temporels.
- Afficher l’intervalle, pas seulement le point, sur l’IHM opérateur — et router vers contrôle humain quand l’intervalle (ou l’ensemble de prédiction) dépasse un seuil métier.
- Documenter la garantie : « couverture marginale 90%, conditionnelle par nuance, valable sous régime stable ; sous dérive, ACI maintient la couverture long-terme. »
Le lien avec la culture Lean Six Sigma
Ce qui rend la conformal prediction adoptable en PME industrielle, c’est qu’elle parle la langue de l’atelier. Un responsable qualité formé au Six Sigma raisonne déjà en intervalles, en risque , en capabilité. La conformal prediction lui rend ce vocabulaire dans le monde du ML :
| Concept Six Sigma | Équivalent conformal |
|---|---|
| Intervalle de tolérance (1-α de la population) | Intervalle de prédiction (couverture 1-α) |
| Risque fournisseur α | Niveau de non-couverture α |
| Sous-groupes rationnels | Conformal de Mondrian (par groupe) |
| Carte de contrôle (dérive) | Monitoring de couverture + ACI |
La conformal prediction n’est pas une rupture avec la statistique industrielle : c’est sa continuation rigoureuse dans l’ère du gradient boosting. Là où le Six Sigma garantissait des intervalles sous hypothèse de normalité, la conformal prediction les garantit sans aucune hypothèse de loi — et s’enroule autour de n’importe quel modèle ML que vous avez déjà déployé.
Conclusion : votre modèle a déjà tout ce qu’il faut
Vous n’avez pas besoin de réentraîner, ni de changer d’algorithme, ni d’adopter une stack bayésienne complexe. Votre XGBoost de prédiction rebut, de durée de vie outil ou d’effort de coupe est déjà là. La conformal prediction lui ajoute, en quelques dizaines de lignes et un jeu de calibration, ce qui lui manquait : une mesure d’incertitude honnête, garantie, et compréhensible par l’atelier.
La trajectoire de maturité est claire :
Point unique → Split conformal → CQR (adaptatif) → Mondrian (par groupe) → ACI (robuste dérive)
Chaque étape est justifiable à son niveau de données et de criticité. L’erreur serait de continuer à afficher des « 4,2% » nus sur un tableau de bord de décision — parce qu’un point sans intervalle, en production, c’est une décision sans gestion du risque.
Si vous avez un modèle en production et un export de quelques centaines de lots labellisés, vous avez de quoi conformaliser et mesurer votre couverture réelle en une après-midi. Le gain — des décisions de tri/expédition prises sur la borne haute plutôt que sur l’espérance — se chiffre, lui aussi, en avoirs clients évités.
BCUB3 accompagne les PME industrielles dans la quantification d’incertitude de leurs modèles ML — du choix du score de non-conformité à l’intégration des intervalles dans le MES. Contactez-nous pour conformaliser vos modèles existants.