☰
贷中风险预测竞赛级Python源码解析:从多表合并到LightGBM建模
2026/10/12 20:05:55 网站建设 项目流程

简介:面向计算机专业毕业设计、期末大作业及金融风控入门实践,这份完整项目包以贷中风险预测为核心任务,基于Python机器学习完整覆盖数据清洗、特征工程、模型训练与效果评估流程,适合需要从零构建实际项目的初中级学习者。资源共50个文件,压缩包约10.83MB,包括.py源码(数据预处理、特征构建、多模型训练与预测脚本)、.csv训练样本及衍生数据、.ipynb探索性分析与调参记录、.docx赛题方案和数据字段说明、.pptx答辩展示材料,及少量txt/xlsx/md辅助文件,按代码、数据、文档分层组织,便于逐项对照学习。项目源自2020年江苏银行杯金融大数据建模挑战赛,配套初赛作品方案与数据字典,并实现随机森林、XGBoost、LightGBM、AdaBoost等主流算法,可直观对比不同模型在贷中风险识别场景下的精准度与稳定性。目前已有62人学习下载,整体评审分98分,源码经本地编译调试可稳定运行,既可作为毕业设计、期末大作业的完整参考,也能为金融风控类竞赛提供可复用的建模范式。

1. 贷中风险预测:一份能直接跑的竞赛级Python源码

贷中风险预测,简单说就是贷款放出去之后盯着每个客户,判断谁在未来三个月内可能出现逾期。这个项目把2020"江苏银行杯"金融大数据建模挑战赛的贷中赛题做成了一套完整的Python实现,从apply(申请信息)、credit(征信数据)、customer_information(客户基本信息)和贷款流水表四张原始表出发,经过多表合并、特征提取、七类模型横向对比,最后输出每个客户的风险概率。它适合正在做金融风控方向毕业设计、期末大作业的学生,也适合想完整跑一遍机器学习建模落地流程的开发者。这套资源不是一个单模型demo,而是包含了数据清洗脚本、特征工程notebook、模型训练代码、方案文档和答辩PPT的成套方案,拿到手之后主要改数据路径就能复现整个流程。

2. 多源数据整合:从申请、征信到流水的预处理与合并

2.1 先看数据:四张原始表和一张标签表

项目解压之后,src目录里躺着四张原始txt和一张label.csv。我拿到手第一件事不是写代码,而是用pandas逐张看字段类型、缺失率和样本粒度,把每张表的主键和粒度搞清楚。这里最容易被忽略的就是粒度差异,它决定了后面合并用left join还是先聚合。

各文件的内容和粒度大致如下:

文件内容粒度典型字段
apply.txt客户申请信息一客一条或一客多条申请金额、申请日期、申请渠道
credit.txt征信数据一客一条或多条查询次数、逾期笔数、当前负债
customer_information.txt客户基本信息通常一客一条年龄、学历、职业、收入
贷款流水表.txt账户交易流水一客多笔交易时间、交易金额、收入/支出方向
label.csv风险标签一客一条客户ID、标签(0好1坏)

贷中预测的标签不是"是否申请过贷款",而是"放款后一段时间内是否出现了实质性风险",所以标签相对申请日期是有时间窗口的。建模之前先把标签分布打出来,确认坏样本占比,因为后面所有评估指标的解读都依赖这个基线。

2.2 单表清洗:handle_*.py 各自的职责

src目录里那一排handle_开头的py文件,就是每张原始表的清洗脚本。handle_apply.py处理申请信息,handle_credit.py处理征信,handle_flow_chart.py处理贷款流水,handle_presentation.py和handle_extra.py处理伴随表与补充数据。各脚本的套路高度一致:读原始txt、删掉高缺失列、处理主键去重、填充缺失值、写出结果csv。

以handle_apply.py为例:

