2. PSI计算原理:分箱法计算PSI的步骤详解

群体稳定性指标(Population Stability Index, PSI)是衡量两个样本分布差异的标准化度量。在信用额度管理中,PSI用于监控模型评分或额度分配策略在时间维度上的稳定性。其核心计算逻辑基于分箱法,通过比较“期望分布”(基准分布)与“实际分布”(当前分布)在每个分箱中的占比差异,量化整体偏移程度。

2.1 分箱法计算PSI的完整步骤

PSI的计算遵循严格的五步流程,每一步都直接影响最终指标的准确性。以下为详细步骤说明:

  1. 数据分箱(Binning):将连续变量(如信用评分、额度使用率)或离散变量划分为K个互斥的区间(箱)。分箱方式包括等距分箱、等频分箱或基于业务规则的分箱。
  2. 计算期望占比(Expected Proportion, Ei):统计基准样本(如模型开发样本或上期数据)落在第i个箱中的样本数,除以基准样本总数,得到期望占比。
  3. 计算实际占比(Actual Proportion, Ai):统计当前监控样本落在第i个箱中的样本数,除以当前样本总数,得到实际占比。
  4. 计算自然对数比值(Ln(Ai/Ei)):对每个箱,计算实际占比与期望占比的比值,再取自然对数。该步骤用于放大微小差异,并赋予正负方向。
  5. 加权求和得到PSI值:将每个箱的差值(Ai - Ei)与自然对数比值相乘,并对所有箱求和,得到最终的PSI值。
PSI计算公式:
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值的完整计算链路,包含分箱、占比计算、对数变换和加权求和四个核心阶段:

PSI分箱法计算流程图 PSI分箱法计算流程图 原始数据输入 步骤1:数据分箱(K个箱) 步骤2:计算期望占比 Ei 步骤3:计算实际占比 Ai 步骤4:计算 ln(Ai/Ei) 步骤5:加权求和 → PSI值 PSI = Σ (Ai - Ei) × ln(Ai/Ei) K个箱求和,i=1..K

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,以定位具体哪个额度段发生了分布漂移。同时,结合业务周期(如月度、季度)设定动态阈值,避免季节性波动引发误告警。