4. 数据漂移检测实战
在金融风控、信用评分和量化交易场景中,模型上线只是起点。当输入特征分布或标签分布随时间发生偏移,模型的预测能力会悄然衰减。本节聚焦五类主流漂移度量指标,从原理、计算流程到Python落地,帮助你把“感觉模型变差了”转化为可量化、可告警、可追溯的工程信号。
一、核心原理:五种指标在度量什么
漂移检测的本质,是比较两个分布之间的差异。参考分布通常来自训练集或上一个稳定窗口,当前分布来自最新生产数据。不同指标对差异的敏感维度不同:
- PSI(群体稳定性指数):基于分箱后的占比差异,计算
Σ(实际占比-预期占比)×ln(实际占比/预期占比),对分箱边界和样本量敏感,是金融风控最常用的稳定性指标。 - KS检验:基于经验累积分布函数的最大垂直距离,输出统计量与p值,适合连续变量,对分布整体位置和形状变化敏感。
- KL散度:衡量用当前分布编码参考分布时的信息损失,非对称,要求两分布支撑集重叠,否则趋于无穷。
- JS散度:KL散度的对称化与平滑版本,取值在0到1之间(以2为底),对零概率更稳健。
- Wasserstein距离:又称推土机距离,衡量把一个分布“搬运”成另一个分布的最小代价,保留几何结构,对分布平移和缩放敏感。
选择建议:风控评分卡优先PSI;连续特征快速筛查用KS;需要对称且稳健的分布距离用JS;关注分布整体迁移幅度且样本量充足时用Wasserstein。
二、方法流程:从数据准备到告警输出
- 确定参考窗口与当前窗口:参考窗口建议取训练集或最近一个稳定周期,当前窗口取最近7天或30天,避免窗口过短导致噪声。
- 特征筛选与预处理:连续变量做分箱或核密度估计,类别变量做频次编码,缺失值单独成箱。
- 逐指标计算:对每个特征分别计算PSI、KS、KL、JS、Wasserstein,形成特征级漂移矩阵。
- 阈值判定与分级:结合业务容忍度设定绿/黄/红三级阈值,输出告警清单。
- 根因定位:对红色特征下钻到分箱、时间切片和客群维度,定位漂移来源。
三、Python实现:五指标统一计算函数
下面代码以信用评分场景为例,使用scipy与numpy实现五指标计算。注意KL与JS需先对连续变量做分箱或直方图归一化,避免零概率导致无穷值。
import numpy as np
from scipy import stats
from scipy.spatial.distance import jensenshannon
def psi(expected, actual, bins=10):
breakpoints = np.percentile(expected, np.linspace(0, 100, bins + 1))
breakpoints[0], breakpoints[-1] = -np.inf, np.inf
e_perc = np.histogram(expected, bins=breakpoints)[0] / len(expected)
a_perc = np.histogram(actual, bins=breakpoints)[0] / len(actual)
e_perc = np.where(e_perc == 0, 1e-6, e_perc)
a_perc = np.where(a_perc == 0, 1e-6, a_perc)
return np.sum((a_perc - e_perc) * np.log(a_perc / e_perc))
def ks_test(expected, actual):
stat, p_value = stats.ks_2samp(expected, actual)
return stat, p_value
def kl_divergence(expected, actual, bins=50):
hist_range = (min(expected.min(), actual.min()), max(expected.max(), actual.max()))
e_hist, _ = np.histogram(expected, bins=bins, range=hist_range, density=True)
a_hist, _ = np.histogram(actual, bins=bins, range=hist_range, density=True)
e_hist = np.where(e_hist == 0, 1e-6, e_hist)
a_hist = np.where(a_hist == 0, 1e-6, a_hist)
return stats.entropy(e_hist, a_hist)
def js_divergence(expected, actual, bins=50):
hist_range = (min(expected.min(), actual.min()), max(expected.max(), actual.max()))
e_hist, _ = np.histogram(expected, bins=bins, range=hist_range, density=True)
a_hist, _ = np.histogram(actual, bins=bins, range=hist_range, density=True)
e_hist = np.where(e_hist == 0, 1e-6, e_hist)
a_hist = np.where(a_hist == 0, 1e-6, a_hist)
return jensenshannon(e_hist, a_hist) ** 2
def wasserstein_distance(expected, actual):
return stats.wasserstein_distance(expected, actual)
# 示例:对特征 credit_score 计算五指标
ref = np.random.normal(650, 50, 10000)
cur = np.random.normal(620, 55, 8000)
print("PSI:", round(psi(ref, cur), 4))
print("KS:", ks_test(ref, cur))
print("KL:", round(kl_divergence(ref, cur), 4))
print("JS:", round(js_divergence(ref, cur), 4))
print("Wasserstein:", round(wasserstein_distance(ref, cur), 4))
KL散度对零概率极其敏感,若某分箱在参考分布中为0,结果会趋于无穷。工程上必须做平滑处理,或改用JS散度。PSI的分箱边界必须固定为参考分布的分位数,否则跨窗口不可比。
四、案例与阈值清单
某消费金融公司对credit_score、debt_ratio、query_count_30d三个特征做周度漂移检测。参考窗口为训练集,当前窗口为最近一周。结果如下表:
| 特征 | PSI | KS统计量 | JS散度 | Wasserstein | 判定 |
|---|---|---|---|---|---|
| credit_score | 0.08 | 0.06 | 0.02 | 12.4 | 绿色 |
| debt_ratio | 0.31 | 0.18 | 0.09 | 0.07 | 红色 |
| query_count_30d | 0.14 | 0.09 | 0.04 | 0.35 | 黄色 |
复盘发现,debt_ratio漂移源于新接入渠道客群负债结构差异,属于渠道结构变化而非模型失效。团队随后按渠道分层重算PSI,红色告警降为黄色,并触发渠道准入策略复核。
五、常见误区与复盘要点
- 误区一:只看PSI不看样本量。小样本下PSI波动剧烈,建议当前窗口样本量不低于5000,否则改用KS或JS。
- 误区二:所有特征用同一阈值。评分卡核心变量阈值应更严,辅助变量可放宽,需按特征重要性分级。
- 误区三:忽略分箱边界一致性。参考窗口与当前窗口必须使用同一套分箱边界,否则PSI不可比。
- 误区四:把漂移等同于模型失效。漂移是信号,需结合业务事件、渠道变化、客群迁移做根因诊断,再决定是否重训。
- 复盘要点:建立特征级漂移台账,记录每次告警的时间、指标值、根因和处置动作,形成可追溯的模型治理闭环。
落地建议:将五指标封装为统一漂移检测函数,按日/周调度,输出特征级漂移矩阵与分级告警。对红色特征自动触发根因下钻任务,对连续三周黄色特征提前预警,把被动救火转为主动治理。