3. PSI基础原理:群体稳定性指标的定义、计算公式、分箱逻辑与数学推导

群体稳定性指标(Population Stability Index, PSI)是风控模型监控中最核心的量化指标,用于衡量模型评分或特征分布在两个时间窗口(通常为建模样本与当前样本)之间的偏移程度。PSI 值越小,说明群体分布越稳定;PSI 值越大,说明模型或特征发生了显著漂移。

3.1 PSI 的定义与业务含义

PSI 本质上是一种基于信息论中 Kullback-Leibler 散度(KL 散度)的对称化变体,用于比较两个概率分布之间的差异。在风控场景中,PSI 通常用来回答以下问题:

  • 模型评分分布:当前月与建模月相比,评分分布是否发生了系统性偏移?
  • 特征分布:某个关键特征(如收入、负债率)的分布是否发生了结构性变化?
  • 客群结构:进件客群是否与建模客群存在显著差异?
业务经验阈值:PSI < 0.1 表示分布稳定,无需干预;0.1 ≤ PSI < 0.25 表示轻度漂移,需关注并排查原因;PSI ≥ 0.25 表示严重漂移,必须触发模型重训或特征重构。

3.2 PSI 计算公式与数学推导

PSI 的计算基于分箱后的实际占比与预期占比。假设我们将评分或特征值划分为 n 个区间(分箱),则 PSI 的通用计算公式为:

PSI = Σ (Actual_i - Expected_i) × ln(Actual_i / Expected_i)

其中:
- Actual_i   = 当前样本在第 i 个分箱中的占比(实际分布)
- Expected_i = 建模样本在第 i 个分箱中的占比(预期分布)
- ln         = 自然对数
- i          = 1, 2, ..., n(分箱编号)

数学推导说明:

  • PSI 的数学基础是 KL 散度:KL(P||Q) = Σ P_i × ln(P_i / Q_i),其中 P 为实际分布,Q 为预期分布。
  • KL 散度是非对称的,即 KL(P||Q) ≠ KL(Q||P)。PSI 通过同时计算 KL(P||Q) + KL(Q||P) 实现对称化,因此 PSI 也被称为 对称化 KL 散度。
  • 当 Actual_i 与 Expected_i 完全相等时,ln(1) = 0,PSI = 0,表示分布完全一致。
  • 当分布差异越大,ln(Actual_i / Expected_i) 的绝对值越大,PSI 值越高。
关键理解:PSI 不是简单的占比差值,而是通过对数变换放大了小占比区间的差异。这意味着即使某个分箱的占比绝对值很小,如果相对变化剧烈(例如从 1% 变为 5%),也会对 PSI 产生显著贡献。

3.3 分箱逻辑:等距分箱 vs 等频分箱

PSI 的计算依赖于分箱策略。不同的分箱方式会直接影响 PSI 的数值和敏感性。风控实践中主要使用以下两种分箱方法:

分箱方式 定义 优点 缺点 适用场景
等距分箱 将评分或特征值按固定宽度划分为 n 个区间(如每 50 分一个箱) 区间边界固定,便于跨时间对比;计算简单 样本分布不均时,某些箱可能为空或占比极低,导致 PSI 不稳定 评分卡模型(如 300-900 分,每 50 分一箱)
等频分箱 将样本按分位数划分为 n 个区间,每个箱包含大致相同的样本量 每个箱都有足够的样本,PSI 计算更稳健 区间边界随样本分布变化,跨时间对比时边界可能不一致 特征分布监控、非评分类模型输出
避坑提示:当使用等距分箱时,如果某个分箱在建模样本中占比为 0%(Expected_i = 0),则公式中 ln(Actual_i / 0) 无定义。此时需要做平滑处理,常见做法是将 0 替换为一个极小值(如 0.0001),或将该箱与相邻箱合并。

3.4 PSI 计算的分步流程

