Un vendredi soir à 23h30
Vous êtes de permanence. La ligne de découpe tourne depuis 6 heures. L’équipe de nuit est en place. Un des trois compresseurs de la ligne émet un bruit légèrement différent — mais le technicien n’est pas sûr, et la pression reste dans les clous. Seuil statique : 6 bars, réglé en 2017. Le technicien décide de laisser tourner.
À 2h15, le roulement casse. Remplacement d’urgence, appel d’un prestataire externe, arrêt de 9 heures. Coût direct : 14 000 €. Coût indirect (délais, surcharge des opérateurs, stress) : difficile à chiffrer, mais réel.
Ce scénario, je l’ai entendu en version différente dans presque chaque usine visitée. Parfois c’est un moteur de convoyeur, parfois un palier de broyeur, parfois une pompe hydraulique. Le dénominateur commun : un seuil statique qui ne voit rien venir jusqu’au moment où c’est trop tard.
La maintenance prédictive ne consiste pas à mettre plus de capteurs. Elle consiste à extraire l’information qui était déjà dans les capteurs existants, et que les seuils statiques ne savaient pas lire.
Voilà pourquoi XGBoost, bien configuré, transforme ce problème.
Pourquoi les seuils statiques échouent
Un seuil statique répond à une question binaire : “est-ce que la valeur est au-dessus de X ?” Il ne répond pas à des questions comme :
- La valeur monte-t-elle plus vite que d’habitude ?
- La combinaison température + vibration est-elle anormale, même si chaque signal pris séparément est normal ?
- Ce comportement ressemble-t-il aux 48 heures qui ont précédé la dernière panne ?
Ces questions, XGBoost peut y répondre. Pas parce qu’il est “magique”, mais parce qu’il opère sur des features construites à partir de fenêtres temporelles, pas sur des valeurs instantanées, et parce qu’il peut capturer les interactions entre variables qu’aucun seuil à une seule dimension ne verra jamais.
La comparaison avec les alternatives est utile pour comprendre pourquoi XGBoost s’est imposé comme défaut industriel pour ce type de problème.
| Méthode | Forces | Limites industrielles |
|---|---|---|
| Seuils statiques | Simple, maintenable par tout le monde | Ne voit pas les tendances ni les interactions |
| Règles expertes (if/then) | Interprétable, validable par le métier | Explosion combinatoire, maintenance coûteuse |
| Random Forest | Robuste, peu d’hyperparamètres | Moins performant que XGBoost sur grands volumes |
| XGBoost | Performances top sur tabulaire, feature importance, rapide | Pas de modélisation explicite de la séquence (à compenser par features lag) |
| LSTM / Transformer | Capture les dynamiques séquentielles longues | Volume requis élevé, maintenance lourde, boîte noire |
| Isolation Forest (anomalie) | Pas besoin de labels | Ne prédit pas, détecte — pas de leadtime |
Pour 80 % des problèmes de maintenance prédictive industrielle, XGBoost avec du feature engineering time-series bien fait surpasse un LSTM qui n’a pas assez de données d’historique de pannes. Et les données de pannes, en industrie, sont rares par définition — c’est la nature même du problème.
Le pipeline : du capteur à la prédiction
Avant de rentrer dans le code, voilà l’architecture complète du pipeline. Elle est volontairement simple — l’objectif est d’être déployable dans une ETI avec deux personnes côté IT.
graph TD
A[Capteurs vibration + température] --> B[Collecte IoT - MQTT / OPC-UA]
B --> C[Buffer temps réel - InfluxDB / Redis]
C --> D[Feature Engineering - fenêtres glissantes]
D --> E[Modèle XGBoost ONNX]
E --> F{Score de risque 0-1}
F -->|Score > 0.7| G[Alerte opérateur - dashboard]
F -->|Score < 0.7| H[Monitoring passif - log]
G --> I[Intervention préventive]
H --> J[Réingest dans historique]
J --> K[Réentraînement mensuel]
K --> E
style A fill:#f5a623,color:#000
style E fill:#4a90d9,color:#fff
style G fill:#e74c3c,color:#fff
style I fill:#27ae60,color:#fff
Chaque bloc a des décisions d’implémentation concrètes. Je vais couvrir les deux blocs qui font ou défont un projet : le feature engineering et l’évaluation.
Feature engineering : transformer un signal en information
Un signal brut de capteur vibratoire à 1 kHz, c’est une colonne de nombres. Un modèle ML qui reçoit cette colonne valeur par valeur ne voit rien d’utile — il lui manque le contexte temporel, la forme du signal, ses caractéristiques statistiques.
Le feature engineering construit ces caractéristiques. Pour les vibrations et la thermique industrielle, voici les familles qui comptent.
Features temporelles (domaine temps)
Sur une fenêtre glissante de W secondes (typiquement 10 à 60 secondes selon la fréquence d’échantillonnage) :
import numpy as np
import pandas as pd
from scipy import stats
from scipy.signal import welch
def extract_temporal_features(window: np.ndarray) -> dict:
"""
Extrait les features temporelles classiques sur une fenêtre de signal.
window : array 1D de valeurs de vibration ou température
"""
rms = np.sqrt(np.mean(window**2))
peak = np.max(np.abs(window))
crest_factor = peak / rms if rms > 0 else 0
return {
"rms": rms, # Root Mean Square — énergie globale
"peak": peak, # Amplitude max — utile pour les chocs
"crest_factor": crest_factor, # Rapport peak/RMS — détecte impulsivité
"kurtosis": stats.kurtosis(window), # Impulsivité — sensible aux défauts roulement
"skewness": stats.skew(window), # Asymétrie — drift directionnel
"std": np.std(window), # Écart-type — variabilité globale
"mean_abs": np.mean(np.abs(window)), # MAV — Mean Absolute Value
"shape_factor": rms / np.mean(np.abs(window)) if np.mean(np.abs(window)) > 0 else 0,
}
Kurtosis mérite une attention particulière. En signal vibratoire sain, la kurtosis d’un roulement est typiquement autour de 3 (distribution gaussienne). Quand un défaut de bille ou de cage apparaît, les chocs périodiques génèrent des impulsions qui font monter la kurtosis à 6, 10, parfois 20. La kurtosis est l’un des premiers indicateurs à réagir à un défaut de roulement naissant, bien avant que le RMS ne bouge.
Features fréquentielles (domaine fréquence)
Le domaine temporel ne montre pas tout. Un désalignement d’arbre génère une harmonique à 2× la fréquence de rotation. Un défaut de bague interne génère une harmonique à une fréquence calculable depuis la géométrie du roulement (Ball Pass Frequency Inner race — BPFI). Ces signatures ne sont visibles que dans le spectre fréquentiel.
def extract_spectral_features(window: np.ndarray, fs: float = 1000.0) -> dict:
"""
Extrait les features spectrales par FFT et densité spectrale de puissance.
fs : fréquence d'échantillonnage en Hz
"""
# Densité spectrale de puissance via Welch (moins bruitée que FFT brute)
freqs, psd = welch(window, fs=fs, nperseg=min(256, len(window)))
total_power = np.sum(psd)
# Fréquence centrale de masse spectrale (centroid)
spectral_centroid = np.sum(freqs * psd) / total_power if total_power > 0 else 0
# Répartition de l'énergie par bande (4 bandes, adapter selon application)
band_edges = [0, fs/8, fs/4, fs/2, fs/2]
band_powers = {}
for i in range(len(band_edges)-1):
mask = (freqs >= band_edges[i]) & (freqs < band_edges[i+1])
band_powers[f"band_{i}_power"] = np.sum(psd[mask]) / total_power if total_power > 0 else 0
# Entropie spectrale (signal sain = entropie élevée, défaut = entropie basse)
psd_norm = psd / total_power if total_power > 0 else psd
spectral_entropy = -np.sum(psd_norm * np.log(psd_norm + 1e-12))
return {
"spectral_centroid": spectral_centroid,
"spectral_entropy": spectral_entropy,
"total_power": total_power,
**band_powers
}
Features de trend et lag
XGBoost ne modélise pas la séquence nativement — il traite chaque ligne indépendamment. Pour lui apporter l’information temporelle, on crée explicitement des features de trend et de lag.
def add_trend_features(df: pd.DataFrame, col: str, windows: list = [5, 15, 30]) -> pd.DataFrame:
"""
Ajoute des features de tendance et d'historique pour un signal.
col : nom de la colonne signal (ex: 'rms_vibration')
windows : fenêtres en minutes (adapter à la cadence de collecte)
"""
for w in windows:
# Moyenne mobile
df[f"{col}_ma_{w}m"] = df[col].rolling(window=w, min_periods=1).mean()
# Dérivée (variation sur la fenêtre)
df[f"{col}_delta_{w}m"] = df[col].diff(periods=w)
# Ratio actuel / moyenne historique (drift normalisé)
df[f"{col}_ratio_{w}m"] = df[col] / (df[f"{col}_ma_{w}m"] + 1e-9)
# Lags directs (valeurs brutes à t-1, t-2, t-5)
for lag in [1, 2, 5]:
df[f"{col}_lag_{lag}"] = df[col].shift(lag)
return df
Le modèle XGBoost : configuration production
Voici le pipeline complet d’entraînement, avec les bonnes pratiques pour éviter les pièges classiques.
import xgboost as xgb
from sklearn.model_selection import TimeSeriesSplit
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.metrics import (
precision_score, recall_score, f1_score,
roc_auc_score, confusion_matrix
)
import numpy as np
def train_predictive_maintenance_model(
X: np.ndarray,
y: np.ndarray,
class_weights: dict = None
) -> tuple:
"""
Entraîne un modèle XGBoost pour maintenance prédictive.
X : features (n_samples, n_features)
y : labels binaires (0=sain, 1=pré-défaut)
class_weights : ex. {0: 1, 1: 10} si défauts rares
Retourne: modèle entraîné + métriques de validation
"""
# TimeSeriesSplit OBLIGATOIRE — jamais k-fold standard sur time-series
tscv = TimeSeriesSplit(n_splits=5)
# Gestion du déséquilibre de classes (défauts rares = classe 1 minoritaire)
neg_pos_ratio = np.sum(y == 0) / max(np.sum(y == 1), 1)
model = xgb.XGBClassifier(
n_estimators=1000, # Haut, early stopping décide du vrai nombre
max_depth=6, # Bonne valeur par défaut, tuner en dernier
learning_rate=0.05, # Conservateur pour mieux généraliser
subsample=0.8, # Régularisation par sous-échantillonnage
colsample_bytree=0.8, # Régularisation sur les features
min_child_weight=5, # Nœuds plus robustess sur données bruitées
scale_pos_weight=neg_pos_ratio, # Compensation déséquilibre de classes
eval_metric="aucpr", # AUC precision-recall, meilleure que AUC-ROC sur déséquilibre
early_stopping_rounds=50, # Stop si pas d'amélioration sur 50 rounds
tree_method="hist", # Plus rapide sur grands datasets
random_state=42,
n_jobs=-1,
)
# Validation croisée temporelle
fold_metrics = []
for fold, (train_idx, val_idx) in enumerate(tscv.split(X)):
X_train, X_val = X[train_idx], X[val_idx]
y_train, y_val = y[train_idx], y[val_idx]
# Scaler fit uniquement sur train — jamais sur tout le dataset
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_val_scaled = scaler.transform(X_val)
model.fit(
X_train_scaled, y_train,
eval_set=[(X_val_scaled, y_val)],
verbose=False
)
# Seuil de décision à 0.5 par défaut — à ajuster selon coût métier
y_pred = model.predict(X_val_scaled)
y_proba = model.predict_proba(X_val_scaled)[:, 1]
metrics = {
"fold": fold,
"precision": precision_score(y_val, y_pred, zero_division=0),
"recall": recall_score(y_val, y_pred, zero_division=0),
"f1": f1_score(y_val, y_pred, zero_division=0),
"auc_pr": roc_auc_score(y_val, y_proba) if len(np.unique(y_val)) > 1 else 0,
}
fold_metrics.append(metrics)
print(f"Fold {fold}: Precision={metrics['precision']:.3f}, "
f"Recall={metrics['recall']:.3f}, AUC-PR={metrics['auc_pr']:.3f}")
return model, fold_metrics
Métriques : precision, recall, et le vrai coût d’une erreur
C’est le sujet que les projets ML industriels bâclent le plus souvent.
Le faux négatif (panne non détectée) coûte : arrêt d’urgence, remplacement pièce en urgence, sous-traitant de nuit, possibles dommages secondaires, impact planning production. Coût typique : 5 000 à 50 000 €.
Le faux positif (fausse alarme) coûte : intervention préventive inutile, pièce commandée non nécessaire, temps technicien mobilisé, et surtout — crédibilité du système entamée. Si les opérateurs apprennent à ignorer les alertes parce qu’une sur deux est fausse, le système ne sert plus à rien. Coût typique : 200 à 1 000 €.
Le ratio est clair : sur la maintenance prédictive, rater une panne coûte en moyenne 20 à 50 fois plus cher qu’une fausse alarme. Ce ratio doit se traduire directement dans la configuration du modèle.
def optimize_threshold_by_cost(
y_true: np.ndarray,
y_proba: np.ndarray,
cost_false_negative: float = 20000.0, # Coût panne non détectée (€)
cost_false_positive: float = 500.0, # Coût fausse alarme (€)
) -> float:
"""
Trouve le seuil de décision qui minimise le coût opérationnel total.
Retourne le seuil optimal.
"""
thresholds = np.arange(0.05, 0.95, 0.01)
costs = []
for threshold in thresholds:
y_pred = (y_proba >= threshold).astype(int)
tn, fp, fn, tp = confusion_matrix(y_true, y_pred).ravel()
total_cost = fn * cost_false_negative + fp * cost_false_positive
costs.append(total_cost)
optimal_threshold = thresholds[np.argmin(costs)]
print(f"Seuil optimal par coût métier : {optimal_threshold:.2f}")
print(f"Coût total estimé : {min(costs):.0f} €")
return optimal_threshold
Ce calcul simple — et il faut se battre pour le faire avec les opérationnels — transforme un problème de ML en décision économique. C’est le langage du directeur industriel, pas du data scientist.
Tableau de comparaison métriques selon seuil
| Seuil | Precision | Recall | F1 | Coût total estimé |
|---|---|---|---|---|
| 0.30 (agressif) | 0.45 | 0.92 | 0.61 | 12 000 € |
| 0.50 (défaut) | 0.68 | 0.74 | 0.71 | 19 000 € |
| 0.70 (conservateur) | 0.84 | 0.51 | 0.63 | 31 000 € |
| Seuil optimal (0.38) | 0.52 | 0.88 | 0.65 | 9 500 € |
Le tableau est illustratif mais les ordres de grandeur sont réalistes. Un seuil “conservateur” qui minimise les fausses alarmes maximise en réalité le coût opérationnel réel. La logique counterintuitive qui fait rater des deals quand on ne la montre pas en COPIL.
Feature importance : ce que le modèle a appris
XGBoost donne accès nativement à l’importance des features. C’est l’un des arguments les plus forts pour choisir XGBoost plutôt qu’un réseau de neurones sur ce type de problème.
import matplotlib.pyplot as plt
def plot_feature_importance(model: xgb.XGBClassifier, feature_names: list, top_n: int = 20):
"""
Affiche les N features les plus importantes selon XGBoost.
Utile pour valider la cohérence physique avec les experts métier.
"""
importance = model.feature_importances_
indices = np.argsort(importance)[::-1][:top_n]
fig, ax = plt.subplots(figsize=(10, 6))
ax.barh(
range(top_n),
importance[indices][::-1],
color="#4a90d9"
)
ax.set_yticks(range(top_n))
ax.set_yticklabels([feature_names[i] for i in indices[::-1]])
ax.set_xlabel("Importance (gain)")
ax.set_title("Top features — Modèle maintenance prédictive")
plt.tight_layout()
return fig
Dans chaque projet que j’ai mené, la réunion de présentation de l’importance des features est celle qui engage le plus les experts métier. Quand le modèle montre que kurtosis_vibration_lag_2h et temperature_delta_30m sont les deux features les plus importantes, et que le mécanicien confirme “oui, c’est exactement ça qu’on surveille manuellement mais c’est dur à formaliser” — c’est le moment de buy-in.
Un LSTM black box ne donne pas ça. C’est pourquoi l’interprétabilité de XGBoost est un argument commercial autant que technique.
Déploiement production : sortir du notebook
Le modèle entraîné ne sert à rien dans un notebook. Il doit tourner en production, inférer en temps quasi-réel, et alimenter un dashboard que les opérateurs utilisent vraiment.
Export ONNX
from skl2onnx import convert_sklearn
from skl2onnx.common.data_types import FloatTensorType
import onnx
def export_model_onnx(model, scaler, feature_names: list, output_path: str):
"""
Exporte le pipeline (scaler + modèle) en format ONNX.
ONNX est indépendant de Python — peut être chargé depuis C#, C++, Rust.
"""
from skl2onnx import to_onnx
from sklearn.pipeline import Pipeline
pipeline = Pipeline([("scaler", scaler), ("xgb", model)])
initial_type = [("float_input", FloatTensorType([None, len(feature_names)]))]
onnx_model = to_onnx(pipeline, initial_types=initial_type)
with open(output_path, "wb") as f:
f.write(onnx_model.SerializeToString())
print(f"Modèle exporté : {output_path}")
print(f"Features attendues : {len(feature_names)}")
Inférence temps réel
import onnxruntime as ort
import numpy as np
class PredictiveMaintenanceInference:
"""
Wrapper d'inférence production pour le modèle ONNX.
Conçu pour tourner dans un service Python qui reçoit les features
du pipeline IoT et renvoie un score de risque toutes les N secondes.
"""
def __init__(self, model_path: str, threshold: float = 0.38):
self.session = ort.InferenceSession(model_path)
self.threshold = threshold
self.input_name = self.session.get_inputs()[0].name
def score(self, features: np.ndarray) -> dict:
"""
features : array 2D (1, n_features) — une observation
Retourne: score, alerte, et contexte pour le dashboard
"""
proba = self.session.run(
None, {self.input_name: features.astype(np.float32)}
)[1][0][1] # Probabilité classe 1
return {
"risk_score": float(proba),
"alert": proba >= self.threshold,
"severity": "high" if proba > 0.8 else "medium" if proba > self.threshold else "ok",
"timestamp": pd.Timestamp.now().isoformat(),
}
Monitoring du drift
Un modèle qui fonctionnait bien en janvier peut se dégrader en juin si une machine a été révisée, si la matière première a changé, ou si un capteur a dérivé. Sans monitoring du drift, on ne détecte la dégradation qu’au moment où les opérateurs se plaignent.
from scipy.stats import ks_2samp
def detect_feature_drift(
reference_data: pd.DataFrame, # Données de la période d'entraînement
current_data: pd.DataFrame, # Données des 7 derniers jours
alpha: float = 0.05
) -> dict:
"""
Test KS (Kolmogorov-Smirnov) sur chaque feature.
Détecte si la distribution actuelle s'écarte significativement
de la distribution de référence.
"""
drift_report = {}
for col in reference_data.columns:
if col not in current_data.columns:
continue
statistic, pvalue = ks_2samp(
reference_data[col].dropna(),
current_data[col].dropna()
)
drift_report[col] = {
"ks_statistic": statistic,
"pvalue": pvalue,
"drift_detected": pvalue < alpha
}
drifted = [k for k, v in drift_report.items() if v["drift_detected"]]
if drifted:
print(f"DRIFT détecté sur {len(drifted)} features : {drifted[:5]}...")
return drift_report
Ce test tourne en cron hebdomadaire dans nos déploiements. S’il détecte un drift significatif sur plus de 20 % des features, il déclenche un ticket de réentraînement automatique.
Ce que les systèmes CMMS ne disent pas
La plupart des industriels ont déjà un CMMS (Computerized Maintenance Management System) — SAP PM, INFOR EAM, ou un outil métier. Ces systèmes gèrent les ordres de travail, les stocks de pièces, les plannings de maintenance préventive.
Ce qu’ils ne font pas : prédire. Ils exécutent des gammes de maintenance calées sur un calendrier (maintenance préventive systématique) ou ils réagissent aux pannes (maintenance curative). La maintenance prédictive ajoute un troisième mode : intervenir quand le modèle dit que le risque dépasse un seuil, ni trop tôt (gaspillage), ni trop tard (panne).
L’intégration avec le CMMS est souvent la partie qui prend le plus de temps, non pas techniquement, mais organisationnellement. Il faut décider qui valide une alerte du modèle, quel délai de réponse est requis, comment l’alerte se transforme en ordre de travail. Sans ce processus, le dashboard ne sert à rien — les opérateurs le regardent, haussent les épaules, et continuent à se baser sur le son du compresseur.
Le facteur limitant d’un projet de maintenance prédictive n’est presque jamais le modèle. C’est le processus de décision autour du modèle.
Résultats terrain : ordres de grandeur réalistes
Pour ne pas vendre du rêve, voici des ordres de grandeur honnêtes tirés de cas similaires :
| Indicateur | Avant | Après |
|---|---|---|
| Taux de pannes imprévues | 100 % (base) | -30 à -50 % |
| Délai moyen de détection avant panne | Zéro (réactif) | 8 à 72 heures |
| Fausses alarmes / semaine | N/A | 2 à 5 (acceptable) |
| Coût maintenance / an | Base | -15 à -25 % |
| Disponibilité équipements | Base | +2 à +5 points |
Ces chiffres ne tombent pas dès le mois 1. Les premiers 3 mois sont consacrés à la collecte de données étiquetées, à la validation du pipeline, et à l’adoption par les équipes maintenance. Les gains se matérialisent sur 6 à 18 mois.
Pour aller plus loin
XGBoost pour la maintenance prédictive n’est pas la solution miracle. C’est un outil dans un pipeline qui inclut la qualité des données capteurs, le feature engineering adapté à la physique de votre machine, une évaluation par le coût métier plutôt que par l’accuracy, et un processus opérationnel pour transformer les alertes en actions.
Le modèle, s’il est bien construit, vous dira que quelque chose change dans la signature vibratoire de votre compresseur 12 à 48 heures avant que le technicien l’entende. C’est ça, le vrai ROI. Pas un notebook avec deux courbes bleues qui se superposent.
Pour les bases statistiques et le framework général d’un projet ML industriel, voir l’article Machine Learning pour l’industrie : du signal au modèle en production. Pour comprendre comment instrumenter les capteurs et les connecter à votre pipeline, l’article Edge devices et capteurs industriels couvre les protocoles de collecte et les architectures IoT terrain.