2. PSI计算公式详解:分箱法计算PSI的步骤、期望分布与实际分布的对比、ln与log的选择

2.1 PSI 核心公式与数学含义

群体稳定性指标(Population Stability Index, PSI)通过衡量两个分布在各分箱上的比例差异,来量化模型评分或特征分布的跨时间漂移程度。其标准计算公式为:

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

其中:

  • 期望占比(Expected):模型开发时(基准期)样本落在第 i 个分箱的比例。
  • 实际占比(Actual):当前监控期样本落在第 i 个分箱的比例。
  • ln:自然对数(以 e 为底)。
  • Σ:对所有分箱求和,得到最终的 PSI 值。
💡 业务解读: PSI 本质是 实际分布与期望分布之间的散度度量。当两个分布完全一致时,每个分箱的 (实际 - 期望) = 0,PSI = 0。PSI 越大,说明分布偏移越严重,模型失效风险越高。

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

分箱法(Binning)是计算 PSI 最常用的工程方法,尤其适用于连续型评分或概率输出。以下是标准 5 步流程:

  1. 确定分箱边界:基于开发样本(期望分布)将评分区间划分为 K 个等宽或等频分箱。信用风控中通常使用 10 分箱(等频),确保每个分箱包含约 10% 的开发样本。
  2. 计算期望占比:统计开发样本在每个分箱中的样本数,除以总样本数,得到期望占比 E_i。
  3. 映射实际样本:将监控期样本按照第 1 步确定的固定边界映射到各分箱,统计每个分箱的实际样本数,计算实际占比 A_i。
  4. 计算分箱贡献:对每个分箱,计算 (A_i - E_i) × ln(A_i / E_i)。
  5. 汇总 PSI:将所有分箱的贡献值求和,得到最终的 PSI 指标。
⚠️ 边界固定原则: 分箱边界必须由开发样本确定,监控期样本只能映射到这些固定边界,不能重新分箱。否则会破坏 PSI 的可比性。

2.3 期望分布 vs 实际分布:对比表格

以下表格以信用评分卡(评分范围 300-850)的 10 分箱为例,展示期望分布与实际分布的对比计算过程:

分箱编号 评分区间 期望占比 E_i 实际占比 A_i A_i - E_i ln(A_i / E_i) 分箱贡献
1 300-420 10.0% 15.0% +5.0% 0.4055 0.0203
2 420-480 10.0% 12.0% +2.0% 0.1823 0.0036
3 480-530 10.0% 10.5% +0.5% 0.0488 0.0002
4 530-580 10.0% 9.5% -0.5% -0.0513 0.0003
5 580-620 10.0% 8.0% -2.0% -0.2231 0.0045
6 620-660 10.0% 7.0% -3.0% -0.3567 0.0107
7 660-700 10.0% 9.0% -1.0% -0.1054 0.0011
8 700-740 10.0% 10.0% 0.0% 0.0000 0.0000
9 740-780 10.0% 11.0% +1.0% 0.0953 0.0010
10 780-850 10.0% 8.0% -2.0% -0.2231 0.0045
PSI 汇总 0.0462
📊 结果解读: 该示例 PSI = 0.0462(约 4.6%)。按照行业通用阈值:PSI < 0.10 表示分布稳定,0.10 ≤ PSI < 0.25 表示轻度漂移需关注,PSI ≥ 0.25 表示严重漂移需立即干预。本例处于稳定区间,但低分段(分箱1-2)已出现明显偏移,建议进一步分析。

2.4 ln 与 log 的选择:数学原理与工程实践

PSI 公式中的对数底数选择直接影响数值量级,但不改变排序和决策逻辑。以下是两种选择的对比:

对比维度 自然对数 ln 常用对数 log₁₀
数学定义 以 e ≈ 2.71828 为底 以 10 为底
与 KL 散度的关系 PSI 直接等于 KL 散度的对称化形式 与 KL 散度差一个常数因子 ln(10) ≈ 2.3026
数值量级 较大(ln(2) ≈ 0.693) 较小(log₁₀(2) ≈ 0.301)
行业惯例 信用风控主流选择(SAS、Python scipy 默认) 部分银行旧系统使用
阈值对应关系 稳定 < 0.10,轻度 < 0.25,严重 ≥ 0.25 稳定 < 0.043,轻度 < 0.109,严重 ≥ 0.109
🚨 关键建议: 在信用风控项目中,统一使用 ln(自然对数)。原因有三:① PSI 源自信息论中的 KL 散度,ln 是标准形式;② 主流风控平台(SAS、Python、R)默认使用 ln;③ 行业公开的 PSI 阈值(0.10/0.25)均基于 ln 定义。若使用 log₁₀,必须同步调整阈值,否则会导致误判。

