简介面向计算机专业毕业设计、期末大作业及金融风控入门实践这份完整项目包以贷中风险预测为核心任务基于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, encodingutf-8) print(原始形状:, apply.shape) print(列名:, apply.columns.tolist()) # 同客户可能有多条申请保留最后一条作为最新申请意图 apply apply.drop_duplicates(subset[客户ID], keeplast) # 缺失率超过80%的列信息量太低直接丢弃 miss_ratio apply.isnull().mean() drop_cols miss_ratio[miss_ratio 0.8].index.tolist() apply apply.drop(columnsdrop_cols) # 其余缺失值用众数填充类别型字段的稳妥做法 fill_values apply.mode().iloc[0] apply apply.fillna(fill_values) apply.to_csv(handle_apply.csv, indexFalse) 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(days30) 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, howleft) df df.merge(flow_group, on客户ID, howleft) print(合并后形状:, df.shape) df.to_csv(merge.csv, indexFalse)这里的关键是先聚合再合并。一对多表如果不聚合直接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, indexFalse)这段代码里有几个细节值得讲。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(days180)).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, howleft) train train.merge(credit_feat, on客户ID, howleft) train train.merge(label, on客户ID, howleft) # 有标签的作训练集没标签的作测试集 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, indexFalse) test_set.to_csv(test.csv, indexFalse)train.csv和test.csv就是这么切出来的按标签空与非空划分而不是按比例随机切。随机切分在竞赛题里容易翻车因为测试集的分布不一定和训练集完全一致。合并完成之后train.csv里会出现不少空值这是left join的天然结果部分客户没有征信记录比如新客户部分客户近30天没有流水。处理这类缺失值我的做法是数值型字段填0并额外加一列是否有该数据的0/1标志类别型字段填一个未知类别。原因很好理解缺失本身在风控场景里就有信息量——没有征信记录往往是征信白的表现和征信很差是完全不同的两种风险含义。4. 建模与模型对比从朴素贝叶斯到LightGBM4.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是XGBoostlgb.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_size0.2, stratifyy, random_state42 ) model lgb.LGBMClassifier( n_estimators800, learning_rate0.02, num_leaves31, subsample0.8, colsample_bytree0.8, random_state42, verbose-1 ) model.fit(X_tr, y_tr) pred model.predict_proba(X_va)[:, 1] print(LGB AUC:, roc_auc_score(y_va, pred))stratifyy是最容易被新手漏掉的一个参数。贷中数据坏样本本来就只有几个百分点如果随机切分验证集里极可能分不到几个坏样本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_rate0.02用早停确定n_estimators然后看验证集AUC微调num_leaves和min_child_samples最后才动采样比例。乱扫网格搜既慢又容易过拟合到验证集。用原生接口配合早停的写法# 原生接口配合Early Stopping自动确定最优树数量 dtr lgb.Dataset(X_tr, labely_tr) dva lgb.Dataset(X_va, labely_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_round2000, 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_weightbalanced或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_typegain) }).sort_values(gain, ascendingFalse) 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也更有业务含义。我从这个项目里养成的习惯是每次拿到新数据先花半小时核对时间线和主键粒度再动任何代码。这两个点只要有一个模糊后面所有特征和模型都是在沙地上盖楼。做贷中预测尤其如此未来信息是最隐蔽的敌人防住它模型效果才真的可信。希望这份拆解能帮你把项目完整跑通少走我走过的弯路。本文还有配套的精品资源点击获取