2. PSI 数学原理:PSI 计算公式详解
群体稳定性指标(Population Stability Index, PSI)是衡量两个分布之间差异的标准化度量。在风控与压力测试场景中,它用于量化模型评分或关键风险指标在 期望分布(开发/基准样本) 与 实际分布(当前/监控样本) 之间的偏移程度。PSI 值越小,分布越稳定;值越大,模型或风险特征发生显著漂移的可能性越高。
2.1 PSI 计算公式
PSI 的计算基于信息论中的 Kullback-Leibler 散度 对称化形式。其核心公式如下:
PSI = Σ (实际占比_i - 期望占比_i) * ln(实际占比_i / 期望占比_i)
其中:
- 实际占比_i:当前监控样本在第 i 个分箱中的样本比例。
- 期望占比_i:基准/开发样本在第 i 个分箱中的样本比例。
- ln:自然对数,用于放大比例差异的非线性效应。
- Σ:对所有分箱(i = 1 到 N)求和。
计算要点: 当某个分箱中期望占比为 0 时,公式会出现除零错误。实际应用中,通常对占比进行微小平滑处理(如加 0.0001),或确保分箱策略保证每个箱内均有样本。
2.2 分箱方法详解
分箱是 PSI 计算的前置步骤,直接影响指标的敏感性与稳定性。选择不当的分箱方法可能导致 PSI 值失真或无法有效捕捉分布漂移。三种主流分箱方法对比如下:
| 分箱方法 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 等距分箱 | 将变量取值范围划分为宽度相等的区间 | 实现简单,区间边界直观 | 数据分布不均时,部分箱可能样本极少或为空 | 变量分布均匀、无极端值 |
| 等频分箱 | 每个分箱包含大致相同数量的样本 | 保证每个箱有足够样本,避免零占比 | 边界值可能落在数据密集区,导致区间宽度差异大 | 样本量充足、分布偏态 |
| 分位数分箱 | 基于数据的分位数(如十分位数、二十分位数)划分边界 | 对异常值鲁棒,能反映数据内在分布结构 | 分位数点计算需足够样本量,边界解释性稍弱 | 压力测试、长尾分布、高维评分卡监控 |
行业实践建议: 在风控模型监控中,分位数分箱(通常为 10 箱或 20 箱)是最常用的方法。它既能保证每个箱内样本量充足,又能捕捉评分分布尾部的细微变化,特别适合压力测试场景下的早期预警。
2.3 对数变换的意义
PSI 公式中的自然对数变换(ln)并非随意选择,它具有深刻的数学与统计意义:
- 非线性放大差异: 当实际占比与期望占比差异较小时,ln 值接近 0;当差异增大时,ln 值呈非线性增长,使 PSI 对中等程度的漂移更为敏感。
- 对称性处理: 对数变换使得 PSI 对“实际占比高于期望”和“实际占比低于期望”两种方向的偏移具有对称的惩罚力度,避免单向偏差。
- 信息论基础: ln 变换源自信息熵与 KL 散度,使 PSI 具有可加性与可解释性——PSI 值可以理解为两个分布之间的“信息损失”或“不确定性增量”。
- 标准化尺度: 经过对数变换后,PSI 值通常落在 0 到 1 之间(极端情况可能超过 1),便于设定统一的预警阈值。
常见误区: 部分实践者误以为 PSI 公式中的 ln 可以用 log10 或 log2 替代。虽然数学上可行,但会导致 PSI 阈值标准失效。行业通用的 PSI 阈值(如 <0.1 为稳定,0.1-0.25 需关注,>0.25 需干预)均基于自然对数推导,使用其他底数需重新校准阈值。
2.4 PSI 计算流程与分布漂移可视化
下图展示了从原始数据到 PSI 值的完整计算流程,以及分布漂移的直观对比:
2.5 分箱方法选择决策树
在实际项目中,选择合适的分箱方法可参考以下决策逻辑:
2.6 PSI 计算代码示例(Python)
以下代码展示了使用等频分箱计算 PSI 的完整实现:
import numpy as np
import pandas as pd
def calculate_psi(expected, actual, bins=10):
"""
计算群体稳定性指标(PSI)
参数:
expected: array-like, 期望分布(基准样本)
actual: array-like, 实际分布(当前监控样本)
bins: int, 分箱数量(默认10箱)
返回:
psi_value: float, PSI值
bin_details: DataFrame, 各分箱详细占比
"""
# 基于期望分布确定分位数边界
percentiles = np.linspace(0, 100, bins + 1)
bin_edges = np.percentile(expected, percentiles)
# 处理边界重复(当数据中存在大量相同值时)
bin_edges = np.unique(bin_edges)
# 对期望分布分箱
expected_counts, _ = np.histogram(expected, bins=bin_edges)
expected_pct = expected_counts / len(expected)
# 对实际分布分箱(使用相同的边界)
actual_counts, _ = np.histogram(actual, bins=bin_edges)
actual_pct = actual_counts / len(actual)
# 平滑处理:避免零占比导致除零错误
epsilon = 1e-6
expected_pct = np.clip(expected_pct, epsilon, 1 - epsilon)
actual_pct = np.clip(actual_pct, epsilon, 1 - epsilon)
# 计算每个分箱的PSI贡献
psi_components = (actual_pct - expected_pct) * np.log(actual_pct / expected_pct)
psi_value = np.sum(psi_components)
# 构建结果DataFrame
bin_details = pd.DataFrame({
'bin_start': bin_edges[:-1],
'bin_end': bin_edges[1:],
'expected_pct': expected_pct,
'actual_pct': actual_pct,
'psi_component': psi_components
})
return psi_value, bin_details
# 示例用法
np.random.seed(42)
expected_scores = np.random.normal(600, 100, 10000)
actual_scores = np.random.normal(620, 110, 10000) # 模拟分布偏移
psi, details = calculate_psi(expected_scores, actual_scores, bins=10)
print(f"PSI值: {psi:.4f}")
print(details.head())
关键代码说明:
np.percentile基于期望分布计算分位数边界,实现分位数分箱。np.clip对占比进行平滑处理,避免 ln(0) 导致的数学错误。- PSI 值 < 0.1 表示分布稳定;0.1-0.25 表示轻度漂移,需关注;> 0.25 表示显著漂移,需立即干预。