资讯动态

车辆贷款违约预测Top1方案:特征工程与LightGBM实战解析

发布时间:2026/10/22 3:39:20 来源:尧图企业网站定制
简介2021科大讯飞车辆贷款违约预测挑战赛Top1方案源码与学习说明定位为可直接复现的算法竞赛项目包主要面向具备Python基础、希望深入数据挖掘实战的计算机、数学、电子信息等专业学生。该方案对应比赛第一名代码采用工程化目录组织能帮助读者完整走通车辆贷款违约预测的建模链路从原始CSV数据读取与清洗到特征构造与筛选再到基于GBDT类模型训练、验证、预测与结果导出学习时可配合Markdown说明文档与脚本注释逐步对照。压缩包共十八个文件整体大小约五十九点三九MB其中包含五份CSV数据、四份Python源码、三份Pickle已训练模型以及两份Shell训练/测试脚本、一份Markdown说明文档目录基本按数据、代码、模型、预测结果划分便于按模块检索。压缩包内已包含比赛训练/测试数据与预测结果读者无需额外准备数据即可启动脚本复现也可在此基础上二次修改特征与参数。当前该项目已有一百四十三人学习浏览。借助已训练好的模型与脚本读者可快速还原Top1方案实验环境重点关注特征重要性筛选、多模型融合与阈值调整等实战细节作为竞赛准备或课程项目参考。1. 车辆贷款违约预测赛题一个被低估的金融风控实战样本车辆贷款违约预测挑战赛表面看是一道典型的二分类题根据用户历史行为、车辆信息、还款记录预测未来是否会逾期违约。但真正跑过这个赛道的人都知道难点从来不在“选什么模型”而在于——怎么把一份充满缺失、偏斜和业务噪音的原始数据整理成模型真正能学到规律的样子。Top1方案之所以能拉开差距靠的也不是某个炫技模型而是对特征工程、样本权重和阈值策略的极致打磨。这个标题里的“源码学习说明”意味着你不只是拿到一份能跑的代码而是拿到一整套可复现的解题思路从数据清洗到特征构造从模型训练到结果融合每一步都有据可查。适合正在备赛的学生、准备转岗金融风控的工程师以及想系统提升建模套路的人。下面我按自己复现这类赛题的习惯把这个Top1方案拆开讲清楚顺带把最容易翻车的地方一起说掉。2. 赛题数据与业务理解先搞清楚“违约”是怎么定义的2.1 赛题给出的字段长什么样先别急着写代码车辆贷款违约预测的核心输入一般由四类信息组成订单信息贷款金额、期数、首付比例、用户画像年龄、性别、收入区间、车辆信息车型、新车/二手车、价格、还款行为历史期数、逾期次数、提前还款标记。拿到数据的第一件事不是看分布而是先读字段说明文档把每个字段的类型和业务含义对齐。我一般会先做一张字段清单标注“数值型/分类型/文本型/时间型”再标“强相关候选”和“缺失率高候选”。这一步看起来很笨但在后面做特征工程时能省大量时间尤其是赛题里经常混着“实际值”和“编码值”两种字段不做区分直接进模型会造出一堆无效特征。import pandas as pd # 假设 train.csv / test.csv 已就位 train pd.read_csv(train.csv) print(train.shape) print(train.dtypes.value_counts()) print(train.isnull().sum().sort_values(ascendingFalse).head(20))这段代码做的是最基础的三件事看行数列数、看字段类型分布、看缺失率最高的字段。参数上train.shape能快速确认样本量级dtypes.value_counts()告诉你数据里有多少数值、多少对象类型isnull().sum()是后续填充策略的直接依据。如果缺失字段超过15%基本可以判断它不适合直接进树模型。2.2 定义标签前先看一下“逾期”和“违约”是不是同一个口径很多新手上来就把“是否违约”当二分类标签直接用结果模型分数做上去却无法解释。真实场景里大部分违约标签是有观察窗口的比如“放款后6个月内是否发生M3逾期”这里的“M3”指逾期超过90天。如果赛题只给了一个标签列你要确认它是历史累计违约还是某个时间点的状态。我建议在建模前把标签分布打印出来同时按订单金额、首付比例分组看违约率。通常首付越低、贷款金额越高违约率越高如果这个规律在你的数据里不成立大概率是字段口径理解错了。不要急着上模型先把业务逻辑验证一遍后面做的特征才有解释力。print(train[is_default].value_counts(normalizeTrue)) print(train.groupby(down_payment_ratio)[is_default].mean().sort_values())value_counts(normalizeTrue)看的是正负样本比例groupby分组看模式。如果样本极度不平衡比如违约率低于5%后面必须考虑采样、权重或评估指标的选择不能只看准确率。这一步发现的问题越早后面返工越少。2.3 数据划分要尊重时间顺序别让“未来信息”漏进训练集车辆贷款数据天然带时间属性。放款日期、首期还款日、逾期记录日期这些都是时间戳。Top1方案里最容易被忽视的细节之一就是训练集和验证集的切分方式。如果做随机划分会出现同一时间段的数据互相泄漏模型在验证集上的表现虚高真正线上推理时却崩掉。常见做法是“按时间切分”用较早月份的数据训练较晚月份的数据做验证。切分点一般选在总时间跨度后20%的位置且要保证验证集里的样本没有跨观察窗口。这个决策比调参重要得多因为时间泄漏一旦发生所有后续指标都不可信。# 按放款月份切分 train[month] pd.to_datetime(train[loan_date]).dt.to_month valid_cutoff train[month].quantile(0.8) train_part train[train[month] valid_cutoff].copy() valid_part train[train[month] valid_cutoff].copy() print(train_part.shape, valid_part.shape)切分后最好检查一下两部分的标签分布是否接近如果验证集违约率明显偏低或偏高可能是切分点恰好落在业务政策调整的节点上这种情况要考虑剔除一段过渡期数据。3. Top1方案的特征工程从原始字段到“能打”的特征体系3.1 缺失值处理有套路别无脑填充先判断缺失是不是一种信号车辆贷款数据里的缺失很多时候不是随机缺失而是“该字段不适用于此人”或“该用户没提交某材料”。比如收入字段缺失可能代表申请者是个体户或自由职业这本身就是风险信号车辆评估价缺失可能代表该车没有正规评估记录。我处理缺失时习惯先分三类完全随机缺失可以填充、业务缺失缺失本身是信息、高缺失率字段直接删掉或做has_flag。Top1方案的常见做法是对高缺失字段同时生成两个特征一个是“是否缺失”的二值标记一个是填充后的原始值。树模型会自己决定哪个分支更重要你只需要把信息都给足。for col in [income, car_value, employer_type]: train[col _missing] train[col].isnull().astype(int) train[col] train[col].fillna(train[col].median())这里对数值型字段用中位数填充因为金融数据普遍右偏中位数比均值抗极端值。对employer_type这种分类型字段要在填充时保留原始类别分布用众数或“未知”类目填充都行但一定记得同时生成缺失标记。参数上fillna(median())在特征较多时速度很快但对缺失率超过50%的字段我建议直接不填充只保留缺失标记因为填充后的数值基本是噪声。3.2 核心特征体系还款行为特征才是违约预测的主力经过多个类似赛题的验证排序类特征和还款行为类特征对违约预测的贡献最大。原始字段里的“总逾期次数”“当前逾期期数”虽然直接但信息量太粗。Top1方案的做法是把还款行为做成“比率”“趋势”“极值”三个维度。比率就是逾期期数/总还款期数趋势是最近3期与之前n期的逾期次数对比极值是历史最大连续逾期时长。# 假设已有 history 表每行对应一笔贷款的多期还款记录 agg history.groupby(loan_id).agg( total_overdue pd.NamedAgg(columnoverdue_flag, aggfuncsum), total_terms pd.NamedAgg(columnterm, aggfunccount), max_consecutive_overdue pd.NamedAgg(columnoverdue_flag, aggfuncmax), ).reset_index() agg[overdue_ratio] agg[total_overdue] / agg[total_terms] agg[never_overdue] (agg[total_overdue] 0).astype(int)这个agg聚合的价值在于把逐期记录压缩成贷款级特征。max_consecutive_overdue用max只能得到一个近似值更精确的连续最大逾期需要自定义函数循环但在样本量巨大时性能消耗很高用滚动窗口加max做近似是行业通行做法。never_overdue这个二值特征特别适合“资质干净”的客群识别在很多赛题里单特征重要性就能排进前三。3.3 用户画像交叉特征把收入、年龄、地区组合出业务指标单一的用户画像字段预测力有限但交叉之后往往能挖出强特征。比如“收入除以贷款金额”可以近似反映月供压力“年龄乘以首付比例”能在一定程度上反映资金规划能力“工作年限减年龄”可以推断职业稳定性。这些交叉特征本身没有太深的算法含量胜在业务含义清晰而且不容易过拟合。train[payment_income_ratio] train[monthly_payment] / (train[income] 1) train[age_work_exp_ratio] train[age] / (train[work_exp] 1) train[car_price_income_ratio] train[car_value] / (train[income] 1)这里加1是为了防止除零。payment_income_ratio在金融风控里对应“债务收入比”超过某一阈值比如50%违约概率显著上升。你可以不调参直接把这些特征扔进模型树模型通常能自动找到合理的切分点。但要注意交叉特征不是越多越好。我见过有人一口气构造上百个比率类特征最后模型训练时间翻了三倍AUC却只涨了0.001。建议一次最多加20个候选跑一轮看特征重要性把排在后面的直接删掉再训练。3.4 时间窗口特征用“最近6个月”而不是“全部历史”车辆贷款的还款记录通常覆盖12到36期如果直接对全部历史做聚合会把早期行为和近期行为混在一起。早期逾期可能已经修复不再代表当前信用状态而近3个月的逾期更能预测未来违约。Top1方案里时间窗口特征的设计通常分三段近3个月、近6个月、全部历史。def window_agg(df, months, label): df_temp df[df[months_since_loan] months].groupby(loan_id)[overdue_flag].agg([sum, mean, count]).reset_index() df_temp.columns [loan_id, f{label}_sum, f{label}_mean, f{label}_count] return df_temp train train.merge(window_agg(history, 3, w3), onloan_id, howleft) train train.merge(window_agg(history, 6, w6), onloan_id, howleft)窗口特征加的越细模型的泛化能力不一定越强比如3个月窗口和6个月窗口高度相关会造成特征冗余。我一般会先跑一版全量特征然后用特征重要性图观察如果近3个月特征的w3_sum重要性明显高于全部历史的total_overdue说明近期行为的预测力确实更强后续可以加深窗口粒度的探索如果差别不大说明违约更受长期习惯驱动保留一个窗口特征就够了。4. 模型选型与训练策略单模型还是融合参数到底怎么调4.1 为什么Top1方案是GBDT而不是深度模型车辆贷款违约预测在绝大多数实现里都用树模型尤其是LightGBM原因有三个数据中大量分类型字段和缺失值树模型原生支持特征之间多为非线性关系树模型不需要显式做标准化或one-hot训练速度上LightGBM在数十万级样本上能快速迭代。深度模型在这些数据上的收益往往不明显除非你能构造出序列特征并对还款时间线做RNN或Transformer建模但那需要更高质量的多期流水数据。这个赛题的样本量一般在几十万量级LGB单模型就能把AUC推到平台期。Top1方案通常会在LightGBM之上叠加XGBoost和CatBoost做融合但前提不是“融合一定更好”而是三个模型在特征使用模式上互补——XGB对稀疏数据更稳CatBoost对类别特征的原生处理更友好LGB在大样本下效率最高。4.2 LightGBM的核心参数怎么设先从默认值出发再调我不建议一上来就搜索参数常见的做法是先固定一组基准参数跑通流程再逐步调高叶子节点数和学习率衰减。下面这组参数是我在类似金融赛题里常用的起点整体以保守为主目的是防止快速过拟合。import lightgbm as lgb params { objective: binary, metric: auc, learning_rate: 0.03, num_leaves: 31, max_depth: 6, feature_fraction: 0.8, bagging_fraction: 0.8, bagging_freq: 1, min_data_in_leaf: 100, lambda_l2: 1.0, verbose: -1 } d_train lgb.Dataset(train_part[features], labeltrain_part[is_default]) d_valid lgb.Dataset(valid_part[features], labelvalid_part[is_default]) model lgb.train( params, d_train, num_boost_round3000, valid_sets[d_valid], callbacks[lgb.early_stopping(100), lgb.log_evaluation(200)] )参数说明learning_rate0.03配合num_boost_round3000是给早停留足空间num_leaves31在LightGBM里比max_depth更能控制模型复杂度值越大模型越容易记住噪声feature_fraction0.8和bagging_fraction0.8是防止过拟合的最有效手段min_data_in_leaf100强制每个叶子至少有100个样本对不平衡数据尤其重要。我见过不少人在这步反复调learning_rate其实更值得调的是min_data_in_leaf和lambda_l2。当你发现验证集AUC还在涨但线上或测试集AUC下降时优先把这两个参数调大比降学习率有效得多。4.3 样本不均衡的两种处理姿势weight和scale_pos_weight怎么选违约率低于10%时通常有两种处理路径。一是给少数类加权重LightGBM里用scale_pos_weight数值设为负样本数除以正样本数二是用样本权重sample_weight对每个违约样本乘以一个系数。两者都能提升召回但效果和风险不同。# 方式一scale_pos_weight params[scale_pos_weight] (train_part[is_default] 0).sum() / (train_part[is_default] 1).sum() # 方式二sample_weight weight train_part[is_default].map({0: 1, 1: 5}).values d_train_sw lgb.Dataset(train_part[features], labeltrain_part[is_default], weightweight)方式一的优点是不需要额外构造向量推荐度更高方式二可以对权重做更细腻的调节比如违约样本里“连逾期3期”的权重给到8“仅逾期1期”的给到3。但注意权重过大容易让模型对少数类样本过拟合一个可行的判断标准是调权后验证集的AUC不降、召回提升才算有效否则宁可不调。4.4 早停、模型融合与伪标签复现Top1的三层提升路径早停的核心是“用验证集损失决定迭代轮数”而不是固定训练600轮。LGB的early_stopping回调会自动保存最优轮数的模型。第一次跑通后通常能得到AUC约0.75到0.8的基础结果。第二次迭代——将LGB、XGB、CatBoost三个模型的预测结果做均值或者用逻辑回归做stacking——通常能再提升0.005到0.01。第三层就是Top1方案里常见的“伪标签”策略用训练好的模型对无标签样本预测把高置信度的预测结果当作标签加入训练集迭代二到三轮。伪标签的收益依赖基模型的置信度质量如果第一轮AUC连0.7都不到不建议尝试噪声会压过信号。# 伪标签示例只取预测概率0.85 或 0.15的无标签样本 test_pred model.predict(test[features]) high_conf_idx (test_pred 0.85) | (test_pred 0.15) pseudo test[high_conf_idx].copy() pseudo[is_default] (test_pred[high_conf_idx] 0.5).astype(int) train_aug pd.concat([train_part, pseudo], axis0, ignore_indexTrue)伪标签等于把半监督信息引入训练风险是“确认偏误”——模型错判的高置信度样本会被放大所以在第二轮后必须重新验证AUC是否有提升如果提升低于0.002可以直接放弃这个方向。5. 复现Top1方案时的常见坑从数据泄漏到特征穿越的血泪经验5.1 坑1把“未来信息”当特征验证集上AUC虚高得吓人现象验证集AUC达到0.92但用测试集一测只有0.75差距巨大。原因构建特征时使用了全量数据聚合比如把整个数据集的均值或目标编码直接填到每个样本里相当于把未来样本的信息泄漏到了训练集。解决所有统计类特征必须在训练集上计算再映射到验证集和测试集严禁在全量数据上先算再切分。具体做法是对需要做目标编码或均值编码的字段只在训练集内计算编码值然后通过map方式传给验证集如果验证集出现新类别用训练集的全局均值兜底。5.2 坑2时序特征里混入了“逾期后”的记录现象还款行为特征里的近3个月逾期率在训练集上预测力极强但模型上线后不工作。原因特征窗口和预测目标点没有对齐计算近3个月逾期时包含了“违约事件发生之后”的还款记录造成目标泄漏。解决切分特征窗口时必须以贷款某个观察日为终点往前回溯观察日之后的数据一律不用。我在第一次做类似的车辆贷款项目时踩过这个坑把全部还款历史都用来聚合特征结果AUC做到0.88以为是特征工程做得好后来才发现窗口没有严格截止真正修正后AUC掉到0.81但那段代码才是可信的。5.3 坑3类别特征直接做标签编码导致树模型分错支现象LightGBM训练完成后特征重要性很高但ACU却不如简单模型。原因对无序类别字段如车型、地区编码直接做整数编码树模型会把它当有序数值处理导致学出来的切分点没有业务含义。解决对有序类别如学历等级、收入级别可以保留编码对无序类别优先用LightGBM的categorical_feature参数显式传入。cat_cols [car_type, region_code, employer_type] for col in cat_cols: train[col] train[col].astype(category) d_train lgb.Dataset(train_part[features], labeltrain_part[is_default], categorical_featurecat_cols)如果类别数特别多比如超过50个可以考虑做目标编码后再传入但目标编码本身也要小心泄漏必须用K折方式在训练集内部完成。5.4 坑4验证集切分不按时间随机K折导致指标波动大现象同一份特征和模型换了K折随机种子AUC波动超过0.02。原因车辆贷款数据有明显的时间聚集性同一时期的用户行为高度相似随机K折会让某几个fold里正好集中了一批准违约用户。解决用时间序划分或者按放款月份分组做GroupKFold保证每个fold的时间分布一致。时间序划分的缺点是验证集长度受限如果总数据只有一年切出后验证集可能只有两个月。这时可以改用“拒绝式验证”——用前10个月做训练后2个月做验证再从训练集里抽最后1个月做调节参数的内部验证。这个方法能最大化利用数据又保留时间真实性。5.5 坑5正负样本加权后概率校准失效现象模型加权重后AUC提升但预测概率整体偏高变成“校准丢失”。原因scale_pos_weight改变了模型学到的先验概率预测结果不再是真实的违约概率。解决如果想输出概率用于风控决策必须做校准。常见做法是保留下游二值化阈值或直接用Platt缩放重新拟合概率。from sklearn.calibration import CalibratedClassifierCV from sklearn.linear_model import LogisticRegression calibrator LogisticRegression() base_pred model.predict(valid_part[features]) calibrator.fit(base_pred.reshape(-1, 1), valid_part[is_default]) calibrated_prob calibrator.predict_proba(base_pred.reshape(-1, 1))[:, 1]这一步的代价是会让AUC轻微下降但在风控场景里概率的真实性比AUC数值更重要。如果赛题只要求排名指标可以直接输出模型原始概率不做校准省去这一步。6. 从源码到自己的方案如何把Top1流程迁移到新数据集6.1 模块化重构把数据处理、特征、训练拆成独立脚本源码能跑通和源码能复用是两回事。Top1方案最大的价值不是那几行lgb.train而是整套流程的组织方式。我建议你拿到源码后先不做任何优化按“数据处理”“特征构造”“模型训练”“预测提交”四个模块拆开分别测试输入输出格式。这样后面换新数据时只需要改数据加载和字段映射部分其余逻辑直接复用。实践上我会用一份很小的抽样数据比如5000行先跑通全流程确认没有数据泄漏和bug再上全量数据。5000行跑一次LGB只要几秒钟全量跑一次可能是10分钟先用小数据验证流程是效率最高的习惯。6.2 先用基线流程复现分数再研究Top1的增量在哪很多新手拿到源码就急着改模型参数结果连基线分数都没复现。正确顺序是用源码自带的特征列表和参数跑出一版分数记录AUC和特征重要性然后每次只改一处比如加一个特征、换一个模型、变一个样本权重重新跑并记录分数变化。这样你能清楚知道Top1方案里的每一分提升来自哪里。常见做法是建一个简单的实验结果表记录日期、改动内容、验证AUC、线上AUC、备注。我经历过几个类似比赛后最大的感受是“不记录实验”才是最大的效率杀手改来改去最后忘了哪版效果最好白白浪费时间。6.3 最后的习惯特征重要性排序和预测分布检查必须每次跑完都看跑完模型后我一般会做两个检查。一是打印特征重要性前20看是否有“不该重要”的特征跑进来二是对验证集预测概率做分布直方图观察是否集中在0.5附近。如果预测概率大量堆在0.4到0.6之间说明特征区分度不足不是调参能解决的要回头检查特征质量。import matplotlib.pyplot as plt imp pd.Series(model.feature_importance(), indexfeatures).sort_values(ascendingFalse) imp.head(20).plot.barh(figsize(10, 8)) plt.gca().invert_yaxis() plt.tight_layout() plt.savefig(feature_importance.png)这两个检查加起来用不了两分钟但能帮你拦住大多数“假高分”。我养成的习惯是每次模型训练完不急着看分数先看这两张图再下结论。希望这些经验能帮你在车辆贷款违约预测或类似的金融风控赛题上少走几个来回。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