2. PSI计算原理:分箱法计算PSI的步骤详解
群体稳定性指标(Population Stability Index, PSI)是衡量两个样本分布差异的标准化度量。在信用额度管理中,PSI用于监控模型评分或额度分配策略在时间维度上的稳定性。其核心计算逻辑基于分箱法,通过比较“期望分布”(基准分布)与“实际分布”(当前分布)在每个分箱中的占比差异,量化整体偏移程度。
2.1 分箱法计算PSI的完整步骤
PSI的计算遵循严格的五步流程,每一步都直接影响最终指标的准确性。以下为详细步骤说明:
- 数据分箱(Binning):将连续变量(如信用评分、额度使用率)或离散变量划分为K个互斥的区间(箱)。分箱方式包括等距分箱、等频分箱或基于业务规则的分箱。
- 计算期望占比(Expected Proportion, Ei):统计基准样本(如模型开发样本或上期数据)落在第i个箱中的样本数,除以基准样本总数,得到期望占比。
- 计算实际占比(Actual Proportion, Ai):统计当前监控样本落在第i个箱中的样本数,除以当前样本总数,得到实际占比。
- 计算自然对数比值(Ln(Ai/Ei)):对每个箱,计算实际占比与期望占比的比值,再取自然对数。该步骤用于放大微小差异,并赋予正负方向。
- 加权求和得到PSI值:将每个箱的差值(Ai - Ei)与自然对数比值相乘,并对所有箱求和,得到最终的PSI值。
PSI计算公式:
PSI = Σi=1K (Ai - Ei) × ln(Ai / Ei)
PSI = Σi=1K (Ai - Ei) × ln(Ai / Ei)
2.2 分箱规则与注意事项
分箱是PSI计算中最关键的预处理步骤。不合理的分箱会导致PSI值失真,掩盖真实的分布偏移。以下是分箱的核心规则:
| 分箱规则 | 说明 | 注意事项 |
|---|---|---|
| 等频分箱 | 每个箱包含大致相等的样本数 | 适用于分布均匀的场景;若数据集中有大量重复值,可能导致分箱边界不稳定 |
| 等距分箱 | 按变量值域等间隔划分 | 简单直观,但可能造成某些箱样本过少(稀疏箱),导致PSI计算不稳定 |
| 业务分箱 | 基于业务阈值(如额度区间、风险等级)划分 | 可解释性强,但需确保基准与监控样本使用完全相同的分箱边界 |
| 箱数选择 | 通常建议K=10~20个箱 | 箱数过少会丢失分布细节;箱数过多会导致噪声放大,建议根据样本量调整 |
⚠️ 关键避坑: 当某个箱的期望占比Ei = 0时,ln(Ai/Ei) 无定义。实际处理中,需对空箱进行合并或添加极小平滑值(如0.0001),避免计算异常。
2.3 自然对数在PSI中的作用
自然对数(ln)是PSI公式中的核心算子,其作用体现在三个方面:
- 对称性处理:ln(Ai/Ei) 使得正向偏移(Ai > Ei)和负向偏移(Ai < Ei)在数值上具有对称的惩罚力度。
- 非线性放大:当Ai与Ei差异较小时,ln值接近0;当差异较大时,ln值呈非线性增长,有效放大显著偏移。
- 方向指示:ln(Ai/Ei) 的正负号直接指示了实际占比相对于期望占比的增减方向,便于定位偏移来源。
2.4 PSI计算流程图
以下SVG图展示了从原始数据到PSI值的完整计算链路,包含分箱、占比计算、对数变换和加权求和四个核心阶段:
2.5 Python代码实现PSI计算
以下代码展示了使用Python实现分箱法PSI计算的完整过程,包含等频分箱、占比计算和对数变换:
import numpy as np
import pandas as pd
def calculate_psi(expected, actual, bins=10, method='quantile'):
"""
计算群体稳定性指标(PSI)
参数:
expected: array-like, 基准样本数据
actual: array-like, 当前监控样本数据
bins: int, 分箱数量
method: str, 分箱方法 ('quantile' 等频, 'uniform' 等距)
返回:
psi: float, PSI值
bin_df: DataFrame, 每个箱的详细计算过程
"""
# 合并数据确定分箱边界
all_data = np.concatenate([expected, actual])
if method == 'quantile':
# 等频分箱:基于基准样本的分位数
bin_edges = np.percentile(expected, np.linspace(0, 100, bins + 1))
elif method == 'uniform':
# 等距分箱
bin_edges = np.linspace(np.min(all_data), np.max(all_data), bins + 1)
else:
raise ValueError("method 参数必须为 'quantile' 或 'uniform'")
# 确保边界唯一(处理重复值)
bin_edges = np.unique(bin_edges)
# 计算每个箱的样本数
expected_counts, _ = np.histogram(expected, bins=bin_edges)
actual_counts, _ = np.histogram(actual, bins=bin_edges)
# 计算占比(添加平滑值避免除零)
epsilon = 1e-6
expected_prop = expected_counts / len(expected) + epsilon
actual_prop = actual_counts / len(actual) + epsilon
# 计算每个箱的PSI分量
ratio = actual_prop / expected_prop
ln_ratio = np.log(ratio)
psi_components = (actual_prop - expected_prop) * ln_ratio
# 汇总PSI
psi = np.sum(psi_components)
# 构建结果DataFrame
bin_df = pd.DataFrame({
'箱边界': [f'{bin_edges[i]:.2f} - {bin_edges[i+1]:.2f}'
for i in range(len(bin_edges)-1)],
'期望占比': expected_prop - epsilon,
'实际占比': actual_prop - epsilon,
'比值(A/E)': ratio,
'ln(A/E)': ln_ratio,
'PSI分量': psi_components
})
return psi, bin_df
# 示例用法
np.random.seed(42)
expected_data = np.random.normal(600, 100, 10000) # 基准分布
actual_data = np.random.normal(620, 120, 10000) # 当前分布(均值偏移)
psi_value, detail_df = calculate_psi(expected_data, actual_data, bins=10)
print(f"PSI值: {psi_value:.4f}")
print(detail_df.head())
2.6 PSI值解读与阈值参考
计算得到PSI值后,需要根据行业经验阈值判断分布偏移的严重程度。以下为通用的PSI解读标准:
| PSI范围 | 稳定性等级 | 建议措施 |
|---|---|---|
| PSI < 0.10 | 稳定 | 无需干预,分布无显著变化 |
| 0.10 ≤ PSI < 0.25 | 轻度偏移 | 需关注,建议排查特征分布变化原因 |
| PSI ≥ 0.25 | 严重偏移 | 立即告警,需重新训练模型或调整额度策略 |
💡 实战建议: 在信用额度监控场景中,建议对每个额度区间(如0-5000元、5000-20000元、20000元以上)分别计算PSI,以定位具体哪个额度段发生了分布漂移。同时,结合业务周期(如月度、季度)设定动态阈值,避免季节性波动引发误告警。