4. PSI 计算基础:分箱方法(等距、等频、分位数)与实际分布与预期分布的计算

群体稳定性指标(PSI)的核心在于比较两个分布之间的差异。在量化风控的高频行情场景中,我们通常将当前监控周期的数据分布称为实际分布,将基准周期(如模型开发期、上线首周)的数据分布称为预期分布。计算 PSI 的第一步,也是决定计算结果可靠性的关键步骤,就是分箱。

4.1 三种主流分箱方法

分箱的目的是将连续变量或离散变量划分为有限的区间(Bins),从而计算每个区间内的样本占比。不同的分箱方法适用于不同的数据特征和监控目标。下表对比了三种最常用的方法:

分箱方法 核心逻辑 适用场景 优点 缺点
等距分箱 将变量取值范围划分为宽度相等的区间 变量分布均匀,无极端离群值 计算简单,区间边界直观 数据稀疏时,部分区间可能样本量为0
等频分箱 每个区间包含大致相等的样本数量 分布偏态严重,或需要保证每个区间都有足够样本 避免空箱,对离群值不敏感 区间宽度差异大,边界值可能不直观
分位数分箱 基于数据的分位数(如十分位数、四分位数)划分区间 需要关注分布尾部的变化,或与模型评分卡对齐 对分布形状自适应,常用于评分卡监控 分位数点计算需注意样本量
实操建议:在高频行情坏账率监控中,推荐优先使用等频分箱或分位数分箱。因为坏账率分布通常呈现右偏(大部分客户坏账率低,少数客户坏账率高),等距分箱容易导致前几个区间样本过多、后几个区间样本过少,使 PSI 计算失真。

4.2 分箱边界确定与分布计算

无论采用哪种分箱方法,计算 PSI 的流程都遵循以下步骤。我们以等频分箱为例,展示从原始数据到分布占比的完整计算过程。

  1. 确定基准分箱边界:基于预期分布(基准数据集)计算分位数,得到每个区间的上下边界。例如,将数据等频分为 10 箱,则边界为第 10%、20%……90% 分位数。
  2. 映射实际分布:将实际分布(当前监控数据集)的每个样本,按照基准分箱边界落入对应的区间。
  3. 计算区间占比:分别统计预期分布和实际分布在每个区间内的样本数量,除以各自的总样本数,得到 P_i(预期占比)和 A_i(实际占比)。
关键原则:分箱边界必须固定在基准数据集上,不能随实际分布的变化而重新计算。否则,PSI 将失去衡量分布偏移的意义。

下面是一个计算示例,假设我们将某评分变量等频分为 5 箱:

分箱编号 区间边界(基于预期) 预期占比 P_i 实际占比 A_i
1 [0, 200) 20.0% 15.0%
2 [200, 400) 20.0% 22.0%
3 [400, 600) 20.0% 25.0%
4 [600, 800) 20.0% 18.0%
5 [800, 1000] 20.0% 20.0%

4.3 实际分布与预期分布的计算逻辑

在得到每个区间的占比后,PSI 的计算公式为:

PSI = Σ ( (A_i - P_i) * ln(A_i / P_i) )

其中:

  • A_i:第 i 个区间的实际分布占比
  • P_i:第 i 个区间的预期分布占比
  • ln:自然对数
避坑提示:当某个区间的 A_i 或 P_i 为 0 时,ln(A_i / P_i) 将无法计算。解决方案:1)使用等频分箱避免空箱;2)对占比进行平滑处理,如加一个极小值(如 0.0001);3)合并样本量过少的相邻区间。

4.4 分箱与分布计算流程总览

下面的 SVG 图展示了从原始数据到 PSI 输出的完整计算链路,重点突出了分箱与分布计算环节:

PSI 计算流程:分箱与分布计算 原始数据 分箱方法选择 等距分箱 等频分箱 分位数分箱 固定分箱边界 (基于预期分布) 分布占比计算 P_i 与 A_i PSI 指标输出 说明: 1. 分箱边界一旦基于预期分布确定,后续监控周期不再改变。 2. 实际分布 A_i 的计算,是将当前数据映射到固定的分箱边界上。 3. 等频分箱可有效避免 A_i 或 P_i 为 0 的情况。

4.5 Python 代码实现:分箱与分布计算

以下代码演示了如何使用 Python 实现等频分箱,并计算预期分布与实际分布的占比:

import numpy as np
import pandas as pd

def calculate_psi(expected, actual, bins=10):
    """
    计算 PSI,使用等频分箱
    :param expected: array-like, 预期分布数据(基准)
    :param actual: array-like, 实际分布数据(当前)
    :param bins: int, 分箱数量
    :return: float, PSI 值
    """
    # 1. 基于预期分布确定分位数边界(等频分箱)
    percentiles = np.linspace(0, 100, bins + 1)
    bin_edges = np.percentile(expected, percentiles)
    # 处理边界重复(如数据中有大量相同值)
    bin_edges = np.unique(bin_edges)
    if len(bin_edges) < 2:
        return 0.0  # 数据无变化,PSI 为 0

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

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

    # 4. 平滑处理:避免 ln(0)
    expected_pct = np.clip(expected_pct, 0.0001, 1.0)
    actual_pct = np.clip(actual_pct, 0.0001, 1.0)

    # 5. 计算 PSI
    psi = np.sum((actual_pct - expected_pct) * np.log(actual_pct / expected_pct))
    return psi

# 示例用法
np.random.seed(42)
expected_data = np.random.normal(0.5, 0.1, 10000)  # 基准分布
actual_data = np.random.normal(0.55, 0.12, 10000)  # 当前分布(发生偏移)

psi_value = calculate_psi(expected_data, actual_data, bins=10)
print(f"PSI 值: {psi_value:.4f}")
代码要点:
  • np.percentile 用于计算等频分箱的边界,确保每个区间样本量大致相等。
  • np.histogram 使用相同的 bin_edges 分别统计预期和实际分布。
  • np.clip 对占比进行下限截断,避免对数计算出现负无穷。

4.6 分箱方法选择决策树

在实际项目中,如何快速选择分箱方法?下面的 SVG 决策树可以帮助你做出判断:

分箱方法选择决策树 数据分布是否均匀? 等距分箱 适用:正态分布、均匀分布 是否关注尾部? 分位数分箱 适用:评分卡、尾部风险监控 等频分箱 适用:偏态分布、通用场景 提示:高频行情坏账率监控中,若数据量充足(N > 10000),优先选择等频分箱;若需与模型评分卡对齐,选择分位数分箱。

4.7 常见问题与避坑指南

  • 分箱数量如何确定? 一般推荐 10~20 箱。箱数太少会丢失分布细节,箱数太多会导致噪声放大。高频场景下,10 箱是兼顾稳定性和敏感度的常用选择。
  • 边界重复怎么办? 当数据中存在大量重复值时,分位数边界可能重合。使用 np.unique 去重,并合并样本量过少的区间。
  • 预期分布如何选取? 通常选取模型上线后第一个完整周期(如首周)的数据作为预期分布。后续每次监控都与此基准对比。
  • 高频行情下的性能优化: 对于每秒千万级的数据流,建议预计算分箱边界并缓存,避免每次计算 PSI 时重复分箱。