Mục lục
- Mục tiêu bài học
- Data drift là gì
- Vì sao data drift xảy ra
- Phân biệt data drift, concept drift, label drift
- PSI — Population Stability Index
- KS test — Kolmogorov-Smirnov
- Jensen-Shannon Divergence và Wasserstein Distance
- Categorical feature — chi-square test
- Multivariate drift — đo toàn bộ feature space
- Embedding drift cho text và image
- Pattern monitor drift trong production
- Evidently — open source drift report
- whylogs — profile-based monitoring
- Chọn reference dataset
- Drift cho LLM input
- Action khi phát hiện drift
- Threshold và common pitfalls
- Tóm tắt
- Bài tiếp theo
Mục tiêu bài học
Sau bài này bạn sẽ:
- ✅ Giải thích được data drift là gì và tại sao model degrade khi xảy ra
- ✅ Phân biệt data drift, concept drift, label drift — 3 khái niệm hay bị nhầm
- ✅ Dùng được PSI, KS test, Jensen-Shannon, Wasserstein để đo drift trên numerical feature
- ✅ Dùng chi-square test cho categorical feature
- ✅ Phát hiện multivariate drift bằng classifier-based approach
- ✅ Biết cách dùng Evidently và whylogs trong production pipeline
- ✅ Biết cách chọn reference dataset và tránh pitfall phổ biến khi đặt alert
Data drift là gì
Model được train trên tập dữ liệu D_train — ví dụ data Q1/2024 từ hệ thống ecommerce. Khi deploy, model nhận data production D_prod — ví dụ data Q3/2024. Nếu distribution của feature input thay đổi đáng kể giữa D_train và D_prod, đó là data drift.
Tại sao model degrade
ML model học pattern từ D_train: các vùng trong feature space mà nó "tin tưởng" là đúng. Khi D_prod shift sang vùng feature space khác, model vẫn áp dụng pattern cũ vào data mới — gây ra lỗi dự đoán dù bản thân code model không thay đổi gì.
Ví dụ cụ thể: model dự đoán fraud cho giao dịch ngân hàng, được train trên data 2023. Năm 2024, ngân hàng mở thêm tệp khách hàng doanh nghiệp — giao dịch mới có transaction_amount trung bình gấp 10 lần. Model chưa bao giờ thấy range này, xác suất dự đoán không còn được hiệu chỉnh đúng.
Tên gọi khác
Data drift còn được gọi là covariate shift (thuật ngữ trong domain adaptation research) hoặc input drift. Tất cả đều mô tả cùng hiện tượng: P(X) thay đổi.
Data drift không phải bug
Không phải mọi drift đều là vấn đề cần fix ngay. Drift nhỏ, ổn định, không ảnh hưởng đến metric business có thể chấp nhận được. Quan trọng là phát hiện sớm để quyết định có cần hành động không, chứ không phải auto-retrain sau mỗi lần phát hiện drift.
Vì sao data drift xảy ra
Thay đổi có chu kỳ (seasonal)
Nhiều business có pattern mùa vụ rõ ràng. Model train trên Q2 (tháng 4–6) sẽ không bắt được pattern Q4 mùa lễ: order tăng đột biến, customer behavior khác, product mix khác. Weekend và weekday cũng có distribution khác nhau trong nhiều domain (traffic web, food delivery, finance).
External event
COVID-19 (Q1/2020) là ví dụ điển hình: chuyển đổi sang ecommerce diễn ra trong vài tuần thay vì vài năm. Mọi model được train trước đó đều gặp drift mạnh. Các sự kiện tương tự quy mô nhỏ hơn: thay đổi chính sách thuế, khủng hoảng chuỗi cung ứng, viral product launch.
Product change
Thêm field mới vào form đăng ký — user behavior thay đổi. Đổi UI flow checkout — conversion funnel khác. A/B test thay đổi tệp user nhìn thấy feature. Những thay đổi này không liên quan đến ML nhưng ảnh hưởng trực tiếp đến distribution của data model nhận.
Demographic shift
App mở rộng sang thị trường mới (vd từ thành thị sang nông thôn, từ VN sang SEA). User mới có hành vi khác user cũ đáng kể. Model train trên user base cũ không tổng quát được sang population mới.
Upstream pipeline change
Feature engineering bị đổi logic: normalizer dùng statistics khác, cách encode category thay đổi, null imputation strategy đổi. Đây là loại drift nguy hiểm nhất vì không phải do business thay đổi mà do lỗi kỹ thuật — data vào model sai về mặt kỹ thuật chứ không phải drift tự nhiên.
Phân biệt data drift, concept drift, label drift
Ba loại drift hay bị nhầm lẫn với nhau vì đều gây model degrade, nhưng cơ chế và cách xử lý khác nhau:
| Loại | Định nghĩa | P(?) thay đổi | Ví dụ |
|---|---|---|---|
| Data drift | Distribution của feature input thay đổi | P(X) | Khách hàng mới có income cao hơn hẳn tệp cũ |
| Concept drift | Quan hệ giữa input và label thay đổi | P(Y|X) | Cùng user profile cũ nhưng nay không còn mua sản phẩm đó nữa |
| Label drift | Distribution của output/label thay đổi | P(Y) | Tỷ lệ churn tăng từ 5% lên 15% do cạnh tranh |
Cùng lúc nhiều loại drift
Trong thực tế, 3 loại drift có thể xảy ra đồng thời. Ví dụ: thêm user mới (data drift) dẫn đến behavior mới (concept drift) và tỷ lệ conversion chung thay đổi (label drift). Khi debug, cần xác định loại drift chính để chọn action phù hợp.
Bài này tập trung vào data drift — P(X). Concept drift sẽ được đề cập riêng trong bài 48.
Khi nào cần label để phát hiện drift
Data drift không cần label — chỉ cần feature X của production request. Đây là lợi thế lớn: trong production, label thường không có ngay (ground truth feedback trễ vài ngày đến vài tuần). Giám sát data drift có thể chạy real-time trong khi giám sát model accuracy thường bị trễ.
PSI — Population Stability Index
PSI xuất phát từ ngành tài chính (credit scoring) nhưng được dùng rộng rãi trong ML monitoring. Ý tưởng: chia feature thành các bins, so sánh tỷ lệ quan sát trong từng bin giữa reference (train) và current (prod).
Công thức
Với mỗi bin i:
PSI = Σ (cur_pct_i - ref_pct_i) × ln(cur_pct_i / ref_pct_i)
Implementation
import numpy as np
def psi(reference: np.ndarray, current: np.ndarray, bins: int = 10) -> float:
"""
Tính PSI giữa reference và current distribution.
Args:
reference: Array 1D từ training / reference window.
current: Array 1D từ production window hiện tại.
bins: Số bins để chia (thường 10 hoặc 20).
Returns:
PSI score. Diễn giải:
< 0.1 : không đáng kể
0.1–0.25: drift vừa, cần theo dõi
> 0.25 : drift đáng kể, cần hành động
"""
# Chia theo quantile của reference để bins đều dữ liệu
breakpoints = np.quantile(reference, np.linspace(0, 1, bins + 1))
breakpoints[0], breakpoints[-1] = -np.inf, np.inf
ref_counts, _ = np.histogram(reference, bins=breakpoints)
cur_counts, _ = np.histogram(current, bins=breakpoints)
# +1e-10 tránh log(0) khi bin trống
ref_pct = ref_counts / len(reference) + 1e-10
cur_pct = cur_counts / len(current) + 1e-10
return float(np.sum((cur_pct - ref_pct) * np.log(cur_pct / ref_pct)))
# Ví dụ sử dụng
rng = np.random.default_rng(42)
ref = rng.normal(loc=0.5, scale=0.1, size=5000) # Training distribution
cur = rng.normal(loc=0.6, scale=0.12, size=1000) # Production, hơi khác
score = psi(ref, cur)
print(f"PSI = {score:.4f}") # ~0.08 — không đáng kể
Chọn số bins
10 bins là mặc định phổ biến trong tài chính. Với distribution có đuôi nặng (heavy-tail), dùng nhiều bins hơn (20) hoặc dùng log-scale bins. Với sample size nhỏ hơn 1000, tránh bins nhiều hơn 10 vì bin thưa gây PSI không ổn định.
PSI so với các test khác
PSI không cho p-value và không có threshold lý thuyết cứng — threshold 0.1/0.25 là quy ước thực hành từ ngành tài chính. Ưu điểm: ổn định với sample size lớn (khác với KS test), dễ diễn giải theo bin, phổ biến trong industry.
KS test — Kolmogorov-Smirnov
KS test là statistical hypothesis test: null hypothesis là 2 sample đến từ cùng distribution. Trả về KS statistic (khoảng cách lớn nhất giữa 2 CDF) và p-value.
from scipy.stats import ks_2samp
stat, p_value = ks_2samp(reference, current)
print(f"KS statistic = {stat:.4f}")
print(f"p-value = {p_value:.6f}")
if p_value < 0.05:
print("Reject H0: hai distribution khác nhau đáng kể")
else:
print("Không đủ bằng chứng để kết luận drift")
Giới hạn quan trọng với sample lớn
KS test rất nhạy với sample size. Với N > 100,000, ngay cả khác biệt rất nhỏ về distribution (không ảnh hưởng gì đến model) cũng cho p-value < 0.05. Đây là statistical significance khác với practical significance.
Cách giải quyết: với large sample, dùng KS statistic trực tiếp thay vì p-value (giá trị < 0.05 là threshold thực hành phổ biến), hoặc kết hợp với PSI để cross-check.
Ưu điểm của KS test
Không cần chia bins — dùng empirical CDF trực tiếp, nên không bị ảnh hưởng bởi cách chọn bins. Phù hợp khi muốn một test thống kê có lý thuyết rõ ràng. Dễ dùng với scipy.stats.ks_2samp.
Jensen-Shannon Divergence và Wasserstein Distance
Jensen-Shannon Divergence (JSD)
JSD là symmetric version của KL divergence. Nhận giá trị [0, 1] khi dùng log base 2. Cần convert sang probability distribution (histogram normalized) trước:
import numpy as np
from scipy.spatial.distance import jensenshannon
def js_divergence(
reference: np.ndarray, current: np.ndarray, bins: int = 50
) -> float:
"""JSD từ 0 (giống nhau hoàn toàn) đến 1 (khác nhau hoàn toàn)."""
# Dùng chung breakpoints để 2 histogram có cùng bins
combined = np.concatenate([reference, current])
breakpoints = np.linspace(combined.min(), combined.max(), bins + 1)
ref_hist, _ = np.histogram(reference, bins=breakpoints, density=True)
cur_hist, _ = np.histogram(current, bins=breakpoints, density=True)
# Normalize về probability mass (tổng = 1)
ref_hist = ref_hist / ref_hist.sum() + 1e-10
cur_hist = cur_hist / cur_hist.sum() + 1e-10
return float(jensenshannon(ref_hist, cur_hist))
Wasserstein Distance (Earth Mover's Distance)
Wasserstein đo lượng "công" tối thiểu cần thiết để biến distribution này thành distribution kia. Trả về giá trị có đơn vị giống đơn vị của feature — dễ diễn giải về mặt domain:
from scipy.stats import wasserstein_distance
wd = wasserstein_distance(reference, current)
print(f"Wasserstein distance = {wd:.4f}")
# Nếu feature là số tiền (VND nghìn đồng),
# wd = 50 nghĩa là distribution shift khoảng 50k VND
So sánh 4 phương pháp
| Phương pháp | Output | Ổn định sample lớn | Đơn vị diễn giải | Threshold chuẩn |
|---|---|---|---|---|
| PSI | [0, ∞) | Tốt | Không | 0.1 / 0.25 |
| KS test | stat [0,1] + p-value | Kém khi N lớn | Không | p < 0.05 hoặc stat > 0.05 |
| JSD | [0, 1] | Tốt | Không | Tùy chỉnh (> 0.1 là drift vừa) |
| Wasserstein | [0, ∞), đơn vị feature | Tốt | Có | Domain-specific |
Trong practice, dùng PSI cho continuous feature với nhiều team đã quen convention 0.1/0.25 là nhanh nhất. Wasserstein hữu ích khi muốn báo cáo drift bằng ngôn ngữ business ("giá trị giao dịch trung bình dịch chuyển 50k VND").
Categorical feature — chi-square test
Các phương pháp ở trên dành cho continuous feature. Với categorical feature (vd loại sản phẩm, tỉnh thành, device type), dùng chi-square test để kiểm tra xem proportion của từng category có thay đổi không:
import numpy as np
from scipy.stats import chi2_contingency
def categorical_drift(
ref_series, cur_series, min_count: int = 5
) -> tuple[float, float]:
"""
Chi-square test cho categorical feature.
Returns:
(chi2_stat, p_value)
"""
# Lấy union của tất cả categories
all_categories = sorted(set(ref_series) | set(cur_series))
ref_counts = np.array([
np.sum(ref_series == cat) for cat in all_categories
])
cur_counts = np.array([
np.sum(cur_series == cat) for cat in all_categories
])
# Bỏ category có quá ít count (chi-square không tin cậy với cell < 5)
valid = (ref_counts >= min_count) & (cur_counts >= min_count)
if valid.sum() < 2:
raise ValueError("Quá ít category đủ điều kiện để test")
contingency = np.vstack([ref_counts[valid], cur_counts[valid]])
chi2, p_value, _, _ = chi2_contingency(contingency)
return chi2, p_value
# Ví dụ
import pandas as pd
ref_cat = pd.Series(["A"] * 400 + ["B"] * 350 + ["C"] * 250)
cur_cat = pd.Series(["A"] * 200 + ["B"] * 500 + ["C"] * 300) # B tăng mạnh
chi2, p = categorical_drift(ref_cat, cur_cat)
print(f"chi2 = {chi2:.2f}, p-value = {p:.6f}")
PSI cho categorical
Có thể dùng PSI cho categorical bằng cách coi mỗi category là 1 bin. Công thức giống y PSI numerical nhưng không cần chia bins — mỗi category là 1 "bin" tự nhiên. Cách này phổ biến hơn chi-square vì dùng chung metric PSI cho toàn bộ feature.
def psi_categorical(ref_series, cur_series) -> float:
all_cats = set(ref_series) | set(cur_series)
psi_val = 0.0
for cat in all_cats:
ref_p = (ref_series == cat).mean() + 1e-10
cur_p = (cur_series == cat).mean() + 1e-10
psi_val += (cur_p - ref_p) * np.log(cur_p / ref_p)
return psi_val
Multivariate drift — đo toàn bộ feature space
Đo drift từng feature riêng lẻ không bắt được trường hợp từng feature riêng ổn nhưng combination thay đổi. Ví dụ: feature age và income đều trong range bình thường, nhưng trong production xuất hiện nhiều user trẻ + thu nhập cao — một combination hiếm trong training data.
Classifier-based drift detection
Ý tưởng: train một classifier phân biệt "sample đến từ reference" (label 0) vs "sample đến từ production" (label 1). Nếu classifier dễ phân biệt được (AUC cao), đó là bằng chứng distribution đã thay đổi:
import numpy as np
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import cross_val_score
def multivariate_drift_auc(
reference: np.ndarray,
current: np.ndarray,
n_estimators: int = 100,
cv: int = 5,
) -> float:
"""
Dùng classifier để detect multivariate drift.
Args:
reference: 2D array, shape (n_ref, n_features)
current: 2D array, shape (n_cur, n_features)
Returns:
AUC score. Diễn giải:
~0.5: không drift (classifier không phân biệt được)
> 0.7: drift đáng kể
> 0.85: drift rất mạnh
"""
# Cân bằng class: lấy min(len(ref), len(cur)) mẫu mỗi phía
n = min(len(reference), len(current))
rng = np.random.default_rng(0)
ref_idx = rng.choice(len(reference), n, replace=False)
cur_idx = rng.choice(len(current), n, replace=False)
X = np.vstack([reference[ref_idx], current[cur_idx]])
y = np.concatenate([np.zeros(n), np.ones(n)])
clf = RandomForestClassifier(n_estimators=n_estimators, random_state=42)
aucs = cross_val_score(clf, X, y, scoring="roc_auc", cv=cv)
return float(aucs.mean())
# Sử dụng
rng = np.random.default_rng(1)
ref_2d = rng.multivariate_normal([0, 0], [[1, 0.5], [0.5, 1]], size=2000)
cur_2d = rng.multivariate_normal([0.5, 0.5], [[1, 0.8], [0.8, 1]], size=500)
auc = multivariate_drift_auc(ref_2d, cur_2d)
print(f"Multivariate drift AUC = {auc:.3f}")
Khi nào dùng multivariate drift
Multivariate drift detection tốn compute hơn per-feature monitoring nhưng bắt được interaction effect. Dùng nó như signal bổ sung khi:
- Có model có nhiều feature tương quan chặt.
- Univariate metric không thấy drift nhưng model accuracy giảm.
- Muốn 1 metric duy nhất thay vì theo dõi N feature riêng lẻ.
Embedding drift cho text và image
Với input là text hoặc image, không có feature numeric rõ ràng để đo PSI/KS. Thay vào đó, compute embedding (vector biểu diễn) cho sample từ reference và current, rồi đo drift trong không gian embedding.
Centroid distance
Phương pháp đơn giản nhất: so sánh trung bình embedding:
import numpy as np
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("all-MiniLM-L6-v2")
ref_texts = [...] # danh sách text từ reference window
cur_texts = [...] # danh sách text từ production window
ref_emb = model.encode(ref_texts, normalize_embeddings=True) # shape (n_ref, 384)
cur_emb = model.encode(cur_texts, normalize_embeddings=True) # shape (n_cur, 384)
# Centroid của mỗi window
ref_centroid = ref_emb.mean(axis=0)
cur_centroid = cur_emb.mean(axis=0)
# Cosine distance: 0 = giống nhau, 1 = khác nhau
cosine_drift = 1 - np.dot(ref_centroid, cur_centroid)
print(f"Embedding centroid drift = {cosine_drift:.4f}")
Wasserstein trên embedding dimensions
Có thể tính Wasserstein distance trên từng dimension của embedding rồi lấy mean, hoặc dùng tool như Evidently tự động xử lý khi được truyền DataFrame có cột embedding.
Topic/cluster shift
Với production traffic lớn: cluster embedding của reference thành K clusters, rồi so sánh tỷ lệ phân bổ cluster giữa ref và cur. Cách này bắt được "user đang hỏi nhiều về topic mới mà training data không có":
from sklearn.cluster import KMeans
from scipy.stats import chi2_contingency
K = 20
kmeans = KMeans(n_clusters=K, random_state=0).fit(ref_emb)
ref_labels = kmeans.predict(ref_emb)
cur_labels = kmeans.predict(cur_emb)
ref_counts = np.bincount(ref_labels, minlength=K)
cur_counts = np.bincount(cur_labels, minlength=K)
# Dùng PSI categorical hoặc chi-square để test cluster distribution
chi2, p = chi2_contingency(np.vstack([ref_counts, cur_counts]))[:2]
print(f"Cluster distribution chi2={chi2:.2f}, p={p:.6f}")
Pattern monitor drift trong production
Monitoring drift không phải chỉ chạy 1 lần — cần chạy định kỳ và track theo thời gian:
Pipeline cơ bản
import numpy as np
import json
from datetime import datetime, timezone
def run_drift_check(
reference: np.ndarray,
current_window: np.ndarray,
feature_names: list[str],
psi_threshold: float = 0.25,
) -> dict:
"""
Chạy drift check cho một batch feature và trả về report.
Gọi định kỳ (vd mỗi giờ hoặc mỗi ngày).
"""
report = {
"checked_at": datetime.now(timezone.utc).isoformat(),
"window_size": len(current_window),
"features": {},
"alert": False,
}
for i, name in enumerate(feature_names):
ref_col = reference[:, i]
cur_col = current_window[:, i]
score = psi(ref_col, cur_col)
report["features"][name] = {
"psi": round(score, 4),
"drift": score > psi_threshold,
}
if score > psi_threshold:
report["alert"] = True
return report
# Ghi report ra file hoặc DB, gửi alert nếu report["alert"] == True
Granularity của window
Không có one-size-fits-all. Một số hướng dẫn thực hành:
- Hourly window: phù hợp với high-traffic service (vd > 10,000 req/giờ). Phát hiện drift sớm nhưng nhiễu hơn.
- Daily window: cân bằng giữa phát hiện sớm và ổn định. Phổ biến nhất.
- Weekly window: phù hợp khi drift dự kiến chậm (vd demographic shift). Ít nhiễu nhưng phát hiện trễ.
Cần đủ sample mỗi window: tối thiểu 200–500 observation để drift metric ổn định. Với feature có tail, cần nhiều hơn.
Time series của drift score
Lưu PSI score theo thời gian vào time series DB (InfluxDB, Prometheus, hoặc đơn giản là CSV/parquet). Vẽ line chart PSI × time để thấy trend và phát hiện sudden shift vs gradual drift.
Evidently — open source drift report
Evidently (evidently.ai) là thư viện open source dành riêng cho ML monitoring. Cung cấp HTML report trực quan với drift per feature, distribution overlay, và test suite.
Cài đặt
pip install evidently
Tạo drift report
import pandas as pd
from evidently.report import Report
from evidently.metric_preset import DataDriftPreset
# df_ref: DataFrame training data (hoặc stable reference window)
# df_cur: DataFrame production window hiện tại
# Cùng schema: cùng tên cột, cùng dtype
df_ref = pd.read_parquet("reference_window.parquet")
df_cur = pd.read_parquet("current_window.parquet")
report = Report(metrics=[DataDriftPreset()])
report.run(reference_data=df_ref, current_data=df_cur)
# Xuất HTML — mở trong browser để xem
report.save_html("drift_report.html")
# Lấy kết quả dạng dict để xử lý programmatically
result = report.as_dict()
drift_detected = result["metrics"][0]["result"]["dataset_drift"]
print(f"Dataset drift detected: {drift_detected}")
Nội dung report
Evidently tự chọn test phù hợp theo dtype của từng column (Wasserstein cho numeric, chi-square cho categorical) và render HTML với:
- Summary table: tổng số feature, bao nhiêu feature bị drift.
- Per-feature: histogram overlay ref vs current, drift score, drift/no-drift badge.
- Dataset drift flag: mặc định True khi hơn 50% feature bị drift.
Tích hợp vào pipeline
from evidently.test_suite import TestSuite
from evidently.tests import (
TestNumberOfDriftedColumns,
TestShareOfDriftedColumns,
)
suite = TestSuite(tests=[
TestShareOfDriftedColumns(lt=0.3), # Fail nếu > 30% feature drift
])
suite.run(reference_data=df_ref, current_data=df_cur)
if not suite.as_dict()["summary"]["all_passed"]:
# Gửi alert hoặc trigger pipeline
raise RuntimeError("Drift check failed — too many features drifted")
whylogs — profile-based monitoring
whylogs (của WhyLabs) dùng approach khác: thay vì lưu raw data để so sánh sau, nó tạo lightweight statistical profile (sketch) cho mỗi batch. Profile nhỏ hơn data gốc rất nhiều (~KB thay vì MB) và merge được.
import whylogs as why
import pandas as pd
df_ref = pd.read_parquet("reference_window.parquet")
df_cur = pd.read_parquet("current_window.parquet")
# Tạo profile cho mỗi dataset
ref_results = why.log(pandas=df_ref)
cur_results = why.log(pandas=df_cur)
ref_view = ref_results.profile().view()
cur_view = cur_results.profile().view()
# Lưu profile để dùng sau (không cần raw data)
ref_results.writer("local").option(base_dir="profiles").write()
# So sánh 2 profile — tính drift metrics
from whylogs.viz import NotebookProfileVisualizer
viz = NotebookProfileVisualizer()
viz.set_profiles(target_profile_view=cur_view, reference_profile_view=ref_view)
# viz.feature_statistics() -- xem trong Jupyter
WhyLabs cloud
whylogs có thể upload profile lên WhyLabs cloud để monitor dashboard real-time, alert, và track drift theo thời gian. Free tier cho 1 model. Nếu cần on-premise hoàn toàn, chỉ dùng local profile compare.
Khi nào chọn whylogs vs Evidently
| Tiêu chí | Evidently | whylogs |
|---|---|---|
| Cần raw data để so sánh | Có | Không (dùng profile) |
| Storage footprint | Cao (cần lưu ref data) | Thấp (KB per profile) |
| HTML report đẹp | Tốt hơn | Cần WhyLabs UI hoặc tự build |
| Streaming / real-time | Kém | Tốt (profile merge được) |
| Self-hosted hoàn toàn | Dễ | Được nhưng mất cloud UI |
Chọn reference dataset
Kết quả drift phụ thuộc nhiều vào chọn reference dataset. Có 3 approach phổ biến:
Training data làm reference
Chuẩn nhất về mặt lý thuyết — drift so với "điều model đã học". Nhược điểm: training data có thể quá cũ so với thực tế triển khai. Nếu deploy nhiều tháng sau khi train, baseline đã bị "stale" từ đầu.
Recent stable production window
Lấy 30 ngày gần nhất trong production khi model còn hoạt động tốt làm reference. Thực tế hơn. Có thể thiếu một số pattern hiếm có trong training data.
Rolling reference
Reference window tự cập nhật: luôn là N ngày trước so với hiện tại (vd N = 30). Ưu điểm: tự điều chỉnh theo trend dài hạn, ít false positive từ seasonal drift. Nhược điểm: nếu drift xảy ra chậm và liên tục, rolling reference có thể bỏ qua ("boiling frog effect").
Multi-reference
Giữ nhiều reference: 1 cố định (training data) + 1 rolling. Alert khi cả 2 đều thấy drift. Giảm false positive nhưng phức tạp hơn.
Kích thước reference tối thiểu
PSI cần tối thiểu 500–1000 observation để ổn định. KS test cần ít hơn (vài trăm) nhưng kém chính xác hơn. Với model mới deploy, warmup period đầu (1–7 ngày) thường có distribution bất thường vì user base chưa ổn định — không nên dùng data trong giai đoạn này làm reference.
Drift cho LLM input
LLM không có feature vector rõ ràng như ML cổ điển, nhưng input vẫn có thể drift và ảnh hưởng đến chất lượng output. Các signal cần monitor:
Distributional signals có thể đo trực tiếp
- Prompt length distribution: PSI trên số token per request. Prompt ngắn dần có thể báo hiệu user frustration hoặc thay đổi use case.
- Tokens per request: bao gồm cả input + output. Tăng đột biến cảnh báo về cost và latency.
- Language distribution: tỷ lệ vi/en/... thay đổi khi mở rộng thị trường.
import tiktoken
enc = tiktoken.get_encoding("cl100k_base")
def count_tokens(text: str) -> int:
return len(enc.encode(text))
# Lấy token count cho mỗi request trong window
ref_token_counts = [count_tokens(p) for p in ref_prompts]
cur_token_counts = [count_tokens(p) for p in cur_prompts]
psi_tokens = psi(
np.array(ref_token_counts, dtype=float),
np.array(cur_token_counts, dtype=float),
)
print(f"Prompt length PSI = {psi_tokens:.4f}")
Topic distribution
Cluster embedding của prompt vào K topics. Theo dõi tỷ lệ phân bổ topic theo thời gian:
- Topic mới xuất hiện nhiều (cluster ít được thấy trong training data).
- Topic cũ giảm mạnh — có thể feature đó không còn được dùng.
Response quality proxy
Không có label ground truth real-time, nhưng có thể dùng proxy:
- finish_reason=length tăng: model bị truncate nhiều hơn — prompt dài hoặc context dài hơn.
- Retry rate tăng: frontend retry nhiều hơn — model trả lỗi hoặc response không hợp lệ.
- User follow-up query rate: nếu user hỏi lại ngay sau query đầu, có thể answer chưa đủ.
Action khi phát hiện drift
Phát hiện drift chỉ là bước đầu. Quan trọng hơn là quyết định làm gì tiếp theo:
Điều tra nguyên nhân trước
Trước khi trigger bất kỳ action tốn kém nào, tìm hiểu drift đến từ đâu:
- Có thay đổi upstream pipeline nào gần đây không? (data bug trước khi là business change)
- Drift có theo pattern mùa vụ không? (weekly, monthly)
- Feature nào drift nhiều nhất? Distribution thay đổi theo chiều nào?
- Model accuracy metric (nếu có label trễ) có giảm tương ứng không?
Soft alert — notify team
Khi PSI vượt ngưỡng nhẹ (vd 0.1–0.25): gửi notification cho team ML (Slack, email). Không auto-action. Con người quyết định có cần làm gì không. Phù hợp khi team còn nhỏ hoặc chưa có runbook rõ ràng.
Trigger retrain
Khi drift đáng kể và model accuracy thực sự giảm: trigger retraining pipeline với data mới nhất. Retrain không phải miễn phí — cần compute, validation time, và review trước khi deploy model mới.
Không nên auto-retrain sau mỗi alert mà không có kiểm tra thêm. Drift thoáng qua (spike 1–2 ngày) không đáng retrain. Drift liên tục trên nhiều feature trong nhiều ngày liên tiếp mới đáng.
Fallback model
Nếu có model backup đơn giản hơn (vd linear model thay vì gradient boosting), switch sang fallback khi detect drift mạnh — đôi khi model đơn giản hơn generalize tốt hơn trên distribution mới.
Investigate và document
Ghi lại: khi nào drift xảy ra, feature nào bị ảnh hưởng, nguyên nhân xác định được, action đã làm, kết quả. Drift log này giúp cải thiện model và monitoring system theo thời gian.
Threshold và common pitfalls
Threshold thực hành
- PSI < 0.1: không đáng kể.
- PSI 0.1–0.25: drift vừa — theo dõi thêm, kiểm tra model accuracy nếu có.
- PSI > 0.25: drift đáng kể — cần điều tra nguyên nhân.
- PSI > 0.5: drift rất mạnh — gần như chắc chắn cần action.
- KS p-value < 0.05: có bằng chứng thống kê về drift — nhưng không dùng duy nhất khi N > 100k.
Threshold trên là starting point. Tune theo đặc thù data: feature có biến động tự nhiên cao (vd transaction amount) nên dùng threshold cao hơn; feature ổn định (vd user age) nên dùng threshold thấp hơn.
Pitfall 1: sample window quá nhỏ
Drift test trên 50–100 observation sẽ rất nhiễu — false positive cao. Với hourly window ít traffic, gộp vào daily hoặc tích lũy 200+ sample trước khi chạy test.
Pitfall 2: KS test với large sample
Sample 500k observation: KS test báo drift ngay cả khi distribution chỉ khác nhau 0.1%. Khác biệt này không có ý nghĩa thực tế. Với N > 100k, dùng PSI hoặc KS statistic với threshold (vd stat > 0.05) thay vì p-value.
Pitfall 3: quên warmup period
Ngày đầu hoặc tuần đầu sau khi deploy model mới thường có distribution bất thường: user base chưa ổn định, nhiều bot/crawler, feature pipeline chưa warm cache. Set warmup period (vd 3–7 ngày) trong đó không trigger alert từ drift check.
Pitfall 4: monitor quá nhiều feature
Model 100 feature → 100 drift checks → nhiều test multiple comparison → false positive tăng. Chỉ monitor top-K feature quan trọng nhất (vd theo feature importance của model) hoặc dùng Bonferroni correction cho p-value.
Pitfall 5: seasonal drift gây false alarm liên tục
Nếu biết rõ feature có seasonal pattern (vd số order giảm mỗi thứ 2), dùng rolling reference hoặc so sánh cùng weekday của tuần trước thay vì reference tĩnh.
Pitfall 6: nhầm drift với data quality issue
Đôi khi "drift" là do upstream data pipeline trả về null nhiều hơn hoặc dùng wrong imputation value. Luôn check data quality (null rate, out-of-range values) song song với drift monitoring. Bài 45 về logging đã đề cập cách log feature statistics — dùng lại pattern đó ở đây.
Tóm tắt
- ✅ Data drift = P(X) thay đổi giữa training và production. Model degrade vì học pattern không còn đúng với data hiện tại
- ✅ Nguyên nhân: seasonal, external event, product change, demographic shift, upstream pipeline bug
- ✅ Data drift (P(X)) khác với concept drift (P(Y|X)) và label drift (P(Y)) — bài 48 sẽ nói về concept drift
- ✅ PSI: không cần p-value, ổn định với large sample. Threshold 0.1/0.25 là convention từ tài chính
- ✅ KS test: có p-value nhưng unreliable với N > 100k — dùng KS statistic trực tiếp khi sample lớn
- ✅ JSD [0,1] và Wasserstein (đơn vị feature) là 2 alternative hữu ích với trade-off khác nhau
- ✅ Categorical feature: chi-square test hoặc PSI per-category
- ✅ Multivariate drift: train classifier phân biệt ref vs cur — AUC > 0.7 là signal đáng kể
- ✅ Evidently: HTML report nhanh, test suite cho CI/CD. whylogs: profile nhẹ, phù hợp streaming
- ✅ Không auto-retrain sau mỗi alert — điều tra nguyên nhân trước, retrain có chủ đích
Bài tiếp theo
Bài 48: Concept Drift — khi quan hệ input/output thay đổi — khi P(Y|X) thay đổi: cách phát hiện concept drift khi có label trễ, các kiểu drift (sudden, gradual, recurring), và chiến lược adapt model.
Tài liệu tham khảo
- Failing Loudly: An Empirical Study of Methods for Detecting Dataset Shift (arXiv:1602.06023)
- Evidently — All Metrics Reference
- Evidently — GitHub
- whylogs documentation
- scipy.stats.ks_2samp
- scipy.stats.wasserstein_distance
- scipy.spatial.distance.jensenshannon
- Alibi Detect: Algorithms for Outlier, Adversarial and Drift Detection (arXiv:2004.03045)
- Chip Huyen — Data Distribution Shifts and Monitoring
