تشخیص Data Drift و مانیتورینگ مدل یادگیری ماشین با Evidently AI در پایتون (۲۰۲۶)
راهنمای عملی تشخیص data drift و مانیتورینگ مدل یادگیری ماشین در پروداکشن با Evidently AI: از Report و TestSuite تا ادغام با MLflow، Airflow و Prometheus/Grafana.
Data drift زمانی رخ میدهد که توزیع داده ورودی مدل یادگیری ماشین در پروداکشن با توزیع داده آموزشی متفاوت شود؛ و Evidently AI کتابخانهای متنباز در پایتون است که با اجرای بیش از ۱۰۰ معیار آماری روی داده مرجع و داده جاری، این تغییرات را قبل از اینکه به افت کیفیت پیشبینی منجر شوند، شناسایی میکند. راستش، من در پنج سال گذشته چندین بار مدلی را دیدهام که در آفلاین AUC = ۰.۹۲ داشته اما بعد از دو هفته در پروداکشن به ۰.۷۶ رسیده، بدون کوچکترین هشداری. Evidently این شکاف را با گزارشهای قابل انتشار در Prometheus/Grafana و ادغام با MLflow پر میکند. در این راهنما یک pipeline کامل مانیتورینگ میسازیم.
Evidently AI با نسخه 0.4.x در سال ۲۰۲۶ از دو رابط اصلی Report و TestSuite برای تشخیص drift و ارزیابی کیفیت مدل استفاده میکند.
سه نوع drift را باید همزمان پایش کرد: feature drift (تغییر توزیع ورودی)، prediction drift (تغییر خروجی مدل) و concept drift (تغییر رابطه ورودی–برچسب).
برای ویژگیهای عددی از تست Kolmogorov–Smirnov و Wasserstein distance، برای دستهای از chi-squared یا Jensen–Shannon و برای مانیتورینگ پایدار از PSI (Population Stability Index) استفاده کنید.
خروجی Evidently قابل ذخیره در MLflow، ارسال به Prometheus برای هشدار و ادغام با Airflow/Prefect برای اجرای زمانبندیشده است.
مانیتورینگ باید بودجه تأخیر (latency budget) کمتری از خود مدل داشته باشد؛ گزارش drift را async و در پسزمینه اجرا کنید، نه در مسیر inference.
برای مدلهای سنگین با میلیونها feature از نمونهگیری (sampling ۱۰–۲۰٪) استفاده کنید تا هزینه گزارشگیری کنترل شود.
Data drift چیست و چرا مدل شما را میکشد؟
Data drift به تغییر پایدار توزیع آماری ویژگیهای ورودی مدل در طول زمان گفته میشود. اگر مدل شما را روی داده ژانویه ۲۰۲۴ آموزش دادهاید، رفتار کاربر در سپتامبر ۲۰۲۶ لزوماً همان الگو را ندارد: میانگین سبد خرید بالاتر رفته، الگوی جغرافیایی عوض شده، یا یک feature ورودی به دلیل تغییر فرم ثبتنام اصلاً مقدار جدیدی دریافت میکند. مدل چون بازآموزی نشده، همچنان پیشبینی میکند، اما با کیفیتی رو به کاهش.
در یک سیستم پیشنهاد محصول با ۱۲ میلیون درخواست روزانه، خودم شاهد بودم که فقط با تغییر یک feature (نسخه اپلیکیشن موبایل بعد از آپدیت) نرخ کلیک از ۴.۸٪ به ۳.۱٪ افت کرد. مدل هیچ هشداری نداد، AUC آفلاین بیمعنا بود، و تا وقتی که تیم بیزنس گزارش نداد کسی متوجه نشد. اگر Evidently را روی جریان daily prediction snapshot اجرا میکردیم، در روز سوم DataDriftPreset با شاخص PSI = ۰.۳۴ روی app_version هشدار میداد.
در ادبیات یادگیری در جریانهای داده، این پدیده «covariate shift» نامیده میشود و از قدیمیترین مشکلات ML در تولید است. اما تا قبل از Evidently و رقبایش مثل NannyML و WhyLabs، بیشتر تیمها آن را با اسکریپتهای SQL دستساز پایش میکردند، راهحلی که در مقیاس واقعاً جواب نمیدهد.
تفاوت data drift و concept drift و prediction drift
خب، این سه پدیده متفاوت هستند و اشتباه گرفتنشان به تصمیمگیری غلط منجر میشود:
نوع Drift
چه چیزی تغییر میکند؟
نشانه
واکنش صحیح
Feature Drift (Covariate Shift)
توزیع ورودیهاP(X)
PSI بالا، KS معنادار روی ویژگیها
بازآموزی، اصلاح feature، تغییر منبع داده
Prediction Drift
توزیع خروجی مدل P(ŷ)
میانگین یا هیستوگرام پیشبینی جابجا شده
بررسی ورودی؛ گاهی بازآموزی
Concept Drift
رابطه P(y|X) بین ورودی و برچسب
افت متریک کیفیت (AUC, RMSE) با ورودی مشابه
بازآموزی الزامی، احتمالاً بازتعریف مسئله
Label Drift
توزیع P(y) در داده جدید
class imbalance تازه
ریبالانس، تنظیم آستانه
یک نکته مهم که خیلی وقتها فراموش میشود: feature drift همیشه به افت کیفیت نمیانجامد. اگر توزیع یک ستون کماهمیت عوض شود، مدل بیتفاوت است. اما concept drift همیشه بحرانی است چون معنی داده عوض شده. Evidently با گزارشهای مختلف برای هرکدام از اینها کار میکند: DataDriftPreset، TargetDriftPreset، ClassificationPreset و RegressionPreset.
نصب Evidently AI و آمادهسازی محیط
Evidently با نسخه 0.4.x در سپتامبر ۲۰۲۶ روی پایتون ۳.۹ به بالا کار میکند. ترجیح شخصی من این است که آن را در یک virtual environment جدا از سرویس inference نگه دارم، چون وابستگیهای سنگین (pandas، scipy، plotly) دارد و نمیخواهم به latency سرویس اصلی اضافه شود.
برای تست سریع از دیتاست کلاسیک Adult Income استفاده میکنیم. آن را به دو بخش «مرجع» (reference) و «جاری» (current) تقسیم میکنیم و در بخش جاری عمداً drift ایجاد میکنیم تا مکانیزم را ببینیم:
import pandas as pd
import numpy as np
from sklearn.datasets import fetch_openml
adult = fetch_openml("adult", version=2, as_frame=True).frame
adult = adult.dropna().reset_index(drop=True)
reference = adult.iloc[:15000].copy()
current = adult.iloc[15000:].copy()
# Injecting synthetic drift: age distribution shifts +5 years,
# education-num compressed, workclass changes proportion
current["age"] = current["age"] + np.random.normal(5, 2, len(current))
current["education-num"] = current["education-num"].clip(upper=12)
current.loc[current["workclass"] == "Private", "workclass"] = np.where(
np.random.rand(len(current[current["workclass"] == "Private"])) < 0.3,
"Self-emp-not-inc", "Private"
)
print(reference.shape, current.shape)
حالا داده مرجع (توزیع «سالم» که مدل روی آن آموزش دیده) و جاری (توزیع «مشکوک» که در پروداکشن میرسد) در اختیار داریم.
ساخت اولین گزارش drift با Report API
کلاس Report در Evidently یک container برای مجموعهای از metricها است. برای data drift از DataDriftPreset استفاده میکنیم که خودش انتخاب تست آماری مناسب هر ستون را انجام میدهد:
from evidently import ColumnMapping
from evidently.report import Report
from evidently.metric_preset import DataDriftPreset, TargetDriftPreset
column_mapping = ColumnMapping(
target="class",
numerical_features=["age", "education-num", "hours-per-week",
"capital-gain", "capital-loss"],
categorical_features=["workclass", "education", "marital-status",
"occupation", "relationship", "race", "sex",
"native-country"],
)
report = Report(metrics=[
DataDriftPreset(drift_share=0.4), # alert if >40% features drift
TargetDriftPreset(),
])
report.run(reference_data=reference, current_data=current,
column_mapping=column_mapping)
report.save_html("drift_report.html")
result = report.as_dict()
print("Dataset drift detected:",
result["metrics"][0]["result"]["dataset_drift"])
print("Number of drifted features:",
result["metrics"][0]["result"]["number_of_drifted_columns"])
خروجی HTML یک داشبورد تعاملی است که هیستوگرام مرجع/جاری، مقادیر p-value و PSI هر ستون را نشان میدهد. برای اتوماسیون، as_dict() ساختار JSON برمیگرداند که در Airflow یا Prefect میتوانید بررسی کنید و در صورت drift هشدار بفرستید.
اعتبارسنجی خودکار با TestSuite برای CI/CD
در حالی که Report برای تحلیل انسانی مناسب است، TestSuite برای اجرای خودکار در pipelineها ساخته شده. هر تست خروجی pass/fail میدهد و کد خروجی صریح دارد که در GitHub Actions یا Jenkins قابل استفاده است.
from evidently.test_suite import TestSuite
from evidently.tests import (
TestNumberOfDriftedColumns,
TestShareOfDriftedColumns,
TestColumnDrift,
TestNumberOfMissingValues,
)
suite = TestSuite(tests=[
TestShareOfDriftedColumns(lt=0.3), # <30% drifted OK
TestColumnDrift(column_name="age", stattest="ks", stattest_threshold=0.05),
TestColumnDrift(column_name="workclass", stattest="chisquare"),
TestNumberOfMissingValues(lte=100),
])
suite.run(reference_data=reference, current_data=current,
column_mapping=column_mapping)
results = suite.as_dict()
failed = [t for t in results["tests"] if t["status"] == "FAIL"]
if failed:
print(f"⚠️ {len(failed)} tests failed, blocking deployment")
for t in failed:
print(f" - {t['name']}: {t['description']}")
exit(1)
این الگو در MLOps استاندارد است: قبل از deploy مدل جدید یا قبل از انتشار predictionهای روزانه، suite اجرا میشود. اگر داده ورودی درست نباشد، pipeline متوقف میشود و بهجای انتشار پیشبینی اشتباه، به تیم on-call اطلاع داده میشود. این همان «shift-left» برای مانیتورینگ داده است.
معیارهای آماری: KS، PSI، Wasserstein و JS
انتخاب تست آماری درست حتی از خود Evidently هم مهمتر است. Evidently بهطور پیشفرض این قاعده را اعمال میکند:
عددی، حجم کوچک (<۱۰۰۰ نمونه): Kolmogorov–Smirnov test با آستانه p-value = ۰.۰۵.
عددی، حجم متوسط: Wasserstein distance که پایدارتر از KS است.
دستهای: chi-squared یا Jensen–Shannon divergence.
مانیتورینگ بلندمدت: PSI با آستانههای معروف صنعت ریسک اعتباری: PSI < 0.1 بدون drift، 0.1 ≤ PSI < 0.25 drift متوسط، PSI ≥ 0.25 drift شدید.
در پروداکشن، شخصاً KS را برای هشدار سریع و PSI را برای dashboard بلندمدت با هم ترکیب میکنم. KS به تغییرات کوچک اما آماری معنادار حساس است (که در سطح میلیون سطر میتواند بیربط باشد). PSI محافظهکارتر است و فقط وقتی هشدار میدهد که تغییر واقعاً بزرگ باشد.
Pipeline مانیتورینگ در پروداکشن با Airflow و MLflow
یک الگوی رایج که در چند پروژه پیاده کردهام:
هر ۲۴ ساعت Airflow یک DAG اجرا میکند که current_data را از data warehouse (BigQuery یا Snowflake) میکشد.
reference_data یک snapshot ثابت است (معمولاً همان ۳۰ روز آخر دوره آموزشی) که در S3/GCS ذخیره شده.
Evidently اجرا میشود، JSON خروجی به MLflow بهعنوان artifact آپلود میشود و متریکهای کلیدی بهعنوان MLflow metric ثبت میشوند.
در صورت fail شدن TestSuite، PagerDuty فراخوانی میشود.
import mlflow
from datetime import datetime
def daily_drift_check(ref_df, cur_df, mapping):
with mlflow.start_run(run_name=f"drift-{datetime.utcnow():%Y%m%d}"):
report = Report(metrics=[DataDriftPreset()])
report.run(reference_data=ref_df, current_data=cur_df,
column_mapping=mapping)
d = report.as_dict()["metrics"][0]["result"]
mlflow.log_metric("dataset_drift", int(d["dataset_drift"]))
mlflow.log_metric("share_drifted", d["share_of_drifted_columns"])
mlflow.log_metric("n_drifted", d["number_of_drifted_columns"])
report.save_html("/tmp/drift.html")
mlflow.log_artifact("/tmp/drift.html")
if d["share_of_drifted_columns"] > 0.3:
raise ValueError("Drift threshold exceeded, alerting on-call")
این با یک PythonOperator در Airflow اجرا میشود و اگر exception بندازد، Airflow خودش alert میسازد. مزیت لاگ در MLflow این است که میتوانید timeline drift را در UI ببینید و بفهمید کدام روز drift شروع شد.
ارسال متریک به Prometheus و ساخت داشبورد Grafana
برای تیمهای SRE، Prometheus زبان مشترک است. Evidently خروجی مستقیم به Prometheus ندارد اما با prometheus_client ساده میشود:
from prometheus_client import Gauge, CollectorRegistry, push_to_gateway
registry = CollectorRegistry()
g_drift = Gauge("ml_dataset_drift_share",
"Share of features with detected drift",
["model", "environment"], registry=registry)
g_cnt = Gauge("ml_drifted_features_count",
"Number of drifted features",
["model", "environment"], registry=registry)
def export_to_prometheus(result, model_name, env="prod"):
g_drift.labels(model_name, env).set(result["share_of_drifted_columns"])
g_cnt.labels(model_name, env).set(result["number_of_drifted_columns"])
push_to_gateway("pushgateway:9091",
job="ml-drift-monitor", registry=registry)
در Grafana یک alert rule میسازید: ml_dataset_drift_share > 0.3 for 2h. این با هشدار مبتنی بر یک اجرای منفرد فرق دارد؛ فقط وقتی drift پایدار است alert میدهد، نه با یک اسپایک تصادفی. برای دیدن روشهای استاندارد آستانهگذاری، راهنمای رسمی alerting Prometheus منبع خوبی است.
اشتباهات رایج و بهترین شیوهها
در پیادهسازی درست Evidently این ۵ چاله رایج را دیدهام:
۱. اجرای گزارش drift در مسیر inference
گزارش Evidently میتواند صدها میلیثانیه طول بکشد. اگر آن را در همان endpoint /predict اجرا کنید، تأخیر سرویس تا ۳۰۰٪ افزایش مییابد. راهحل: prediction را در Kafka یا BigQuery ذخیره کنید و مانیتورینگ را batch در پسزمینه اجرا کنید.
۲. تعریف نامناسب reference
گاهی تیمها test set را reference میگذارند که فقط ۲۰٪ داده است. reference باید کل توزیع آموزشی را نماینده کند، حداقل ۱۰٬۰۰۰ نمونه یا ۳۰ روز داده.
۳. نادیده گرفتن feature importance
هر ۱۰۰ ستون یک اهمیت ندارند. مدل SHAP شما ۵ ستون top را میشناسد. اگر drift روی ستونهای کماهمیت باشد، هشدار noise است. Evidently اجازه میدهد وزندار تست کنید یا فقط زیرمجموعه feature را پایش کنید.
۴. نبود baseline از drift طبیعی
حتی بدون هیچ مشکلی، هر روز مقداری «drift زمینه» هست. قبل از راهاندازی آستانه، ۳۰ روز داده تاریخی را اجرا کنید و توزیع PSI روزانه را ببینید. آستانهتان باید بالاتر از percentile ۹۵ این baseline باشد.
وقتی drift تشخیص داده شد، مرحله بعد بازآموزی است. اگر pipeline بازآموزی شما با Optuna hyperparameter tuning ادغام شده باشد، مدل جدید در چند ساعت آماده deploy میشود. برای طراحی feature pipeline استاندارد، مقاله Pipeline در scikit-learn و ColumnTransformer نقطه شروع خوبی است. feature engineering منسجم، احتمال drift مصنوعی را کم میکند.
ترکیب با تحلیل سری زمانی برای الگویابی
drift اغلب الگوی زمانی دارد: روزهای تعطیل، تعطیلات سالیانه، فصلی بودن. با ذخیره PSI روزانه در یک DataFrame و استفاده از تکنیکهای تحلیل سریهای زمانی با Pandas میتوانید bar روزانه را از پسزمینه فصلی جدا کنید. برای مدلهای حساس، rolling PSI هفتگی هشدار بهتری از مقدار مطلق روزانه است.
پرسشهای متداول
Evidently AI رایگان است یا باید هزینه پرداخت کرد؟
کتابخانه open-source Evidently تحت لایسنس Apache 2.0 کاملاً رایگان است و برای بیشتر تیمها کافی است. Evidently Cloud یک SaaS پولی است که ذخیرهسازی تاریخی گزارش، هشدار خودکار و همکاری تیمی را اضافه میکند، اما ضروری نیست. میتوانید همان قابلیتها را با MLflow + Grafana خودتان بسازید.
تفاوت Evidently با NannyML و WhyLabs چیست؟
Evidently سادهتر است و کد open-source کامل ارائه میدهد. NannyML روی تخمین کیفیت مدل بدون برچسب واقعی تمرکز دارد (CBPE و PAPE) که Evidently ندارد. WhyLabs یک محصول ابری با pricing per-model است. برای شروع، Evidently را انتخاب کنید و اگر بعداً به تخمین بدون برچسب نیاز داشتید، NannyML را در کنارش بگذارید.
آیا Evidently برای NLP و مدلهای embedding کار میکند؟
بله. از نسخه 0.4، Evidently شامل TextEvals و متریکهای embedding drift است (مثل Wasserstein روی نمایشهای PCA-reduced). برای LLMها گزارشهای خاص quality با معیارهایی مثل toxicity و hallucination score نیز اضافه شده.
تشخیص data drift را با چه فرکانسی باید اجرا کرد؟
برای بیشتر سیستمهای آنلاین روزانه کافی است. برای مدلهای high-stakes (مثل تشخیص کلاهبرداری) هر ساعت. اجرای real-time روی هر پیشبینی معمولاً هزینه بالا با ارزش کم دارد، چون drift پدیدهای است که در بازه چند ساعت تا چند روز آشکار میشود، نه در چند دقیقه.
وقتی drift تشخیص داده شد، آیا حتماً باید مدل بازآموزی شود؟
خیر. اول بررسی کنید که drift روی ویژگیهای مهم مدل است یا نه (SHAP feature importance). اگر روی ستون کماهمیت است، صرفاً log کنید. اگر روی feature مهم است، ابتدا کیفیت مدل را روی sample داده تازه بسنجید. تنها اگر افت کیفیت هم دیدید، بازآموزی توجیه دارد، چون بازآموزی خود ریسک deploy جدید دارد.