特征漂移指标 PSI
背景描述
稳定性指的是参与对比两者相同指标差异性很小。机器学习使用训练数据(训练集和验证集)建模,使用测试数据模拟生产环境数据测试模型结果,其建模的假设是:训练数据涵盖了该问题所有的案例数据,即训练数据和测试(生产)数据之间的差异是很小的。
但实际上这个假设是很难成立的,原因:
- 受限于数据收集方法,不可能收集到该问题的所有案例数据。
- 模型投产后,生产环境的数据随时间会受到自然环境、政策环境、市场环境等影响而发生变化并且不可预知。
模型的输出很可能是决策的依据,如果模型不能适应新数据,这是很大的风险点。因此,监控数据的稳定性并且及时更新模型是一件很重要的事情。
群体稳定性指标
群体稳定指标(Population Stability Index,PSI)反映了验证样本在各分箱段的分布与建模样本分布的稳定性,通常被用来筛选特征变量、评估模型稳定性。计算方法如下所示:
这里就先遵循拿来主义,“这个公式为什么就能表示稳定性”在此不做深究。下面对公式说一些说明:
A (actual)表示实际分布,即生产数据,E(expected)表示期望分布,即训练数据。期望生产数据尽可能和训练数据的分布一样。i 表示每个分箱。这个公式的意思是:将每个分箱下的生产数据和期望数据做数学计算后再将结果求和。
PSI | 稳定性 |
---|---|
0~0.1 | 稳定性很好 |
0.1~0.2 | 稍微有点不稳定 |
大于 0.2 | 不稳定,分析生产数据并判断是否要重新训练模型 |
PSI 代码实现
1.创建模拟数据
size = 5000
# 期望数据
p2 = np.random.normal(loc = 3, scale = 1, size = size)
# 实际数据
a2 = np.random.normal(loc = 3.5, scale = 0.75, size = size)
- 计算每个分箱的边界,推荐分箱数位 10-20,此处设置 10 个分箱
num_bins = 10
eps = 1e-4
min_val = min(min(p2), min(a2))
max_val = max(max(a2), max(p2))
bins = [min_val + (max_val - min_val)*(i)/num_bins for i in range(num_bins+1)]
bins[0] = min_val - eps # 修正下界
bins[-1] = max_val + eps # 修正上界
print(bins)
# [-0.4810252475657688, 0.229173950184835, 0.9392731479354388, 1.649372345686043, 2.3594715434366464, 3.0695707411872504, 3.7796699389378547, 4.489769136688458, 5.199868334439062, 5.909967532189666, 6.620166729940269]
- 将实际数据和期望数据分箱
# 计算数组总的元素属于哪个分箱
bins_p2 = pd.cut(p2, bins = bins, labels = range(1,num_bins+1))
# 将元素和封箱号对齐
df_p2 = pd.DataFrame({'p2': p2, 'bin': bins_p2})
# 统计每个封箱中的元素数量
grp_p2 = df_p2.groupby('bin').count()
# 计算每个分箱中元素数量占总数的百分比
grp_p2['percent_p2'] = grp_p2['p2'] / sum(grp_p2['p2'])
# 对实际数据也进行分享操作
bins_a2 = pd.cut(a2, bins = bins, labels = range(1,num_bins+1))
df_a2 = pd.DataFrame({'a2': a2, 'bin': bins_a2})
grp_a2 = df_a2.groupby('bin').count()
grp_a2['percent_a2'] = grp_a2['a2'] / sum(grp_a2['a2'])
# 比较 p2 和 a2 的分箱数据
psi_df = grp_p2.join(grp_a2, on = "bin", how = "inner")
print(psi_df)
bin | p2 | percent_p2 | a2 | percent_a2 |
---|---|---|---|---|
1 | 12 | 0.002400 | 0 | 0.0000 |
2 | 75 | 0.015003 | 3 | 0.0006 |
3 | 338 | 0.067614 | 36 | 0.0072 |
4 | 919 | 0.183837 | 309 | 0.0618 |
5 | 1297 | 0.259452 | 1061 | 0.2122 |
6 | 1293 | 0.258652 | 1883 | 0.3766 |
7 | 704 | 0.140828 | 1223 | 0.2446 |
8 | 278 | 0.055611 | 430 | 0.0860 |
9 | 70 | 0.014003 | 54 | 0.0108 |
10 | 13 | 0.002601 | 1 | 0.0002 |
- 计算各分箱的 psi
# 当 percent_p* 是 0 时,给其加上 eps。防止 0 参与运算抛异常。
psi_df['percent_p2'] = psi_df['percent_p2'].apply(lambda x: eps if x == 0 else x)
psi_df['percent_a2'] = psi_df['percent_a2'].apply(lambda x: eps if x == 0 else x)
# 计算每行的 psi
psi_df['psi'] = (psi_df['percent_p2'] - psi_df['percent_a2']) * np.log(psi_df['percent_p2'] / psi_df['percent_a2'])
print(psi_df)
bin | p2 | percent_p2 | a2 | percent_a2 | psi |
---|---|---|---|---|---|
1 | 12 | 0.002400 | 0 | 0.0000 | 0.007312 |
2 | 75 | 0.015003 | 3 | 0.0006 | 0.046364 |
3 | 338 | 0.067614 | 36 | 0.0072 | 0.135310 |
4 | 919 | 0.183837 | 309 | 0.0618 | 0.133038 |
5 | 1297 | 0.259452 | 1061 | 0.2122 | 0.009500 |
6 | 1293 | 0.258652 | 1883 | 0.3766 | 0.044313 |
7 | 704 | 0.140828 | 1223 | 0.2446 | 0.057291 |
8 | 278 | 0.055611 | 430 | 0.0860 | 0.013248 |
9 | 70 | 0.014003 | 54 | 0.0108 | 0.000832 |
10 | 13 | 0.002601 | 1 | 0.0002 | 0.006158 |
- 各分箱的 psi 求和
psi = psi_df['psi'].sum()
print(psi)
# 0.4533650280982507
通过上述代码实现可以看出:特征漂移实际是在计算预期数据和实际数据的分布差异情况。。
工程中的实际应用:PSI 和 CSI
PSI
以回归算法 ElasticNet()
拟合 y = a1*x1 + a2*x2 + a3*x3 + b
函数为例演示 psi 在工程中的使用。
size = 5000
# 合成训练需要的数据
x1 = np.random.normal(loc = 0, scale = 2, size = size)
x2 = np.random.normal(loc = 3, scale = 1, size = size)
x3 = np.random.normal(loc = 5, scale = 2, size = size)
y = [-2*x1 + 3.1415*x2 + 2.7183*x3 + 1.6180 for (x1, x2, x3) in zip(x1,x2,x3)]
# 划分训练集和验证集
X = pd.DataFrame({'x1': x1, 'x2': x2, 'x3': x3})
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.2)
# 训练模型(该模型尽可能拟合出 y = a1*x1 + a2*x2 + a3*x3 + b 表达式)
model = ElasticNet()
model.fit(X_train, y_train)
# 验证结果(模型线下的评测结果)
y_pred = model.predict(X_test)
# 再合成生产环境(线上)数据
z1 = np.random.normal(loc = 0, scale = 2, size = size)
z2 = np.random.normal(loc = 3.5, scale = 0.75, size = size)
z3 = np.random.normal(loc = 8, scale = 3, size = size)
# 计算模型的线上推理结果
Z = pd.DataFrame({'x1': z1, 'x2': z2, 'x3': z3})
z_pred = model.predict(Z)
"""
此时线下模型效果 y_pred 和线上模型效果 z_pred 都是一维的,令 p2 = y_pred, a2 = z_pred。
套用“PSI 代码实现部分”就可以计算出群体性稳定指标 PSI,最终判定出模型稳定性处于什么程度。
"""
通过计算 训练模型时的推理结果 和 线上模型的推理结果 的 psi 就能初步得出模型的稳定程度。
CSI
psi 只能宏观判断出模型的稳定程度,如果不稳定,到底是哪些特征引起的不稳定?此时就需要使用特征稳定性指标(CSI)进行判断。计算方式没有什么变化,只是用的数据不一样而已。
上述代码中训练数据特征 x1、x2、x3,线上数据特征 z1、z2、z3。
令 p2 = x1, a2 = z1, 计算出 psi1;
令 p2 = x2, a2 = z2, 计算出 psi2;
令 p2 = x3, a2 = z3, 计算出 psi3;
通过 psi* 就能看出是哪个特征引起的不稳定。
名词解释
-
在机器学习中,回归和分类分别适用什么场景?
归回使用预测值连续的场景。回归问题的目标是找到输入特征与输出值之间的关系,以便能够对未知数据进行预测。房价预测就是个典型的回归问题。分类问题适用于样本划分不同类型的场景。当目标变量是离散的、具有预定义类别的数据时,分类算法被用来预测样本的类别。分类任务的目标是构建一个模型,该模型根据输入特征将样本划分到正确的类型中。垃圾邮件分类就是分类问题。
-
模型评价指标已经能判断模型效果了,为什么还要用 psi 判断要不要重新训练模型?
两种评价的目的不一样,模型指标是判断模型训练的好不好,准确率有多高,能不能投产使用。psi 是模型已经上线了,受环境影响生产中的数据时刻在变化,psi 判断当前模型能不能适应这些变化的数据。psi 还有个作用是用来筛选特征。
参考资料
https://towardsdatascience.com/checking-model-stability-and-population-shift-with-psi-and-csi-6d12af008783