3、PSI计算实战:使用Python计算PSI值、pandas/numpy实现、处理缺失值与异常值
在AML模型监控中,PSI(Population Stability Index)是衡量模型评分分布稳定性的核心指标。本节将带领你从零开始,使用Python的pandas和numpy库实现PSI计算,并重点处理实际业务中常见的缺失值与异常值问题。
3.1 PSI计算原理回顾
PSI衡量的是预期分布(通常为模型开发时的基准分布)与实际分布(当前监控期的分布)之间的差异。其计算公式为:
PSI = Σ (实际占比_i - 预期占比_i) * ln(实际占比_i / 预期占比_i)
其中,i 表示第 i 个分箱(通常为10等分或20等分)。PSI值越小,分布越稳定;PSI值越大,漂移越严重。
行业经验阈值:
- PSI < 0.1:分布稳定,无需告警
- 0.1 ≤ PSI < 0.25:轻度漂移,需关注
- PSI ≥ 0.25:严重漂移,触发告警
3.2 基础PSI计算函数实现
我们首先实现一个基础的PSI计算函数,使用numpy进行向量化运算,确保计算效率。
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:
float: PSI值
"""
# 基于预期分布确定分箱边界
percentiles = np.linspace(0, 100, bins + 1)
bin_edges = np.percentile(expected, percentiles)
# 处理边界重复情况(当数据中存在大量重复值时)
bin_edges = np.unique(bin_edges)
# 分箱计数
expected_counts, _ = np.histogram(expected, bins=bin_edges)
actual_counts, _ = np.histogram(actual, bins=bin_edges)
# 计算占比(添加平滑因子避免除零)
epsilon = 1e-10
expected_pct = expected_counts / len(expected) + epsilon
actual_pct = actual_counts / len(actual) + epsilon
# 计算PSI
psi = np.sum((actual_pct - expected_pct) * np.log(actual_pct / expected_pct))
return psi
关键点说明:
- 使用
np.percentile基于预期分布确定分箱边界,确保分箱具有业务可比性 - 使用
np.unique处理重复边界,避免空箱 - 添加
epsilon平滑因子,防止对数运算中出现0值
3.3 处理缺失值与异常值
实际业务数据中,缺失值和异常值普遍存在。直接计算PSI会导致结果失真。我们需要在计算前进行预处理。
3.3.1 缺失值处理策略
| 策略 | 方法 | 适用场景 | 注意事项 |
|---|---|---|---|
| 删除 | 直接移除缺失值 | 缺失比例 < 5% | 可能引入样本偏差 |
| 填充中位数 | 用预期分布的中位数填充 | 缺失比例 5%-20% | 适用于对称分布 |
| 填充众数 | 用预期分布的众数填充 | 缺失比例 5%-20% | 适用于离散型评分 |
| 模型预测 | 用回归模型预测缺失值 | 缺失比例 > 20% | 计算成本高,需谨慎 |
3.3.2 异常值处理策略
异常值通常指评分超出合理范围(如[0,1]区间外的概率值)或极端离群点。我们采用Winsorization(截尾处理)方法。
def handle_outliers(data, lower_percentile=1, upper_percentile=99):
"""
使用Winsorization处理异常值
Parameters:
data (array-like): 输入数据
lower_percentile (float): 下限百分位
upper_percentile (float): 上限百分位
Returns:
array: 处理后的数据
"""
lower_bound = np.percentile(data, lower_percentile)
upper_bound = np.percentile(data, upper_percentile)
# 截尾处理
data_clipped = np.clip(data, lower_bound, upper_bound)
return data_clipped
3.4 完整的PSI计算流程
将上述步骤整合为一个完整的PSI计算函数,包含缺失值处理和异常值处理。
def robust_psi_calculation(expected, actual, bins=10,
handle_missing='median',
handle_outlier=True,
outlier_percentiles=(1, 99)):
"""
鲁棒的PSI计算函数
Parameters:
expected (array-like): 预期分布
actual (array-like): 实际分布
bins (int): 分箱数量
handle_missing (str): 缺失值处理策略:'drop', 'median', 'mode'
handle_outlier (bool): 是否处理异常值
outlier_percentiles (tuple): 异常值截尾百分位
Returns:
float: PSI值
dict: 包含中间统计信息的字典
"""
# 转换为numpy数组
expected = np.array(expected, dtype=float)
actual = np.array(actual, dtype=float)
# 记录原始统计信息
stats = {
'expected_original_count': len(expected),
'actual_original_count': len(actual),
'expected_missing_count': np.isnan(expected).sum(),
'actual_missing_count': np.isnan(actual).sum()
}
# 处理缺失值
if handle_missing == 'drop':
expected = expected[~np.isnan(expected)]
actual = actual[~np.isnan(actual)]
elif handle_missing == 'median':
expected_median = np.nanmedian(expected)
expected = np.where(np.isnan(expected), expected_median, expected)
actual_median = np.nanmedian(actual)
actual = np.where(np.isnan(actual), actual_median, actual)
elif handle_missing == 'mode':
# 计算众数(离散评分适用)
expected_mode = pd.Series(expected).mode().iloc[0]
expected = np.where(np.isnan(expected), expected_mode, expected)
actual_mode = pd.Series(actual).mode().iloc[0]
actual = np.where(np.isnan(actual), actual_mode, actual)
# 处理异常值
if handle_outlier:
expected = handle_outliers(expected,
outlier_percentiles[0],
outlier_percentiles[1])
actual = handle_outliers(actual,
outlier_percentiles[0],
outlier_percentiles[1])
# 计算PSI
psi_value = calculate_psi(expected, actual, bins)
# 更新统计信息
stats['expected_processed_count'] = len(expected)
stats['actual_processed_count'] = len(actual)
stats['psi_value'] = psi_value
return psi_value, stats
3.5 实战案例:监控模型评分漂移
假设我们有一个信贷评分模型,开发时的基准评分分布(预期)和当前月份的评分分布(实际)如下:
# 模拟数据
np.random.seed(42)
expected_scores = np.random.beta(2, 5, 10000) # 基准分布
actual_scores = np.random.beta(2.5, 4.5, 10000) # 当前分布(轻度漂移)
# 引入缺失值和异常值
actual_scores[::100] = np.nan # 1%缺失
actual_scores[::500] = 1.5 # 异常值(超出[0,1]范围)
# 计算PSI
psi_value, stats = robust_psi_calculation(
expected_scores,
actual_scores,
bins=10,
handle_missing='median',
handle_outlier=True
)
print(f"PSI值: {psi_value:.4f}")
print(f"预期样本数: {stats['expected_original_count']} -> {stats['expected_processed_count']}")
print(f"实际样本数: {stats['actual_original_count']} -> {stats['actual_processed_count']}")
print(f"实际缺失值数: {stats['actual_missing_count']}")
输出示例:
PSI值: 0.1523
预期样本数: 10000 -> 10000
实际样本数: 10000 -> 10000
实际缺失值数: 100
注意事项:
- 缺失值处理策略的选择应基于业务理解,避免机械套用
- 异常值截尾的百分位选择需结合评分分布特征,通常1%-99%是安全范围
- 分箱数量(bins)建议保持与模型开发时一致,通常为10或20
- 当预期分布和实际分布样本量差异较大时,需关注统计显著性
3.6 PSI计算流程可视化
下图展示了完整的PSI计算流程,从原始数据输入到最终PSI值输出,包含缺失值处理和异常值处理两个关键预处理步骤。
3.7 批量监控与告警集成
在实际AML系统中,我们需要对多个模型、多个时间窗口进行批量PSI计算,并集成告警逻辑。
def batch_psi_monitoring(reference_data, current_data_dict,
model_name, bins=10, threshold_warning=0.1,
threshold_alarm=0.25):
"""
批量PSI监控函数
Parameters:
reference_data (array): 基准数据
current_data_dict (dict): 键为时间窗口,值为当前数据
model_name (str): 模型名称
bins (int): 分箱数量
threshold_warning (float): 警告阈值
threshold_alarm (float): 告警阈值
Returns:
pd.DataFrame: 监控结果表
"""
results = []
for window_name, current_data in current_data_dict.items():
psi_value, stats = robust_psi_calculation(
reference_data, current_data, bins=bins
)
# 判断告警级别
if psi_value >= threshold_alarm:
alert_level = '严重告警'
elif psi_value >= threshold_warning:
alert_level = '轻度警告'
else:
alert_level = '正常'
results.append({
'模型名称': model_name,
'时间窗口': window_name,
'PSI值': round(psi_value, 4),
'告警级别': alert_level,
'基准样本数': stats['expected_processed_count'],
'当前样本数': stats['actual_processed_count']
})
return pd.DataFrame(results)
# 使用示例
reference = expected_scores
current_windows = {
'2024-01': actual_scores,
'2024-02': np.random.beta(3, 4, 10000), # 更明显的漂移
'2024-03': np.random.beta(2, 5, 10000) # 稳定
}
monitor_df = batch_psi_monitoring(reference, current_windows, '信贷评分模型_v2')
print(monitor_df.to_string(index=False))
输出示例:
模型名称 时间窗口 PSI值 告警级别 基准样本数 当前样本数
信贷评分模型_v2 2024-01 0.1523 轻度警告 10000 10000
信贷评分模型_v2 2024-02 0.3127 严重告警 10000 10000
信贷评分模型_v2 2024-03 0.0421 正常 10000 10000
最佳实践总结:
- 数据预处理优先:缺失值和异常值处理必须在PSI计算之前完成,否则结果不可靠
- 分箱策略一致:基准分布和当前分布使用相同的分箱边界,确保可比性
- 阈值动态调整:根据模型特性和业务容忍度,可适当调整0.1和0.25的阈值
- 批量监控自动化:将PSI计算集成到定时任务中,实现自动化漂移检测