4. PSI 计算步骤详解:分箱、计算占比、计算对数比、加权求和

群体稳定性指标(PSI)的核心价值在于量化特征分布随时间或样本集的变化程度。其计算过程可拆解为四个标准步骤:分箱 → 计算占比 → 计算对数比 → 加权求和。下面将逐一拆解每个环节的数学逻辑与工程实现。

4.1 步骤一:分箱(Binning)

分箱是将连续特征或离散特征划分为若干区间(箱),以便后续计算每个区间内的样本占比。分箱方式直接影响 PSI 的敏感度与稳定性。

分箱方式 适用场景 注意事项
等距分箱 特征值分布均匀,无极端异常值 若数据稀疏,部分箱可能为空
等频分箱 特征分布偏态,需保证每箱样本量均衡 边界值可能落在同一箱内,需处理重复值
自定义分箱 业务有明确分段标准(如信用评分区间) 需确保训练集与测试集使用相同分箱边界
工程建议: 推荐使用等频分箱,将样本划分为 10 箱(即每箱约 10% 的样本)。若特征取值较少(如枚举型),可直接将每个取值作为一个箱。

分箱完成后,需记录每个箱的边界值(如 [0, 10), [10, 20), ...),并确保后续步骤中测试集与训练集使用完全相同的分箱边界。

4.2 步骤二:计算占比(Proportion)

分别计算训练集(预期分布)和测试集(实际分布)在每个箱中的样本占比。

  • 训练集占比(\(P_i\)):第 \(i\) 个箱中的样本数 / 训练集总样本数
  • 测试集占比(\(Q_i\)):第 \(i\) 个箱中的样本数 / 测试集总样本数

若某个箱在测试集中样本数为 0,则 \(Q_i = 0\),此时对数比计算会引发数学错误(log(0) 无定义)。常见处理方式:

避坑提示: 当 \(Q_i = 0\) 或 \(P_i = 0\) 时,建议对该箱占比进行微小平滑处理,例如将占比替换为 \( \frac{1}{2N} \)(\(N\) 为总样本数),或直接将该箱合并到相邻箱中。

4.3 步骤三:计算对数比(Log Ratio)

对每个箱,计算测试集占比与训练集占比的比值,并取自然对数:

LogRatio_i = ln(Q_i / P_i)

该值反映了第 \(i\) 个箱中样本占比的相对变化方向与幅度:

  • LogRatio > 0:测试集在该箱的占比高于训练集,即该区间样本增多
  • LogRatio < 0:测试集在该箱的占比低于训练集,即该区间样本减少
  • LogRatio = 0:占比无变化
数学意义: 对数比是 PSI 公式中的“变化信号”部分,它放大了占比差异的敏感度,尤其当占比差异较小时,对数比仍能提供可区分的数值。

4.4 步骤四:加权求和(Weighted Sum)

将每个箱的对数比乘以该箱的测试集占比,然后对所有箱求和,得到最终的 PSI 值:

PSI = Σ [ (Q_i - P_i) * ln(Q_i / P_i) ]

等价形式:

PSI = Σ [ (Q_i) * ln(Q_i / P_i) ] - Σ [ (P_i) * ln(Q_i / P_i) ]

但实际计算中,通常直接使用第一个公式,因为 \(Q_i - P_i\) 可正可负,而 \( \ln(Q_i / P_i) \) 符号与之对应,确保每个箱的贡献始终为非负值。

4.5 完整计算流程 SVG 图

下图展示了从原始数据到最终 PSI 值的完整计算链路,包含四个核心步骤及关键决策点:

PSI 完整计算流程 输入:训练集 & 测试集特征 步骤1:分箱 等频 / 等距 / 自定义 → 确定箱边界 步骤2:计算占比 P_i = 训练集箱占比 & Q_i = 测试集箱占比 步骤3:计算对数比 LogRatio_i = ln(Q_i / P_i) 步骤4:加权求和 PSI = Σ[(Q_i - P_i) × ln(Q_i / P_i)] 输出:PSI 标量值

4.6 计算示例(手工演算)

假设某特征分箱为 3 箱,训练集与测试集各 1000 样本,数据如下:

箱编号 训练集样本数 测试集样本数 \(P_i\) \(Q_i\) \(Q_i - P_i\) \(\ln(Q_i / P_i)\) 贡献值
1 300 250 0.30 0.25 -0.05 -0.1823 0.0091
2 400 450 0.40 0.45 0.05 0.1178 0.0059
3 300 300 0.30 0.30 0.00 0.0000 0.0000

最终 PSI = 0.0091 + 0.0059 + 0.0000 = 0.0150。该值小于 0.1,表明特征分布稳定性良好。

4.7 代码实现(Python 伪代码)

import numpy as np
import pandas as pd

def calculate_psi(train_series, test_series, bins=10):
    # 步骤1:分箱(等频分箱)
    _, bin_edges = pd.qcut(train_series, q=bins, retbins=True, duplicates='drop')
    
    # 步骤2:计算占比
    train_bins = pd.cut(train_series, bins=bin_edges, include_lowest=True)
    test_bins = pd.cut(test_series, bins=bin_edges, include_lowest=True)
    
    train_counts = train_bins.value_counts().sort_index()
    test_counts = test_bins.value_counts().sort_index()
    
    train_pct = train_counts / len(train_series)
    test_pct = test_counts / len(test_series)
    
    # 步骤3:计算对数比(加微小平滑避免除零)
    epsilon = 1e-6
    log_ratio = np.log((test_pct + epsilon) / (train_pct + epsilon))
    
    # 步骤4:加权求和
    psi = np.sum((test_pct - train_pct) * log_ratio)
    
    return psi
关键点: 代码中 pd.qcut 实现等频分箱,retbins=True 返回箱边界供测试集复用。平滑因子 epsilon 仅用于防止数值异常,不影响整体 PSI 趋势。

4.8 分箱与 PSI 敏感度关系 SVG 图

下图展示了不同分箱数量对 PSI 值敏感度的影响:箱数过少会掩盖局部偏移,箱数过多则可能放大噪声。

分箱数量对 PSI 敏感度的影响 箱数过少(如 2-3 箱) 分布偏移被平均化 PSI 值偏低,漏检风险高 箱数适中(10 箱) 均衡捕捉局部偏移 PSI 值稳定且可解释 箱数过多(>20 箱) 噪声被放大 PSI 值虚高,误报频繁 推荐策略 连续特征:等频分箱 10 箱 离散特征:按原始取值分箱,若取值过多则合并低频箱

通过以上四个步骤的拆解与示例,PSI 的计算逻辑已完全透明。在实际特征巡检中,建议将 PSI 计算封装为自动化函数,并定期对每个特征执行全量计算,以便及时发现分布漂移。