数据分布漂移检测:训练集与线上数据对比
模型上线后识别率下降,十有八九是数据分布变了。我见过太多团队花几周调模型结构,最后发现是线上数据跟训练集根本不是一码事。说白了,数据分布漂移是模型退化的头号元凶。
这一章我们聚焦三个核心手段:训练集与线上数据对比、PSI(群体稳定性指标)、特征分布可视化(KS检验)。掌握了这些,你就能快速定位「数据变了」还是「模型坏了」。
为什么数据分布会漂移?
先看几个真实场景:
- 用户行为变化:疫情期间线上购物暴增,疫情后回落。模型在疫情期间训练,解封后自然不准。
- 数据采集方式变更:换了埋点 SDK,某个字段的缺失率从 5% 变成 30%。
- 业务策略调整:推荐系统改了排序规则,用户点击分布完全变了。
我个人习惯把漂移分成两类:协变量漂移(特征分布变了)和概念漂移(特征与标签的关系变了)。本章主要处理前者。
核心原则:先确认数据分布是否漂移,再谈模型优化。不要一上来就调参。
训练集与线上数据对比:第一步该做什么
拿到线上数据后,我通常会做三件事:
- 统计量对比:均值、方差、缺失率、极值。每个特征都算一遍。
- 分布形状对比:直方图叠在一起看,或者用核密度估计图。
- 类别特征频次对比:看每个类别的占比是否剧烈变化。
举个例子,假设训练集里「用户年龄」均值是 32 岁,线上突然变成 28 岁。这就要警惕了——是不是新用户群体涌入?
我的经验:对比时别只看均值。有一次我排查一个 CTR 模型,均值完全没变,但方差翻了一倍。后来发现是某个渠道的流量质量波动了。均值掩盖了问题。
PSI(群体稳定性指标):量化分布差异
PSI 是金融风控领域的老牌指标,用来衡量两个样本集在同一个特征上的分布差异。公式不复杂:
PSI = Σ (P_i - Q_i) * ln(P_i / Q_i)
其中:
P_i = 线上数据在第 i 个分箱中的占比
Q_i = 训练集在第 i 个分箱中的占比
计算步骤:
- 对特征分箱(等频或等距,通常 10 箱)。
- 分别统计训练集和线上数据在每个箱中的占比。
- 代入公式求和。
判断标准:
| PSI 值 | 含义 | 建议操作 |
|---|---|---|
| < 0.1 | 无显著漂移 | 继续监控 |
| 0.1 - 0.25 | 轻度漂移 | 需关注,准备重训 |
| > 0.25 | 严重漂移 | 立即排查,考虑重训或回滚 |
避坑指南:PSI 对分箱方式敏感。我曾经用等距分箱算出一个特征 PSI=0.3,换成等频分箱变成 0.08。建议两种都试一下,取更保守的结果。
代码实现很简单:
import numpy as np
import pandas as pd
def calculate_psi(train, online, bins=10):
# 等频分箱
_, edges = pd.qcut(train, q=bins, retbins=True, duplicates='drop')
train_bins = pd.cut(train, bins=edges, include_lowest=True).value_counts(normalize=True).sort_index()
online_bins = pd.cut(online, bins=edges, include_lowest=True).value_counts(normalize=True).sort_index()
# 防止除零
online_bins = online_bins.replace(0, 1e-6)
train_bins = train_bins.replace(0, 1e-6)
psi = np.sum((online_bins - train_bins) * np.log(online_bins / train_bins))
return psi
特征分布可视化:KS 检验
PSI 给出一个全局数值,但有时候我们需要更细粒度的判断——某个特征在训练集和线上数据上的分布是否显著不同。这时候用 KS 检验(Kolmogorov-Smirnov test)。
KS 统计量是两个累积分布函数(CDF)的最大垂直距离。值越大,分布差异越大。
KS = max |F_train(x) - F_online(x)|
其中 F 是累积分布函数
我一般配合可视化一起看:
- 画两条 CDF 曲线,标出最大差距的位置。
- 如果 KS 值 > 0.1 且 p-value < 0.05,认为分布有显著差异。
个人习惯:KS 检验对样本量敏感。线上数据量很大时,很小的差异也会显著。这时候别只看 p-value,结合业务判断「这个差异有没有实际影响」。
可视化代码:
import matplotlib.pyplot as plt
from scipy.stats import ks_2samp
def plot_ks(train, online, feature_name):
stat, p_value = ks_2samp(train, online)
# 计算 CDF
sorted_train = np.sort(train)
sorted_online = np.sort(online)
cdf_train = np.arange(1, len(sorted_train)+1) / len(sorted_train)
cdf_online = np.arange(1, len(sorted_online)+1) / len(sorted_online)
plt.figure(figsize=(10, 6))
plt.plot(sorted_train, cdf_train, label='Train', color='#3498db')
plt.plot(sorted_online, cdf_online, label='Online', color='#e74c3c')
# 标出最大差距
diffs = np.abs(cdf_train - np.interp(sorted_train, sorted_online, cdf_online))
max_idx = np.argmax(diffs)
plt.vlines(sorted_train[max_idx], cdf_train[max_idx],
np.interp(sorted_train[max_idx], sorted_online, cdf_online),
colors='gray', linestyles='--', label=f'KS={stat:.3f}')
plt.xlabel(feature_name)
plt.ylabel('CDF')
plt.legend()
plt.title(f'KS Test: {feature_name} (p={p_value:.2e})')
plt.show()
整体检测流程
下面这张图概括了数据分布漂移检测的完整流程:
实际案例:一次完整的漂移排查
去年我接手一个广告点击率模型,线上 AUC 从 0.78 掉到 0.71。团队慌了,准备重新训练。我说先别急,看看数据。
排查过程:
- 统计量对比:发现「用户活跃天数」这个特征,训练集均值 12.3 天,线上均值 8.1 天。方差也变了。
- PSI 计算:该特征 PSI = 0.31,严重漂移。
- KS 检验:KS = 0.24,p-value < 0.001,确认分布显著不同。
- 根因:原来业务方上线了一个新用户拉新活动,大量低活跃用户涌入。模型没见过这种分布。
解决方案不是调模型,而是:
- 用最近 7 天的线上数据做增量训练。
- 在特征工程中加入「用户生命周期阶段」这个新特征。
- 建立 PSI 监控看板,每天自动报警。
关键教训:数据漂移检测不是一次性工作。我建议每周跑一次 PSI,每次模型迭代前也跑一次。养成习惯后,你能在识别率下降之前就发现问题。
总结一下
数据分布漂移检测是模型诊断的第一道防线。记住三个工具:
- 统计量对比:快速扫描,发现异常特征。
- PSI:量化漂移程度,适合批量监控。
- KS 检验 + 可视化:深入分析,确认分布差异的具体位置。
嗯,这一章就到这里。工具和方法都给了,关键是用起来。下次模型掉点,先别急着调参,跑一遍漂移检测再说。