4. PSI 计算步骤详解:数据分箱、计算实际占比与预期占比、计算 PSI 值

群体稳定性指标(PSI)的计算过程分为三个核心步骤:数据分箱、计算实际占比与预期占比、计算 PSI 值。每一步都有严格的数学定义和工程注意事项,下面逐一拆解。

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

分箱是将连续变量或离散变量划分为若干个区间(箱),这是 PSI 计算的基础。分箱方式直接影响 PSI 的敏感度和稳定性。

核心原则:预期样本(训练集)和实际样本(生产集)必须使用完全相同的分箱边界,否则 PSI 会失真。

4.1.1 常见分箱方法对比

分箱方法 适用场景 优点 缺点
等距分箱 变量分布均匀,无极端值 实现简单,边界直观 对长尾分布不友好,空箱风险高
等频分箱 大多数评分卡场景 每箱样本量均衡,PSI 稳定 边界可能落在密集区域
分位数分箱 变量分布偏态严重 适应数据实际分布 分箱数需手动调参
自定义分箱 业务规则明确(如信用评分分段) 业务可解释性强 依赖专家经验,可能过拟合
工程建议:推荐使用 等频分箱,通常将数据分为 10 箱(deciles)。如果样本量较小(< 1000),可减少至 5 箱;如果变量取值离散且类别少,可直接使用原始类别作为箱。

4.1.2 分箱边界对齐示例

假设训练集(预期)中某特征“年龄”的等频分箱边界为:[18, 25, 35, 45, 55, 65, 100]。生产集(实际)必须使用完全相同的边界,即使生产集中没有 18 岁以下的样本,也要保留该箱(占比为 0)。

# Python 示例:使用 pandas 进行等频分箱
import pandas as pd
import numpy as np

# 训练集(预期)分箱
train_data = pd.DataFrame({'age': np.random.randint(18, 70, 1000)})
_, bins = pd.qcut(train_data['age'], q=10, retbins=True, duplicates='drop')
print("分箱边界:", bins)

# 生产集(实际)使用相同边界
prod_data = pd.DataFrame({'age': np.random.randint(20, 65, 500)})
train_data['age_bin'] = pd.cut(train_data['age'], bins=bins, include_lowest=True)
prod_data['age_bin'] = pd.cut(prod_data['age'], bins=bins, include_lowest=True)

4.2 步骤二:计算实际占比与预期占比

分箱完成后,分别计算每个箱中样本占总样本的比例。

  • 预期占比(Expected Proportion, Ei):训练集(基准)中第 i 个箱的样本数 / 训练集总样本数。
  • 实际占比(Actual Proportion, Ai):生产集(当前)中第 i 个箱的样本数 / 生产集总样本数。
避坑提示:如果某个箱在实际数据中样本数为 0,则 Ai = 0。此时 ln(Ai / Ei) 会因除以 0 而无穷大。工程上通常对占比做 平滑处理(如加一个小常数 0.001),或直接将该箱的 PSI 贡献置为 0。

4.2.1 占比计算示例表

箱编号 区间范围 预期样本数 预期占比 (Ei) 实际样本数 实际占比 (Ai)
1 [18, 25) 100 0.10 30 0.06
2 [25, 35) 100 0.10 80 0.16
3 [35, 45) 100 0.10 50 0.10
4 [45, 55) 100 0.10 40 0.08
5 [55, 100] 100 0.10 50 0.10

4.3 步骤三:计算 PSI 值

PSI 的数学公式为:

PSI = Σ (A_i - E_i) * ln(A_i / E_i)   (i = 1 到 n)

其中 n 为箱总数。每个箱的贡献值 (A_i - E_i) * ln(A_i / E_i) 累加得到最终 PSI。

4.3.1 逐箱计算过程

以上表数据为例,计算每个箱的 PSI 贡献:

箱编号 Ai Ei Ai - Ei Ai / Ei ln(Ai / Ei) 贡献值
1 0.06 0.10 -0.04 0.60 -0.5108 0.0204
2 0.16 0.10 0.06 1.60 0.4700 0.0282
3 0.10 0.10 0.00 1.00 0.0000 0.0000
4 0.08 0.10 -0.02 0.80 -0.2231 0.0045
5 0.10 0.10 0.00 1.00 0.0000 0.0000
PSI 总和 0.0531

