2. PSI计算原理:分箱方法与分布对比逻辑

群体稳定性指标(PSI)的核心思想是量化两个概率分布之间的差异。在实际业务中,我们通常将模型评分或特征值离散化(分箱),然后比较每个箱内样本占比的变化。本章将深入解析分箱的三种主流方法,以及实际分布与预期分布的对比逻辑。

2.1 分箱方法:等距、等频与分位数

分箱是将连续变量或离散值划分为若干个区间(箱)的过程。不同的分箱方法会直接影响PSI的计算结果和敏感度。以下是三种最常用的分箱方法对比:

分箱方法 核心原理 优点 缺点 适用场景
等距分箱 将数值范围划分为宽度相等的区间 计算简单,箱边界直观 数据分布不均时,部分箱可能为空或样本极少 数据分布均匀,或需要固定刻度对比
等频分箱 使每个箱包含大致相同数量的样本 避免空箱,对长尾分布友好 箱边界不固定,可能割裂相似数值 样本分布偏态严重,或关注样本量均衡
分位数分箱 基于数据的分位数(如十分位、二十分位)划分边界 对异常值鲁棒,边界具有统计意义 计算成本略高,需要排序 金融风控、信用评分等对分布尾部敏感的场景
实战建议:在模型监控中,分位数分箱是最常用的方法。通常选择10个或20个分位数箱(即十分位或二十分位),既能保证每个箱有足够的样本量,又能捕捉到分布尾部的细微变化。

2.2 分箱流程与PSI计算步骤

无论采用哪种分箱方法,PSI的计算都遵循以下标准流程:

  1. 确定预期分布(Expected):通常使用模型训练集或验证集的样本分布作为基准。
  2. 确定实际分布(Actual):使用当前监控周期(如月度、周度)的样本分布。
  3. 分箱对齐:使用预期分布的箱边界,对实际分布进行相同的分箱操作。
  4. 计算每个箱的占比:
    • 预期占比 \( E_i = \frac{\text{预期样本数}_i}{\text{预期总样本数}} \)
    • 实际占比 \( A_i = \frac{\text{实际样本数}_i}{\text{实际总样本数}} \)
  5. 计算每个箱的PSI贡献: \( (A_i - E_i) \times \ln(\frac{A_i}{E_i}) \)
  6. 汇总PSI值: \( PSI = \sum_{i=1}^{n} (A_i - E_i) \times \ln(\frac{A_i}{E_i}) \)
避坑提示:当某个箱的 \( E_i \) 或 \( A_i \) 为0时,对数计算会报错。通常做法是:若 \( E_i = 0 \),则将该箱的PSI贡献设为0;若 \( A_i = 0 \),则使用极小值(如0.0001)替代,或直接跳过该箱。

2.3 实际分布与预期分布的对比逻辑

PSI的本质是衡量两个分布之间的“距离”。对比逻辑并非简单的数值相减,而是通过加权对数比率来放大差异。下图展示了分箱后分布对比的核心流程:

PSI分布对比逻辑流程图 预期分布 (E) 实际分布 (A) 分箱(等距/等频/分位数) 使用E的箱边界分箱 计算各箱占比 E_i 计算各箱占比 A_i 逐箱对比:(A_i - E_i) × ln(A_i / E_i) 汇总:PSI = Σ 各箱贡献 箱边界必须对齐 使用E的边界划分A 对数放大差异 正负方向均可识别

2.4 对比逻辑的数学本质

从信息论角度看,PSI实际上是相对熵(KL散度)的一种对称化变体。对比逻辑的核心在于:

  • 方向性:当 \( A_i > E_i \) 时,该箱的PSI贡献为正,表示实际分布在该区间聚集了更多样本;反之则为负贡献。
  • 非线性放大:对数函数 \( \ln(A_i / E_i) \) 对微小差异不敏感,但对较大差异(尤其是当 \( A_i \) 远大于 \( E_i \) 时)会显著放大,从而突出分布漂移。
  • 对称性:虽然PSI本身不对称(交换A和E结果不同),但在监控场景中,我们固定E为基准,A为当前,因此方向一致,具有可比性。
