3. PSI计算步骤详解:分箱策略、分布占比、对数变换与求和

群体稳定性指标(PSI)的计算严格遵循“分箱 → 计算占比 → 对数变换 → 加权求和”四步流程。每一步的精度直接影响最终漂移判断的可靠性。本节将拆解每个步骤的数学原理与工程实现。

核心公式回顾:
PSI = Σ (实际占比i - 期望占比i) × ln(实际占比i / 期望占比i)
其中 i 表示第 i 个分箱,实际占比 = 当前样本数 / 当前总样本数,期望占比 = 基准样本数 / 基准总样本数。

3.1 第一步:分箱(Binning)策略

分箱是PSI计算的基础,决定了后续分布对比的粒度。评分卡场景下,分数通常为连续变量(如300~850分),必须离散化为有限个区间。

分箱策略 方法描述 适用场景 注意事项
等距分箱 将分数范围等分为K个区间(如每50分一箱) 分数分布均匀、业务解释性强 极端值可能导致空箱
等频分箱 按基准样本的分数分位数划分,每箱样本数大致相等 分布偏态严重、需要避免空箱 边界值可能跨整数分数
业务分箱 按评分卡实际使用的分数段划分(如A/B/C/D/E等级) 生产环境监控、与业务规则对齐 需保证基准与当前使用相同边界
决策树分箱 利用目标变量(如好坏标签)进行有监督分箱 建模阶段、特征筛选 PSI监控中较少使用
工程建议: 生产环境推荐使用等频分箱(通常10~20箱),以基准样本的分数分位点为固定边界。这样既能避免空箱,又能保证每箱有足够的统计样本量(建议每箱≥100个样本)。

分箱边界一旦确定,必须固定复用到所有后续监控周期。改变分箱边界会导致PSI值不可比。

3.2 第二步:计算实际分布与期望分布占比

分箱完成后,分别计算基准样本(期望分布)和当前样本(实际分布)在每个箱中的占比。

数学定义:

  • 期望占比(Expected Proportion, Ei) = 基准样本中落入第 i 箱的样本数 / 基准总样本数
  • 实际占比(Actual Proportion, Ai) = 当前样本中落入第 i 箱的样本数 / 当前总样本数
避坑要点:
  • 若某箱在基准或当前样本中样本数为0,直接计算会导致 ln(0) 或除以0错误。常见处理方式:对占比做微小平滑(如加0.001),或将该箱与相邻箱合并。
  • 占比必须使用百分比(小数)而非绝对计数,否则PSI值会随样本量放大而失真。

以下示例展示一个10箱等频分箱的占比计算过程:

分箱编号 分数区间 基准样本数 期望占比 Ei 当前样本数 实际占比 Ai
1300~3501,0000.1001,2000.120
2351~4001,0000.1001,1000.110
3401~4501,0000.1009500.095
4451~5001,0000.1009000.090
5501~5501,0000.1008500.085
6551~6001,0000.1008000.080
7601~6501,0000.1007500.075
8651~7001,0000.1007000.070
9701~7501,0000.1006500.065
10751~8501,0000.1006000.060

3.3 第三步:对数变换与求和

得到每箱的期望占比 Ei 和实际占比 Ai 后,执行核心计算:

  1. 计算差值: (Ai - Ei)
  2. 计算对数比: ln(Ai / Ei)
  3. 计算单箱贡献: (Ai - Ei) × ln(Ai / Ei)
  4. 求和: PSI = Σ 单箱贡献

继续使用上表数据,计算过程如下:

箱号 Ai Ei Ai - Ei Ai / Ei ln(Ai / Ei) 单箱贡献
10.1200.100+0.0201.200+0.18230.00365
20.1100.100+0.0101.100+0.09530.00095
30.0950.100-0.0050.950-0.05130.00026
40.0900.100-0.0100.900-0.10540.00105
50.0850.100-0.0150.850-0.16250.00244
60.0800.100-0.0200.800-0.22310.00446
70.0750.100-0.0250.750-0.28770.00719
80.0700.100-0.0300.700-0.35670.01070
90.0650.100-0.0350.650-0.43080.01508
100.0600.100-0.0400.600-0.51080.02043
PSI = Σ 0.06621

本例计算得到 PSI ≈ 0.066,属于中等漂移(通常阈值:PSI < 0.1 为轻度,0.1~0.25 为中度,> 0.25 为重度),需要触发告警并排查原因。

对数变换的数学意义:
ln(Ai / Ei) 具有对称性——当 Ai 偏离 Ei 相同比例时,无论方向如何,贡献的绝对值相同。例如 Ai=0.12 与 Ei=0.10 的贡献,和 Ai=0.10 与 Ei=0.12 的贡献一致。这保证了PSI对分布偏移方向不敏感,只度量整体分布差异程度。

3.4 PSI计算流程总览

以下SVG图完整展示了从原始分数到PSI值的四步计算流水线:

