ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

贷中风险预测机器学习实战:从数据清洗到多模型对比全流程

贷中风险预测机器学习实战:从数据清洗到多模型对比全流程 简介这是基于“江苏银行杯”金融大数据建模挑战赛赛题开发的贷中风险预测模型完整工程面向金融风控建模学习者、高校竞赛选手及相关从业者。压缩包共49个文件约10.97MB包含19个Python脚本数据清洗、特征构建、多模型训练与融合、5个Jupyter Notebook特征工程、分析与调优、11个CSV数据训练/测试及中间处理结果并附3份Word文档初赛方案、字段示例、项目说明、2份PPT报告和1份项目介绍Markdown。内容覆盖贷前申请数据、客户信息、贷款流水等业务字段的预处理思路提供决策树、XGBoost、LightGBM、AdaBoost、随机森林及朴素贝叶斯等对比脚本可快速复现从数据准备到模型评估的完整建模流程。已有69人学习下载适合赛事复盘、课程设计、毕业设计或入门金融机器学习时参考能直接借鉴其数据工程与模型调优经验。1. 机器学习贷中风险预测这份源码包能直接落地到什么程度做贷中风险预测的机器学习项目大多数人卡在第一步数据还没洗干净模型就急着往上堆。这份源码包来自“江苏银行杯”金融大数据建模挑战赛的贷中风险预测赛题是一套能完整跑通的 Python 源码——从 apply、credit、customer_information、extra 四张明细表出发经过 handle_*.py 清洗、merge.py 合并宽表、feature_engineering.ipynb 特征工程最后落到 DTC、LGB、XGB、RF、AdaBoost、GBDT 多个模型的横向对比和预测结果输出。它解决的是贷中预警场景里“数据怎么组织、特征怎么提、模型怎么比”三个具体问题。适合金融风控方向的高校课设/毕设、想复现比赛完整流程的入门者以及想快速搭建风控建模 baseline 的从业者。2. 贷中数据长什么样四张明细表如何变成一张可用于建模的宽表拿到包先别急着跑模型。项目介绍.md 和 flow_chart.txt 把整条数据链路讲得很清楚我一般会先花五分钟把这两个文件过一遍再决定从哪个脚本开始。贷中场景和贷前审批最大的差别是贷前只有申请时点的静态信息贷中却有客户进入贷款周期之后陆续产生的流水、行为和信用变化数据组织复杂度高一个量级。2.1 apply、credit、customer_information、extra 的分工源码包里的原始数据文件是 apply.txt、credit.txt、customer_information.txt、贷款流水表.txt外加一个 extra.csv。从命名和建模常识看apply 是申请信息credit 是授信/信用记录customer_information 是客户基本属性贷款流水表是资金进出明细extra 大概率是补充特征或赛题另给的数据。字段的真实含义要以“数据字段示例说明.docx”为准建模前把这张表逐行读一遍。这四张表在模型里的角色不一样。apply 和 customer_information 相对静态适合直接做客户维度特征credit 和贷款流水表是行为数据一个客户可能有多条记录必须聚合后才能进宽表。label.csv 是监督信号train.csv 和 test.csv 是赛题划分好的建模样本。分工清楚之后再看预处理脚本逻辑就顺了。数据文件内容角色对应预处理脚本apply.txt申请信息相对静态handle_apply.pycredit.txt授信/信用行为记录handle_credit.pycustomer_information.txt客户基本属性handle_customer_information.py贷款流水表.txt资金流水明细一对多merge 前单独聚合extra.csv补充特征数据handle_extra.py这里的关键判断是哪些表是“一行一个样本”哪些表是“一个样本多行记录”。前者清洗后可以直接 join后者必须先聚合再做 left join否则 merge 出来的宽表会行数爆炸。merge.py 里保留了这个逻辑。2.2 handle_apply.py 这类预处理脚本的读法handle_apply.py、handle_credit.py、handle_customer_information.py、handle_extra.py 四个脚本的骨架是一样的读原始文本、去重、补缺失、统一类型、输出清洗后的 CSV。我以 apply 的清洗为例拆一下重点import pandas as pd def handle_apply(src: str, dst: str) - None: # 贷中赛题的原始文件大多是文本表分隔符和编码要按实际文件确认 df pd.read_csv(src, sep\t, encodinggbk, low_memoryFalse) print(f原始行数: {len(df)}, 列数: {len(df.columns)}) # 主键去重同一个申请记录只保留一条 key_cols [id] # 具体字段以数据字段示例说明 docx 为准 df df.drop_duplicates(subsetkey_cols, keepfirst) # 数值列缺失填 -999类别列缺失填 unknown num_cols df.select_dtypes(include[int64, float64]).columns df[num_cols] df[num_cols].fillna(-999) cat_cols df.select_dtypes(include[object]).columns df[cat_cols] df[cat_cols].fillna(unknown) df.to_csv(dst, indexFalse) print(f清洗后行数: {len(df)}, 输出: {dst})read_csv里sep\t和encodinggbk是两个最容易被坑的参数。赛题原始文件经常是 Windows 环境下导出的制表符分隔文本编码可能是 gbk读取失败时再试utf-8。数值缺失填-999而不是0是为了让树模型能区分“本来就缺失”和“数值为 0”两种情况LGB 和 XGB 都吃这一套。类别列统一补unknown防止模型训练时抛异常。去重的 key 列不一定是 id。有的赛题里申请单号、客户号、时间戳三个字段联合才构成唯一键这个信息写死在脚本里跑之前要和 docx 核对一遍避免误删有效样本或漏删重复行。2.3 用 merge.py 做表合并主键、连接方式与行数校验预处理脚本跑完会产出 handle_apply.csv、handle_credit.csv、handle_customer_information.csv、handle_extra.csv 四个文件。merge.py 的工作是把它们合并成一张宽表 merge.csvmerge_test.py 则负责测试集。核心逻辑是逐个 left joinimport pandas as pd def merge_all(cust_id: str id): apply_df pd.read_csv(handle_apply.csv) credit_df pd.read_csv(handle_credit.csv) customer_df pd.read_csv(handle_customer_information.csv) extra_df pd.read_csv(handle_extra.csv) base apply_df.copy() for name, df in [(credit, credit_df), (customer, customer_df), (extra, extra_df)]: before base.shape[0] base base.merge(df, oncust_id, howleft, suffixes(, f_{name})) after base.shape[0] assert before after, fmerge {name} 后行数变化: {before} - {after} return basehowleft的意思是保留 apply 里全部样本其他表只有匹配上的信息才附加进来。用 left 而不是 inner是为了防止某张表缺记录时把整个样本行丢掉贷中场景里样本本来就难得丢一行都心疼。suffixes参数解决字段名冲突两张表都有amount时后一张会自动变成amount_credit。assert before after是整段里最值钱的一行——一旦明细表聚合漏了这里立刻报错而不是让错误数据悄悄流进模型。提示merge_test.py 必须和 merge.py 用同一套字段、同一套连接方式否则训练集和测试集的特征分布会对不上线上预测结果基本没法看。合并完成的 merge.csv 是后面所有建模工作的输入。到这里“数据组织”这个问题算解决了下一步是特征工程。3. 特征工程与第一个模型从 PPT 里的特征思路到 LGB 跑通贷中风险预测和常见的分类赛题有个明显区别它不靠一两个强特征取胜而是靠一大批弱特征叠出排序能力。源码包里那份“大佬的 ppt特征提取十分值得学习.pptx”把特征方向讲得很透flow_chart.txt 里画了完整的特征工程流程。我按自己的经验把重点拆成三块讲。3.1 贷中特征提取的优先方向授信使用率、逾期次数、还款稳定性贷中建模的特征优先级最高的是“客户进入贷款周期后的行为变化”。授信使用率当前未还本金/授信额度是贷中场景里最经典的指标之一使用率长期高企的客户往往资金链紧张。其次是近 3 或 6 或 12 个月的逾期次数时间窗口越近的信号越强。还款稳定性用近 6 个月按时还款占比来刻画比单一金额类特征稳得多。def build_time_features(df: pd.DataFrame) - pd.DataFrame: # 授信使用率注意分母加 1e-9 防止除零 df[credit_util] df[current_loan] / (df[credit_limit] 1e-9) # 按客户分组滚动统计近 3 个月逾期次数避免跨客户累计 df df.sort_values([id, dt]) df[overdue_cnt_3m] ( df.groupby(id)[overdue_flag] .rolling(3, min_periods1).sum() .reset_index(level0, dropTrue) ) return dfrolling(3)的窗口要跟业务口径对齐是按自然月数还是按行为记录条数赛题说明里通常会写。min_periods1保证客户早期数据不足 3 条时不会全是 NaN。reset_index(level0, dropTrue)把 groupby 产生的外层索引去掉让结果能按原行序直接对齐。如果overdue_flag在流水明细表里就先在明细表上做这个聚合再把特征 merge 回宽表。提示features 里的滚动统计只能基于当前时点之前的数据一旦把未来月份的标记算进窗口AUC 会虚高但上线就崩这一点后面的避坑章还会展开。3.2 先用决策树和朴素贝叶斯验证链路dtc.py 与 MNBC.py源码包里 dtc.py 和 MNBC.py 是整套代码里最早的两个模型文件。它们的作用不只是出结果更重要的是验证数据链路通不通。如果决策树能稳定跑出 AUC 0.6 以上说明 merge 没错位、标签对上了、特征没有空一片如果 AUC 卡在 0.5 附近问题大概率在前面的环节而不是模型不行。from sklearn.tree import DecisionTreeClassifier from sklearn.model_selection import train_test_split from sklearn.metrics import roc_auc_score X train.drop(columns[label]) y train[label] X_train, X_val, y_train, y_val train_test_split( X, y, test_size0.2, random_state42, stratifyy ) clf DecisionTreeClassifier(max_depth4, min_samples_leaf50, class_weightbalanced) clf.fit(X_train, y_train) print(fvalid AUC: {roc_auc_score(y_val, clf.predict_proba(X_val)[:, 1]):.4f})max_depth4限制树深度min_samples_leaf50要求叶子节点至少 50 个样本这两个参数把单棵树的过拟合压住。class_weightbalanced按样本比例自动加权坏客户少、好客户多时模型不会一上来就全判好客户。第一次跑 baseline 别花时间在调参上只要链路通、指标不是 0.5就立刻往下走。MNBC.py 是朴素贝叶斯训练更快用来做交叉验证时的第一个对照基线很合适。3.3 把主力模型切到 LGB_lgb.py 的参数落点链路验证完主力模型可以切到 LightGBM。_lgb.py 里用到的参数我拆一下import lightgbm as lgb model lgb.LGBMClassifier( n_estimators500, learning_rate0.05, num_leaves31, max_depth6, subsample0.8, colsample_bytree0.8, class_weightbalanced, random_state42 ) model.fit( X_train, y_train, eval_set[(X_val, y_val)], eval_metricauc, callbacks[lgb.early_stopping(50), lgb.log_evaluation(50)] )n_estimators500是最大迭代轮数实际会在早停处截断learning_rate0.05是每轮步长调小到 0.01 会更稳但训练时间变长num_leaves31是 LGB 最敏感的复杂度参数特征多时可试 63subsample0.8做行采样colsample_bytree0.8做列采样两者相当于内置正则贷中数据量大时能明显抑制过拟合。early_stopping(50)表示验证集 AUC 连续 50 轮不涨就停最后取最优迭代轮。这套参数在多数贷中数据集上是个说得过去的起点不用一上来就搜参。第一个模型跑通后接下来要做的是把其他几个模型都拉进来对比看谁更适合这份数据。4. 多模型横向对比与结果落盘XGB、RF、AdaBoost、GBDT 的组合用法源码包里除了 LGB还有 _xgb.py、randomforest.py、AdaBoost.py、_GBDT.py 四个模型文件。贷中项目里多模型对比不是走形式而是为了确认模型选择没被单点运气左右。这一章的实操拆成三块对比脚本怎么组织、结果字段怎么读、测试集链路怎么接。4.1 对比脚本的组织方式统一接口、统一数据划分、统一评估函数多模型对比最忌讳各跑各的train_test_split 的随机种子不同、评估指标不统一、数据划分不一致得出的对比结论全是噪声。源码包里这几个模型脚本能直接对比是因为它们都跑在同一份 X_train / X_val 上。我一般会把对比逻辑收敛成一个函数from xgboost import XGBClassifier from sklearn.ensemble import RandomForestClassifier, AdaBoostClassifier, GradientBoostingClassifier from sklearn.metrics import roc_auc_score def evaluate_model(name, model, X_train, y_train, X_val, y_val): model.fit(X_train, y_train) prob model.predict_proba(X_val)[:, 1] auc roc_auc_score(y_val, prob) print(f{name:12s} AUC{auc:.4f}) return {model: name, auc: auc} results [] results.append(evaluate_model(lgb, lgb.LGBMClassifier(...), X_train, y_train, X_val, y_val)) results.append(evaluate_model(xgb, XGBClassifier(eval_metricauc, class_weightbalanced), X_train, y_train, X_val, y_val)) results.append(evaluate_model(rf, RandomForestClassifier(n_estimators300, max_depth8), X_train, y_train, X_val, y_val)) results.append(evaluate_model(adaboost, AdaBoostClassifier(n_estimators200), X_train, y_train, X_val, y_val)) results.append(evaluate_model(gbdt, GradientBoostingClassifier(n_estimators200, max_depth4), X_train, y_train, X_val, y_val))AUC 只看排序能力不依赖阈值适合贷中这种样本比例严重失衡的场景。每个模型用同一份训练/验证集结论才有可比性。RF 的max_depth8和 GBDT 的max_depth4是两组比较稳的初始值RF 对缺失值和异常值不敏感GBDT 在深度上做限制能防止在强特征上过拟合。AdaBoost 在这个链路里更多是看个对照它不适合大数据量和高维稀疏场景如果训练明显变慢可以不加进最终对比。4.2 结果字段的含义result.csv 与 result.xlsx 里该看哪几列对比跑完结果会写到 result.csv 和 result.xlsx 里。result.xlsx 一般存的是多模型对比汇总每一行是一个模型模型名、AUC、KS、训练时间。result.csv 存的是测试集最终预测字段结构大致如下输出文件关键列说明result.xlsxmodel、auc、ks多模型对比汇总选模型看这里result.csvid、pred_prob、pred_label测试集预测pred_label 由阈值切出pred_prob 是模型输出的违约概率pred_label 是概率大于阈值后拍出来的硬标签。这里要提醒pred_label 在风控里不是最终决策依据。同样 0.6 的概率在客户量大、资产充足的时候可能放行在资产收紧时要拦截阈值本质上是业务参数不是模型参数。源码包里 result.csv 的阈值如果是写死的 0.5上线前一定按业务要求重新标定。4.3 测试集预测链路build_test.py 与 test_time.py 的分工模型选完最后一步是把预测管线接到测试集上。build_test.py 做的是标准流程加载 test.csv、用训练时同一个特征函数做变换、加载最优模型、输出 result.csv。test_time.py 做的是另一件事按时间切分验证集检验模型在不同时间窗口上的稳定性。import joblib # build_test.py 核心流程测试集必须复用训练时的变换逻辑 test pd.read_csv(test.csv) test transform_same_as_train(test) # 同一个特征函数统计量来自 train model joblib.load(best_lgb.pkl) test[pred_prob] model.predict_proba(test[feat_cols])[:, 1] test[pred_label] (test[pred_prob] 0.5).astype(int) test.to_csv(result.csv, indexFalse)transform_same_as_train是这段代码的灵魂。缺失值填补的均值、标准化用的均值和方差都必须从训练集算好再套到测试集上绝不能拿测试集自己的统计量再算一遍。joblib.load和predict_proba是最稳的预测方式比重新加载训练脚本再跑一遍快得多也避免了二次训练带来的随机差异。test_time.py 我用得不多但贷中场景值得养成习惯同一个模型在时间靠前的窗口上 AUC 高、靠后的窗口上 AUC 明显下滑说明特征时效性不足要回到特征工程找原因。到这里从数据到模型的路已经通了。真正的麻烦往往在跑通之后才出现。5. 避坑清单贷中建模最容易翻车的五个点代码能跑通只是开始。贷中场景的数据特性决定了坑都埋在前面的细节里。这五条是我复盘这套源码包和同类赛题后最想写下来的每一条都按“现象-原因-解决”说清楚。5.1 样本不平衡accuracy 虚高不等于模型可用现象训练集里好客户占了绝大多数模型预测准确率高达 0.96但验证集 AUC 只有 0.58。原因模型把所有样本都判为好客户准确率自然高但它完全没有区分能力用 accuracy 评估贷中模型是典型的评估指标错配。解决评估一律用 AUC 和 KS训练时给模型加class_weightbalanced。如果还是压不住再考虑对坏样本做 SMOTE 过采样——样本量不大时先试加权不用一上来就上采样。5.2 时间穿越特征统计窗口越过预测点分数全是幻觉现象随机划分验证时 AUC 0.85换成按时间切分的验证马上掉到 0.70。原因特征在生成时用了预测时点之后的数据。比如用整个观察期内的“逾期次数”预测观察期中段的违约模型等于提前拿到了答案。解决所有时序特征在生成时只允许使用 t 日之前的数据验证集合也按时间滚动切分。贷中场景的数据量够大完全有条件做 time-based 验证别偷懒用随机划分。5.3 标签泄漏某些“好字段”其实是目标变量的影子现象某个字段单独和标签的 IV 值高得离谱比如“是否结清”直接能把好坏客户分开。原因这个字段本身就是目标变量的影子或者是违约之后才产生的记录它进了特征就等于把答案写进了 X。解决每个特征过一遍业务含义凡是时点晚于目标判定的字段一律剔除。源码包里 label.csv 和 train.csv 要对着“数据字段示例说明.docx”一个个字段核别因为“当时没细想”把泄漏特征留到最后。5.4 合并错位流水表明细直接 join 会炸出大量重复行现象merge 之后行数从五万膨胀到几百万模型训练直接卡死或内存溢出。原因贷款流水表是明细表一个客户有多行流水直接按客户 id join 会产生一对多膨胀每一行申请记录被复制成 N 行。解决明细表必须先按客户维度做 groupby 聚合算 total_amount、max_amount、row_count 这类汇总特征再和其他表做 left join。merge.py 里的行数断言就是防这个的merge 前后行数必须一致。5.5 环境与编码中文路径、库版本不一致会让脚本一动就挂现象脚本一运行就抛 UnicodeDecodeError 或 ModuleNotFoundError有的模型脚本跑完结果却对不上。原因Windows 下中文路径、原始文件编码是 gbk、本地 Python 环境缺库或库版本不一致。解决项目目录强制用英文路径读取 .txt 时先试encodingutf-8再试encodinggbk在终端一次性装齐依赖pandas、scikit-learn、lightgbm、xgboostPython 版本建议 3.8 以上。如果本地没装 lightgbm_lgb.py 会在 import 处直接报错先把依赖装齐再按项目介绍.md 的顺序跑。最后补一个我常用的进阶技巧。6. 进阶技巧多模型融合前先看一眼相关性矩阵单模型对比完别急着收工。源码包里五个模型都出了预测概率把这些概率合起来通常能再捞回零点几个点的 AUC。但融合有前提不是无脑平均。6.1 加权融合与阈值搜索的落地写法融合前第一步是算模型预测概率的两两相关性。相关性高于 0.9 的两个模型融合收益很小等于把同一份答案算了两遍相关性在 0.6 以下的模型融合往往有惊喜。import numpy as np from scipy.stats import spearmanr # lgb_oof / xgb_oof / rf_oof 分别来自三个模型在验证集上的 predict_proba 输出 lgb_oof ... # 由 _lgb.py 的模型对验证集预测得到 xgb_oof ... rf_oof ... # 先看相关性再决定要不要融合 print(spearmanr(lgb_oof, xgb_oof)) print(spearmanr(lgb_oof, rf_oof)) # 加权融合权重按验证集 AUC 比例分配 w_sum 0.40 0.35 0.25 stack (0.40 * lgb_oof 0.35 * xgb_oof 0.25 * rf_oof) / w_sum # 阈值搜索按验证集 KS 最大点找切分概率 best_th, best_ks 0.5, 0.0 for th in np.arange(0.3, 0.8, 0.01): ks compute_ks(y_val, stack, th) # 风控常用 KS好坏样本累计分布最大差 if ks best_ks: best_ks, best_th ks, th权重直接取验证集 AUC 是工程上最省事的做法不需要再套一层 meta model。早停轮数和特征列表都按验证集调好了融合权重也按验证集定最后统一在测试集上只跑一次避免在测试集上反复试阈值造成隐性过拟合。6.2 一次可以复用的验证小习惯有一次我在一个贷中数据集上单个模型验证集 AUC 只有 0.79随手把 LGB 和 XGB 的概率做了个简单平均AUC 到 0.81。一开始以为是运气后来查了一下相关性矩阵两个模型预测概率的 spearman 相关系数只有 0.6 出头说明它们捕捉到的模式确实不同融合才有收益。从那以后我每次跑完一堆单模型都强制先看一遍相关性矩阵再决定要不要融合这一步花不了两分钟却经常是提高名次最划算的两分钟。希望帮到你。本文还有配套的精品资源点击获取
返回列表