4、PSI计算步骤详解:数据分箱、计算占比、计算WOE与差异、累加得到PSI值

PSI(Population Stability Index,群体稳定性指标)的计算过程严格遵循四个核心步骤:数据分箱 → 计算占比 → 计算WOE与差异 → 累加得到PSI值。每一步都直接影响最终监控结果的准确性,尤其在催收触达场景中,分箱方式的选择对模型敏感度至关重要。

核心原则:PSI衡量的是“实际分布”相对于“预期分布”的偏移程度。在催收触达中,通常将模型开发时的样本分布作为预期(Expected),将当前监控周期的触达结果分布作为实际(Actual)。

4.1 步骤一:数据分箱(Binning)

分箱是将连续变量或离散变量划分为若干个区间(箱)的过程。分箱质量直接决定PSI能否有效捕捉分布偏移。催收触达场景中,常用的分箱方式有三种:

分箱方式 方法描述 催收触达适用场景 注意事项
等距分箱 按变量值域等间隔划分 评分卡分数、逾期天数 容易导致某些箱样本过少
等频分箱 按样本数量均分,每箱样本量相近 催收评分、客户分层 边界值可能割裂同质群体
最优分箱 基于决策树或卡方检验自动划分 高风险客群识别 计算复杂,需保证业务可解释性
实战建议:催收触达PSI监控推荐使用等频分箱,通常分为10箱。若某箱样本占比低于5%,需合并相邻箱,避免统计波动导致PSI虚高。

分箱完成后,需确保预期样本(Expected)和实际样本(Actual)使用完全相同的分箱边界。例如,若预期数据将评分区间[300, 800]分为10个等频箱,实际数据必须沿用这9个边界点,不能重新分箱。

数据分箱流程示意图 原始触达数据 选择分箱方式 等距分箱 等频分箱 最优分箱 预期/实际统一边界 分箱结果(K个箱)

4.2 步骤二:计算各箱占比(Proportion)

分箱完成后,分别计算每个箱在预期样本和实际样本中的占比。公式如下:

预期占比(%i_Expected)= 第i箱预期样本数 / 预期总样本数 × 100%
实际占比(%i_Actual)= 第i箱实际样本数 / 实际总样本数 × 100%

假设我们将催收评分分为5箱,某次监控的计算示例如下:

箱编号 评分区间 预期样本数 预期占比(%) 实际样本数 实际占比(%)
1 [300, 400) 2,000 20.0 1,500 15.0
2 [400, 500) 2,000 20.0 2,200 22.0
3 [500, 600) 2,000 20.0 2,500 25.0
4 [600, 700) 2,000 20.0 1,800 18.0
5 [700, 800] 2,000 20.0 2,000 20.0
合计 — 10,000 100.0 10,000 100.0
避坑提示:若某箱实际占比为0%,会导致后续WOE计算出现无穷大。此时需将该箱与相邻箱合并,或对占比做平滑处理(如加0.5%的极小值)。

4.3 步骤三:计算WOE与差异(WOE & Difference)

WOE(Weight of Evidence,证据权重)用于衡量每个箱中实际占比相对于预期占比的偏移方向与幅度。计算公式为:

WOE_i = ln(实际占比_i / 预期占比_i)

差异项 = (实际占比_i - 预期占比_i) × WOE_i

WOE的正负含义:

  • WOE > 0:实际占比高于预期,该箱客群占比上升
  • WOE < 0:实际占比低于预期,该箱客群占比下降
  • WOE = 0:占比无变化,分布完全稳定

以上述数据为例,计算各箱WOE与差异项:

箱编号 预期占比(%) 实际占比(%) WOE 差异项
1 20.0 15.0 ln(15/20) = -0.2877 (15%-20%) × (-0.2877) = 0.0144
2 20.0 22.0 ln(22/20) = 0.0953 (22%-20%) × 0.0953 = 0.0019
3 20.0 25.0 ln(25/20) = 0.2231 (25%-20%) × 0.2231 = 0.0112
4 20.0 18.0 ln(18/20) = -0.1054 (18%-20%) × (-0.1054) = 0.0021
5 20.0 20.0 ln(20/20) = 0.0000 (20%-20%) × 0.0000 = 0.0000

4.4 步骤四:累加得到PSI值

将各箱的差异项求和,即得到最终的PSI值:

PSI = Σ[(实际占比_i - 预期占比_i) × ln(实际占比_i / 预期占比_i)]

根据上表数据:

PSI = 0.0144 + 0.0019 + 0.0112 + 0.0021 + 0.0000 = 0.0296

计算得到PSI = 0.0296(即2.96%)。根据行业通用判断标准:

PSI值范围 稳定性判断 催收触达应对建议
PSI < 0.10 稳定,无显著偏移 正常监控,无需干预
0.10 ≤ PSI < 0.25 轻度偏移,需关注 排查分箱内客群特征变化
PSI ≥ 0.25 严重偏移,需立即行动 触发模型重训或策略调整
催收场景解读:上述示例PSI=0.0296,表明当前触达客群分布与模型开发时基本一致。若PSI超过0.10,需重点检查箱1(低分客群占比下降)和箱3(中分客群占比上升),这可能意味着催收策略调整导致高风险客群被提前过滤。
PSI计算四步流程总览 步骤1:数据分箱 等频/等距/最优 步骤2:计算占比 %i_Expected %i_Actual 步骤3:WOE与差异 WOE = ln(实际/预期) 差异 = (实际-预期)×WOE 步骤4:累加PSI Σ 差异项 PSI计算公式 PSI = Σ[(%i_Actual - %i_Expected) × ln(%i_Actual / %i_Expected)] i = 1, 2, ..., K(K为分箱总数) PSI < 0.10 → 稳定 正常监控 0.10 ≤ PSI < 0.25 轻度偏移,需关注 PSI ≥ 0.25 严重偏移,立即行动

4.5 完整计算代码示例(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_value: 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_counts, _ = np.histogram(expected, bins=bin_edges)
    actual_counts, _ = np.histogram(actual, bins=bin_edges)
    
    expected_pct = expected_counts / len(expected)
    actual_pct = actual_counts / len(actual)
    
    # 步骤3:计算WOE与差异项
    # 避免除零和取对数问题
    eps = 0.0001
    expected_pct = np.clip(expected_pct, eps, 1 - eps)
    actual_pct = np.clip(actual_pct, eps, 1 - eps)
    
    woe = np.log(actual_pct / expected_pct)
    diff = (actual_pct - expected_pct) * woe
    
    # 步骤4:累加得到PSI值
    psi_value = np.sum(diff)
    
    # 构建结果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,
        'WOE': woe,
        '差异项': diff
    })
    
    return psi_value, psi_df

# 使用示例
if __name__ == "__main__":
    # 模拟催收评分数据
    np.random.seed(42)
    expected_scores = np.random.normal(600, 100, 10000)
    actual_scores = np.random.normal(610, 110, 10000)
    
    psi, details = calculate_psi(expected_scores, actual_scores)
    print(f"PSI值: {psi:.4f}")
    print("\n各箱详细计算结果:")
    print(details)
代码要点:
  • 使用np.percentile基于预期数据确定等频分箱边界,确保实际数据沿用相同边界
  • 通过np.clip将占比限制在[0.0001, 0.9999]区间,避免WOE计算出现无穷大
  • 返回的psi_df可直接用于监控报表,定位偏移最大的箱

通过以上四个步骤,即可完成PSI的完整计算。在实际催收触达监控中,建议将PSI计算封装为自动化函数,每日/每周定时运行,并将结果写入监控看板,实现分布偏移的实时预警。