📑 本页目录(点开跳转)
06 · 数据漂移与概念漂移
⏱ 38 分钟 | ⭐ 分清它们,才知道该修什么
🎯 一句话
数据漂移:进来的东西变了。 概念漂移:进来的东西没变,但它和答案的关系变了。 前者重训能修,后者重训未必能修 —— 而且它更难发现。
📐 一、用概率写清楚区别
模型学的是 $P(y \mid x)$,而你看到的数据由 $P(x, y) = P(y|x)\,P(x)$ 决定。
| 类型 | 变的是 | 直觉 |
|---|---|---|
| 数据漂移(covariate shift) | $P(x)$ 变,$P(y\|x)$ 不变 | 来的人变了,但「什么样的人会买」这个规律没变 |
| 概念漂移(concept drift) | $P(y\|x)$ 变了 ⭐ | 同样的人,现在的行为和以前不一样了 |
| 标签漂移(prior shift) | $P(y)$ 变 | 正样本整体比例变了(如欺诈率上升) |
💡 一组对照例子(同一个信贷模型):
结果对照
⭐ 最后一行是个实用的省力点: 标签漂移常常只需要调整阈值或做一次概率重校准, 比重训模型便宜得多。先判断类型,再决定动作。
🔍 二、怎么检测数据漂移
方法 1:逐特征统计检验
| 特征类型 | 常用方法 | 说明 |
|---|---|---|
| 数值 | KS 检验 | 比较两个分布,无需假设正态 |
| 数值 | PSI(群体稳定性指数)⭐ | 工业界最常用,有公认阈值 |
| 类别 | 卡方检验 / PSI |
PSI 的公式和阈值(记住这个就够用):
$$\text{PSI} = \sum_i (p_i^{\text{线上}} - p_i^{\text{训练}}) \cdot \ln\frac{p_i^{\text{线上}}}{p_i^{\text{训练}}}$$
计算前两步
- 把特征分成 10 个桶(按训练集的分位数切)
- 算每个桶的占比变化
| PSI 范围 | 原文判断 |
|---|---|
| < 0.1 | 稳定 ✅ |
| 0.1 ~ 0.25 | 有变化 ⚠️ 关注 |
| > 0.25 | 显著漂移 🔴 该查了 |
import numpy as np
def psi(base, live, bins=10):
"""base/live 是同一个特征的两批取值。按 base 的分位数切桶。"""
edges = np.quantile(base, np.linspace(0, 1, bins + 1))
edges[0], edges[-1] = -np.inf, np.inf # ⭐ 防止线上出现超界值被丢掉
b = np.histogram(base, edges)[0] / len(base)
l = np.histogram(live, edges)[0] / len(live)
eps = 1e-6 # ⭐ 防 log(0)
b, l = np.clip(b, eps, None), np.clip(l, eps, None)
return float(np.sum((l - b) * np.log(l / b)))
⚠️ PSI 的两个坑:
| 坑 | 说明 |
|---|---|
| 分桶必须按训练集切,且边界要存下来 | 每次按各自分位数切,永远算不出漂移 💀 |
| 样本量小时不稳 | 少于 1000 条时 PSI 波动很大,别急着告警 |
方法 2:对抗验证(一次看全部特征)
流程图
🔗 这个方法你在 Kaggle 23 见过 (那里用它检测训练集和测试集的分布差异)。 同一个工具,换个场景:那里比的是训练集 vs 测试集,这里比的是训练集 vs 线上流量。
⭐ 它比逐特征检验强的地方:能发现「联合分布」的漂移 —— 每个特征单看都正常,但它们的组合变了(比如「高收入 + 年轻」这个组合突然变多)。
🕵️ 三、概念漂移:难在哪、怎么发现
对照
数据漂移:看输入就能发现 ← 不需要标签 ✅
概念漂移:必须知道【真实答案】才能发现 ← 需要标签 💀
这就是它难的根本原因:标签常常要等很久(第 7 章专门讲这个)。
三种形态
关键信息
- ① 突变(sudden)
- 政策变了、竞品上线、疫情开始
- 表现:某一天指标断崖式下跌
- ② 渐变(gradual)⭐ 最危险
- 用户习惯慢慢改变
- 表现:每天掉一点点,单看哪天都在正常波动内
- 三个月后回头看,已经掉了 20%
- ③ 周期性(recurring)
- 季节、节假日、工作日/周末
- 不是"坏",但模型要能处理
⭐ ② 最危险的原因:任何单日的告警阈值都不会触发。 对策:除了日环比,一定要看「和 30 天前比」「和 90 天前比」。
一个便宜的检测手段:滑动窗口 AUC
把有标签的样本按时间排,算每周的 AUC
| 周 | AUC 与观察 |
|---|---|
| 周1 | 0.82 |
| 周2 | 0.81 |
| 周3 | 0.81 |
| 周4 | 0.79 ← 单看是正常波动 |
| 周5 | 0.78 |
| 周6 | 0.76 ← 和周1比已经掉了 0.06 🔴 |
看哪里
- ⭐ 关键是【和基线比】,不是和上周比
🛠️ 四、发现了漂移,做什么
别急着重训。按这个顺序判断:
操作步骤
| 情况 | 该做的 | 不该做的 |
|---|---|---|
| 管道故障 | 修管道 | ❌ 重训 |
| 数据漂移 | 重训 / 加权 / 扩样本 | ❌ 只调阈值 |
| 概念漂移 | 重训 + 调整训练窗口 | ❌ 无脑用全量历史 |
| 标签漂移 | 重校准 / 调阈值 | ❌ 大动干戈重训 |
🔗 重训练的具体策略(窗口多长、多久一次、怎么验证)→ 第 16 章
💀 五、一个"PSI 全绿但模型已经烂了"的真事故
这是概念漂移最典型的样子,也是漂移监控最尴尬的时刻。
结果对照
真正发生的事:
关键信息
| 检测手段 | 结果 | 为什么 |
|---|---|---|
| PSI / KS | ❌ 全绿 | 它们只看 $P(x)$ |
| 对抗验证 | ❌ AUC 0.52 | 同上,只看输入 |
| 预测分布 | ❌ 没动 | 模型的输入没变,输出自然没变 |
| 滑动窗口 MAE | ✅ 6.1 → 9.8 分钟 ⭐ | 唯一看了真实答案的 |
| 用户投诉率 | ✅ 涨了 3 倍 | 最晚,也最贵 |
⭐ 这个事故给出的是本章最重要的结论: 漂移监控(PSI / 对抗验证)只能发现数据漂移,对概念漂移是完全失明的。 无论你的漂移看板多绿,都必须另外有一条"看真实答案"的线。
💡 这个案例还有一个附加教训:概念漂移的原因常常在你的系统之外 —— 政策、规则、竞品、天气、上下游的业务决策。 对策:让业务方的重大变更(规则调整、活动、定价)以事件的形式打到你的监控时间轴上, 排查时能一眼对上。🔗 第 11 章会讲这条"变更时间轴"。
🔬 六、对抗验证:完整可跑版
第二节说了思路,这里给能直接抄的实现。
import numpy as np, pandas as pd, lightgbm as lgb
from sklearn.model_selection import cross_val_predict
from sklearn.metrics import roc_auc_score
def adversarial_validation(train_df, live_df, n=50000, seed=0):
"""返回 (AUC, 每个特征的重要性)。AUC≈0.5 说明两批数据分不开。"""
rng = np.random.default_rng(seed)
a = train_df.sample(min(n, len(train_df)), random_state=seed)
b = live_df.sample(min(n, len(live_df)), random_state=seed)
X = pd.concat([a, b], ignore_index=True)
y = np.r_[np.zeros(len(a)), np.ones(len(b))] # ⭐ 训练集=0,线上=1
# ⭐ 一定要打乱,否则按时间排列的数据在 CV 里会泄漏
idx = rng.permutation(len(X)); X, y = X.iloc[idx], y[idx]
clf = lgb.LGBMClassifier(n_estimators=200, learning_rate=.05,
num_leaves=31, verbose=-1)
# ⭐ 用交叉验证的 out-of-fold 概率算 AUC,避免自己评自己
oof = cross_val_predict(clf, X, y, cv=5, method="predict_proba")[:, 1]
auc = roc_auc_score(y, oof)
clf.fit(X, y)
imp = pd.Series(clf.feature_importances_, index=X.columns) \
.sort_values(ascending=False)
return auc, imp
| AUC 范围 | 原文判断 |
|---|---|
| ≤ 0.55 | 两批数据基本一致 ✅ |
| 0.55 ~ 0.70 | 有可察觉的漂移 ⚠️ 看 imp 前几名 |
| 0.70 ~ 0.90 | 明显漂移 🔴 |
| > 0.90 | 💀 几乎可以完美区分 |
—— 先怀疑【泄漏了时间类特征】(如 event_date、id 自增)
⚠️ AUC > 0.9 时的第一反应不该是"漂移好严重",而是 "我是不是把时间戳、自增 ID 这类特征喂进去了" —— 这类特征天然能完美区分新旧数据,会把真正的漂移信号淹掉。 规则:对抗验证前先剔除所有单调递增/时间相关的字段。
⚠️ 漂移检测的六个坑
| 坑 | 后果 | 怎么修 |
|---|---|---|
| 分桶边界每次重算 | 永远算不出漂移(两边各自等分位)💀 | 边界随模型一起保存 ⭐ |
| 样本量太小就报警 | 1000 条样本下 PSI 天然波动 0.02~0.05 | 定最小样本量;小样本时看置信区间 |
| 对抗验证喂了时间类特征 | AUC 虚高到 0.99,掩盖真信号 | 先剔除 id / 时间戳 ⭐ |
| 只看特征不看标签延迟后的指标 | 对概念漂移完全失明 💀💀 | 必须有滑动窗口的真实指标 |
| 把周期性当成漂移 | 每周一、每个月初都告警 | 用同比(第 8 章)而不是环比 |
| 发现漂移就自动重训 | 管道故障时把错误固化 💀 | 先分类再动作(第四节那张表)⭐ |
📐 一个能省很多误报的数字
PSI 阈值离不开样本量
| 样本量 | PSI 的典型随机波动 | 该怎么读 |
|---|---|---|
| 500 | 0.05 ~ 0.12 | 阈值 0.1 会天天报警 |
| 2,000 | 0.02 ~ 0.05 | 正常波动仍然不可忽略 |
| 10,000 | 0.005 ~ 0.015 | 经典阈值开始更可信 |
| 100,000 | < 0.005 | 0.1 才是有意义的阈值 |
PSI 的经典阈值(0.1 / 0.25)默认每个格子有上万条样本;分人群、分渠道后,误报会显著增加。
🔗 七、和站内其他章的关系
| 相关的地方 | 和这一章的关系 |
|---|---|
| 第 1 章四种变差方式 | 这一章展开其中的 ②③ |
| 第 5 章数据层监控 | 漂移检测是它的一部分 |
| Kaggle 23对抗验证 | 同一个工具,换了场景 |
| 数学原理 08 | 分布变了意味着测试分布≠训练分布 |
✅ 检查点
- 数据漂移、概念漂移、标签漂移分别是什么变了?
- 用信贷的例子说明三者的区别。
- 哪一种最省力?为什么?
- PSI 的三档阈值是什么?它有哪两个坑?
- 对抗验证怎么做?它比逐特征检验强在哪?
- 为什么概念漂移比数据漂移难发现?
- 概念漂移的三种形态里哪种最危险?对策是什么?
- 发现漂移后的判断顺序是什么?哪种情况绝不能重训?
- ETA 那个事故里,为什么 PSI、对抗验证、预测分布全都没报?唯一报出来的是什么?
- 这个事故给出的最重要结论是什么?附加教训呢?
- 对抗验证 AUC > 0.9 时,第一反应应该是什么?
- 漂移检测的六个坑里,哪两个会让你对概念漂移完全失明 / 把错误固化?
- PSI 的经典阈值默认你有多少样本?「拆细之后到处都在报警」的真正原因是什么?
👀 答案
- 数据漂移 P(x) 变、P(y|x) 不变;概念漂移 P(y|x) 变;标签漂移 P(y) 变。
- 数据漂移:渠道换了,来的人从 35 岁白领变 22 岁学生,但"收入高违约低"仍成立。概念漂移:经济变了,同样收入年龄的人违约率整体上升,关系本身变了。标签漂移:风控放松,坏账率 2%→5%,排序可能还对但概率不再校准。
- 标签漂移。常常只需要重新校准或调阈值,比重训便宜得多。
- PSI <0.1 稳定、0.1~0.25 关注、>0.25 显著漂移。两个坑:分桶必须按训练集切且边界存下来(各自切永远算不出漂移)、样本量小于 1000 时不稳。
- 训练集标 0、线上标 1,训分类器区分;AUC≈0.5 无漂移,明显>0.5 有漂移;更值钱的是看它的特征重要性找出元凶。强在能发现联合分布的漂移——每个特征单看正常但组合变了。
- 因为数据漂移看输入就能发现(不需要标签),概念漂移必须知道真实答案(需要标签),而标签常常要等很久。
- 渐变最危险——每天掉一点点,任何单日阈值都不触发,三个月后已掉 20%。对策:除了日环比,要看和 30 天前、90 天前比。
- ①管道坏了吗 ②数据漂移吗 ③概念漂移吗 ④标签漂移吗。管道故障时绝不能重训——会把错误固化。
- 因为平台改了骑手并单规则,输入特征一个都没变(距离、时段、骑手密度全一样),变的是"同样输入下实际配送时长多了 4 分钟",即 P(y|x)。PSI 全 <0.06、对抗验证 AUC 0.52、预测均值 31.2→31.5,它们全都只看 P(x)。唯一报出来的是滑动窗口 MAE:6.1 → 9.8 分钟(唯一看了真实答案的指标)。
- 漂移监控(PSI/对抗验证)只能发现数据漂移,对概念漂移是完全失明的——无论漂移看板多绿,都必须另外有一条"看真实答案"的线。附加教训:概念漂移的原因常常在你的系统之外(政策、规则、竞品、上下游业务决策),所以要让业务方的重大变更以事件的形式打到监控时间轴上。
- 不是"漂移好严重",而是"我是不是把时间戳、自增 ID 这类单调特征喂进去了"——它们天然能完美区分新旧数据,会把真正的漂移信号淹掉。对抗验证前先剔除所有时间相关字段。
- 只看特征不看标签延迟后的真实指标 → 对概念漂移完全失明;发现漂移就自动重训 → 管道故障时把错误固化。后者的修法是先分类再动作。
- 默认你有上万条样本(500 条时 PSI 的随机波动就有 0.05~0.12,阈值 0.1 会天天报警)。「拆细之后到处报警」的真正原因是分人群/分渠道后每个格子的样本量掉得很快,PSI 的随机波动随之变大。
🛑 可以停在这里
⚡ 走神救援
⭐ 三种漂移,分清才知道该做什么:数据漂移是来的人变了、规律没变;概念漂移是同样的人行为变了、关系本身变了;标签漂移是正例比例变了——⭐ 标签漂移最省力,常常只要重新校准或调阈值,不用重训。
检测数据漂移两把工具。PSI 的两个坑:分桶必须按训练集切且边界要存下来、⚠️ 样本少的时候它本身就不稳。⭐ 对抗验证更值钱的地方不是那个 AUC,是它的特征重要性——它直接告诉你元凶是谁,而且能发现逐特征看不出来的联合分布漂移。
⭐ 概念漂移难在必须有标签才能发现(数据漂移看输入就够)。三种形态里渐变最危险:⚠️ 每天掉一点点,任何单日阈值都不触发——所以要和一个月前、三个月前比,不能只看日环比。
发现漂移后的处置有顺序:先问管道是不是坏了(💀 管道坏了绝不能重训,那会把错误固化),再分数据漂移、概念漂移、标签漂移分别处理。
💀 那个「PSI 全绿但模型已经烂了」的事故是全章的支点:平台改了一条业务规则,输入特征一个都没变——PSI、对抗验证、预测均值全部正常,而同样的输入下真实结果已经偏了,唯一报出来的是那条看真实答案的误差曲线。
⭐⭐ 结论:PSI 和对抗验证只能发现数据漂移,对概念漂移完全失明——漂移看板再绿,也必须另有一条「看真实答案」的线。 ⭐ 附加教训:概念漂移的原因常常在你的系统之外(政策、规则、竞品)——把业务方的重大变更打到监控时间轴上。
⚠️ 两个反向的坑:对抗验证 AUC 高得离谱时,先怀疑自己喂了时间戳或自增 ID,而不是「漂移严重」;📐 PSI 的经典阈值默认你有上万条样本——样本少时随机波动就能超过阈值,⭐ 这才是「拆细之后到处都在报警」的真正原因。
下一节 👉 07-没有标签怎么发现退化.md ⭐