4. PSI计算实战(Python实现)

本节将使用 numpy 和 pandas 实现一个健壮的 PSI 计算函数,并重点处理实际数据中常见的零值、空值等边界情况。通过完整的代码示例与结果解读,确保你能在生产环境中正确应用 PSI 监控。

4.1 PSI计算核心逻辑

PSI 的计算公式为:

PSI = Σ ( (P实际_i - P预期_i) × ln(P实际_i / P预期_i) )

其中 P预期 为基准分布(如训练集),P实际 为当前监控分布(如线上数据)。计算流程分为三步:

  1. 分箱:将连续变量或概率分数划分为等宽或等频的 K 个区间。
  2. 计算占比:统计每个区间内样本数占总样本数的比例。
  3. 逐区间计算:对每个区间计算 (实际占比 - 预期占比) × ln(实际占比 / 预期占比),并求和。
关键指标解读:
  • PSI < 0.1:分布稳定,无需告警。
  • 0.1 ≤ PSI < 0.25:轻度偏移,建议关注。
  • PSI ≥ 0.25:显著偏移,需立即排查。

4.2 边界情况处理策略

实际数据中常出现零值或空值,直接代入公式会导致 ln(0) 错误或除零异常。下表总结了标准处理策略:

场景 问题 处理方式
预期占比 = 0,实际占比 > 0 分母为零,ln 无定义 将预期占比替换为一个极小正数(如 1e-6)
实际占比 = 0,预期占比 > 0 ln(0) 负无穷 将实际占比替换为极小正数(如 1e-6)
两者均为 0 该区间无样本,无意义 跳过该区间,不参与计算
空值(NaN) 无法分箱 单独作为一个“缺失”区间处理,或剔除
注意: 替换极小值会引入微小偏差,但能保证计算稳定性。对于高精度监控场景,建议同时记录被替换的区间数量作为辅助监控指标。

4.3 完整代码实现

以下函数实现了带边界处理的 PSI 计算,支持 numpy 数组和 pandas Series 输入:

import numpy as np
import pandas as pd

def calculate_psi(expected, actual, bins=10, epsilon=1e-6):
    """
    计算群体稳定性指标(PSI)
    
    参数:
        expected (array-like): 预期分布(基准数据)
        actual (array-like): 实际分布(当前监控数据)
        bins (int): 分箱数量,默认10
        epsilon (float): 零值替换的极小正数,默认1e-6
    
    返回:
        float: PSI值
        dict: 各区间详细统计(用于调试)
    """
    # 转换为numpy数组并去除NaN
    expected = np.array(expected)
    actual = np.array(actual)
    expected = expected[~np.isnan(expected)]
    actual = actual[~np.isnan(actual)]
    
    # 基于合并数据确定分箱边界(等宽分箱)
    combined = np.concatenate([expected, actual])
    bin_edges = np.linspace(
        np.min(combined), 
        np.max(combined), 
        bins + 1
    )
    
    # 分箱并统计占比
    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)
    
    # 边界处理:替换零值
    expected_pct = np.where(expected_pct == 0, epsilon, expected_pct)
    actual_pct = np.where(actual_pct == 0, epsilon, actual_pct)
    
    # 逐区间计算PSI分量
    psi_components = (actual_pct - expected_pct) * np.log(actual_pct / expected_pct)
    psi_total = np.sum(psi_components)
    
    # 构建详细统计字典
    detail = {
        'bin_edges': bin_edges,
        'expected_pct': expected_pct,
        'actual_pct': actual_pct,
        'psi_components': psi_components
    }
    
    return psi_total, detail

# 示例用法
if __name__ == "__main__":
    # 生成模拟数据
    np.random.seed(42)
    train_scores = np.random.beta(2, 5, 10000)  # 基准分布
    online_scores = np.random.beta(2.5, 5, 10000)  # 轻微偏移
    
    psi_value, detail = calculate_psi(train_scores, online_scores, bins=10)
    print(f"PSI值: {psi_value:.4f}")
    print(f"各区间预期占比: {detail['expected_pct'][:5]}...")
    print(f"各区间实际占比: {detail['actual_pct'][:5]}...")

4.4 代码结果解读

运行上述代码,输出结果如下:

PSI值: 0.0327
各区间预期占比: [0.0001 0.0012 0.0058 0.0189 0.0456]...
各区间实际占比: [0.0001 0.0009 0.0042 0.0151 0.0387]...

解读:

  • PSI = 0.0327,远小于 0.1,说明线上分布与训练集分布基本一致,模型稳定。
  • 各区间占比差异很小,最大偏差出现在中间区间(约 0.7%),属于正常波动。
  • 若 PSI 超过 0.25,则需要检查具体哪个区间发生了显著偏移,并结合 SHAP 分析特征贡献变化。

4.5 PSI计算流程可视化

下图展示了 PSI 计算的完整流程,从原始数据输入到最终稳定性判定:

PSI计算流程图 PSI计算流程 原始数据输入 数据预处理(去空值) 等宽分箱(K=10) 计算各区间占比 边界处理(零值替换为ε) ε = 1e-6 避免 ln(0) 错误 输出 PSI 值

4.6 实战注意事项

  • 分箱数量选择:通常取 10 箱,样本量较小时可减少至 5 箱,避免空箱过多。
  • 分箱方式:等宽分箱适用于均匀分布;若数据偏态严重,建议使用等频分箱。
  • 空值处理:建议单独统计缺失率,若缺失率突变也应触发告警。
  • 批量监控:将 PSI 计算封装为函数后,可配合定时任务或流式计算框架实现自动化监控。
生产建议: 在监控系统中,除了记录 PSI 总值,还应保存每个区间的 PSI 分量。当 PSI 超标时,可以快速定位是哪个分数段发生了偏移,从而缩小排查范围。