# handle_apply.py —— 申请信息表清洗 import pandas as pd # 原始申请表,赛题数据多为制表符分隔 apply = pd.read_csv('apply.txt', sep='\t', encoding='utf-8') print('原始形状:', apply.shape) print('列名:', apply.columns.tolist()) # 同客户可能有多条申请,保留最后一条作为最新申请意图 apply = apply.drop_duplicates(subset=['客户ID'], keep='last') # 缺失率超过80%的列信息量太低,直接丢弃 miss_ratio = apply.isnull().mean() drop_cols = miss_ratio[miss_ratio > 0.8].index.tolist() apply = apply.drop(columns=drop_cols) # 其余缺失值用众数填充,类别型字段的稳妥做法 fill_values = apply.mode().iloc[0] apply = apply.fillna(fill_values) apply.to_csv('handle_apply.csv', index=False) print('清洗后形状:', apply.shape)

这段代码的逻辑很直接:drop_duplicates是针对申请表这种可能一客多条的记录,保留最后一条表示客户最新的申请状态;缺失率80%这个阈值是经验值,金融数据里很多补充字段只有少数样本有值,留了只会制造噪声;众数填充适合类别型字段居多的表,如果字段是连续数值,我会改成中位数填充。

需要特别提醒的是,credit表和贷款流水表不能照搬这套逻辑。征信表一客可能有多条,要用聚合而不是去重;流水表一客几十上百笔,去重会把行为特征都丢掉。这两张表的处理放到2.3和第三章里讲。

2.3 多表合并:merge.py 的主键设计与一对多处理

merge.py 是整套数据整合的核心脚本。合并主键显然是客户ID,但四张表的粒度不一致:清洗后的apply一客一条,credit可能一客多条,贷款流水一客几十甚至上百条。如果直接三张表left join,行数会成倍膨胀,一个流水多的客户能把整个表撑出几十行重复记录。

正确做法是先把一对多的表按客户聚合成一行,再和主表做关联:

# merge.py —— 多表合并,先聚合再关联 import pandas as pd apply = pd.read_csv('handle_apply.csv') credit = pd.read_csv('handle_credit.csv') flow = pd.read_csv('handle_flow_chart.csv') # 征信表按客户聚合:查询次数求和、逾期笔数求和 credit_group = credit.groupby('客户ID').agg( 征信查询次数=('查询次数', 'sum'), 逾期总笔数=('逾期笔数', 'sum'), 负债总额=('当前负债', 'sum'), ).reset_index() # 流水表先截取近30天,再按客户聚合 flow['交易时间'] = pd.to_datetime(flow['交易时间']) cutoff = flow['交易时间'].max() - pd.Timedelta(days=30) flow_recent = flow[flow['交易时间'] >= cutoff] flow_group = flow_recent.groupby('客户ID').agg( 近30天交易笔数=('交易金额', 'count'), 近30天交易总额=('交易金额', 'sum'), ).reset_index() # 主表是申请信息,逐张left join df = apply.merge(credit_group, on='客户ID', how='left') df = df.merge(flow_group, on='客户ID', how='left') print('合并后形状:', df.shape) df.to_csv('merge.csv', index=False)

这里的关键是先聚合再合并。一对多表如果不聚合直接merge,客户有多少条流水就会复制多少行,后面做特征是在这种膨胀数据上做,不仅慢,而且groupby结果会出错。聚合窗口选近30天,是因为贷中风险更看重近期行为变化——以前还款正常、最近突然频繁大额支出,往往意味着资金链出问题。

合并完成后要检查df.shape是否与主表apply的行数一致,多出来的行说明主键在子表里重复,需要回到上一步重新聚合。源码里的temp.xlsx和temp1.xlsx就是这种中间检查产物,相当于我自己核对合并结果时导出的人工抽检文件。这个"每步打shape、导出抽查"的习惯,建议保留下来。

3. 特征工程:把流水与征信变成模型能用的特征

3.1 流水表的滚动统计特征:把行为变成数字

贷中和贷前最大的区别在于:贷前只能看申请资料和征信报告,贷中手里还有真实的账户流水。特征工程的核心工作就是从流水里挖行为特征。feature_engineering.ipynb里做的就是这个,按客户和时间窗口做聚合,把几十上百笔流水压缩成几个特征。

下面这段是典型的月度滚动聚合:

