Danh sách bài viết

Bài 47: Data Drift — khi distribution input thay đổi

Data drift xảy ra khi distribution của feature input trong production khác với distribution lúc train. Model vẫn chạy bình thường về kỹ thuật nhưng độ chính xác suy giảm vì nó học trên pattern không còn phản ánh thực tế hiện tại. Bài này trình bày nguyên nhân, 4 phương pháp đo drift thống kê, multivariate drift, embedding drift cho LLM, cách dùng Evidently và whylogs, và các pitfall thường gặp khi cài alert threshold.

27/05/2026
0 lượt xem
1

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
2

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.

3

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.

4

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ễ.

5

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.

6

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.

7

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 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").

8

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
9

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 ageincome đề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ẻ.
10

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}")
11

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.

12

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")
13

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 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
14

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.

15

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 đủ.
16

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.

17

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.

18

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
19

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.