← Blog

XGBoost pour la maintenance prédictive industrielle

Comment XGBoost surpasse les seuils statiques pour prédire les défaillances machines à partir de signaux vibratoires et thermiques.

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éthodeForcesLimites industrielles
Seuils statiquesSimple, maintenable par tout le mondeNe voit pas les tendances ni les interactions
Règles expertes (if/then)Interprétable, validable par le métierExplosion combinatoire, maintenance coûteuse
Random ForestRobuste, peu d’hyperparamètresMoins performant que XGBoost sur grands volumes
XGBoostPerformances top sur tabulaire, feature importance, rapidePas de modélisation explicite de la séquence (à compenser par features lag)
LSTM / TransformerCapture les dynamiques séquentielles longuesVolume requis élevé, maintenance lourde, boîte noire
Isolation Forest (anomalie)Pas besoin de labelsNe 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

SeuilPrecisionRecallF1Coût total estimé
0.30 (agressif)0.450.920.6112 000 €
0.50 (défaut)0.680.740.7119 000 €
0.70 (conservateur)0.840.510.6331 000 €
Seuil optimal (0.38)0.520.880.659 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 :

IndicateurAvantAprès
Taux de pannes imprévues100 % (base)-30 à -50 %
Délai moyen de détection avant panneZéro (réactif)8 à 72 heures
Fausses alarmes / semaineN/A2 à 5 (acceptable)
Coût maintenance / anBase-15 à -25 %
Disponibilité équipementsBase+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.

Cet article vous a été utile ?

BCUB3 est une petite structure. Si vous pensez à un collègue ou un partenaire qui pourrait en tirer quelque chose, la meilleure manière de nous aider est de partager le lien. Et si vous avez un cas concret à discuter, parlons-en directement.

Prendre un RDV de cadrage