# feature_engineering.ipynb —— 流水滚动特征 import pandas as pd import numpy as np flow = pd.read_csv('handle_flow_chart.csv', parse_dates=['交易时间']) flow['交易月份'] = flow['交易时间'].dt.to_period('M') # 月度聚合:每个客户每个月的交易习惯 monthly = flow.groupby(['客户ID', '交易月份']).agg( 月交易笔数=('交易金额', 'count'), 月交易总额=('交易金额', 'sum'), 月最大单笔=('交易金额', 'max'), 月最小单笔=('交易金额', 'min'), ).reset_index() # 从月度表派生稳定性特征 flow_feat = monthly.groupby('客户ID').agg( 近3月交易总额=('月交易总额', lambda x: x.tail(3).sum()), 交易额标准差=('月交易总额', 'std'), 交易笔数CV=('月交易笔数', lambda x: x.std() / (x.mean() + 1e-6)), ).reset_index() flow_feat.to_csv('flow_features.csv', index=False)

这段代码里有几个细节值得讲。lambda x: x.tail(3).sum()取的是最近三个月的滚动和,而不是全期总和,这是故意回避"全期统计"这个坑——贷中模型的评估点是放款后的某个时间,推理时只有当时之前的历史数据,全期统计等于把未来的信息混进了特征。交易笔数CV是变异系数,标准差除以均值,用来衡量客户交易行为的稳定性,CV越大说明交易时多时少越不稳定,这类客户往往是风险人群。

跑完聚合先看flow_features的describe(),看均值、分位数是否异常。如果最大单笔比其他特征高出几个数量级,说明原始流水里有对公转账之类的异常大额,可以考虑截断或单独做标记。

3.2 征信与申请的交叉特征:额度使用率与查询密度

征信数据里最有价值的不是原始条数,而是几个衍生比率。风控圈里常讲"额度使用率",当前负债除以授信总额,这个比值上升很快往往意味着客户开始依赖已有额度,资金紧张。另外近6个月的征信查询次数也很关键,查询越频繁说明客户越在到处找钱。

# 交叉特征:额度使用率、查询密度 credit = pd.read_csv('handle_credit.csv') credit['查询日期'] = pd.to_datetime(credit['查询日期']) # 额度使用率,加1防止除零 credit['额度使用率'] = credit['当前负债'] / (credit['授信总额'] + 1) # 近6个月查询次数:按客户分组,统计查询日期距今180天内的记录数 credit['近6月查询次数'] = credit.groupby('客户ID')['查询日期'].transform( lambda x: x.ge(x.max() - pd.Timedelta(days=180)).sum() ) credit_group = credit.groupby('客户ID').agg( 平均额度使用率=('额度使用率', 'mean'), 近6月查询次数=('近6月查询次数', 'max'), ).reset_index()

额度使用率把负债和授信两个字段变成0到几之间的无量纲比率,消除了不同客户之间授信规模差异的影响,同一客户不同时期的这个值也可以直接对比。那行transform的写法稍绕,它是按客户分组后,逐条判断查询日期是否在"该客户最近一次查询日往前180天"之内,统计就是这个窗口内的查询次数,最后按客户取max,避免一客多征信记录时算重。

这类交叉特征的核心是"比率优于绝对值"。原始字段的绝对数值受客户资质、产品类型影响很大,比率特征天然做了一次归一化。feature_engineering.ipynb里还有性别、职业、学历等类别特征的处理,用的是sklearn的LabelEncoder或map映射成数值,这部分代码比较常规,不展开。

3.3 特征落表与训练集构建:train.csv和test.csv的分工

特征做完之后,要把apply、credit、flow三路特征以客户ID为键合并成最终的特征表,这就是build_test.py和merge_test.py在做的事。竞赛数据通常只给一部分客户打标签,另一部分客户只有特征,后者就是提交预测结果的测试集。

# build_test.py —— 构建训练集与测试集 import pandas as pd base = pd.read_csv('merge.csv') flow_feat = pd.read_csv('flow_features.csv') credit_feat = pd.read_csv('credit_features.csv') label = pd.read_csv('label.csv') train = base.merge(flow_feat, on='客户ID', how='left') train = train.merge(credit_feat, on='客户ID', how='left') train = train.merge(label, on='客户ID', how='left') # 有标签的作训练集,没标签的作测试集 train_set = train[train['标签'].notnull()].copy() test_set = train[train['标签'].isnull()].copy() print('训练集:', train_set.shape, '测试集:', test_set.shape) train_set.to_csv('train.csv', index=False) test_set.to_csv('test.csv', index=False)