本例中 PSI = 0.0531,属于轻微漂移(通常阈值:PSI < 0.1 为稳定,0.1 ~ 0.25 为轻度漂移,> 0.25 为严重漂移)。

4.4 PSI 计算流程总览

PSI 计算三步流程 步骤 1:数据分箱 等频 / 等距 / 自定义 步骤 2:计算占比 预期占比 E_i vs 实际占比 A_i 步骤 3:计算 PSI Σ (A_i - E_i) * ln(A_i / E_i) 关键工程细节 • 分箱边界必须完全一致(训练集 vs 生产集) • 空箱处理:占比为 0 时需平滑或跳过 • 推荐箱数:5 ~ 20 箱,常用 10 箱(deciles)

4.5 完整 PSI 计算代码实现

以下 Python 函数实现了完整的 PSI 计算,包含空箱平滑处理:

import numpy as np
import pandas as pd

def calculate_psi(expected, actual, bins=10, epsilon=1e-4):
    """
    计算群体稳定性指标 (PSI)
    
    参数:
        expected: array-like, 预期分布(训练集)
        actual: array-like, 实际分布(生产集)
        bins: int 或 array-like, 分箱数或自定义边界
        epsilon: float, 平滑常数,防止除零
    
    返回:
        psi: float, PSI 值
        psi_table: DataFrame, 逐箱明细
    """
    # 1. 分箱
    if isinstance(bins, int):
        # 等频分箱
        _, bin_edges = pd.qcut(expected, q=bins, retbins=True, duplicates='drop')
    else:
        bin_edges = bins
    
    # 2. 计算各箱频数
    expected_bins = pd.cut(expected, bins=bin_edges, include_lowest=True)
    actual_bins = pd.cut(actual, bins=bin_edges, include_lowest=True)
    
    expected_counts = expected_bins.value_counts().sort_index()
    actual_counts = actual_bins.value_counts().sort_index()
    
    # 3. 计算占比(加平滑)
    expected_pct = (expected_counts + epsilon) / (expected_counts.sum() + epsilon * len(expected_counts))
    actual_pct = (actual_counts + epsilon) / (actual_counts.sum() + epsilon * len(actual_counts))
    
    # 4. 计算 PSI
    psi_value = np.sum((actual_pct - expected_pct) * np.log(actual_pct / expected_pct))
    
    # 5. 构建明细表
    psi_table = pd.DataFrame({
        'bin': expected_pct.index,
        'expected_pct': expected_pct.values,
        'actual_pct': actual_pct.values,
        'contribution': (actual_pct - expected_pct) * np.log(actual_pct / expected_pct)
    })
    
    return psi_value, psi_table

# 使用示例
train_score = np.random.beta(2, 5, 10000)  # 模拟训练集评分
prod_score = np.random.beta(2.5, 4.5, 5000)  # 模拟生产集评分(有漂移)

psi, details = calculate_psi(train_score, prod_score, bins=10)
print(f"PSI = {psi:.4f}")
print(details.head())
工程最佳实践:
  • 将 PSI 计算封装为独立函数,便于在 MLOps 流水线中重复调用。
  • 每次计算时保存分箱边界(pickle 或 JSON),确保生产环境与训练环境边界一致。
  • 设置 PSI 告警阈值:PSI > 0.25 触发紧急告警,0.1 < PSI <= 0.25 触发观察告警。

4.6 常见问题与避坑指南

问题 原因 解决方案
PSI 为无穷大 某箱实际占比为 0,ln(0) 无定义 加平滑常数 epsilon(如 1e-4)
PSI 为 0 但数据明显漂移 分箱太粗,掩盖了分布变化 增加分箱数(如从 5 箱增至 10 箱)
PSI 波动剧烈 样本量太小,统计不稳定 使用累积窗口(如 7 天滑动窗口)计算
分箱边界不一致 训练集与生产集分别分箱 固定边界,从训练集导出后复用

掌握以上三步计算逻辑和工程细节,你就能在生产环境中准确、稳定地计算 PSI,为数据漂移监控打下坚实基础。