简介:这份资源是面向计算机相关专业学生与项目实战学习者的贷中风险预测完整项目包,以Python机器学习为核心,围绕金融大数据建模赛题展开,适合用作毕业设计、期末大作业或技能练习,难度适中。压缩包共50个文件,约10.83MB,包含19个py脚本、11个csv数据集、5个ipynb笔记本、4个docx文档、3个xlsx表格、2个pptx演示文稿及若干txt说明,覆盖数据处理、特征工程、模型训练与结果输出全流程。项目源自金融大数据建模挑战赛初赛方案,内含赛题说明、数据字段解释、特征提取思路与多模型对比代码,涉及决策树、LightGBM、XGBoost、随机森林、AdaBoost、高斯朴素贝叶斯等算法实现,并附答辩PPT与最终版风险预测演示,便于理解建模逻辑与汇报要点。已有62人学习,源码经本地编译调试可运行,文档与代码配套,能帮助读者快速复现实验、掌握贷中风险预测的完整实现路径。
1. 贷中风险预测模型:为什么AUC 0.85的模型上线后还是被业务方骂
很多做风控算法的同学都有过这种经历:用 Python 和机器学习在历史数据上把贷中风险预测模型调到 AUC 0.85,KS 0.4,兴冲冲交给业务方,结果上线一个月被骂得狗血淋头。问题不在模型本身,而在于贷中这个场景和贷前完全是两码事。贷前是「这个人会不会坏」,贷中是「这个已经放了款的人,未来几个月会不会坏」,样本定义、观察窗口、表现期、变量可用性全都不一样。这个项目标题里的「贷中风险预测模型」,核心就是解决存量客户在还款周期内的违约预警问题,典型场景包括额度调整、催收策略分级、提前结清判断。适合谁看?有 Python 基础、懂一点机器学习、但没完整做过信贷风控落地的人;也适合正在准备答辩、需要一套能讲清楚业务逻辑和代码细节的项目的同学。源码和文档只是载体,真正值钱的是这套从数据到模型到评估的完整链路,以及那些只有踩过才知道的坑。
2. 贷中风险预测的数据底座:样本定义和特征工程怎么做才不翻车
2.1 贷中样本的观察期与表现期怎么切
贷中模型和贷前最大的区别在于样本定义。贷前通常用「申请时点」作为观察起点,贷中则要用「某个存量时点」作为观察起点。常见做法是:取每个客户每个账期的还款日作为观察点,往前推 6 个月作为特征观察期,往后推 3 个月作为表现期。如果客户在表现期内出现逾期超过 30 天,标记为坏样本,否则为好样本。
这里有个血泪经验:表现期不能太短,否则会把「暂时忘记还款但后来还了」的人误判为坏客户;也不能太长,否则模型上线时坏样本还没成熟,导致训练集和线上分布不一致。我一般会做 vintage 分析,按放款月份分组看不同表现期的坏账率是否收敛。如果 3 个月和 6 个月的坏账率差异小于 5%,说明 3 个月表现期够用。
import pandas as pd import numpy as np # 假设原始还款流水表 loan_repay 包含:cust_id, loan_id, due_date, repay_date, overdue_days # 构造贷中样本:以每个账单日为观察点 def build_mid_loan_samples(repay_df, obs_months=6, perf_months=3): """ repay_df: 还款流水,含 cust_id, due_date, repay_date, overdue_days obs_months: 特征观察期长度(月) perf_months: 表现期长度(月) """ df = repay_df.copy() df['due_date'] = pd.to_datetime(df['due_date']) df['repay_date'] = pd.to_datetime(df['repay_date']) # 观察点 = 每个账单日 df['obs_date'] = df['due_date'] # 特征窗口起点 df['feat_start'] = df['obs_date'] - pd.DateOffset(months=obs_months) # 表现窗口终点 df['perf_end'] = df['obs_date'] + pd.DateOffset(months=perf_months) # 标记坏样本:表现期内最大逾期天数 > 30 df['is_bad'] = (df['overdue_days'] > 30).astype(int) # 去重:同一客户同一观察点只保留一条 df = df.sort_values(['cust_id', 'obs_date', 'overdue_days'], ascending=[True, True, False]) df = df.drop_duplicates(subset=['cust_id', 'obs_date'], keep='first') return df[['cust_id', 'obs_date', 'feat_start', 'perf_end', 'is_bad']]这段代码的关键参数是obs_months和perf_months。观察期太短,特征不够稳定;太长,会引入太久远的行为,反而稀释近期风险信号。我一般设 6 和 3,但具体要看产品账期。如果是等额本息 12 期,表现期可以缩到 2 个月;如果是先息后本,表现期要拉到 6 个月。另外注意overdue_days要取表现期内的最大值,不是观察点当天的值,否则会漏掉中间逾期又还上的情况。
2.2 贷中特征工程:从还款行为里挖出风险信号
贷中模型的特征和贷前差异很大。贷前看的是申请信息、征信查询、多头借贷;贷中看的是还款行为、额度使用、账龄变化。我一般把特征分成四类:还款行为类、额度使用类、账龄类、外部变量类。
还款行为类包括:过去 6 个月平均逾期天数、最大逾期天数、逾期次数、提前还款次数、还款日当天还款比例。额度使用类包括:当前额度使用率、过去 6 个月平均使用率、使用率变化趋势。账龄类包括:开户时长、已还期数、剩余期数。外部变量类包括:近 3 个月征信查询次数、其他机构贷款笔数。
def build_repay_features(repay_df, sample_df): """ 基于还款流水构造贷中特征 """ df = repay_df.merge(sample_df[['cust_id', 'obs_date', 'feat_start']], on='cust_id') # 只保留观察期内的还款记录 df = df[(df['due_date'] >= df['feat_start']) & (df['due_date'] <= df['obs_date'])] feats = df.groupby(['cust_id', 'obs_date']).agg( avg_overdue_days=('overdue_days', 'mean'), max_overdue_days=('overdue_days', 'max'), overdue_cnt=('overdue_days', lambda x: (x > 0).sum()), early_repay_cnt=('repay_date', lambda x: (x < df.loc[x.index, 'due_date']).sum()), on_time_rate=('overdue_days', lambda x: (x == 0).mean()), total_periods=('due_date', 'count') ).reset_index() # 额度使用率需要额外从额度表关联,这里用占位 feats['credit_util'] = np.nan # 实际项目从额度表计算 return feats这里有个容易翻车的地方:early_repay_cnt的计算用了 lambda 里嵌套 df 索引,实际跑的时候如果数据量大,性能会很差。我一般会先把repay_date < due_date标记成一列布尔值,再 groupby 求和。另外on_time_rate在样本里如果全是 1,方差为 0,树模型会直接忽略,但逻辑回归会报错,需要做方差过滤。
提示:贷中特征一定要做时间切片验证。用 2023 年 1 月到 6 月的数据训练,用 7 月到 9 月的数据测试,如果 KS 下降超过 30%,说明特征不稳定,大概率是引入了未来信息或者外部变量口径变了。
3. 用 Python 把贷中风险预测模型跑起来:从逻辑回归到 LightGBM 的完整代码
3.1 基线模型:逻辑回归 + WOE 分箱为什么还是首选
虽然现在 LightGBM 很火,但贷中风险预测模型我仍然建议先做逻辑回归 + WOE 分箱作为基线。原因有三个:一是可解释性强,业务方和风控策略同学能看懂每个变量的方向;二是稳定性好,逻辑回归对异常值和缺失值不敏感;三是监管友好,很多金融机构要求模型必须能给出评分卡形式的解释。
WOE 分箱的核心是把连续变量离散化,然后计算每个箱的 WOE 值。WOE = ln(坏样本占比 / 好样本占比),IV = sum((坏样本占比 - 好样本占比) * WOE)。一般选 IV > 0.02 的变量入模,IV > 0.5 的要警惕,可能是变量泄露。
import pandas as pd import numpy as np from sklearn.linear_model import LogisticRegression from sklearn.metrics import roc_auc_score, roc_curve def calc_woe_iv(df, feature, target, bins=5): """ 计算单个变量的 WOE 和 IV """ df = df[[feature, target]].copy() df['bin'] = pd.qcut(df[feature], q=bins, duplicates='drop') grouped = df.groupby('bin')[target].agg(['count', 'sum']) grouped.columns = ['total', 'bad'] grouped['good'] = grouped['total'] - grouped['bad'] grouped['bad_rate'] = grouped['bad'] / grouped['bad'].sum() grouped['good_rate'] = grouped['good'] / grouped['good'].sum() grouped['woe'] = np.log(grouped['bad_rate'] / grouped['good_rate']) grouped['iv'] = (grouped['bad_rate'] - grouped['good_rate']) * grouped['woe'] iv = grouped['iv'].sum() return grouped, iv # 示例:对 avg_overdue_days 做分箱 # grouped, iv = calc_woe_iv(train_df, 'avg_overdue_days', 'is_bad', bins=5) # print(f"IV = {iv:.4f}")参数bins控制分箱数量,一般 5 到 10 箱。太少会损失信息,太多会导致每个箱样本不足,WOE 波动大。我一般先试 5 箱,如果 IV 不显著再增加到 8 箱。另外pd.qcut是等频分箱,如果变量有大量重复值,duplicates='drop'会自动合并,但要注意合并后的箱是否有业务含义。
3.2 LightGBM 调参:贷中场景下这几个参数最影响 KS
LightGBM 在贷中风险预测里表现通常比逻辑回归好,尤其是特征交互复杂的时候。但调参不能瞎调,贷中场景下最影响 KS 的参数是num_leaves、learning_rate、min_child_samples和subsample。
num_leaves控制树的复杂度,贷中样本通常几万到几十万,我一般设 31 到 63。设太大容易过拟合,设太小欠拟合。learning_rate我一般设 0.05,配合n_estimators=500到 1000,用早停防止过拟合。min_child_samples设 50 到 100,保证每个叶子节点有足够样本,避免学到噪声。subsample设 0.8,行采样增加泛化能力。
import lightgbm as lgb from sklearn.model_selection import train_test_split # 假设 X 是特征矩阵,y 是标签 X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42) params = { 'objective': 'binary', 'metric': 'auc', 'boosting_type': 'gbdt', 'num_leaves': 31, 'learning_rate': 0.05, 'min_child_samples': 50, 'subsample': 0.8, 'subsample_freq': 1, 'colsample_bytree': 0.8, 'reg_alpha': 0.1, 'reg_lambda': 0.1, 'random_state': 42, 'n_jobs': -1 } dtrain = lgb.Dataset(X_train, label=y_train) dvalid = lgb.Dataset(X_test, label=y_test, reference=dtrain) model = lgb.train( params, dtrain, num_boost_round=1000, valid_sets=[dvalid], callbacks=[lgb.early_stopping(50), lgb.log_evaluation(100)] ) # 评估 y_pred = model.predict(X_test) auc = roc_auc_score(y_test, y_pred) print(f"Test AUC: {auc:.4f}") # 特征重要性 importance = pd.DataFrame({ 'feature': model.feature_name(), 'importance': model.feature_importance(importance_type='gain') }).sort_values('importance', ascending=False) print(importance.head(10))这段代码里early_stopping(50)表示如果验证集 AUC 连续 50 轮不提升就停止训练,防止过拟合。importance_type='gain'表示用信息增益衡量特征重要性,比split更可靠。实际项目中我还会做 5 折交叉验证,看 AUC 的均值和方差,方差太大说明模型不稳定。
注意:LightGBM 对缺失值自动处理,但贷中数据里缺失值往往有业务含义。比如「近 3 个月征信查询次数」缺失,可能是客户没有征信记录,这本身就是风险信号。我一般会额外加一列缺失标记,让模型自己学。
4. 模型评估与上线前的避坑清单:AUC 高不代表能用
4.1 KS、PSI、Lift 这三个指标怎么看
AUC 只反映排序能力,贷中模型还要看 KS、PSI 和 Lift。KS 衡量的是模型区分好坏样本的最大能力,贷中模型 KS 一般要求 0.3 以上。PSI 衡量的是训练集和测试集(或线上)的分布差异,PSI < 0.1 表示稳定,0.1 到 0.25 需要关注,大于 0.25 说明模型可能失效。Lift 看的是 top 10% 分数段的坏账率是整体的多少倍,一般要求 3 倍以上。
def calc_ks(y_true, y_pred): fpr, tpr, thresholds = roc_curve(y_true, y_pred) ks = max(tpr - fpr) return ks def calc_psi(expected, actual, bins=10): """ 计算 PSI,expected 是训练集分数,actual 是测试集分数 """ breakpoints = np.percentile(expected, np.arange(0, 100, 100/bins)) expected_perc = np.histogram(expected, breakpoints)[0] / len(expected) actual_perc = np.histogram(actual, breakpoints)[0] / len(actual) psi = np.sum((expected_perc - actual_perc) * np.log(expected_perc / actual_perc)) return psi ks = calc_ks(y_test, y_pred) psi = calc_psi(model.predict(X_train), y_pred) print(f"KS: {ks:.4f}, PSI: {psi:.4f}")calc_psi里的bins一般设 10,但要注意如果分数分布很集中,分箱后某些箱样本为 0,会导致 log 计算报错。我一般会加一个极小值1e-6防止除零。另外 PSI 计算要用训练集的分数分布作为基准,不是用标签。
4.2 上线前必须检查的 5 个坑
第一个坑:特征穿越。比如用了「未来 3 个月是否逾期」来构造特征,模型 AUC 能到 0.99,但上线就废。检查方法很简单,看特征的时间戳是否都在观察点之前。
第二个坑:样本不均衡。贷中坏样本通常只有 1% 到 5%,直接训练模型会偏向好样本。我一般用scale_pos_weight或者欠采样,但要注意欠采样后概率校准会偏,需要做 Platt Scaling。
第三个坑:分数分布漂移。训练集分数集中在 0.1 到 0.3,线上分数集中在 0.4 到 0.6,说明特征口径变了。上线前一定要用最近 3 个月的数据做一次 PSI 检查。
第四个坑:缺失值处理不一致。训练时用中位数填充,上线时用 0 填充,模型直接翻车。我一般会把填充逻辑封装成函数,训练和推理共用。
第五个坑:模型版本管理混乱。改了特征没改版本号,线上跑的还是旧模型。我一般用model_YYYYMMDD.pkl命名,并在文件里存特征列表和参数。
提示:答辩 PPT 里不要只放 AUC 和 KS,要把 PSI 和 Lift 也放上去,再放一张分数分布对比图。评委老师一看就知道你是真做过落地,不是只跑了个 demo。
5. 从源码到答辩:怎么把贷中风险预测模型讲成一个高分项目
5.1 源码目录怎么组织才像真实项目
很多同学把代码全堆在一个main.py里,答辩时老师一看就觉得是临时凑的。我一般会按真实项目结构组织:
credit_risk_model/ ├── config/ │ └── config.yaml # 数据库连接、模型参数 ├── data/ │ ├── raw/ # 原始数据 │ └── processed/ # 清洗后数据 ├── features/ │ ├── build_features.py # 特征工程 │ └── feature_config.py # 特征列表和分箱配置 ├── models/ │ ├── train.py # 训练脚本 │ ├── predict.py # 推理脚本 │ └── evaluate.py # 评估脚本 ├── utils/ │ ├── db.py # 数据库工具 │ └── logger.py # 日志 ├── notebooks/ │ └── eda.ipynb # 探索性分析 ├── requirements.txt └── README.mdconfig.yaml里放数据库连接和模型超参,不要硬编码在代码里。feature_config.py里放每个变量的分箱边界和 WOE 映射,这样上线时直接读配置就行。train.py和predict.py分开,训练用历史数据,推理用线上数据,但特征工程逻辑要复用同一个模块。
5.2 答辩 PPT 的 10 页结构
答辩 PPT 不要超过 12 页,我一般用 10 页:第 1 页封面,第 2 页业务背景和问题定义,第 3 页数据说明和样本定义,第 4 页特征工程(放 IV 排名前 10 的变量),第 5 页模型选型对比(逻辑回归 vs LightGBM),第 6 页评估指标(AUC、KS、PSI、Lift),第 7 页分数分布和阈值选择,第 8 页上线策略建议,第 9 页项目难点和解决方案,第 10 页总结和展望。
第 7 页的阈值选择很关键。贷中模型一般会设两个阈值:高分位(比如 top 10%)触发人工审核,低分位(比如 bottom 30%)自动通过。阈值不是拍脑袋定的,要根据业务成本和收益算。我一般会画一张阈值 - 通过率 - 坏账率曲线,让业务方自己选。
5.3 一个容易被忽略的技巧:用 SHAP 解释单笔预测
答辩时老师经常会问「这个客户为什么被判定为高风险」。如果你只回答「模型算出来的」,分数直接砍半。我一般会用 SHAP 做单笔解释:
import shap explainer = shap.TreeExplainer(model) shap_values = explainer.shap_values(X_test) # 单笔解释 shap.force_plot(explainer.expected_value, shap_values[0], X_test.iloc[0])SHAP 会告诉你每个特征对这笔预测的贡献。比如「近 3 个月逾期次数 = 2」贡献了 +0.15 的风险分,「额度使用率 = 0.3」贡献了 -0.05 的风险分。这样业务方就能理解模型不是黑匣子,也愿意配合落地。
我做了这么多年贷中模型,最大的教训就是:不要只盯着 AUC,要多花时间在样本定义和特征稳定性上。一个 AUC 0.75 但 PSI 稳定的模型,比 AUC 0.85 但三个月就失效的模型有价值得多。源码和 PPT 只是敲门砖,真正让你在答辩和工作中站稳的,是这套从业务到数据到模型的完整思考方式。希望帮到你。
本文还有配套的精品资源,点击获取