3、PSI计算步骤详解:分箱、计算占比、计算对数比、求和
群体稳定性指标(PSI)的计算严格遵循四个核心步骤:分箱 → 计算占比 → 计算对数比 → 加权求和。每一步都直接影响最终PSI值的准确性与可解释性。下面我们逐一拆解,并配合图示与代码示例,确保你能够独立完成PSI的完整计算。
3.1 步骤一:分箱(Binning)
分箱是将连续型特征或离散型特征的取值划分为若干个互不相交的区间(箱)。分箱的目的是将原始分布“离散化”,使得后续的占比计算与比较具有统计意义。
分箱的常用策略:
| 分箱方法 | 描述 | 适用场景 |
|---|---|---|
| 等距分箱 | 将特征值范围均匀划分为K个区间,每个区间宽度相同。 | 特征分布均匀,无极端离群点。 |
| 等频分箱 | 使每个箱内包含大致相同数量的样本。 | 分布偏态严重,需保证每个箱都有足够样本。 |
| 决策树分箱 | 利用决策树算法,基于目标变量自动寻找最优切分点。 | 需要分箱与目标变量强关联时。 |
| 自定义分箱 | 根据业务经验或监管要求手动设定边界。 | 信用评分卡、利率区间等强业务约束场景。 |
💡 实践建议: 在监控场景中,推荐使用等频分箱(通常K=10或K=20)。等频分箱能确保每个箱在训练集(基准分布)中都有足够的样本量,避免因某个箱样本过少导致对数比计算不稳定。
分箱完成后,我们得到K个箱的边界。这些边界将同时应用于基准分布(训练集)和当前分布(监控集)。
3.2 步骤二:计算占比(Proportion Calculation)
在分箱完成后,我们需要分别计算每个箱在基准分布和当前分布中的样本占比。
计算公式:
P_i = n_i / N
- P_i:第 i 个箱的样本占比
- n_i:第 i 个箱中的样本数量
- N:总样本数量
关键注意事项:
- 基准占比(A_i)使用训练集或模型开发时的样本计算。
- 当前占比(B_i)使用监控周期内的实时样本计算。
- 若某个箱的样本占比为0,需进行平滑处理(如加一个小常数ε = 0.0001),避免后续对数计算出现无穷大。
⚠️ 平滑处理陷阱: 如果基准分布中某个箱占比为0,而当前分布中该箱占比不为0,直接计算会导致PSI值异常偏大。建议在分箱阶段就确保每个箱在基准分布中至少有1%的样本,从而避免频繁使用平滑。
3.3 步骤三:计算对数比(Log Ratio)
对数比是PSI公式的核心非线性部分,用于衡量两个占比之间的“偏离程度”。
计算公式:
对数比_i = ln(B_i / A_i)
- B_i:当前分布在第 i 个箱的占比
- A_i:基准分布在第 i 个箱的占比
- ln:自然对数(以e为底)
对数比的数学性质:
| 场景 | B_i / A_i | ln(B_i / A_i) | 对PSI的贡献 |
|---|---|---|---|
| 占比不变 | 1.0 | 0 | 无贡献 |
| 占比增加 | > 1.0 | 正值 | 正向贡献 |
| 占比减少 | < 1.0 | 负值 | 负向贡献 |
注意:对数比本身可以是负数,但经过下一步的加权求和后,PSI值始终为非负数。
3.4 步骤四:加权求和(Weighted Summation)
最后一步,将每个箱的占比差与对数比相乘,并对所有箱求和,得到最终的PSI值。
完整PSI公式:
PSI = Σ [(B_i - A_i) * ln(B_i / A_i)]
其中,求和符号 Σ 表示对 i = 1 到 K 个箱进行累加。
计算示例(K=3):
| 箱编号 | 基准占比 A_i | 当前占比 B_i | 占比差 (B_i - A_i) | 对数比 ln(B_i/A_i) | 单项贡献 |
|---|---|---|---|---|---|
| 1 | 0.30 | 0.25 | -0.05 | -0.1823 | 0.0091 |
| 2 | 0.40 | 0.45 | 0.05 | 0.1178 | 0.0059 |
| 3 | 0.30 | 0.30 | 0.00 | 0.0000 | 0.0000 |
| PSI = Σ 单项贡献 | 0.0150 | ||||
上例中,PSI = 0.0150,属于较低水平,表明当前分布与基准分布差异很小。
3.5 完整计算代码示例(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_value: float, PSI值
breakdown: DataFrame, 每个箱的详细贡献
"""
# 步骤1:分箱
if method == 'quantile':
# 等频分箱:基于基准分布计算分位数边界
percentiles = np.linspace(0, 100, bins + 1)
bin_edges = np.percentile(expected, percentiles)
# 确保边界唯一(处理重复分位数)
bin_edges = np.unique(bin_edges)
elif method == 'uniform':
# 等距分箱
bin_edges = np.linspace(np.min(expected), np.max(expected), bins + 1)
else:
raise ValueError("method 必须为 'quantile' 或 'uniform'")
# 步骤2:计算占比
expected_counts, _ = np.histogram(expected, bins=bin_edges)
actual_counts, _ = np.histogram(actual, bins=bin_edges)
expected_prop = expected_counts / len(expected)
actual_prop = actual_counts / len(actual)
# 平滑处理:避免0占比导致对数计算错误
epsilon = 1e-6
expected_prop = np.where(expected_prop == 0, epsilon, expected_prop)
actual_prop = np.where(actual_prop == 0, epsilon, actual_prop)
# 步骤3:计算对数比
log_ratio = np.log(actual_prop / expected_prop)
# 步骤4:加权求和
psi_components = (actual_prop - expected_prop) * log_ratio
psi_value = np.sum(psi_components)
# 构建详细输出
breakdown = pd.DataFrame({
'bin_start': bin_edges[:-1],
'bin_end': bin_edges[1:],
'expected_count': expected_counts,
'actual_count': actual_counts,
'expected_prop': expected_prop,
'actual_prop': actual_prop,
'log_ratio': log_ratio,
'contribution': psi_components
})
return psi_value, breakdown
# 使用示例
np.random.seed(42)
train_data = np.random.normal(0, 1, 10000) # 基准分布
monitor_data = np.random.normal(0.2, 1.1, 5000) # 当前分布(均值偏移0.2,标准差增大)
psi, details = calculate_psi(train_data, monitor_data, bins=10)
print(f"PSI = {psi:.4f}")
print(details.head())
🔍 代码解读:
- 分箱阶段:默认使用等频分箱(quantile),基于基准分布计算分位数边界。
- 平滑处理:将占比为0的箱替换为1e-6,避免ln(0)错误。
- 输出内容:返回PSI总值以及每个箱的详细贡献,便于定位漂移严重的区间。
3.6 常见计算陷阱与避坑指南
| 陷阱 | 表现 | 解决方案 |
|---|---|---|
| 分箱数量不一致 | 基准与当前分布使用不同分箱边界 | 始终使用基准分布的分箱边界,固定不变 |
| 零占比未平滑 | PSI计算报错或出现无穷大 | 加入小常数ε(1e-4 ~ 1e-6)进行平滑 |
| 样本量过小 | PSI值波动剧烈,不可靠 | 确保每个箱至少有50个样本,或减少分箱数 |
| 忽略缺失值 | 缺失值被排除,导致分布失真 | 将缺失值单独作为一个箱处理 |
掌握以上四步计算流程,你就具备了手动计算PSI的能力。在实际工程中,建议将上述代码封装为监控服务的一部分,实现自动化PSI计算与告警。