2.5 PSI 分箱计算流程:SVG 架构图

下图展示了从原始评分到最终 PSI 输出的完整计算链路:

PSI 分箱计算流程架构图 开发样本评分 监控样本评分 确定分箱边界 计算期望占比 E_i 计算实际占比 A_i 计算各分箱贡献 (A_i - E_i) × ln(A_i / E_i) PSI = Σ 分箱贡献

2.6 分箱贡献可视化:PSI 成分分解图

为了直观理解每个分箱对 PSI 的贡献,下图展示了 2.3 节示例中各分箱的贡献值分布:

各分箱 PSI 贡献分解(基于 2.3 节示例) 0.000 0.005 0.010 0.015 0.020 0.0203 0.0036 0.0045 0.0107 0.0045 1 2 3 4 5 6 7 8 9 10 分箱编号 高贡献(≥0.01) 中贡献(0.003-0.01) 低贡献(<0.003)
🔍 分析要点: 从分解图可见,分箱 1(低分段)和分箱 6(中高分段)贡献了 PSI 的主要部分。这提示我们:漂移并非均匀分布,低分段客户占比上升和高分段客户占比下降是本次漂移的主要驱动因素。在后续告警处置中,应优先针对这两个分箱进行特征级根因分析。

2.7 代码实现:Python 分箱法 PSI 计算

以下 Python 代码实现了标准的分箱法 PSI 计算,包含边界固定、零值处理和结果输出:

import numpy as np
import pandas as pd

def calculate_psi(expected, actual, bins=10, method='quantile'):
    """
    计算分箱法 PSI

    Parameters:
    expected: array-like, 开发样本评分
    actual: array-like, 监控样本评分
    bins: int, 分箱数量
    method: str, 'quantile'(等频) 或 'equal'(等宽)

    Returns:
    psi: float, PSI 值
    bin_df: DataFrame, 各分箱明细
    """
    # 1. 基于开发样本确定分箱边界
    if method == 'quantile':
        # 等频分箱:每个分箱包含相同数量的开发样本
        percentiles = np.linspace(0, 100, bins + 1)
        # 使用百分位数作为边界,处理重复值
        bin_edges = np.percentile(expected, percentiles)
        # 去重:如果相邻边界相同,微调
        bin_edges = np.unique(bin_edges)
        if len(bin_edges) < bins + 1:
            # 若去重后边界不足,使用等宽作为 fallback
            bin_edges = np.linspace(expected.min(), expected.max(), bins + 1)
    else:
        # 等宽分箱
        bin_edges = np.linspace(expected.min(), expected.max(), bins + 1)

    # 2. 计算期望占比
    expected_counts, _ = np.histogram(expected, bins=bin_edges)
    expected_pct = expected_counts / len(expected)

    # 3. 计算实际占比(使用相同边界)
    actual_counts, _ = np.histogram(actual, bins=bin_edges)
    actual_pct = actual_counts / len(actual)

    # 4. 计算各分箱 PSI 贡献
    psi_components = []
    for i in range(len(expected_pct)):
        e = expected_pct[i]
        a = actual_pct[i]

        # 零值处理:若期望占比为0,跳过该分箱
        if e == 0:
            continue

        # 实际占比为0时,使用极小值避免 ln(0)
        a_safe = a if a > 0 else 1e-10

        component = (a - e) * np.log(a_safe / e)
        psi_components.append(component)

    # 5. 汇总 PSI
    psi = np.sum(psi_components)

    # 构建明细 DataFrame
    bin_df = 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, bin_df

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

psi_value, detail_df = calculate_psi(expected_scores, actual_scores, bins=10)
print(f"PSI = {psi_value:.4f}")
print(detail_df.head())
📌 工程要点:
  • 边界固定:bin_edges 仅由 expected 计算,actual 使用 np.histogram(..., bins=bin_edges) 强制映射。
  • 零值处理:当 actual_pct = 0 时,使用 1e-10 替代,避免 ln(0) 导致 -inf。
  • 等频 vs 等宽:信用风控推荐等频分箱,确保每个分箱有足够的统计稳定性;等宽分箱在评分分布严重偏态时可能导致空箱。