train.csv和test.csv就是这么切出来的,按标签空与非空划分,而不是按比例随机切。随机切分在竞赛题里容易翻车,因为测试集的分布不一定和训练集完全一致。

合并完成之后,train.csv里会出现不少空值,这是left join的天然结果:部分客户没有征信记录,比如新客户;部分客户近30天没有流水。处理这类缺失值,我的做法是数值型字段填0并额外加一列"是否有该数据"的0/1标志,类别型字段填一个"未知"类别。原因很好理解:缺失本身在风控场景里就有信息量——没有征信记录往往是征信白的表现,和"征信很差"是完全不同的两种风险含义。

4. 建模与模型对比:从朴素贝叶斯到LightGBM

4.1 评估指标:为什么风控建模不看准确率

贷中样本里好客户通常占90%以上,坏客户不到10%。如果模型把所有客户都预测为好客户,准确率能到90%以上,但它一个风险客户也识别不出来,业务上等于没做。所以这类建模的评估指标是AUC和KS,而不是accuracy。

AUC的含义是:随机抽一个好客户和一个坏客户,模型给坏客户打分更高的概率。0.5等于瞎猜,0.7以上算基本可用,竞赛场景里通常要到0.75以上才有竞争力。KS是好坏两组累计分布的差,风控部门常用KS来确定cutoff阈值。这两个指标关心的是排序能力,而不是概率绝对值,正好匹配"找出更可能违约的那批人"这个目标。

# 评估指标:AUC 与 KS from sklearn.metrics import roc_auc_score, roc_curve import numpy as np def ks_score(y_true, y_prob): fpr, tpr, _ = roc_curve(y_true, y_prob) return float(max(tpr - fpr)) # 假设已有验证集预测概率 y_prob print('AUC: %.4f' % roc_auc_score(y_va, y_prob)) print('KS: %.4f' % ks_score(y_va, y_prob))

手工实现ks_score只需要三行,因为KS本质上就是ROC曲线里TPR减FPR的最大值。每次训练完我习惯把AUC和KS一起打印,两个指标同时看,用来判断模型是否存在过拟合嫌疑——如果训练集AUC和验证集AUC相差0.1以上,就该回头查特征了。

4.2 七类模型横向对比:models目录下的实验记录

models目录里是作者做过的全部模型实验:_GaussianNB.py是高斯朴素贝叶斯,MNBC.py是多项式朴素贝叶斯,dtc.py是决策树,randomforest.py是随机森林,_GBDT.py是GBDT,_xgb.py是XGBoost,lgb.py和_lgb.py是LightGBM的两版实现,外加AdaBoost.py。这种"全家桶"式对比在竞赛项目里很常见,先用几个基线模型跑通流程,再重点调最强的梯度提升模型。

以LightGBM为例,核心训练代码是这样的:

# models/lgb.py —— LightGBM 训练与验证 import lightgbm as lgb from sklearn.model_selection import train_test_split from sklearn.metrics import roc_auc_score import pandas as pd train_data = pd.read_csv('train.csv') X = train_data.drop(columns=['客户ID', '标签']) y = train_data['标签'] # stratify保证训练/验证集里好坏样本比例一致 X_tr, X_va, y_tr, y_va = train_test_split( X, y, test_size=0.2, stratify=y, random_state=42 ) model = lgb.LGBMClassifier( n_estimators=800, learning_rate=0.02, num_leaves=31, subsample=0.8, colsample_bytree=0.8, random_state=42, verbose=-1 ) model.fit(X_tr, y_tr) pred = model.predict_proba(X_va)[:, 1] print('LGB AUC:', roc_auc_score(y_va, pred))

stratify=y是最容易被新手漏掉的一个参数。贷中数据坏样本本来就只有几个百分点,如果随机切分,验证集里极可能分不到几个坏样本,AUC算出来虚高或者干脆报错。stratif分层抽样保证训练和验证的坏样本占比一致,模型的评估才可信。