以下为 PSI 计算的标准操作步骤,适用于评分或特征监控:

  1. 确定基准分布:选取建模样本或历史稳定期的评分/特征值作为预期分布(Expected)。
  2. 确定当前分布:选取当前监控周期的样本作为实际分布(Actual)。
  3. 选择分箱方式:根据业务场景选择等距分箱或等频分箱,通常建议分箱数量为 10-20 个。
  4. 计算各箱占比:分别统计预期分布和实际分布在每个分箱中的样本占比。
  5. 计算每个分箱的贡献值:(Actual_i - Expected_i) × ln(Actual_i / Expected_i)。
  6. 求和得到 PSI:将所有分箱的贡献值相加,得到最终的 PSI 值。
  7. 阈值判断:根据 PSI 值判断分布稳定性,触发相应动作。

3.5 PSI 计算示例(等距分箱)

假设某评分卡模型输出范围为 300-900 分,建模样本与当前样本各 1000 条,按等距分箱(每 100 分一箱)计算 PSI:

评分区间 预期样本数 预期占比 (Expected_i) 实际样本数 实际占比 (Actual_i) 贡献值
300-400 50 0.05 80 0.08 (0.08-0.05)×ln(0.08/0.05) = 0.03×0.470 = 0.0141
400-500 150 0.15 200 0.20 (0.20-0.15)×ln(0.20/0.15) = 0.05×0.287 = 0.0144
500-600 300 0.30 280 0.28 (0.28-0.30)×ln(0.28/0.30) = (-0.02)×(-0.069) = 0.0014
600-700 300 0.30 250 0.25 (0.25-0.30)×ln(0.25/0.30) = (-0.05)×(-0.182) = 0.0091
700-800 150 0.15 130 0.13 (0.13-0.15)×ln(0.13/0.15) = (-0.02)×(-0.143) = 0.0029
800-900 50 0.05 60 0.06 (0.06-0.05)×ln(0.06/0.05) = 0.01×0.182 = 0.0018
合计 1000 1.00 1000 1.00 PSI = 0.0437

本例中 PSI = 0.0437 < 0.1,说明当前样本分布与建模样本分布基本一致,模型稳定性良好。

3.6 PSI 与 KL 散度的关系

为了更直观地理解 PSI 的数学本质,以下对比 PSI 与 KL 散度的异同:

指标 公式 对称性 取值范围 风控应用
KL 散度 KL(P||Q) = Σ P_i × ln(P_i / Q_i) 非对称 [0, +∞) 较少直接使用,作为 PSI 的理论基础
PSI Σ (P_i - Q_i) × ln(P_i / Q_i) 对称 [0, +∞) 广泛用于模型监控、特征漂移检测

从数学上可以证明:PSI = KL(P||Q) + KL(Q||P),即 PSI 是 KL 散度的对称化版本。这使得 PSI 在衡量分布差异时更加均衡,不会因为基准分布的选择而偏向某一方向。

3.7 PSI 计算流程示意图

以下 SVG 图展示了从原始数据到 PSI 输出的完整计算流程:

PSI 计算流程示意图 建模样本分布 当前样本分布 等距/等频分箱 等距/等频分箱 计算 Expected_i 计算 Actual_i Σ (Actual_i - Expected_i) × ln(Actual_i / Expected_i) PSI 值输出

3.8 PSI 分箱策略对比示意图

以下 SVG 图直观对比等距分箱与等频分箱在相同数据分布下的差异:

等距分箱 vs 等频分箱对比 等距分箱(固定宽度) 300-400 400-500 500-600 600-700 700-800 800-900 等频分箱(等样本量) Q1 Q2 Q3 Q4 Q5 Q6 Q7 Q8 关键差异: • 等距分箱:区间宽度固定,但各箱样本量可能差异巨大(如中间箱样本多,两端箱样本少)。 • 等频分箱:各箱样本量相等,但区间宽度随数据密度变化(数据密集处区间窄,稀疏处区间宽)。 实践建议: 评分卡模型推荐使用等距分箱(便于跨时间对比);特征分布监控推荐使用等频分箱(避免空箱问题)。 分箱数量建议控制在 10-20 个,过少会丢失细节,过多会导致噪声放大。

