4. PSI计算工具与代码实现:使用Python(pandas、numpy)实现PSI计算函数、自动化脚本框架
在催收触达监控体系中,PSI(Population Stability Index)的计算需要高效、可复用的工具支撑。本节将提供基于Python的PSI计算函数实现,并构建一套适用于催收场景的自动化监控脚本框架,帮助团队快速部署稳定性监控。
4.1 PSI计算核心函数
PSI的计算本质是衡量两个分布(实际分布 vs 预期分布)在各分箱上的偏差。以下函数支持等频分箱与等距分箱两种模式,并内置了分箱合并与零值处理逻辑。
import pandas as pd
import numpy as np
def calculate_psi(actual, expected, bins=10, method='quantile'):
"""
计算群体稳定性指数 (PSI)
参数:
actual (pd.Series): 实际分布数据(如当前周期触达率)
expected (pd.Series): 预期分布数据(如基准周期触达率)
bins (int): 分箱数量,默认10
method (str): 分箱方法,'quantile' 等频分箱,'uniform' 等距分箱
返回:
float: PSI值
pd.DataFrame: 分箱明细表
"""
# 合并数据,确定分箱边界
all_data = pd.concat([actual, expected], ignore_index=True)
if method == 'quantile':
# 等频分箱:基于预期数据的分位数
bin_edges = np.percentile(expected.dropna(), np.linspace(0, 100, bins + 1))
elif method == 'uniform':
# 等距分箱:基于预期数据的范围
bin_edges = np.linspace(expected.min(), expected.max(), bins + 1)
else:
raise ValueError("method 参数仅支持 'quantile' 或 'uniform'")
# 确保边界唯一,避免分箱失败
bin_edges = np.unique(bin_edges)
if len(bin_edges) < 2:
return 0.0, pd.DataFrame()
# 分箱标签
labels = [f'Bin_{i+1}' for i in range(len(bin_edges) - 1)]
# 对实际和预期数据分别分箱
actual_bins = pd.cut(actual, bins=bin_edges, labels=labels, include_lowest=True, right=False)
expected_bins = pd.cut(expected, bins=bin_edges, labels=labels, include_lowest=True, right=False)
# 计算各箱占比
actual_pct = actual_bins.value_counts(normalize=True).reindex(labels, fill_value=0)
expected_pct = expected_bins.value_counts(normalize=True).reindex(labels, fill_value=0)
# 零值平滑处理:将0替换为极小值,避免除零
epsilon = 1e-6
actual_pct = actual_pct.replace(0, epsilon)
expected_pct = expected_pct.replace(0, epsilon)
# 计算各箱PSI贡献
psi_components = (actual_pct - expected_pct) * np.log(actual_pct / expected_pct)
psi_total = psi_components.sum()
# 构建明细表
detail_df = pd.DataFrame({
'分箱': labels,
'实际占比': actual_pct.values,
'预期占比': expected_pct.values,
'PSI贡献': psi_components.values
})
return round(psi_total, 6), detail_df
函数设计要点:
- 分箱方法选择:催收触达率通常呈偏态分布,推荐使用
quantile等频分箱,确保每箱样本量均衡。 - 零值平滑:当某箱实际占比为0时,替换为
1e-6,避免对数计算报错。 - 边界唯一性:当数据高度集中时,分位数可能重复,使用
np.unique自动合并重复边界。
4.2 自动化监控脚本框架
在催收运营中,PSI监控需要按日/周周期运行,并自动输出告警。以下框架封装了数据加载、PSI计算、阈值判断与结果存储四个环节。
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
class PSIMonitor:
"""催收触达PSI自动化监控框架"""
def __init__(self, config):
self.config = config
self.psi_threshold = config.get('psi_threshold', 0.1)
self.warning_threshold = config.get('warning_threshold', 0.25)
self.bins = config.get('bins', 10)
self.method = config.get('method', 'quantile')
def load_data(self, date_str):
"""模拟加载指定日期的催收触达数据"""
# 实际场景中替换为数据库查询或文件读取
# 此处生成示例数据
np.random.seed(hash(date_str) % 2**32)
n = 10000
data = pd.DataFrame({
'contact_rate': np.random.beta(2, 5, n), # 模拟触达率分布
'customer_id': range(n)
})
return data
def get_expected_distribution(self):
"""获取基准分布(如上一周或上月同期)"""
# 实际场景中从基准表读取
expected_data = self.load_data('2025-01-01')
return expected_data['contact_rate']
def run_daily_check(self, date_str):
"""执行单日PSI检查"""
# 加载当日数据
actual_data = self.load_data(date_str)
actual_series = actual_data['contact_rate']
# 获取基准分布
expected_series = self.get_expected_distribution()
# 计算PSI
psi_value, detail_df = calculate_psi(
actual_series, expected_series,
bins=self.bins, method=self.method
)
# 阈值判断
status = '正常'
if psi_value > self.warning_threshold:
status = '严重漂移'
elif psi_value > self.psi_threshold:
status = '轻度漂移'
# 记录结果
result = {
'date': date_str,
'psi': psi_value,
'status': status,
'sample_size': len(actual_series),
'check_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
}
return result, detail_df
def run_batch(self, start_date, end_date):
"""批量运行多日监控"""
date_range = pd.date_range(start=start_date, end=end_date, freq='D')
results = []
for single_date in date_range:
date_str = single_date.strftime('%Y-%m-%d')
result, _ = self.run_daily_check(date_str)
results.append(result)
print(f"[{date_str}] PSI = {result['psi']:.4f}, 状态: {result['status']}")
results_df = pd.DataFrame(results)
return results_df
# 使用示例
if __name__ == '__main__':
config = {
'psi_threshold': 0.1,
'warning_threshold': 0.25,
'bins': 10,
'method': 'quantile'
}
monitor = PSIMonitor(config)
# 单日检查
result, detail = monitor.run_daily_check('2025-03-15')
print(f"单日PSI: {result['psi']}, 状态: {result['status']}")
# 批量检查
batch_results = monitor.run_batch('2025-03-01', '2025-03-15')
框架扩展建议:
- 数据源接入:将
load_data方法替换为数据库查询(如MySQL、ClickHouse),支持按催收策略分组计算。 - 告警推送:在
run_daily_check中集成钉钉/企业微信机器人,当状态为“严重漂移”时自动发送告警。 - 结果持久化:将
results_df写入监控结果表,用于长期趋势分析。
4.3 PSI计算流程与数据流转
下图展示了从原始触达数据到PSI监控结果的完整数据流转与计算流程,帮助理解各模块之间的依赖关系。
4.4 参数配置与阈值调整
PSI监控框架的灵活性体现在参数可配置化。以下表格列出了核心参数及其推荐配置,团队可根据催收策略的敏感度进行调整。
| 参数名称 | 默认值 | 推荐范围 | 说明 |
|---|---|---|---|
psi_threshold |
0.1 | 0.05 ~ 0.15 | 轻度漂移阈值,超过此值触发黄色告警 |
warning_threshold |
0.25 | 0.20 ~ 0.35 | 严重漂移阈值,超过此值触发红色告警 |
bins |
10 | 5 ~ 20 | 分箱数量,数据量大时可适当增加 |
method |
quantile | quantile / uniform | 分箱方法,偏态分布推荐 quantile |
epsilon |
1e-6 | 1e-8 ~ 1e-4 | 零值平滑系数,避免除零错误 |
阈值调整注意事项:
- 策略敏感度:对于高价值客户催收策略,建议降低
psi_threshold至 0.05,提前发现分布偏移。 - 分箱数量:分箱过少会掩盖局部偏移,分箱过多则对噪声敏感。建议根据样本量动态调整:样本量 > 10万时使用 20 箱,样本量 < 1万时使用 5 箱。
- 基准更新频率:基准分布建议每周或每月更新一次,避免因业务周期性波动导致误告警。
4.5 批量运行与结果输出
以下代码展示了如何调用 PSIMonitor 框架进行批量监控,并将结果保存为CSV文件,便于后续分析。
# 批量运行并保存结果
monitor = PSIMonitor(config)
batch_results = monitor.run_batch('2025-03-01', '2025-03-15')
# 保存监控结果
batch_results.to_csv('psi_monitor_results.csv', index=False)
print("监控结果已保存至 psi_monitor_results.csv")
# 查看漂移日期
drifted_days = batch_results[batch_results['status'] != '正常']
print(f"漂移天数: {len(drifted_days)}")
print(drifted_days[['date', 'psi', 'status']])
结果分析建议:
- 趋势分析:将每日PSI值绘制成折线图,观察漂移是否呈持续上升趋势。
- 归因分析:当PSI超过阈值时,查看
detail_df中各分箱的PSI贡献,定位具体偏移区间。 - 策略联动:将PSI监控结果与催收策略调整记录关联,评估策略变更对分布稳定性的影响。
4.6 代码实现常见问题与避坑
| 常见问题 | 原因 | 解决方案 |
|---|---|---|
| PSI计算结果为 NaN | 分箱后某箱占比为0,对数计算报错 | 使用 epsilon 平滑零值,或合并空箱 |
| 分箱边界重复导致分箱失败 | 数据高度集中,分位数重复 | 使用 np.unique 去重,或增加分箱数量 |
| PSI值异常大(>1.0) | 实际分布与基准分布差异极大,或分箱方法不当 | 检查数据源是否异常,改用等频分箱 |
| 批量运行速度慢 | 每次运行都重新加载基准分布 | 将基准分布缓存为全局变量,或从数据库一次性读取 |