简介:面向金融数据分析师、风控建模从业者及Python学习者,是一套与《Python金融大数据风控建模实战》书籍配套的完整源码资源,聚焦风控模型落地全流程,覆盖数据清洗、特征工程、信用评分、欺诈检测、聚类分析、模型验证与优化等核心环节。包内共收录106个文件,含40个py源码脚本、19个pkl模型文件、16个csv金融数据集、20个png可视化图表、3个ipynb案例笔记及2个依赖whl,整体约19.67MB,目录结构便于按章节与主题检索学习。目前已有1001人学习下载,适合通过运行代码理解逻辑回归、随机森林、XGBoost、Isolation Forest等算法在金融风控场景中的具体落地方式。借助书中配套数据与模型文件,可快速复现信用评分、欺诈检测与客户细分流程,并通过可视化结果直观对比模型表现,有效缩短从理论到实战的上手路径。
1. 这套Python金融大数据风控建模源码包:从两个真实数据集开始的实战闭环
如果你正在学Python数据分析,又不想停留在iris鸢尾花和titanic泰坦尼克号这种玩具数据集上,这套《Python金融大数据风控建模实战》源码包值得花时间拆一遍。压缩包里带了LoanStats_2019Q1.csv和german.csv两个真实风控场景数据,前者是Lending Club平台2019年第一季度的贷款记录,后者是德国信用数据集——这两个文件足够支撑你把数据清洗、特征工程、信用评分、模型验证、评分卡转换这一整条风控建模流水线完整跑通。对刚入行信贷风控、或者准备转行做金融数据分析的从业者来说,它的价值在于把「理论上的风控模型」变成「能运行的Python代码」,每个环节的坑都暴露在真实数据里,你能直观看到数据不平衡、时间穿越、WOE分箱这些问题是怎么影响模型结果的。我实际复现过一遍,这套资源用的都是Pandas、Scikit-learn、XGBoost这些主流库,环境好搭、代码能直接改着用。
2. 数据准备与预处理:把LoanStats_2019Q1.csv变成可训练样本的关键操作
2.1 两个数据集的字段差异与适用场景
先搞清楚你手上有什么。LoanStats_2019Q1.csv是Lending Club的贷款绩效数据,包含贷款的申请信息(金额、期限、年收入、FICO评分区间)和还款状态(Current、Fully Paid、Charged Off、Late等),单文件就能看到上万个样本,适合做逾期预测和坏账识别。german.csv是德国信用数据集,1000条样本、20个特征,特征已经数值化,包含账户状态、贷款用途、信用历史、居住时长、就业状态等维度,标签是1(好客户)和2(坏客户)——非常适合用来快速验证建模思路。
两者的处理重点完全不同:Lending Club的数据需要你自己定义目标变量(把Charged Off和Late超过一定天数的样本标记为坏客户),还必须面对严重的样本不平衡;german.csv则已经清洗好了,缺失值少、特征含义明确,直接做标准化和模型训练即可。我的习惯是先在german.csv上把代码逻辑调通,再切到LoanStats_2019Q1.csv上做完整的风控建模流程,这样排查代码问题时会快很多。
2.2 用Pandas完成缺失值处理与目标变量构造
先处理Lending Club数据。原始CSV里有大量空值列,比如mths_since_last_delinq、mths_since_last_record这些字段,很多客户没有逾期记录,所以自然没有值。常见做法是先按缺失比例做一轮粗筛,再对保留字段做填充或删除处理。
import pandas as pd import numpy as np # 读取Lending Club数据,dtype统一为str避免ID列被解析为数字 df = pd.read_csv('LoanStats_2019Q1.csv', low_memory=False, dtype={'id': str}) # 粗筛:删除缺失率超过70%的列,这些列信息量太低 missing_ratio = df.isnull().mean() drop_cols = missing_ratio[missing_ratio > 0.7].index.tolist() df = df.drop(columns=drop_cols) # 构造目标变量:Loans在2019Q1还没结束,用loan_status定义好坏客户 # Charged Off视为坏客户(1),Fully Paid视为好客户(0),其余先剔除 df['bad_flag'] = np.where(df['loan_status'] == 'Charged Off', 1, np.where(df['loan_status'] == 'Fully Paid', 0, np.nan)) df = df.dropna(subset=['bad_flag']) # 数值型特征填充中位数,类别型填充众数 num_cols = df.select_dtypes(include=[np.number]).columns cat_cols = df.select_dtypes(include=['object']).columns for col in num_cols: df[col] = df[col].fillna(df[col].median()) for col in cat_cols: df[col] = df[col].fillna(df[col].mode()[0]) print(f'样本量: {len(df)}, 坏客户占比: {df["bad_flag"].mean():.4f}')这段代码有几个关键参数值得注意:low_memory=False是读大CSV的必须项,否则Pandas会按块推断类型导致dtype报警;缺失率阈值设成0.7是我试过的经验值,低于这个阈值的信息量还不够判断是否值得保留;目标变量定义时剔除了所有还在还款周期内的样本,因为那些贷款的结果还没揭晓。跑完之后你会看到坏客户占比通常在5%-10%之间,这是一个典型的极度不平衡的信贷场景。
2.3 特征工程:手工构造风控语义变量
原始字段不能直接喂给模型,需要加工出有业务含义的特征。比如int_rate是贷款利率,annual_inc是年收入,dti是负债收入比——这些是最基础的风险信号。我一般会额外构造两类特征:一类是比率特征,比如loan_amt_to_income(贷款额/收入),用来衡量还款压力;另一类是交叉特征,比如把delinq_2yrs和inq_last_6mths相加得到信用查询频次总分。
# 收入字段中的'+'号处理:70k+形式需要清洗 df['annual_inc'] = df['annual_inc'].astype(str).str.replace('+', '', regex=False) df['annual_inc'] = pd.to_numeric(df['annual_inc'], errors='coerce') # 比率特征:贷款金额占年收入的比例衡量杠杆率 df['loan_to_income'] = df['loan_amnt'] / df['annual_inc'] # 频次加总特征:近两年逾期次数+近6个月查询次数 df['credit_query_score'] = df['delinq_2yrs'].fillna(0) + df['inq_last_6mths'].fillna(0) # 期限特征:还款期限越长,不确定性越大,转为数值等级 df['term_binary'] = df['term'].map({' 36 months': 0, ' 60 months': 1}) # 特征子集预热,先跑通再扩展 feature_cols = ['loan_amnt', 'term_binary', 'int_rate', 'installment', 'annual_inc', 'dti', 'fico_range_low', 'open_acc', 'revol_util', 'total_acc', 'loan_to_income', 'credit_query_score'] X = df[feature_cols] y = df['bad_flag']errors='coerce'这个参数很关键,收入字段里混入了非数字字符串时,Pandas会把它转成NaN而不是直接报错。fico_range_low是Lending Club公布的FICO评分区间下限,样本里它是客观反映申请人信用质量的强变量,建模时不管用逻辑回归还是XGBoost都应该把它放在第一批特征里。如果你想把特征工程做得更细,还可以对revol_util(循环额度使用率)做分箱,它的缺失值往往意味着客户从未使用过循环信贷,这部分客户的违约模式和新用户是不一样的。
2.4 german.csv的快速预处理路径
german.csv的处理就轻量得多,它的特征原本就是编码好的数值或有序类别,唯一需要注意的是把标签从1/2映射为0/1,并且输出列顺序要固定。
# german.csv:标签1好客户,2坏客户,统一映射为0/1 german = pd.read_csv('german.csv', header=None) # 最后一列是标签,把2转为1(坏客户) X_ger = german.iloc[:, :-1] y_ger = (german.iloc[:, -1] - 1).values from sklearn.preprocessing import StandardScaler scaler = StandardScaler() X_ger_scaled = scaler.fit_transform(X_ger) print(X_ger_scaled.shape)这里用iloc[:, :-1]而不是按列名选,是因为UCI原版German Credit数据集就是无表头的纯数值格式,按位置操作最稳妥。StandardScaler标准化对逻辑回归是必须的,不标准化的话系数大小无法横向比较,各特征的权重解释性会大打折扣;而对树模型来说标准化不是必需步骤,这点后面建模时要注意区分。
3. 信用评分模型构建:逻辑回归与XGBoost的完整训练对比
3.1 为什么不直接上神经网络:风控场景的可解释性约束
金融风控建模选型有个特殊约束:模型必须能向监管和业务方解释「为什么拒绝这个客户」。神经网络虽然精度高,但黑匣子属性在信贷审批场景里很难过合规审查。所以行业内最主流的做法是逻辑回归——它的系数直接对应每个特征的风险方向,能输出违约概率还能方便地映射成标准评分卡;同时用GBDT类模型(XGBoost、LightGBM)作为精度提升手段,在离线评估里做对比验证,但上线时往往还会辅以SHAP值解释。
这套源码包里两个模型都给了,正好可以实操对比。我一般会先跑逻辑回归建立基准(baseline),再跑XGBoost看上限在哪,如果XGBoost的AUC只比逻辑回归高一点点,那从部署成本和可解释性角度考虑,我更愿意用逻辑回归上线。
3.2 逻辑回归:用Pipeline封装标准化与建模流程
逻辑回归在风控里的用法,是输出一个违约概率,再通过评分卡公式映射成整数分数。先看训练部分。
from sklearn.model_selection import train_test_split from sklearn.linear_model import LogisticRegression from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.3, random_state=42, stratify=y) # 注意:stratify=y保证训练/测试集坏样本占比一致 lr_pipeline = Pipeline([ ('scaler', StandardScaler()), ('lr', LogisticRegression(class_weight='balanced', C=0.1, max_iter=1000, random_state=42)) ]) lr_pipeline.fit(X_train, y_train) # 查看系数与截距:解释每个特征的风险方向 coef_df = pd.DataFrame({ 'feature': feature_cols, 'coef': lr_pipeline.named_steps['lr'].coef_[0] }).sort_values('coef', ascending=False) print(coef_df)class_weight='balanced'是处理样本不平衡的第一道防线,逻辑回归会根据类别频率自动放大少数类样本的权重,让模型不会光顾着预测「好客户」。C=0.1是正则化强度的倒数,值越小正则化越强,对信贷这种特征间多重共线性明显(比如loan_amnt和installment高度相关)的场景,强正则化能压掉一部分系数波动。stratify=y必须写,否则随机切分可能让测试集里的坏客户比例漂移,导致评估指标失真。
3.3 XGBoost:调参重点与早停机制
XGBoost在风控建模里主要用来打磨精度上限,它的优势是能自动捕捉非线性关系和特征交互,比如「低薪+高负债+多次逾期」这种组合风险信号。下面是核心训练代码。
import xgboost as xgb model = xgb.XGBClassifier( n_estimators=300, max_depth=3, learning_rate=0.05, subsample=0.8, colsample_bytree=0.8, scale_pos_weight=9, # 坏客户:好客户 ≈ 1:9,反向加权 eval_metric='auc', use_label_encoder=False, random_state=42 ) model.fit(X_train, y_train, eval_set=[(X_test, y_test)], early_stopping_rounds=20, verbose=False) best_iter = model.best_iteration + 1 print(f'最优树数量: {best_iter}')scale_pos_weight是最关键的一个参数,它等于负类样本数除以正类样本数,我们这场景大约9:1,设为9相当于给坏客户样本加权9倍,效果类似于逻辑回归的class_weight但力度更可控。max_depth=3在信贷数据上是经验值,试过5和7,AUC提升微乎其微但过拟合风险明显增大,因为每个特征分箱后的信息量有限,深树容易记住噪声。eval_set配early_stopping_rounds是防止过拟合的兜底手段,模型在验证集AUC连续20轮不提升就自动停止,训练结束后用best_iteration重新预测。
3.4 两个模型的评估对比:AUC、KS值与混淆矩阵
模型好不好不能只看准确率——在坏客户只占10%的数据集上,全预测好客户正确率也有90%。要重点看三个指标:AUC(区分度)、KS(好坏客户累计分布最大差距)、混淆矩阵(坏客户召回率)。AUC通常要在0.7以上才算有可用性,0.75-0.8是Lending Club这类数据上的常见区间。
from sklearn.metrics import roc_auc_score, confusion_matrix # 逻辑回归预测概率 lr_prob = lr_pipeline.predict_proba(X_test)[:, 1] lr_auc = roc_auc_score(y_test, lr_prob) # XGBoost预测概率 xgb_prob = model.predict_proba(X_test)[:, 1] xgb_auc = roc_auc_score(y_test, xgb_prob) print(f'逻辑回归AUC: {lr_auc:.4f}') print(f'XGBoost AUC: {xgb_auc:.4f}') # 混淆矩阵:选择阈值为0.5,查看坏客户召回情况 xgb_pred = (xgb_prob > 0.5).astype(int) tn, fp, fn, tp = confusion_matrix(y_test, xgb_pred).ravel() print(f'坏客户召回率: {tp / (tp + fn):.4f}')注意阈值0.5只是默认值,实际风控场景很少直接用0.5做决策。常见做法是调阈值让坏客户召回率达到业务目标(比如80%),代价是误杀更多好客户。你可以写一个循环,从0.3到0.7每隔0.05计算一次混淆矩阵,再做阈值选择。另外要留意XGBoost在预测概率上存在轻微偏向(因为它最小化的是对数损失),输出概率绝对值不能直接当违约概率解释,但逻辑回归的概率可比性强——这也是两者最终落地方式不同的原因之一。
4. 风控模型评估与评分卡转换:从概率到分数的完整映射
4.1 用KS和IV值判断特征区分度
在正式把模型推向业务之前,风控最看重的是两个东西:特征的IV值(Information Value,信息量)和模型的KS值(Kolmogorov-Smirnov统计量)。IV值用来筛选和排序特征,一般规则是IV小于0.02的特征基本没区分度可以剔除,0.02-0.1区分度弱,0.1-0.3区分度中等,大于0.3是强变量但也要警惕过度拟合。KS值用来衡量模型整体的好坏客户区分能力,行业惯例是KS大于0.4模型表现优秀,0.3-0.4可用。
def calculate_ks(y_true, y_pred_prob): df_ks = pd.DataFrame({'y': y_true, 'prob': y_pred_prob}) df_ks = df_ks.sort_values('prob', ascending=False).reset_index(drop=True) df_ks['cum_bad'] = df_ks['y'].cumsum() / df_ks['y'].sum() df_ks['cum_good'] = (1 - df_ks['y']).cumsum() / (1 - df_ks['y']).sum() df_ks['diff'] = df_ks['cum_bad'] - df_ks['cum_good'] return df_ks['diff'].max() ks_lr = calculate_ks(y_test, lr_prob) ks_xgb = calculate_ks(y_test, xgb_prob) print(f'逻辑回归KS: {ks_lr:.4f}') print(f'XGBoost KS: {ks_xgb:.4f}')实现时有个细节:cum_bad和cum_good是累计占比曲线,KS就是两条曲线最大垂直距离。排序必须从概率高到低,因为违约概率高的样本应该集中在头部——如果坏客户累计占比曲线在尾部才快速抬升,说明模型排序能力有问题。IV值的计算需要配合分箱,比如用pd.qcut把连续特征切成5箱,再按箱内坏好占比算WOE。
def calc_iv(df, feature, target, bins=5): # 用分位数分箱确保每箱样本量均衡 df['bin'] = pd.qcut(df[feature], q=bins, duplicates='drop') grouped = df.groupby('bin', observed=True)[target].agg(['sum', 'count']) grouped['bad'] = grouped['sum'] grouped['good'] = grouped['count'] - grouped['sum'] grouped['bad_dist'] = grouped['bad'] / grouped['bad'].sum() grouped['good_dist'] = grouped['good'] / grouped['good'].sum() grouped['woe'] = np.log((grouped['bad_dist'] + 1e-6) / (grouped['good_dist'] + 1e-6)) iv = ((grouped['bad_dist'] - grouped['good_dist']) * grouped['woe']).sum() return iv iv_values = {col: calc_iv(df, col, 'bad_flag') for col in feature_cols[:6]} print(iv_values)duplicates='drop'处理了分位点重复导致的空箱问题,1e-6是为了防止取对数时出现除零。WOE的含义是「该箱内坏客户占全部坏客户的比例」与「好客户占比比例」的差异,WOE绝对值越大说明这个箱的区分力越强,IV是所有箱WOE的加权和。注意IV计算依赖分箱方案,同样的特征不同分箱IV值可能差很多,所以做IV筛选前先固定分箱逻辑。
4.2 标准评分卡公式:把模型概率映射成整数分数
信用评分卡的标准形式是Score = Offset + Factor * ln(odds),其中odds是违约与正常的概率比。业界通常约定:总分660分对应好坏比20:1(即odds=20),每增加20分odds翻倍。推导后可以反推出Offset和Factor两个常数,再把逻辑回归的线性表达式带入。
# 评分卡参数:默认odds=20时660分,PDO=20(每20分odds翻倍) factor = 20 / np.log(2) offset = 660 - factor * np.log(20) # 逻辑回归的线性部分:线性得分 = 截距 + sum(系数 * 特征值) lr_model = lr_pipeline.named_steps['lr'] scaler = lr_pipeline.named_steps['scaler'] X_test_scaled = scaler.transform(X_test) linear_score = lr_model.intercept_[0] + np.dot(X_test_scaled, lr_model.coef_[0]) # 转为标准评分卡分数 score_card = offset - factor * linear_score # 负号:线性得分越高违约概率越低,分数越高 print(f'样本分数范围: {score_card.min():.0f} - {score_card.max():.0f}') pd.Series(score_card).describe()这里有个最容易搞混的点:标准的Score = Offset - Factor * (intercept + sum(coef*x)),因为逻辑回归的线性部分越大,违约概率越小,所以分数=常数减去线性得分。实际操作中,行业里更常用的是直接对WOE做线性回归构建标准评分卡,而不用原始特征值——因为WOE是单调的,能保证每个分箱的得分方向一致。如果资源包里没有现成的评分卡转换代码,按上面这个公式写就能出结果,这也是信贷风控面试里最常见的考察点。
4.3 分箱与WOE编码对模型表现的提升
做标准评分卡时,分箱这一步直接决定模型上限。常见做法是把连续特征按分位数切成5-10箱,每箱内计算坏客户占比,画出趋势图确认单调性。如果不单调,就需要调整箱数或者手动合并相邻箱,比如dti在中间区间出现波动,就把相邻箱合并成一个。分箱完成后用WOE值替换原始数值,再做一次逻辑回归,得到的系数稳定性往往比直接用原始数值更好,因为WOE编码本质上做了非线性变换,把分段区间的信息压缩成了一个数值。
def woe_transform(df, feature, target, bins=5): df['bin'] = pd.qcut(df[feature], q=bins, duplicates='drop') grouped = df.groupby('bin', observed=True)[target].agg(['sum', 'count']) grouped['bad'] = grouped['sum'] grouped['good'] = grouped['count'] - grouped['sum'] grouped['bad_dist'] = grouped['bad'] / grouped['bad'].sum() grouped['good_dist'] = grouped['good'] / grouped['good'].sum() grouped['woe'] = np.log((grouped['bad_dist'] + 1e-6) / (grouped['good_dist'] + 1e-6)) mapping = grouped['woe'].to_dict() return df['bin'].map(mapping) df['dti_woe'] = woe_transform(df, 'dti', 'bad_flag', bins=6)map(mapping)把分箱结果映射成WOE值,注意mapping的键是Interval对象,所以要先保证df['bin']的箱子边界和mapping一致。做完WOE替换后可以重新跑一次逻辑回归,通常KS能提升0.02-0.05。如果你发现提升很小,可能不是WOE的问题,而是特征集合本身区分度不够。
5. 风控建模常见问题与避坑指南:四个真实翻车现场
5.1 目标变量定义错误导致模型失效
现象:模型AUC只有0.55左右,和随机猜测差不多,但数据检查时发现坏客户定义里混入了「当前正常还款但历史有逾期记录」的样本。
原因:LoanStats_2019Q1的loan_status是多分类的,包括Current、Fully Paid、Charged Off、Late 31-120天、Late 16-30天等状态。如果把所有非Fully Paid的样本都标成坏客户,那么还在正常还款期内的Current客户、只晚还几天的客户都会被错标,目标变量被严重污染。
解决:严格按业务口径定义好坏。我一般只把Charged Off和Late超过90天的样本标为坏客户,Fully Paid标为好客户,其余全部剔除。如果你做的是申请评分卡,目标变量应该是「开户后前12个月内是否逾期90天以上」,按这个口径重新构造标签后,AUC会明显回升。
5.2 样本不平衡让模型学不到坏客户特征
现象:模型在测试集上整体准确率90%以上,但看混淆矩阵发现坏客户召回率几乎为0,模型把所有客户都判成了好客户。
原因:坏客户占比只有10%不到,逻辑回归如果不加处理,最优策略就是全预测好客户——因为错误成本被最小化了。XGBoost如果不设置scale_pos_weight,同样会往多数类倾斜。
解决:先做两件事:一是训练时用class_weight='balanced'或scale_pos_weight,二是评估时不看accuracy,只看AUC、KS、坏客户召回率。更进一步,可以尝试SMOTE过采样,但对信贷数据效果不稳定,我试下来还是样本加权最稳。如果坏客户占比低于2%,考虑用欠采样+集成的方式,训练多个模型再取平均。
5.3 时间穿越:用了未来信息导致指标虚高
现象:模型在训练集上AUC高达0.9,KS超过0.6,但上线后实际表现大幅下滑。
原因:这是风控建模里最隐蔽的坑。Lending Club的数据里有些字段是贷款发放后累积的,比如total_pymnt(客户已还总额)在申请时点是未知的。如果建模时用了这类未来信息,模型等于「偷看答案」,离线评估自然好看,上线时这些字段根本拿不到。
解决:建模前先自查特征时间性。区分申请信息(申请时就能拿到,如收入、负债、FICO)和行为信息(贷款发放后逐步产生,如还款记录),申请评分卡只能使用申请时点的信息。养成习惯:每次构造新特征前先问一句「这个特征在决策时点是否已知」,未知的坚决剔除。你可以把total_pymnt这类字段从特征列表里删掉重新训练,AUC通常会掉回0.7-0.75的正常区间。
5.4 WOE分箱过拟合导致跨样本表现不稳
现象:训练集KS很高,但测试集KS断崖式下跌,回放历史数据时分数分布漂移严重。
原因:WOE分箱是监督式变换,分箱边界是根据训练集好坏分布定的。如果箱数设得太细(比如10箱以上),每箱的样本量变小,WOE值对噪声特别敏感,尤其是某些箱好坏样本都很少时,WOE值会被个别样本带偏。
解决:控制分箱数量在5-8箱,且保证每箱样本量不少于总样本的5%。分箱后观察WOE趋势是否单调,如果不单调,强制合并相邻箱。另外可以尝试在验证集上重新计算WOE,对比训练集和验证集WOE差异,差异过大的箱直接剔除。
# 排查思路:对比训练集与验证集WOE分布差异 train_woe = df_train.groupby('dti_bin')['bad_flag'].agg(['mean', 'count']) test_woe = df_test.groupby('dti_bin')['bad_flag'].agg(['mean', 'count']) woe_diff = (train_woe['mean'] - test_woe['mean']).abs() print(woe_diff[woe_diff > 0.02]) # 差异超过2个百分点就要警惕5.5 阈值的随意选择带来误杀率失控
现象:模型上线后批准率大幅下降,业务方投诉通过率太低,但模型KS指标并没有变差。
原因:直接用0.5作为审批阈值,在坏客户占比10%的数据上,0.5意味着要求模型对客户违约概率有极高置信度才放行,大量中风险客户被拒之门外。信贷业务的盈利最优解往往不是0.5。
解决:用业务指标选阈值。画出阈值-累积坏客户拦截率曲线,找到累计拦截80%坏客户对应的阈值点,同时计算这个阈值下的好客户误杀率,跟业务方对齐可接受的误杀范围。我最后一般会在代码里写一个threshold_tuning函数,遍历多个阈值输出混淆矩阵,让业务方参与决策。
6. 模型上线前的最后一道检查:PSI稳定性监测与冠军-挑战者机制
模型训练完不是终点,上线才是真正考验的开始。信贷客群的结构会随时间变化,比如2020年之后整体收入结构变化导致特征分布漂移,当年训练出来的模型可能在半年后就不再适用。所以上线前必须算一遍PSI(Population Stability Index,群体稳定性指标)。
PSI衡量的是训练集样本分布和上线后实际样本分布的差异,公式是PSI = SUM((实际分布占比 - 训练分布占比) * ln(实际分布占比 / 训练分布占比))。行业惯例是:PSI小于0.1表示分布稳定,模型可以继续用;0.1-0.25说明有轻微漂移,需要持续监测;大于0.25说明分布严重漂移,必须重新训练。
def calculate_psi(expected, actual, bins=10): # expected为训练集预测分数,actual为新样本预测分数 breaks = np.percentile(expected, np.linspace(0, 100, bins + 1)) expected_counts = np.histogram(expected, bins=breaks)[0] actual_counts = np.histogram(actual, bins=breaks)[0] expected_pct = (expected_counts + 1e-6) / expected_counts.sum() actual_pct = (actual_counts + 1e-6) / actual_counts.sum() psi = ((actual_pct - expected_pct) * np.log(actual_pct / expected_pct)).sum() return psi train_score = offset - factor * linear_score # 训练集分数 new_data_score = score_card # 新样本分数,线上实时打分结果 psi_value = calculate_psi(train_score, new_data_score) print(f'PSI: {psi_value:.4f}')np.percentile按训练集分数分位设定断点,保证每个区间在训练集上样本量均匀,这样计算出来的PSI才不会被断点选择干扰。+1e-6防止区间占比为0时log报错。PSI计算的单位是训练集的分数分布,所以上线后每一次批量打分都要把分数落库,定期和训练集分布做对比。
另外一个我强烈建议养成的习惯是「冠军-挑战者」机制。上线初期,把新模型作为挑战者,旧模型作为冠军,同时跑一段时间,观察两者在真实流量下的AUC、通过率、坏账率差异。只有当挑战者在连续3个月的滚动样本上KS和坏账率都稳定优于冠军,才切换。这个机制看似保守,但能避免很多「离线指标好、上线就翻车」的尴尬。
具体操作上,我一般会在打分服务里加一个model_version字段,每次批量打分记录版本号和分数,按月汇总对比不同版本的PSI和好坏分布。这套资源里的源码逻辑其实是通用的,你完全可以把评分卡那段代码抽出来,改成你的业务特征重新训练一遍。从那以后我每次做风控模型,都会强制走一遍「定义目标变量-检查特征时间性-计算IV/KS-阈值联动-上线前PSI基线」这个流程,肉眼可见地少踩了很多模型失效的坑。这套源码包涉及的Pandas数据处理、Scikit-learn建模、评分卡转换代码都很完整,希望帮到你。
本文还有配套的精品资源,点击获取