Il est 7h45. Le responsable planning ouvre son ERP, regarde le Gantt, et sait déjà que ça ne collera pas. L’OF 2341 est censé passer en fraisage pendant 2h30. Mais c’est Rémi qui est là aujourd’hui — pas Marc — et sur le centre d’usinage numéro 3 qui sort d’une maintenance. En réalité, cette opération va durer au moins 3h15. Il ajuste à la main. Comme chaque matin. Comme depuis 12 ans.
Ce pattern se répète dans 90% des ateliers français. Les temps gamme de l’ERP sont des moyennes historiques calculées lors de la mise en production, il y a 5, 8, parfois 15 ans. Ils ne reflètent pas la réalité de 2026 : turnover des opérateurs, vieillissement des machines, variabilité des lots, complexité croissante des pièces. Résultat : des plannings qui décrochent du réel dès 10h du matin, des OTD qui stagnent entre 70 et 80%, et un responsable planning qui passe ses journées à corriger des erreurs systémiques avec son bon sens personnel.
La bonne nouvelle : ce problème est parfaitement résolu par du Machine Learning. Pas de la deep learning ésotérique — du XGBoost, un modèle de gradient boosting robuste, interprétable, et qui tourne en quelques secondes. Voici comment l’implémenter concrètement.
Le vrai problème : l’écart temps gamme / temps réel
Avant de coder quoi que ce soit, il faut mesurer l’ampleur du problème. Dans les projets industriels sur lesquels on intervient, les écarts temps gamme / temps réel observés se situent typiquement dans cette fourchette :
| Type d’opération | Écart moyen | Écart max observé |
|---|---|---|
| Usinage CNC | +22% | +85% |
| Assemblage manuel | +31% | +120% |
| Contrôle qualité | +18% | +60% |
| Soudure / chaudronnerie | +27% | +95% |
Ces écarts ne sont pas du hasard. Ils suivent des patterns identifiables : certains opérateurs sont systématiquement plus lents sur certaines références, certaines machines accumulent des micro-arrêts après 14h, les lundis matin voient des coefficients de régime différents des jeudis après-midi. Ce sont exactement ces patterns qu’un modèle supervisé peut apprendre.
Le temps gamme ERP est une valeur nominale figée. XGBoost apprend le multiplicateur dynamique qui transforme ce nominal en durée prédite selon le contexte opérationnel réel.
Architecture de la solution
Le principe est simple : on ne remplace pas l’ERP ni la GPAO. On les augmente. XGBoost se glisse entre le MES (source de vérité terrain) et l’ordonnanceur, en produisant des durées prédites qui alimentent le calcul de charge.
flowchart LR
A[ERP / GPAO\nTemps gamme théoriques\nOF + nomenclature] --> B[Feature Engineering\nOpérateur × Machine\nRéférence encodée\nJour, setup, lot]
C[MES\nPointages historiques\nTemps réels mesurés\n3-5 ans d'historique] --> B
B --> D[XGBoost Regressor\nEntraîné sur historique MES\nMise à jour mensuelle]
D --> E[Durées prédites\npar opération / contexte]
E --> F[Ordonnanceur\nGantt réaliste\nCharge / capacité]
F --> G[Planning terrain\nOTD amélioré\nTRS optimisé]
style D fill:#ff6b35,color:#fff
style G fill:#2d6a4f,color:#fff
Feature engineering : ce qui compte vraiment
La qualité du modèle dépend à 80% des features. Voici les features qui comptent, et pourquoi.
Features primaires (disponibles dans l’ERP)
ref_produit_encoded: encodage de la référence article. Pas un one-hot naïf (trop de cardinalité) — utiliser un target encoding ou un embedding de 8 dimensions.temps_gamme_nominal: le temps ERP lui-même. C’est une feature, pas la target. Le modèle apprend l’écart par rapport au nominal.quantite_lot: un lot de 500 pièces n’a pas le même overhead qu’un lot de 10.num_operation: la 3ème opération d’une gamme a souvent un profil différent de la 1ère.
Features terrain (MES + RH)
operateur_id: encodage de l’opérateur. Feature critique. Les écarts inter-opérateurs atteignent régulièrement 30-40%.machine_id: l’âge de la machine, ses dérives récentes, son dernier arrêt — tout ça se traduit en variance implicite.jour_semaine: [0-6]. Les patterns lundi/vendredi sont réels et mesurables.heure_debut: matin vs après-midi. La productivité n’est pas uniforme.setup_precedent: type d’OF précédent sur la même machine. Capture les temps de changement de série.jours_depuis_maintenance: si disponible dans la GMAO.
Interactions importantes
L’interaction opérateur × machine est souvent la feature la plus prédictive. XGBoost capture ces interactions naturellement via ses arbres, mais les rendre explicites en feature (operateur_machine_combo) améliore souvent le RMSE de 8-12%.
Implémentation Python
import pandas as pd
import numpy as np
from xgboost import XGBRegressor
from sklearn.model_selection import TimeSeriesSplit
from sklearn.metrics import mean_absolute_error, mean_absolute_percentage_error
from sklearn.preprocessing import LabelEncoder
import joblib
# ─── 1. Chargement des données MES ───
df = pd.read_csv("mes_pointages_historique.csv", parse_dates=["ts_debut", "ts_fin"])
# Calcul de la durée réelle (target)
df["duree_reelle_min"] = (df["ts_fin"] - df["ts_debut"]).dt.total_seconds() / 60
# Filtrage des outliers évidents (pointages oubliés)
q_low = df["duree_reelle_min"].quantile(0.02)
q_high = df["duree_reelle_min"].quantile(0.98)
df = df[(df["duree_reelle_min"] >= q_low) & (df["duree_reelle_min"] <= q_high)]
# ─── 2. Feature engineering ───
df["jour_semaine"] = df["ts_debut"].dt.dayofweek
df["heure_debut"] = df["ts_debut"].dt.hour
df["mois"] = df["ts_debut"].dt.month
# Target encoding pour les références produit
ref_mean = df.groupby("ref_produit")["duree_reelle_min"].mean()
df["ref_target_enc"] = df["ref_produit"].map(ref_mean)
# Interaction opérateur × machine
df["op_machine_combo"] = df["operateur_id"].astype(str) + "_" + df["machine_id"].astype(str)
le_op = LabelEncoder()
le_machine = LabelEncoder()
le_combo = LabelEncoder()
df["operateur_enc"] = le_op.fit_transform(df["operateur_id"].astype(str))
df["machine_enc"] = le_machine.fit_transform(df["machine_id"].astype(str))
df["combo_enc"] = le_combo.fit_transform(df["op_machine_combo"])
# Ratio historique par opération
gamme_ratio = df.groupby("code_operation").apply(
lambda x: (x["duree_reelle_min"] / x["temps_gamme_min"]).mean()
).rename("ratio_historique_op")
df = df.join(gamme_ratio, on="code_operation")
FEATURES = [
"temps_gamme_min", "quantite_lot", "num_operation",
"operateur_enc", "machine_enc", "combo_enc",
"ref_target_enc", "jour_semaine", "heure_debut",
"mois", "ratio_historique_op",
]
TARGET = "duree_reelle_min"
# ─── 3. Split temporel (IMPORTANT : pas de random split sur des TS) ───
df = df.sort_values("ts_debut")
tscv = TimeSeriesSplit(n_splits=5)
# ─── 4. Entraînement XGBoost ───
model = XGBRegressor(
n_estimators=500,
learning_rate=0.05,
max_depth=6,
subsample=0.8,
colsample_bytree=0.8,
min_child_weight=5,
reg_alpha=0.1,
reg_lambda=1.0,
objective="reg:squarederror",
n_jobs=-1,
random_state=42,
early_stopping_rounds=30,
)
X = df[FEATURES]
y = df[TARGET]
maes, mapes = [], []
for train_idx, val_idx in tscv.split(X):
X_train, X_val = X.iloc[train_idx], X.iloc[val_idx]
y_train, y_val = y.iloc[train_idx], y.iloc[val_idx]
model.fit(X_train, y_train, eval_set=[(X_val, y_val)], verbose=False)
preds = model.predict(X_val)
maes.append(mean_absolute_error(y_val, preds))
mapes.append(mean_absolute_percentage_error(y_val, preds))
print(f"MAE moyen : {np.mean(maes):.1f} min")
print(f"MAPE moyen : {np.mean(mapes)*100:.1f}%")
# Entraînement final
model.fit(X, y, verbose=False)
joblib.dump(model, "xgboost_durees_operations.pkl")
# ─── 5. Prédiction pour un nouvel OF ───
def predict_duree(of_data: dict, model, encoders: dict) -> float:
ts = pd.Timestamp(of_data["ts_debut_prevu"])
combo = f"{of_data['operateur_id']}_{of_data['machine_id']}"
row = {
"temps_gamme_min": of_data["temps_gamme_min"],
"quantite_lot": of_data["quantite_lot"],
"num_operation": of_data["num_operation"],
"operateur_enc": encoders["le_op"].transform([str(of_data["operateur_id"])])[0],
"machine_enc": encoders["le_machine"].transform([str(of_data["machine_id"])])[0],
"combo_enc": encoders["le_combo"].transform([combo])[0],
"ref_target_enc": encoders["ref_mean"].get(of_data["ref_produit"],
encoders["ref_mean"].mean()),
"jour_semaine": ts.dayofweek,
"heure_debut": ts.hour,
"mois": ts.month,
"ratio_historique_op": encoders["gamme_ratio"].get(of_data["code_operation"], 1.0),
}
X_pred = pd.DataFrame([row])
return float(model.predict(X_pred)[0])
Intégration avec la GPAO / MES existant
La solution ne remplace rien. Elle s’interface.
Option 1 — Middleware API : un microservice FastAPI expose /predict_duree. L’ERP l’appelle au moment du calcul de charge.
Option 2 — Export CSV batch : un script tourne chaque nuit, génère durees_predites_demain.csv, l’ERP l’importe.
Option 3 — Connecteur MES natif : trigger SQL sur l’insertion d’un nouvel OF.
XGBoost est un oracle consulté, pas un remplaçant de l’ERP. Les temps gamme restent la source de vérité contractuelle (devis, prix de revient). Les durées prédites servent exclusivement à l’ordonnancement opérationnel.
Impact mesurable sur les KPIs
| KPI | Avant | Après 6 mois | Delta |
|---|---|---|---|
| OTD (On-Time Delivery) | 74% | 86% | +12 pts |
| Écart planning/réel journalier | 23% | 9% | -14 pts |
| Temps ajustement manuel planning | 45 min/jour | 12 min/jour | -73% |
| TRS (indicateur indirect) | 68% | 71% | +3 pts |
| Taux de rush livraison express | 18% | 7% | -11 pts |
Le gain OTD est le plus spectaculaire parce qu’il est cumulatif : un planning réaliste en début de journée évite l’effet boule de neige.
Section honnête : ce qui ne marche pas
La qualité des données MES est souvent catastrophique
C’est le vrai problème n°1. Les pointages MES sont erronés dans 15 à 40% des cas. Causes : pointages groupés en fin de poste, pointages oubliés rattrapés le lendemain, temps d’arrêt machine inclus dans le temps opération, pauses incluses ou exclues selon l’opérateur.
Sans nettoyage sérieux des données, XGBoost apprend les erreurs de pointage. La phase de data quality prend 2 à 4 semaines.
Les opérateurs nouveaux et les références rares
Pour un opérateur < 2 mois d’historique ou une référence < 20 opérations, le modèle se rabat sur les moyennes de la famille produit. Mieux que rien, mais loin d’être précis.
La résistance culturelle
Le responsable planning qui ajuste à la main depuis 12 ans ne va pas abandonner son expertise du jour au lendemain. Stratégie : afficher la durée prédite à côté du temps gamme pendant 3 mois. Quand il constate que le modèle a raison 80% du temps, l’adoption se fait naturellement.
XGBoost ne résout pas le problème de données sales. Il amplifie la qualité de l’historique MES — en bien si les données sont propres, en biais si elles ne le sont pas.
Prochaines étapes
-
Prédiction des pannes et micro-arrêts : enrichir avec données GMAO pour passer d’une durée prédite à une distribution de durées (quantiles 50/80/95).
-
Optimisation de séquence : avec des durées réalistes, attaquer le Job Shop Scheduling (OR-Tools, algorithmes génétiques).
-
Boucle fermée MES → modèle → ERP : automatiser le cycle complet. Le responsable planning redevient un superviseur qui valide des exceptions.
Vous avez un MES qui collecte des pointages mais un planning qui décroche régulièrement ? Contactez BCUB3 pour un diagnostic data de votre situation.