2. PSI 数学原理:PSI 计算公式详解

群体稳定性指标(Population Stability Index, PSI)是衡量两个分布之间差异的标准化度量。在风控与压力测试场景中,它用于量化模型评分或关键风险指标在 期望分布(开发/基准样本) 与 实际分布(当前/监控样本) 之间的偏移程度。PSI 值越小,分布越稳定;值越大,模型或风险特征发生显著漂移的可能性越高。

2.1 PSI 计算公式

PSI 的计算基于信息论中的 Kullback-Leibler 散度 对称化形式。其核心公式如下:

PSI = Σ (实际占比_i - 期望占比_i) * ln(实际占比_i / 期望占比_i)

其中:

  • 实际占比_i:当前监控样本在第 i 个分箱中的样本比例。
  • 期望占比_i:基准/开发样本在第 i 个分箱中的样本比例。
  • ln:自然对数,用于放大比例差异的非线性效应。
  • Σ:对所有分箱(i = 1 到 N)求和。
计算要点: 当某个分箱中期望占比为 0 时,公式会出现除零错误。实际应用中,通常对占比进行微小平滑处理(如加 0.0001),或确保分箱策略保证每个箱内均有样本。

2.2 分箱方法详解

分箱是 PSI 计算的前置步骤,直接影响指标的敏感性与稳定性。选择不当的分箱方法可能导致 PSI 值失真或无法有效捕捉分布漂移。三种主流分箱方法对比如下:

分箱方法 原理 优点 缺点 适用场景
等距分箱 将变量取值范围划分为宽度相等的区间 实现简单,区间边界直观 数据分布不均时,部分箱可能样本极少或为空 变量分布均匀、无极端值
等频分箱 每个分箱包含大致相同数量的样本 保证每个箱有足够样本,避免零占比 边界值可能落在数据密集区,导致区间宽度差异大 样本量充足、分布偏态
分位数分箱 基于数据的分位数(如十分位数、二十分位数)划分边界 对异常值鲁棒,能反映数据内在分布结构 分位数点计算需足够样本量,边界解释性稍弱 压力测试、长尾分布、高维评分卡监控
行业实践建议: 在风控模型监控中,分位数分箱(通常为 10 箱或 20 箱)是最常用的方法。它既能保证每个箱内样本量充足,又能捕捉评分分布尾部的细微变化,特别适合压力测试场景下的早期预警。

2.3 对数变换的意义

PSI 公式中的自然对数变换(ln)并非随意选择,它具有深刻的数学与统计意义:

  • 非线性放大差异: 当实际占比与期望占比差异较小时,ln 值接近 0;当差异增大时,ln 值呈非线性增长,使 PSI 对中等程度的漂移更为敏感。
  • 对称性处理: 对数变换使得 PSI 对“实际占比高于期望”和“实际占比低于期望”两种方向的偏移具有对称的惩罚力度,避免单向偏差。
  • 信息论基础: ln 变换源自信息熵与 KL 散度,使 PSI 具有可加性与可解释性——PSI 值可以理解为两个分布之间的“信息损失”或“不确定性增量”。
  • 标准化尺度: 经过对数变换后,PSI 值通常落在 0 到 1 之间(极端情况可能超过 1),便于设定统一的预警阈值。
常见误区: 部分实践者误以为 PSI 公式中的 ln 可以用 log10 或 log2 替代。虽然数学上可行,但会导致 PSI 阈值标准失效。行业通用的 PSI 阈值(如 <0.1 为稳定,0.1-0.25 需关注,>0.25 需干预)均基于自然对数推导,使用其他底数需重新校准阈值。

2.4 PSI 计算流程与分布漂移可视化

下图展示了从原始数据到 PSI 值的完整计算流程,以及分布漂移的直观对比:

PSI 计算流程与分布漂移示意 原始评分数据 分箱处理 计算各箱占比 PSI 求和计算 输出 PSI 值 期望分布 实际分布(偏移) 实际分布相对期望分布向右偏移,PSI 值将显著升高

2.5 分箱方法选择决策树

在实际项目中,选择合适的分箱方法可参考以下决策逻辑:

分箱方法选择决策树 数据分布特征分析 均匀分布 / 无极端值 推荐:等距分箱 偏态分布 / 长尾 / 异常值 样本量充足 推荐:分位数分箱 样本量有限 推荐:等频分箱

2.6 PSI 计算代码示例(Python)

以下代码展示了使用等频分箱计算 PSI 的完整实现:

import numpy as np
import pandas as pd

def calculate_psi(expected, actual, bins=10):
    """
    计算群体稳定性指标(PSI)
    
    参数:
        expected: array-like, 期望分布(基准样本)
        actual: array-like, 实际分布(当前监控样本)
        bins: int, 分箱数量(默认10箱)
    
    返回:
        psi_value: float, PSI值
        bin_details: DataFrame, 各分箱详细占比
    """
    # 基于期望分布确定分位数边界
    percentiles = np.linspace(0, 100, bins + 1)
    bin_edges = np.percentile(expected, percentiles)
    
    # 处理边界重复(当数据中存在大量相同值时)
    bin_edges = np.unique(bin_edges)
    
    # 对期望分布分箱
    expected_counts, _ = np.histogram(expected, bins=bin_edges)
    expected_pct = expected_counts / len(expected)
    
    # 对实际分布分箱(使用相同的边界)
    actual_counts, _ = np.histogram(actual, bins=bin_edges)
    actual_pct = actual_counts / len(actual)
    
    # 平滑处理:避免零占比导致除零错误
    epsilon = 1e-6
    expected_pct = np.clip(expected_pct, epsilon, 1 - epsilon)
    actual_pct = np.clip(actual_pct, epsilon, 1 - epsilon)
    
    # 计算每个分箱的PSI贡献
    psi_components = (actual_pct - expected_pct) * np.log(actual_pct / expected_pct)
    psi_value = np.sum(psi_components)
    
    # 构建结果DataFrame
    bin_details = pd.DataFrame({
        'bin_start': bin_edges[:-1],
        'bin_end': bin_edges[1:],
        'expected_pct': expected_pct,
        'actual_pct': actual_pct,
        'psi_component': psi_components
    })
    
    return psi_value, bin_details

# 示例用法
np.random.seed(42)
expected_scores = np.random.normal(600, 100, 10000)
actual_scores = np.random.normal(620, 110, 10000)  # 模拟分布偏移

psi, details = calculate_psi(expected_scores, actual_scores, bins=10)
print(f"PSI值: {psi:.4f}")
print(details.head())
关键代码说明:
  • np.percentile 基于期望分布计算分位数边界,实现分位数分箱。
  • np.clip 对占比进行平滑处理,避免 ln(0) 导致的数学错误。
  • PSI 值 < 0.1 表示分布稳定;0.1-0.25 表示轻度漂移,需关注;> 0.25 表示显著漂移,需立即干预。