PSI计算四步流程 PSI计算四步流程总览 步骤1:分箱 等频/等距/业务分箱 固定K个区间边界 步骤2:计算占比 期望占比 Ei 实际占比 Ai 步骤3:对数变换 ln(Ai / Ei) (Ai - Ei) × ln(...) 步骤4:求和 PSI = Σ 单箱贡献 分箱策略对比 • 等距分箱:区间宽度固定,简单直观 • 等频分箱:每箱样本数相等,避免空箱 • 业务分箱:对齐评分卡等级,可解释性强 • 推荐:生产环境使用等频分箱(10~20箱) 计算注意事项 • 空箱处理:加微小平滑或合并相邻箱 • 占比使用小数,避免绝对计数 • 对数底数固定为自然对数 e • 分箱边界一旦确定,后续不可更改 输出:PSI = Σ (Ai - Ei) × ln(Ai / Ei) 数值越大,分布漂移越严重

3.5 Python代码实现

以下代码实现了完整的PSI计算函数,包含空箱平滑处理:

import numpy as np
import pandas as pd

def calculate_psi(expected, actual, bins=10, method='quantile', epsilon=1e-6):
    """
    计算群体稳定性指标(PSI)
    
    Parameters:
    -----------
    expected : array-like
        基准样本分数
    actual : array-like
        当前样本分数
    bins : int
        分箱数量(等频分箱时有效)
    method : str
        分箱方法:'quantile'(等频), 'equal_width'(等距)
    epsilon : float
        平滑因子,防止除以0或ln(0)
    
    Returns:
    --------
    psi : float
        PSI值
    bin_df : DataFrame
        每箱的详细计算过程
    """
    # 1. 确定分箱边界(基于基准样本)
    if method == 'quantile':
        # 等频分箱:使用基准样本的分位数
        percentiles = np.linspace(0, 100, bins + 1)
        # 处理极端值:使用1%和99%分位数作为边界
        low = np.percentile(expected, 1)
        high = np.percentile(expected, 99)
        # 在[low, high]范围内计算分位数
        edges = np.percentile(
            np.clip(expected, low, high), 
            percentiles[1:-1]
        )
        edges = np.concatenate([[low], edges, [high + 1]])
    elif method == 'equal_width':
        # 等距分箱
        low = np.min(expected)
        high = np.max(expected)
        edges = np.linspace(low, high + 1, bins + 1)
    else:
        raise ValueError("method must be 'quantile' or 'equal_width'")
    
    # 2. 计算每箱样本数
    expected_counts, _ = np.histogram(expected, bins=edges)
    actual_counts, _ = np.histogram(actual, bins=edges)
    
    # 3. 计算占比(加平滑)
    expected_pct = (expected_counts + epsilon) / (np.sum(expected_counts) + epsilon * bins)
    actual_pct = (actual_counts + epsilon) / (np.sum(actual_counts) + epsilon * bins)
    
    # 4. 计算单箱贡献
    ratio = actual_pct / expected_pct
    contribution = (actual_pct - expected_pct) * np.log(ratio)
    
    # 5. 求和得到PSI
    psi = np.sum(contribution)
    
    # 6. 构建结果DataFrame
    bin_df = pd.DataFrame({
        'bin_lower': edges[:-1],
        'bin_upper': edges[1:],
        'expected_count': expected_counts,
        'actual_count': actual_counts,
        'expected_pct': expected_pct,
        'actual_pct': actual_pct,
        'ratio': ratio,
        'log_ratio': np.log(ratio),
        'contribution': contribution
    })
    
    return psi, bin_df

# 使用示例
np.random.seed(42)
expected_scores = np.random.normal(600, 100, 10000)
actual_scores = np.random.normal(580, 120, 10000)  # 模拟漂移

psi_value, detail_df = calculate_psi(expected_scores, actual_scores, bins=10)
print(f"PSI = {psi_value:.4f}")
print(detail_df.head())
代码要点说明:
  • 使用 np.percentile 实现等频分箱,自动处理边界值
  • 通过 epsilon 平滑因子避免空箱导致的数学错误
  • 返回的 detail_df 包含每箱完整计算过程,便于审计和排查
  • 建议将分箱边界持久化存储(如数据库或配置文件),确保跨周期一致性

3.6 常见计算陷阱与规避

陷阱 现象 解决方案
空箱问题 某箱样本数为0,导致 ln(0) 或除以0 加微小平滑(如1e-6)或合并相邻箱
分箱边界不一致 不同周期使用不同分箱,PSI不可比 固定分箱边界,存储为配置文件
样本量差异过大 基准100万,当前1万,占比波动大 使用占比而非绝对计数,或对当前样本做重采样
极端值干扰 个别极端分数拉宽分箱区间 使用1%~99%分位数截断后分箱
对数底数混淆 使用log10或log2代替自然对数 统一使用自然对数 ln(numpy.log)

掌握以上四步计算流程和工程实现细节,即可在生产环境中稳定、准确地计算PSI,为后续的漂移告警阈值设定和自动化监控奠定基础。