3.9 PSI 计算的代码实现(Python)

以下为 PSI 计算的 Python 实现,包含等距分箱与等频分箱两种方式:

import numpy as np
import pandas as pd

def calculate_psi(expected, actual, bins=10, method='equal_width'):
    """
    计算群体稳定性指标 (PSI)
    
    参数:
        expected: array-like, 建模样本(预期分布)
        actual: array-like, 当前样本(实际分布)
        bins: int, 分箱数量
        method: str, 'equal_width' 或 'equal_frequency'
    
    返回:
        psi: float, PSI 值
        bin_df: DataFrame, 各分箱明细
    """
    # 合并数据确定分箱边界
    all_data = np.concatenate([expected, actual])
    
    if method == 'equal_width':
        # 等距分箱
        bin_edges = np.linspace(np.min(all_data), np.max(all_data), bins + 1)
    elif method == 'equal_frequency':
        # 等频分箱(基于预期分布的分位数)
        bin_edges = np.percentile(expected, np.linspace(0, 100, bins + 1))
        # 确保边界唯一
        bin_edges = np.unique(bin_edges)
    else:
        raise ValueError("method 必须为 'equal_width' 或 'equal_frequency'")
    
    # 计算各箱占比
    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)
    
    # 平滑处理:将 0 替换为极小值
    eps = 1e-6
    expected_pct = np.where(expected_pct == 0, eps, expected_pct)
    actual_pct = np.where(actual_pct == 0, eps, actual_pct)
    
    # 计算每个分箱的贡献值
    contribution = (actual_pct - expected_pct) * np.log(actual_pct / expected_pct)
    
    # 计算 PSI
    psi = np.sum(contribution)
    
    # 构建明细 DataFrame
    bin_df = pd.DataFrame({
        'bin_start': bin_edges[:-1],
        'bin_end': bin_edges[1:],
        'expected_count': expected_counts,
        'actual_count': actual_counts,
        'expected_pct': expected_pct,
        'actual_pct': actual_pct,
        'contribution': contribution
    })
    
    return psi, bin_df

# 示例用法
np.random.seed(42)
expected_scores = np.random.normal(600, 100, 10000)
actual_scores = np.random.normal(620, 110, 10000)

psi_value, psi_detail = calculate_psi(expected_scores, actual_scores, bins=10, method='equal_width')
print(f"PSI 值: {psi_value:.4f}")
print(psi_detail.head())
代码要点:
  • 等频分箱时,分箱边界基于预期分布的分位数确定,确保预期分布各箱样本量大致相等。
  • 平滑处理(eps)是防止分母为 0 导致计算错误的关键步骤。
  • 返回的明细 DataFrame 可用于排查具体哪个分箱贡献了最大的偏移。

3.10 PSI 的局限性

尽管 PSI 是风控稳定性监控的核心指标,但在实际应用中仍需注意以下局限性:

  • 对分箱数量敏感:分箱越多,PSI 越容易受噪声影响;分箱过少,可能掩盖局部偏移。
  • 无法区分偏移方向:PSI 只反映分布差异的大小,不反映偏移的方向(如客群变好还是变差)。
  • 对样本量敏感:样本量过小时,PSI 的统计稳定性下降,容易出现假阳性。
  • 无法定位原因:PSI 只能检测到分布发生了偏移,但无法自动识别偏移的原因(如特征缺失、客群变化、外部环境变化等)。
重要提醒:PSI 是监控工具,不是诊断工具。当 PSI 触发告警时,需要结合特征重要性分析、分箱明细排查、业务逻辑判断等手段,才能准确定位漂移根因。