Vue d’ensemble : MLOps moderne : du modèle entraîné à la production scalable
Pourquoi un modèle « parfait » se dégrade en silence
Un modèle de scoring de crédit fonctionne à 87 % d’AUC pendant trois mois. Au quatrième mois, le taux de défaut réel grimpe de 4,1 % à 6,8 % sans que personne ne s’en aperçoive. La cause : la distribution du revenu des nouveaux demandeurs a glissé, et le modèle, entraîné sur l’ancienne population, surnote les profils risqués. Ce tutoriel montre comment instrumenter ce type de pipeline avec Evidently AI 0.7 — calcul du data drift, du concept drift et de la dérive de la cible, génération d’un rapport HTML, export des métriques vers Prometheus et déclenchement d’alertes Slack via Alertmanager. À la fin, vous saurez détecter la dérive avant que le métier ne la voie.
Un modèle de machine learning apprend une relation entre des features d’entrée et une cible de sortie sur un dataset de référence, généralement constitué à un instant t. En production, deux choses bougent en permanence : les données d’entrée (les utilisateurs changent, les saisons défilent, une promotion ou un événement métier modifie le comportement d’achat) et la relation elle-même entre ces données et la cible (un nouveau concurrent fait baisser le pouvoir d’achat, une réforme fiscale décale les revenus déclarés). Sans instrumentation, vous ne voyez la dégradation qu’au moment où le KPI métier explose, c’est-à-dire trop tard.
La détection de dérive consiste à comparer, à intervalles réguliers, la distribution des données récentes de production à la distribution du dataset d’entraînement. Si l’écart dépasse un seuil, on lève une alerte avant que la performance ne soit affectée. Evidently AI, projet open source maintenu activement, automatise ce calcul : il choisit le bon test statistique selon le type de colonne, agrège les résultats et produit un rapport HTML interactif. La version 0.7.x stabilise une API simplifiée qui remplace l’ancienne hiérarchie Dashboard / ColumnMapping des versions 0.3.
Data drift, concept drift, prediction drift : trois choses différentes
Les trois termes circulent souvent comme synonymes, mais désignent des phénomènes distincts qu’il faut traiter séparément.
Data drift (ou covariate shift) : la distribution des features d’entrée change entre la référence et la production. Exemple : la proportion de demandeurs de moins de 25 ans passe de 18 % à 34 %. C’est le drift le plus facile à mesurer car il ne demande pas la cible réelle, seulement les inputs récents. Evidently teste chaque colonne individuellement avec un test statistique adapté à son type.
Concept drift : la relation entre les features et la cible change, même si les distributions d’entrée restent identiques. Exemple : un revenu mensuel donné prédisait un faible risque de défaut deux ans auparavant, mais après une hausse du coût du loyer urbain, le même revenu prédit désormais un risque élevé. Le concept drift demande la cible réelle, donc un retour de ground truth qui peut prendre des semaines.
Prediction drift (ou target drift sur la sortie) : la distribution des prédictions du modèle change. Cela peut être un effet du data drift (les inputs ont changé, donc les outputs aussi) ou un signal d’alerte indépendant. Surveiller le prediction drift permet de réagir dès le jour zéro, sans attendre la ground truth.
En pratique, on instrumente les trois, avec des seuils et des fréquences différentes. Le data drift et le prediction drift tournent en quasi-temps réel, le concept drift hebdomadairement ou mensuellement quand la ground truth arrive.
Prérequis
Python 3.10 minimum (3.11 ou 3.12 recommandés), Evidently 0.7.x, pandas et scikit-learn pour préparer les datasets. Un environnement virtuel propre évite les conflits avec d’anciennes versions d’Evidently dont l’API est incompatible. Pour la partie monitoring, vous aurez besoin d’un Prometheus accessible en push (via Pushgateway) ou en scrape (via un endpoint /metrics exposé par votre service), et d’un Alertmanager configuré avec un webhook Slack ou Mattermost.
Étape 1 — Installer Evidently et préparer l’environnement
On crée un venv dédié pour isoler la dépendance. Evidently embarque ses propres versions de pandas, numpy, scikit-learn et un moteur de templating HTML pour les rapports, ce qui peut entrer en conflit avec un projet existant.
python -m venv .venv-drift
source .venv-drift/bin/activate # sous Windows : .venv-drift\Scripts\activate
pip install --upgrade pip
pip install "evidently>=0.7,<0.8" pandas scikit-learn prometheus_client
Vérifiez la version installée pour vous assurer que l’API du tutoriel correspond.
python -c "import evidently; print(evidently.__version__)"
La sortie doit afficher quelque chose comme 0.7.21. Si vous voyez 0.4.x ou 0.3.x, vous êtes sur l’ancienne API et les imports evidently.report et evidently.metric_preset du présent guide ne fonctionneront pas — désinstallez et réinstallez la version pinned ci-dessus.
Étape 2 — Préparer un dataset de référence et un dataset courant
Le calcul de dérive a besoin de deux DataFrames pandas : la référence (typiquement votre dataset d’entraînement ou une fenêtre de production réputée saine) et le courant (la production récente que vous voulez auditer). Pour l’exemple, on charge le dataset Adult Income de scikit-learn et on simule un drift en biaisant artificiellement la partie « courante » : on filtre pour ne garder que les profils à hauts revenus, ce qui décale plusieurs features d’un coup.
import pandas as pd
from sklearn.datasets import fetch_openml
adult = fetch_openml(name="adult", version=2, as_frame=True)
df = adult.frame.dropna()
# Référence : un échantillon "sain" de 5000 lignes
reference = df.sample(n=5000, random_state=42)
# Courant : on biaise pour créer une dérive visible
current = df[df["age"] > 45].sample(n=2000, random_state=7)
print(reference.shape, current.shape)
print("Âge moyen reference :", reference["age"].mean())
print("Âge moyen current :", current["age"].mean())
La sortie attendue montre une différence nette d’âge moyen, par exemple 38.6 côté référence contre 55.2 côté courant. C’est ce signal qu’Evidently va quantifier statistiquement plutôt que de le laisser à l’œil humain.
Étape 3 — Lancer un rapport Report avec le preset Data Drift
L’API 0.7 d’Evidently expose un objet Report qui prend en argument une liste de métriques ou de presets. Un preset est un groupe de métriques cohérentes — DataDriftPreset couvre la dérive de toutes les colonnes du DataFrame en choisissant automatiquement le bon test statistique pour chacune. On appelle ensuite .run(current, reference) (attention à l’ordre : courant d’abord, référence ensuite).
from evidently import Report
from evidently.presets import DataDriftPreset
report = Report([DataDriftPreset()], include_tests=True)
snapshot = report.run(current_data=current, reference_data=reference)
snapshot.save_html("drift_report.html")
Le paramètre include_tests=True ajoute automatiquement des conditions pass/fail au rapport — utile pour la suite avec la CI/CD. Le fichier drift_report.html est un rapport autonome que vous pouvez ouvrir dans n’importe quel navigateur. Le signal de réussite : à l’ouverture, vous voyez en tête un bloc « Dataset Drift » avec un statut Detected/Not Detected, suivi d’un tableau colonne par colonne.
Étape 4 — Lire le rapport HTML (drift_share et drift par feature)
Le rapport se lit en trois niveaux. Au sommet, l’indicateur global drift_share : la proportion de colonnes pour lesquelles le test a détecté une dérive significative. Par défaut, Evidently déclare le dataset comme « driftant » si plus de 50 % des colonnes dérivent. Sur notre exemple biaisé, attendez-vous à un drift_share autour de 0,7 à 0,9 selon la composition du dataset.
Au niveau intermédiaire, un tableau liste chaque colonne avec : le type détecté (numérique, catégoriel), le test utilisé, la valeur de la statistique (ou p-value selon le test), et le verdict drift / no drift. Pour les colonnes numériques avec plus de mille observations, Evidently utilise par défaut la distance de Wasserstein normalisée ; pour les catégorielles, le test du Chi² ou la distance de Jensen-Shannon selon la cardinalité. En dessous de mille observations, le Kolmogorov-Smirnov prend le relais sur les numériques.
Au niveau détaillé, en cliquant sur une colonne, vous obtenez les histogrammes superposés des deux distributions. C’est cette vue qu’il faut envoyer à un data scientist pour décider si la dérive est un vrai signal ou un artefact d’échantillonnage.
Pour automatiser la lecture, on n’ouvre pas le HTML mais on extrait le dictionnaire de résultats.
result = snapshot.dict()
# Le résultat est une structure imbriquée ; on cherche le bloc DataDriftPreset
for metric in result["metrics"]:
if "DriftedColumnsCount" in metric.get("metric_id", ""):
print("Colonnes en dérive :", metric["value"])
if "DataDriftTable" in metric.get("metric_id", ""):
for col, info in metric["value"].get("drift_by_columns", {}).items():
if info.get("drift_detected"):
print(f" - {col} ({info.get('stattest_name')}) : score={info.get('drift_score'):.3f}")
La sortie attendue liste les colonnes touchées avec leur score. C’est ce dictionnaire qui alimentera ensuite les métriques Prometheus.
Étape 5 — Configurer des seuils personnalisés
Les seuils par défaut conviennent à un POC mais pas à un système en production où le coût d’un faux positif (réveiller l’équipe data à 3 h du matin) doit être pondéré contre le coût d’un faux négatif (laisser un modèle dériver une semaine). Deux leviers se règlent indépendamment : le seuil par colonne (à partir de quelle valeur de statistique on déclare une colonne en dérive) et le seuil global drift_share (quelle proportion de colonnes en dérive fait basculer tout le dataset).
from evidently.presets import DataDriftPreset
preset = DataDriftPreset(
method="psi", # impose le Population Stability Index pour toutes les colonnes
threshold=0.2, # seuil PSI : > 0.2 = drift significatif (convention métier)
drift_share=0.3, # le dataset est en dérive si > 30 % des colonnes dérivent
)
report = Report([preset], include_tests=True)
snapshot = report.run(current_data=current, reference_data=reference)
Pourquoi PSI plutôt que Wasserstein ? Le PSI est la convention historique de l’industrie financière (scoring crédit, risk management). Ses paliers sont mémorisables : < 0,1 stable, 0,1 à 0,2 attention, > 0,2 dérive significative. Le Wasserstein, lui, n’a pas de palier universel et demande un benchmark interne. Choisissez la métrique qui matche les habitudes de votre équipe risque ou produit. Pour des colonnes spécifiques avec un comportement particulier (par exemple une feature très asymétrique), on peut surcharger le test colonne par colonne avec per_column_method={"feature_x": "jensenshannon"}.
Étape 6 — Mesurer la dérive de la cible (régression ou classification)
Le data drift ne dit rien sur la qualité du modèle, seulement sur l’évolution des entrées. Pour suivre la cible — soit la cible réelle observée a posteriori, soit la distribution des prédictions — on combine deux presets supplémentaires. Le code ci-dessous suppose un cas de classification binaire avec une colonne prediction et une colonne target.
from evidently import Report, DataDefinition
from evidently.presets import DataDriftPreset, ClassificationPreset
# Déclare explicitement les rôles des colonnes
schema = DataDefinition(
classification=[{"target": "income", "prediction_labels": "prediction"}]
)
report = Report(
metrics=[
DataDriftPreset(columns=["age", "hours-per-week", "education-num"]),
ClassificationPreset(),
],
include_tests=True,
)
snapshot = report.run(
current_data=current,
reference_data=reference,
data_definition=schema,
)
snapshot.save_html("drift_and_target.html")
Pour de la régression, remplacez ClassificationPreset par RegressionPreset et adaptez le schéma avec regression=[{"target": "...", "prediction": "..."}]. Le rapport généré contient alors trois blocs : data drift sur les features sélectionnées, distribution de la cible, et métriques de performance (accuracy / RMSE selon le cas) calculées sur le current et le reference, ce qui révèle immédiatement une chute de performance.
Étape 7 — Intégrer les tests à un pipeline CI/CD
Avec include_tests=True, le rapport contient des assertions pass/fail. On peut récupérer le statut global et en faire un code de sortie de script — idéal pour bloquer un déploiement de nouveau modèle si les données de validation diffèrent trop du training set.
import sys
result = snapshot.dict()
tests = result.get("tests", [])
failed = [t for t in tests if t.get("status") == "FAIL"]
for t in failed:
print(f"FAIL : {t.get('name')} — {t.get('description')}")
if failed:
print(f"\n{len(failed)} test(s) en échec, déploiement bloqué.")
sys.exit(1)
print("Tous les tests passent, déploiement autorisé.")
Branché dans une GitHub Action ou un job GitLab CI au stage validate, ce script échoue avec un code 1 dès qu’un seuil est franchi, et le pipeline s’arrête avant la promotion en staging. Le rapport HTML peut être uploadé comme artefact pour que l’équipe consulte la cause exacte de l’échec.
Étape 8 — Exposer les métriques vers Prometheus
Evidently ne pousse pas nativement vers Prometheus, mais le dictionnaire de résultats expose toutes les valeurs scalaires dont on a besoin. La méthode propre est d’exécuter le calcul de dérive dans un job périodique (cron Kubernetes, Airflow, Prefect) et de pousser les métriques vers une Pushgateway Prometheus, ou d’exposer un endpoint /metrics scrapé par Prometheus si le service tourne en permanence.
from prometheus_client import CollectorRegistry, Gauge, push_to_gateway
registry = CollectorRegistry()
drift_share_gauge = Gauge(
"ml_drift_share",
"Proportion de colonnes en dérive sur le dataset courant",
["model", "env"],
registry=registry,
)
drifted_count_gauge = Gauge(
"ml_drifted_columns_count",
"Nombre absolu de colonnes en dérive",
["model", "env"],
registry=registry,
)
result = snapshot.dict()
for metric in result["metrics"]:
mid = metric.get("metric_id", "")
if "DriftedColumnsCount" in mid:
drifted_count_gauge.labels(model="credit-scoring", env="prod").set(
metric["value"].get("count", 0)
)
drift_share_gauge.labels(model="credit-scoring", env="prod").set(
metric["value"].get("share", 0.0)
)
push_to_gateway("pushgateway.monitoring.svc:9091", job="evidently-drift", registry=registry)
print("Métriques poussées vers Prometheus.")
Vous pouvez ajouter une gauge par colonne surveillée pour observer quelle feature dérive et de combien. En pratique, deux ou trois métriques clés (drift_share global, count de colonnes critiques, valeur PSI sur la feature business la plus sensible) suffisent à déclencher les bonnes alertes sans saturer Prometheus de séries temporelles peu utiles.
Côté infra, la Pushgateway expose ces gauges sur son endpoint, et votre Prometheus la scrape comme une cible normale. Évitez la Pushgateway pour des métriques à très haute fréquence : elle est conçue pour des jobs batch courts. Pour un drift calculé toutes les 15 minutes, c’est parfait.
Étape 9 — Brancher Alertmanager et Slack
Une fois les gauges dans Prometheus, on définit des règles d’alerte PromQL. La règle ci-dessous lève une alerte si le drift_share dépasse 30 % pendant plus de 30 minutes consécutives — la fenêtre temporelle évite les faux positifs liés à un batch atypique.
# prometheus-rules.yaml
groups:
- name: ml-drift
interval: 1m
rules:
- alert: HighDataDrift
expr: ml_drift_share{env="prod"} > 0.3
for: 30m
labels:
severity: warning
team: ml-platform
annotations:
summary: "Drift élevé détecté sur {{ $labels.model }}"
description: "{{ $value | humanizePercentage }} des colonnes dérivent sur les 30 dernières minutes."
- alert: CriticalDataDrift
expr: ml_drift_share{env="prod"} > 0.6
for: 10m
labels:
severity: critical
team: ml-platform
annotations:
summary: "Drift CRITIQUE sur {{ $labels.model }} — re-training à envisager"
Côté Alertmanager, on route la sévérité critical vers un webhook Slack et la sévérité warning vers un canal d’information moins bruyant. La configuration minimale en YAML.
# alertmanager.yaml
route:
receiver: slack-warnings
group_by: [alertname, model]
routes:
- match:
severity: critical
receiver: slack-critical
continue: true
receivers:
- name: slack-warnings
slack_configs:
- api_url: https://hooks.slack.com/services/T0000/B0000/XXXXXXXX
channel: "#ml-monitoring"
send_resolved: true
- name: slack-critical
slack_configs:
- api_url: https://hooks.slack.com/services/T0000/B0001/YYYYYYYY
channel: "#ml-incidents"
send_resolved: true
title: "DRIFT CRITIQUE : {{ .CommonLabels.model }}"
Le signal de réussite : au prochain calcul de drift qui dépasse les seuils, un message structuré arrive dans Slack avec le nom du modèle, la valeur courante et un lien vers le rapport HTML hébergé. L’équipe peut alors décider en quelques minutes si la dérive est légitime (changement métier connu) ou si elle déclenche un re-training.
Étape 10 — Workflow complet de réaction
Détecter ne suffit pas : il faut décider quoi faire. Trois niveaux de réaction se mettent en place selon l’amplitude et la nature du drift.
Niveau 1 — Information silencieuse. Le drift_share dépasse 10 % mais reste sous 30 %. On loggue la métrique, on l’affiche sur un dashboard Grafana, mais on n’alerte personne. Cette catégorie sert de baseline pour observer la variation naturelle du système.
Niveau 2 — Alerte humaine. Le drift_share dépasse 30 % ou une feature critique (revenu, âge, montant de transaction) dérive isolément avec un score PSI > 0,25. Un message Slack arrive, le data scientist d’astreinte ouvre le rapport HTML, identifie la cause et décide. Cause externe connue (campagne marketing, événement saisonnier) : on accepte et on met à jour la référence. Cause inconnue : on lance un re-training en mode investigation.
Niveau 3 — Re-training automatique. Le drift_share dépasse 60 % et la performance mesurée sur la ground truth récente (si disponible) chute de plus de 5 points par rapport au baseline. Le pipeline déclenche automatiquement un job de re-training avec les données des trente derniers jours, l’enregistre comme nouvelle version candidate dans le registre de modèles, et lance la batterie de tests qualité avant promotion. L’automatisation totale n’est rentable que si vous avez une ground truth rapide (clic, achat, retour utilisateur en moins de 24 h) ; pour un scoring de défaut bancaire dont la cible met six mois à se révéler, restez au niveau 2.
La référence elle-même doit être maintenue : tous les six mois, ou après tout re-training majeur, on remplace le dataset de référence par une fenêtre récente réputée saine. Une référence figée comparée à une production deux ans plus tard produit du bruit permanent sans valeur d’alerte.
Erreurs fréquentes
| Symptôme | Cause probable | Correctif |
|---|---|---|
ImportError sur evidently.report ou evidently.metric_preset |
Vous utilisez la doc ancienne (≤ 0.6.7) avec une version 0.7+ | Remplacer par from evidently import Report et from evidently.presets import ... |
| Drift détecté sur toutes les colonnes dès le premier run | Datasets trop petits, p-value des tests dégradée | Augmenter à ≥ 1000 lignes par dataset, ou forcer method="psi" qui est plus stable |
| Aucun drift détecté alors que la production a clairement changé | Reference trop large couvrant déjà la variation observée | Recalibrer la reference sur une fenêtre étroite (1 à 3 mois) représentative |
| Rapport HTML lent à ouvrir (> 30 s) | Trop de colonnes (200+) avec histogrammes complets | Sélectionner les colonnes critiques via DataDriftPreset(columns=[...]) |
| Pushgateway n’expose pas la métrique | Job nommé identiquement écrase les précédentes valeurs | Ajouter un label instance unique au push pour conserver l’historique court terme |
| Alertes Slack en boucle toutes les minutes | for: 0m ou absent dans la règle Prometheus |
Toujours mettre un for: 10m ou plus pour stabiliser |
| Concept drift toujours « pas de données » | Pas de ground truth dans le dataset courant | Implémenter un système de capture du label réel (clic, validation, retour métier) avant de surveiller le concept drift |
Ressources officielles
- Documentation Evidently AI — référence complète de l’API 0.7
- Documentation DataDriftPreset — paramètres, méthodes supportées, seuils par défaut
- Dépôt GitHub Evidently — exemples notebook, changelog, issues
- Guide Prometheus Pushgateway — bonnes pratiques pour les jobs batch
- Documentation Alertmanager — routage et déduplication des alertes
Tutoriels associés
- MLOps moderne en production scalable — vue d’ensemble du cycle complet de mise en production
- Suivre ses expériences avec MLflow — versionner modèles, datasets et métriques d’entraînement
- Servir un modèle ML en production avec BentoML — packager et déployer l’API d’inférence
- Orchestrer un workflow ML avec Kubeflow — automatiser l’enchaînement training-validation-déploiement
- Mettre en place un feature store avec Feast — garantir la cohérence des features entre training et inférence
- MLflow Model Registry et CI/CD pour orchestrer les promotions
FAQ
Quelle fréquence de calcul de drift en production ? Toutes les 15 à 60 minutes pour le data drift et le prediction drift sur un volume de prédictions modéré (quelques milliers par heure). En dessous de mille lignes par fenêtre, les tests statistiques manquent de puissance — agrégez sur une fenêtre plus longue (4 h, 24 h) plutôt que de calculer du bruit. Pour le concept drift, cadence hebdomadaire ou mensuelle alignée sur la disponibilité de la ground truth.
Faut-il toujours utiliser DataDriftPreset, ou choisir les métriques une à une ? Le preset suffit pour 80 % des cas et reste maintenable. Choisir métrique par métrique devient pertinent quand vous avez plus de 50 features et que vous voulez ignorer celles qui dérivent naturellement (timestamp, ID utilisateur) ou imposer un test précis sur une feature business critique. Dans ce cas, importez ValueDrift ou DriftedColumnsCount depuis evidently.metrics et composez la liste à la main.
Le PSI est-il toujours le bon choix ? Le PSI est solide sur des distributions continues bien remplies et reste interprétable par des non-data-scientists. Il devient instable sur des distributions très asymétriques (queues longues, valeurs rares) où Wasserstein normalisé ou Jensen-Shannon donnent un signal plus fiable. Pour des features catégorielles à très haute cardinalité (> 50 modalités), la distance de Jensen-Shannon est généralement préférable au Chi².
Comment éviter qu’une saison ou un événement métier connu déclenche des alertes ? Deux approches. La plus simple : élargir la reference pour qu’elle couvre déjà la variation saisonnière (référence d’un an glissant plutôt qu’un trimestre). La plus rigoureuse : maintenir plusieurs références (jour de semaine, mois) et calculer le drift contre la reference appariée. Cette seconde approche demande plus d’orchestration mais évite les faux positifs systématiques.
Peut-on utiliser Evidently sur des données non tabulaires (images, texte) ? Oui partiellement. Evidently 0.7 supporte les embeddings drift pour des données texte ou image vectorisées — vous calculez l’embedding en amont (via un modèle sentence-transformers ou un CLIP), passez la matrice résultante à Evidently et utilisez EmbeddingsDriftMetric. Pour les sorties brutes de LLM (texte généré), la bibliothèque expose désormais des métriques de qualité LLM qui sortent du cadre de la dérive classique et entrent dans le domaine de l’observabilité AI générative.
Comment archiver l’historique des rapports plutôt que de les écraser à chaque run ? Evidently 0.7 propose un Workspace local : vous créez un projet, et chaque report.run() produit un snapshot JSON ajouté au workspace via project.add_run(). Vous lancez ensuite l’UI Evidently en mode self-hosted qui agrège tous les snapshots dans des dashboards historiques. C’est la voie native si vous ne voulez pas dépendre de Prometheus/Grafana pour la consultation.