朴素贝叶斯和决策树的脚本写法完全一样,只是换了sklearn的类。从实际效果看,高斯NB在这种多表合并的特征上表现一般,因为特征之间并不独立,而NB恰恰假设独立;决策树单模型过拟合明显,AUC比集成模型低一截;随机森林对异常值稳健但上限不如梯度提升。这套对比实验的价值在于:它证明了"梯度提升树是这类表格数据的默认选择"这句话,而不是靠感觉选模型。

4.3 LightGBM关键参数与调参方向

在表格数据上,LightGBM几乎总是最终模型。它的直方图算法让训练速度比XGBoost快一个量级,而且对类别特征有原生支持。真正影响效果的是下面几个参数:

参数作用典型设置
learning_rate每棵树贡献的权重0.1降到0.02,越低越稳但越慢
n_estimators树的数量500~2000,配合早停
num_leaves单棵树最大叶子数31默认,数据量大可调大
max_depth树的深度-1不限深度,常用6~8防过拟合
subsample每轮训练的样本采样比例0.8左右
colsample_bytree每轮训练的特征采样比例0.8左右
min_child_samples叶子节点最少样本数20~50,调大能压过拟合

调参的顺序不是全参数乱扫。我一般先固定learning_rate=0.02,用早停确定n_estimators,然后看验证集AUC微调num_leaves和min_child_samples,最后才动采样比例。乱扫网格搜,既慢又容易过拟合到验证集。

用原生接口配合早停的写法:

# 原生接口配合Early Stopping,自动确定最优树数量 dtr = lgb.Dataset(X_tr, label=y_tr) dva = lgb.Dataset(X_va, label=y_va) params = { 'objective': 'binary', 'learning_rate': 0.02, 'num_leaves': 31, 'subsample': 0.8, 'colsample_bytree': 0.8, 'metric': 'auc', 'verbose': -1 } model = lgb.train( params, dtr, num_boost_round=2000, valid_sets=[dva], callbacks=[lgb.early_stopping(100)] ) print('best_iteration:', model.best_iteration) print('best AUC:', model.best_score['valid_0']['auc'])

为什么这里用lgb.train而不是LGBMClassifier?因为只有原生接口的callbacks才支持early_stopping。n_estimators设为2000只是上限,连续100轮验证集AUC不涨就自动停,模型通常会在500~800轮停下来。停止后要用model.best_iteration重新训练整个训练集,这是很多教程不讲的细节——早停得到的最优轮数是基于验证集的,想让模型用上全部数据,就带着这个轮数在X、y上再fit一遍。

5. 避坑清单:贷中建模最容易翻车的五个问题

5.1 训练集AUC 0.96、验证集AUC 0.60:数据泄露

现象:模型在训练集上接近完美,线下验证集上AUC却掉到0.6左右,怎么调参都救不回来。

原因:特征里混进了未来信息。典型场景是统计流水时用了全期数据——用放款后甚至整个观察期的流水去算特征,训练时这些特征天然包含"结果"的信息。评估时模型看到的是历史,特征却来自未来,性能自然崩盘。

解决:所有特征只基于评估点之前的数据计算。流水的滚动窗口取评估日往前推30天、90天,杜绝"全期汇总"。做特征之前先画一张时间线图,确认每个特征的计算截止时间点明确,并且和标签观察窗口不重叠。

提示:判断是否泄露的简单办法是,按时间排序把前70%的样本作训练、后30%作验证,如果这样验证AUC比随机切分掉很多,数据泄露基本实锤。

5.2 merge之后行数从几千变成几百万

现象:merge.csv的行数比预期多出几十倍,一张几千个客户的表变成了上百万行。

原因:直接对一卡多贷、一客多笔流水的子表做了left join,客户ID在子表里出现多次,每出现一次父表行就复制一次。

解决:合并前先给每张表查重。用groupby('客户ID').size()看看最大重复次数,凡是最大重复次数大于1的子表,要么先聚合到一客一行,要么明确业务上需要展开的粒度——比如流水表只展开近30天明细的时候可以接受多次。合并后立刻检查shape,行数不等于主表行数说明前面出了纰漏。

5.3 坏样本只有3%,准确率98%的模型其实没用

