📑 本页目录(点开跳转)
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)$ 变 | 正样本整体比例变了(如欺诈率上升) |
💡 一组对照例子(同一个信贷模型):
数据漂移:
营销渠道换了 → 来的用户从 35 岁白领变成 22 岁学生
→ 「收入高的人违约率低」这条规律【依然成立】
→ 重新训练(用新人群的数据)就能修好 ✅
概念漂移:
经济环境变了 → 同样收入、同样年龄的人,违约率整体上升
→ 「收入 → 违约率」这条【关系本身变了】
→ 必须用新数据重训,而且【旧数据可能有害】⭐
标签漂移:
风控策略放松 → 通过率提高 → 坏账率从 2% 涨到 5%
→ 模型排序可能还对,但【输出的概率不再校准】
→ 修法可能只需要重新校准,不用重训 ⭐
⭐ 最后一行是个实用的省力点: 标签漂移常常只需要调整阈值或做一次概率重校准, 比重训模型便宜得多。先判断类型,再决定动作。
🔍 二、怎么检测数据漂移
方法 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 有变化 ⚠️ 关注
PSI > 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:对抗验证(一次看全部特征)⭐⭐
① 训练集样本标成 0,线上样本标成 1
② 训一个分类器区分它们
③ 如果 AUC ≈ 0.5 → 两批数据分不开 → 没有漂移 ✅
如果 AUC 明显 > 0.5 → 有漂移 🔴
⭐ 更值钱的是第 ④ 步:
④ 看这个分类器的【特征重要性】—— 排在最前的就是漂移的元凶
🔗 这个方法你在 Kaggle 23 见过 (那里用它检测训练集和测试集的分布差异)。 同一个工具,换个场景:那里比的是训练集 vs 测试集,这里比的是训练集 vs 线上流量。
⭐ 它比逐特征检验强的地方:能发现「联合分布」的漂移 —— 每个特征单看都正常,但它们的组合变了(比如「高收入 + 年轻」这个组合突然变多)。
🕵️ 三、概念漂移:难在哪、怎么发现
数据漂移:看输入就能发现 ← 不需要标签 ✅
概念漂移:必须知道【真实答案】才能发现 ← 需要标签 💀
这就是它难的根本原因:标签常常要等很久(第 7 章专门讲这个)。
三种形态
① 突变(sudden)
政策变了、竞品上线、疫情开始
└ 表现:某一天指标断崖式下跌
② 渐变(gradual)⭐ 最危险
用户习惯慢慢改变
└ 表现:每天掉一点点,单看哪天都在正常波动内
└ 三个月后回头看,已经掉了 20%
③ 周期性(recurring)
季节、节假日、工作日/周末
└ 不是"坏",但模型要能处理
⭐ ② 最危险的原因:任何单日的告警阈值都不会触发。 对策:除了日环比,一定要看「和 30 天前比」「和 90 天前比」。
一个便宜的检测手段:滑动窗口 AUC
把有标签的样本按时间排,算每周的 AUC
周1 0.82
周2 0.81
周3 0.81
周4 0.79 ← 单看是正常波动
周5 0.78
周6 0.76 ← 和周1比已经掉了 0.06 🔴
⭐ 关键是【和基线比】,不是和上周比
🛠️ 四、发现了漂移,做什么
别急着重训。按这个顺序判断:
① 是数据管道坏了吗?
→ 突变 + 只有个别特征异常 + 缺失率变化
→ 【先修管道,绝不重训】⭐(重训会把错误固化,见第 1 章)
② 是数据漂移吗(P(x) 变)?
→ 输入分布变了,但有标签的样本上模型还准
→ 用新数据重训 / 加样本权重
③ 是概念漂移吗(P(y|x) 变)?
→ 输入分布没怎么变,但预测准确率掉了
→ 必须用新数据重训,且【要考虑丢掉太旧的数据】⭐
④ 是标签漂移吗(P(y) 变)?
→ 排序还对,但概率不准了
→ 可能只需重新校准,不用重训 ⭐ 最便宜
| 情况 | 该做的 | 不该做的 |
|---|---|---|
| 管道故障 | 修管道 | ❌ 重训 |
| 数据漂移 | 重训 / 加权 / 扩样本 | ❌ 只调阈值 |
| 概念漂移 | 重训 + 调整训练窗口 | ❌ 无脑用全量历史 |
| 标签漂移 | 重校准 / 调阈值 | ❌ 大动干戈重训 |
🔗 重训练的具体策略(窗口多长、多久一次、怎么验证)→ 第 16 章
💀 五、一个"PSI 全绿但模型已经烂了"的真事故
这是概念漂移最典型的样子,也是漂移监控最尴尬的时刻。
某外卖平台的配送时长(ETA)预估模型
特征:距离、时段、天气、商家出餐历史、骑手密度……
某个月开始,用户投诉"预计 30 分钟实际 45 分钟"暴涨
查漂移监控:
· 全部 47 个特征的 PSI 都 < 0.06 ✅ 全绿
· 对抗验证 AUC = 0.52 ✅ 分不开
· 预测值分布:均值 31.2 → 31.5 分钟 ✅ 几乎没动
→ 监控体系一个字都没说
真正发生的事:
平台调整了骑手的接单规则(一次最多并单从 3 单改成 5 单)
→ 【输入特征一个都没变】:距离一样、时段一样、骑手密度一样
→ 但【同样的输入,实际配送时长普遍多了 4 分钟】
⭐ 这就是教科书定义里的 P(y|x) 变了 —— 而且是外部规则造成的
| 检测手段 | 结果 | 为什么 |
|---|---|---|
| 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 的随机波动和样本量的关系(10 桶、无真实漂移时的经验值):
样本量 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 的随机波动随之变大。
🛑 可以停在这里
⚡ 走神救援
⭐三种漂移:数据漂移 P(x)变(来的人变了,规律没变)、概念漂移 P(y|x)变(同样的人行为变了,关系本身变了)、标签漂移 P(y)变(正例比例变,排序可能还对但概率不再校准)。⭐标签漂移最省力——常常只要重新校准或调阈值,不用重训。检测数据漂移:PSI(<0.1稳定 / 0.1~0.25关注 / >0.25显著漂移;两个坑:分桶必须按训练集切且边界存下来、样本<1000时不稳);⭐⭐对抗验证(训练集标0线上标1,训分类器,AUC≈0.5说明无漂移;更值钱的是看它的特征重要性找元凶;比逐特征强在能发现联合分布漂移)。⭐概念漂移难在必须有标签才能发现(数据漂移看输入就行)。三形态里渐变最危险——每天掉一点点,任何单日阈值都不触发,三个月后掉20% → 要看和30天前、90天前比,不只是日环比。发现漂移后的顺序:①管道坏了吗(💀绝不重训,会把错误固化) ②数据漂移(重训/加权)③概念漂移(重训+调整训练窗口,丢掉太旧的数据)④标签漂移(重校准即可)。💀一个"PSI全绿但模型已经烂了"的真事故:外卖 ETA 模型,平台把骑手并单上限从 3 改到 5,输入特征一个都没变(PSI 全 <0.06、对抗验证 AUC 0.52、预测均值 31.2→31.5),但同样输入下实际时长多了 4 分钟;唯一报出来的是滑动窗口 MAE 6.1→9.8。⭐⭐结论:PSI / 对抗验证只能发现数据漂移,对概念漂移完全失明——漂移看板再绿,也必须另有一条"看真实答案"的线。附加教训:概念漂移的原因常在你的系统之外(政策/规则/竞品)→ 把业务方的重大变更打到监控时间轴上。⚠️对抗验证 AUC>0.9 时先怀疑喂了时间戳/自增ID,不是"漂移严重"。六个坑:分桶边界每次重算、样本太小就报警、喂了时间类特征、只看特征不看真实指标、把周期性当漂移、发现漂移就自动重训。📐PSI 的经典阈值默认你有上万条样本:500 条时随机波动就有 0.05~0.12 —— 这才是"拆细之后到处都在报警"的真正原因。
下一节 👉 07-没有标签怎么发现退化.md ⭐⭐