关键理解:PSI不是简单的百分比差异,而是加权后的对数比率。这意味着:即使两个箱的绝对差异相同,如果其中一个箱的预期占比很小,其PSI贡献会更大——这正是PSI能够敏感捕捉尾部漂移的原因。

2.5 分箱方法选择对PSI的影响

不同的分箱方法会导致PSI值产生显著差异。以下是一个模拟对比示例:

分箱方法 箱数 PSI值(模拟数据) 对尾部漂移敏感度 对整体偏移敏感度
等距分箱 10 0.015 低(尾部箱可能为空) 中
等频分箱 10 0.023 中 高
分位数分箱(十分位) 10 0.028 高 高
分位数分箱(二十分位) 20 0.041 极高 极高
注意:箱数越多,PSI对分布变化的敏感度越高,但也更容易受到随机波动的影响。在实际工程中,建议从10个分位数箱开始,根据监控效果逐步调整。箱数过多会导致频繁误告警,过少则可能漏掉重要漂移。

2.6 代码实现示例

以下Python代码展示了使用分位数分箱计算PSI的完整流程:

import numpy as np
import pandas as pd

def calculate_psi(expected, actual, bins=10):
    """
    使用分位数分箱计算PSI
    
    Parameters:
    expected: array-like, 预期分布数据
    actual: array-like, 实际分布数据
    bins: int, 分箱数量(默认10个分位数箱)
    
    Returns:
    psi: float, PSI值
    psi_df: DataFrame, 各箱明细
    """
    # 1. 基于预期分布确定分位数边界
    percentiles = np.linspace(0, 100, bins + 1)
    bin_edges = np.percentile(expected, percentiles)
    # 处理边界重复(当数据中有大量重复值时)
    bin_edges = np.unique(bin_edges)
    
    # 2. 对预期和实际分布进行分箱
    expected_bins = np.digitize(expected, bin_edges, right=True)
    actual_bins = np.digitize(actual, bin_edges, right=True)
    
    # 3. 计算每个箱的样本占比
    expected_counts = np.bincount(expected_bins, minlength=len(bin_edges)+1)[1:]
    actual_counts = np.bincount(actual_bins, minlength=len(bin_edges)+1)[1:]
    
    expected_pct = expected_counts / len(expected)
    actual_pct = actual_counts / len(actual)
    
    # 4. 处理零值,避免对数计算错误
    eps = 1e-6
    expected_pct = np.where(expected_pct == 0, eps, expected_pct)
    actual_pct = np.where(actual_pct == 0, eps, actual_pct)
    
    # 5. 计算每个箱的PSI贡献
    psi_components = (actual_pct - expected_pct) * np.log(actual_pct / expected_pct)
    
    # 6. 汇总PSI
    psi = np.sum(psi_components)
    
    # 构建明细DataFrame
    psi_df = pd.DataFrame({
        '箱边界': [f'({bin_edges[i]:.2f}, {bin_edges[i+1]:.2f}]' 
                    for i in range(len(bin_edges)-1)],
        '预期占比': expected_pct,
        '实际占比': actual_pct,
        'PSI贡献': psi_components
    })
    
    return psi, psi_df

# 示例用法
np.random.seed(42)
expected_data = np.random.normal(0, 1, 10000)
actual_data = np.random.normal(0.2, 1.1, 10000)  # 模拟轻微漂移

psi_value, psi_details = calculate_psi(expected_data, actual_data, bins=10)
print(f"PSI值: {psi_value:.4f}")
print(psi_details.head())
工程实践:在生产环境中,建议将分箱边界持久化存储(如数据库或配置文件),确保每次计算使用相同的箱边界。同时,监控系统应支持动态调整箱数,以适应不同阶段的监控粒度需求。