现象:所有客户预测为好客户,准确率高达98%,但坏客户一个没抓出来,AUC只有0.55。

原因:样本不平衡加上默认阈值0.5。模型发现全预测成好客户损失最小,于是学会了偷懒。

解决:先用class_weight='balanced'或scale_pos_weight让模型关注少数类,再用AUC和KS评估排序能力,最后在验证集上用KS最大值对应的阈值做cutoff,而不是默认0.5。竞赛提交一般要求输出概率,所以训练阶段调整权重比调阈值更常用。

5.4 测试集特征大量为空,填0后AUC反而下降

现象:测试集部分客户没有征信或流水记录,特征空值填0之后提交分数下降。

原因:把"缺数据"和"数值为0"混为一谈。征信白户和征信差是完全不同的两类人,填0等于抹掉了这个区分信息。

解决:数值型缺失填0的同时,新增一列"有无征信记录""近30天有无流水"的0/1标志;类别型缺失填"未知"类别。让模型自己学习缺失的含义,而不是武断地用一个数值代替。

5.5 线下验证用随机切分,提交后分数差一截

现象:随机切分验证AUC有0.78,提交后官方评分只有0.72。

原因:随机切分把同一时间窗口的客户同时放进了训练集和验证集,评估时模型见过相邻样本,高估了泛化能力。测试集往往是更晚时间段的客户,分布有偏移。

解决:按时间顺序切分,用前80%时间的样本训练、后20%作验证,模拟真实上线场景。如果时间信息不完整,至少保证随机切分时用多个种子取平均,不要拿一次结果定结论。

这五条是从复现这种贷中项目里捡出来的血泪经验,每条都曾让AUC掉两三个百分点。查数据泄露和时间对齐问题最费时间,建议在处理阶段就把时间字段、窗口边界写清楚,不要等到模型效果不对再回来找。

6. 进阶:特征重要性分析与模型融合的落地技巧

模型跑通之后,下一步不是急着调参,而是看特征重要性,把特征里的噪声和重复信息清掉。LightGBM的feature_importance可以输出每个特征在分裂时的gain贡献,gain越高说明它对区分好坏客户的贡献越大:

# 特征重要性排序 import pandas as pd imp = pd.DataFrame({ 'feature': X.columns, 'gain': model.feature_importance(importance_type='gain') }).sort_values('gain', ascending=False) print(imp.head(20))

注意importance_type要用'gain'而不是默认的'split'。split只统计分裂次数,某个特征被反复用但每次增益很小也会排前面;gain是按分裂带来的信息增益累计,更接近真实贡献。看top20特征里如果流水类特征占了大半,说明行为数据确实在贷中预测里起主导作用。

特征重要性还有另一个用途:做特征筛选。相关性高、gain又低的特征直接删掉,模型训练会更快,验证AUC往往不降反升。如果两个特征gain差别不大但相关性极高,保留其中一个即可,比如"近30天交易总额"和"近90天交易总额"如果相关系数超过0.9,保留后者更稳。

融合是竞赛里刷分的常见手段。最朴素的做法是概率加权,权重在验证集上试0.9/0.1、0.8/0.2、0.7/0.3几组,选AUC最高的那一档。两个模型相关性越低,融合收益越大;如果两个模型完全相同,融合等于没做。更讲究的做法是用逻辑回归做stacking,把各模型的概率当作新特征训练,但在样本量不大时容易过拟合,加权反而是性价比最高的。

最后提一嘴概率校准。竞赛评分通常只看AUC或KS,不关心概率绝对值,但如果项目要落地到业务决策,比如设定一个阈值触发人工催收,就需要让概率接近真实违约率。sklearn的CalibratedClassifierCV可以对LGB输出做Platt缩放,校准后概率分布更合理,cutoff也更有业务含义。

我从这个项目里养成的习惯是:每次拿到新数据,先花半小时核对时间线和主键粒度,再动任何代码。这两个点只要有一个模糊,后面所有特征和模型都是在沙地上盖楼。做贷中预测尤其如此,未来信息是最隐蔽的敌人,防住它,模型效果才真的可信。希望这份拆解能帮你把项目完整跑通,少走我走过的弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询