3. PSI 指标原理:群体稳定性指标
群体稳定性指标(Population Stability Index, PSI)是衡量两个样本分布之间差异程度的统计量,在风控、营销和模型监控领域被广泛用于检测特征或模型分数的分布漂移。PSI 的核心思想是:如果模型在训练集上的预测分布与当前生产环境中的预测分布一致,则模型表现稳定;反之,分布差异越大,模型失效的风险越高。
3.1 数学定义与公式
PSI 的计算基于信息论中的 KL 散度(Kullback-Leibler Divergence)的对称化变体。其标准公式如下:
PSI = Σ (P_i - Q_i) * ln(P_i / Q_i)
其中:
- P_i:实际分布(Actual)在第 i 个分箱中的样本占比,通常指当前生产数据或验证数据。
- Q_i:期望分布(Expected)在第 i 个分箱中的样本占比,通常指训练数据或基准数据。
- ln(P_i / Q_i):两个占比比值的自然对数,衡量该分箱内的偏移程度。
- Σ:对所有分箱求和,得到最终的 PSI 值。
数学本质: PSI 实际上是两个多项分布之间的对称化 KL 散度。当 P 和 Q 完全相同时,每个分箱的贡献为 0,PSI = 0;差异越大,PSI 值越高。
3.2 分箱策略
PSI 的计算依赖分箱操作,不同的分箱策略会直接影响 PSI 的敏感度和稳定性。常用的分箱方法有三种:
| 分箱策略 | 方法描述 | 适用场景 | 注意事项 |
|---|---|---|---|
| 等距分箱 | 将数值范围均匀划分为 N 个区间,每个区间宽度相同 | 特征值分布均匀、无极端异常值 | 容易产生空箱或样本极少的箱子,导致 ln(P_i/Q_i) 计算不稳定 |
| 等频分箱 | 按样本数量均分,使每个箱子包含大致相同的样本数 | 大多数通用场景,尤其是模型分数监控 | 分箱边界随数据分布变化,跨时间对比时需固定分箱点 |
| 自定义分箱 | 根据业务知识或模型分数阈值手动设定分箱边界 | 评分卡模型、信贷风险等级划分 | 需要领域专家参与,分箱边界一旦确定不宜频繁修改 |
推荐做法: 在模型监控中,通常使用训练集的分位数作为固定分箱边界,对生产数据应用相同的分箱点。这样既能保证分箱的稳定性,又能避免因数据分布变化导致分箱结构改变。
分箱数量 N 的选择也至关重要。N 过小会掩盖局部漂移,N 过大则容易引入噪声。经验上,对于模型分数(如 0~1 的概率值),推荐使用 10~20 个分箱;对于连续特征,可根据样本量调整,通常取 10~30 个分箱。
3.3 PSI 阈值解读
PSI 的数值没有绝对的上限,但在实际工程中,行业通用阈值如下:
| PSI 范围 | 漂移程度 | 建议行动 |
|---|---|---|
| PSI < 0.1 | 无显著漂移 | 模型稳定,无需干预 |
| 0.1 ≤ PSI < 0.25 | 轻度漂移 | 需关注,建议检查近期数据质量或业务环境变化 |
| PSI ≥ 0.25 | 严重漂移 | 必须触发告警,立即排查原因,考虑模型重训或回滚 |
阈值不是绝对的: 上述阈值来源于信贷风控领域的经验总结。对于不同业务场景(如推荐系统、异常检测),建议根据历史数据分布和业务容忍度自行校准阈值。例如,高精度要求的医疗诊断模型,可能将告警阈值下调至 0.15。
3.4 PSI 计算流程与可视化
下图展示了 PSI 从数据输入到告警决策的完整计算流程:
3.5 代码实现示例
以下 Python 代码展示了 PSI 的完整计算过程,包含等频分箱和阈值判断:
import numpy as np
import pandas as pd
def calculate_psi(expected, actual, bins=10, bucket_type='quantile'):
"""
计算群体稳定性指标 (PSI)
Parameters:
-----------
expected : array-like, 基准分布(训练集)
actual : array-like, 实际分布(生产数据)
bins : int, 分箱数量
bucket_type : str, 'quantile' 等频分箱, 'uniform' 等距分箱
Returns:
--------
psi : float, PSI 值
breakdown : DataFrame, 每个分箱的详细贡献
"""
# 1. 基于基准分布确定分箱边界
if bucket_type == 'quantile':
# 等频分箱:使用分位数
percentiles = np.linspace(0, 100, bins + 1)
breakpoints = np.percentile(expected, percentiles)
else:
# 等距分箱:均匀划分
breakpoints = np.linspace(
np.min(expected), np.max(expected), bins + 1
)
# 2. 确保边界唯一,避免除零
breakpoints = np.unique(breakpoints)
# 3. 对两个分布分别分箱
expected_bins = np.digitize(expected, breakpoints, right=True)
actual_bins = np.digitize(actual, breakpoints, right=True)
# 4. 计算每个分箱的占比
expected_counts = np.bincount(expected_bins, minlength=len(breakpoints)+1)[1:]
actual_counts = np.bincount(actual_bins, minlength=len(breakpoints)+1)[1:]
# 添加平滑因子,防止 ln(0)
epsilon = 1e-6
expected_pct = expected_counts / len(expected) + epsilon
actual_pct = actual_counts / len(actual) + epsilon
# 5. 计算 PSI
psi_values = (actual_pct - expected_pct) * np.log(actual_pct / expected_pct)
psi = np.sum(psi_values)
# 6. 构建详细分解表
breakdown = pd.DataFrame({
'分箱下限': breakpoints[:-1],
'分箱上限': breakpoints[1:],
'基准占比': expected_pct,
'实际占比': actual_pct,
'单项贡献': psi_values
})
return psi, breakdown
# 示例用法
np.random.seed(42)
train_scores = np.random.beta(2, 5, 10000) # 基准分布
prod_scores = np.random.beta(2.5, 4.5, 10000) # 生产分布(轻度漂移)
psi_value, detail = calculate_psi(train_scores, prod_scores, bins=10)
print(f"PSI = {psi_value:.4f}")
if psi_value < 0.1:
print("结论:无显著漂移,模型稳定")
elif psi_value < 0.25:
print("结论:轻度漂移,建议关注")
else:
print("结论:严重漂移,立即告警!")
工程注意点: 实际生产环境中,建议将分箱边界持久化存储(如数据库或配置文件),确保每次计算使用相同的分箱点。同时,对空箱或极少数样本的分箱添加平滑因子(如代码中的 epsilon),避免计算不稳定。