简介:消费金融业务中,车贷违约预测是一类典型的监督学习二分类问题,核心是利用用户画像、历史借贷与车辆信息评估还款风险。由于真实场景中坏样本占比极低,模型需同时应对样本不平衡与排序精度挑战,AUC 作为对类别分布不敏感且能度量区分度的评估指标,成为风控建模的首选。在工程实现上,基于特征工程构造贷款成数、月供压力等业务语义变量,并以 LightGBM、XGBoost 等树模型构建基线,结合模型融合与缺失值策略,可有效提升预测稳定性。这类方法广泛适用于汽车金融、信贷审批等场景,帮助机构在控制坏账率的同时降低优质客户误杀。本文完整复盘一场车辆贷款违约预测竞赛,覆盖赛题拆解、数据探索、特征构造、调参优化及线上踩坑实录,为同类场景提供可落地的建模路径。 我从一开始就在关注这类金融风控方向的算法赛事,车辆贷款违约预测赛题尤其典型,核心就是通过用户的基础信息、历史借贷表现、车辆相关数据,构建一个能尽量准确判断“这笔车贷未来会不会坏账”的模型。用 Python 做这类建模,整体链路其实非常清晰:从数据分析、特征工程,到模型训练、评估调参,再到输出预测结果。这篇博文会结合我当时的完整参赛经历,把整个项目从赛题拆解、数据探索、特征构造到模型落地完整复盘一遍,里面会穿插大量实际踩坑和针对金融风控场景的经验总结,希望能给正在准备同类赛事或刚接触风控建模的朋友一些真实可用的参考。
这篇内容适合几类人阅读:正在备战各类数据挖掘竞赛的学生或转行新人,想要了解汽车金融风控建模基本流程的从业者,以及所有对 Python 建模全流程感兴趣、想看看真实项目长什么样的人。看完之后,你至少能快速建立起一套自己的风控建模方法论,并且对样本不平衡、特征构造、模型融合这些高频问题有更落地的认识。
1. 赛题拆解与企业级风控思维
1.1 一次典型的二分类风控任务
先看赛题表面的目标:给定一批用户的历史信息和车辆贷款相关信息,预测用户是否会发生贷款违约。本质上,这是一个监督学习中的二分类问题,标签通常就是“是否违约”这样的 0/1 变量。但如果你只把它当成一个普通分类问题来做,忽略它背后的金融风控场景,很容易在建模过程中走偏。
我记得比赛开始后的前几天,很多人直接套了一些常规分类模型跑榜单,效果一直不理想。原因很简单:汽车贷款场景下的违约样本占比通常非常低,极端情况下可能只有 1% 到 3%。这种类别极度不平衡的数据,如果不做任何处理,模型会倾向把所有样本都预测为“好用户”,因为这样整体的准确率看起来依然很高,但业务上一旦真正上线,根本筛不出坏客户,风控等于形同虚设。
所以拿到赛题资料之后,第一步不是急着调模型,而是先把赛题背后对应的业务逻辑想清楚。车辆贷款属于典型的消费金融业务,用户分期买车,按期还款,一旦中途断供,资方就会面临损失。对于这类业务,风控模型关注的核心指标并不完全是准确率,而是能否在尽可能低的通过率损失下,识别出足够多的坏客户。换句话说,就是模型的区分度,也就是好坏客户分数分布之间拉开的差距。
这种思维决定了后面所有的技术选择。你选的评估指标会决定模型优化的方向,你做的特征工程要围绕“用户还款能力和还款意愿”这两个核心维度,你处理缺失值和异常值的方式也要小心,不能让模型把缺失直接当成一种随机噪声。
1.2 赛题数据和评估指标里的门道
紧接着我翻了赛事提供的数据说明。训练集给了不少字段,主要分为几个维度:用户基础信息(年龄、性别、学历、职业等),用户历史借贷和还款行为(比如历史逾期次数、信贷记录相关衍生指标),以及车辆相关信息(车辆价格、车辆品牌、贷款金额、贷款期限等)。
熟悉信贷风控的人看到这些字段基本能猜出数据源大概率来自某个助贷或融租公司,很多字段和征信报告上的变量高度相似。竞赛为了保密,通常会把字段名做匿名化处理,比如命名成cust_xxx、veh_xxx之类的,但字段含义基本可以通过取值分布和名称语义去还原。
赛题通常还会明确告知评估指标,这点极为关键。我当时参加的这场赛事,评估标准采用 AUC(Area Under the Curve),也就是 ROC 曲线下的面积。AUC 衡量的是模型把随机正样本排在随机负样本前面的概率,对类别不平衡相对不敏感,同时又很看重排序能力,和风控业务“把好用户和坏用户区分开”的目标高度一致。
确定指标是 AUC 之后,我整个建模策略都是围绕“排序准确性”来设计的。比如后期做阈值选择时,我不需要像做精确率召回率平衡那样专门去卡一个业务阈值;做样本权重调整时,也无需担心权重设得过高会破坏 AUC 排序。理解了这个逻辑,团队内部的沟通也会顺畅很多,遇到线上分数波动时,第一反应是检查特征分布是否偏移,而不是盲目改阈值。
2. 数据初探与业务字段里的隐藏信号
2.1 先跑通 EDA 再谈建模
拿到数据我一般不会直接进模型,而是花至少半天时间做探索性数据分析(EDA)。这个环节的作用有两个:一是验证自己对字段的猜测,二是提前发现数据里的坑。比如我见过不少比赛数据,表面上字段很多,但有一些字段缺失率达到 80% 以上,这种字段如果直接丢进模型,不仅贡献不了信息,还可能增加过拟合风险。
我当时的处理方式是先做一份字段清单表,记录每个字段的缺失率、唯一值个数、数据类型、均值标准差等基础统计量,然后针对性地看每个字段在好坏样本上的分布差异。字段不多时,直接分组对比均值就很直观;字段一旦多起来,就得画分布图或者计算信息价值(IV)来筛选。
比如用户年龄字段,我做过一次简单的分组统计,发现在坏样本里,25 岁以下和 45 岁以上的用户占比明显偏高,25 到 35 岁之间相对稳健。这个现象合理,年轻用户收入不稳定,年龄偏大的用户可能还款能力在下降。当然单变量分析只是参考,真实建模时多个字段组合起来才能产生更强的区分度,但至少它给你一个搜索方向,知道应该在哪些特征上多做组合尝试。
2.2 警惕标签泄漏和未来函数
数据初探阶段还有一个非常重要的点:检查是否存在标签泄漏,也就是某些特征的取值在建模时点根本不可能拿到。比如说,如果一个字段叫“当前逾期天数”,并且它是在贷款发放后记录的,那它在预测时就是未来变量,直接使用会让模型在训练集上表现极好,但线上完全失效。
这类坑在金融类竞赛数据里不算少见,尤其是那些看起来过于“好用”的字段,比如非常精准地反映用户当前还款状态的变量,或者某些与目标变量高度相关的征信衍生指标。遇到这种情况,我一般建议做一次“按时间切分验证”,也就是把训练集按时间顺序分成前半段和后半段,用前半段训练、后半段验证,看 AUC 有没有显著下降。如果下降非常明显,基本可以怀疑数据里有时序相关或标签泄漏问题,这时候就要进一步排查具体字段。
因为竞赛数据通常是离线切片好的,很难 100% 还原真实的放款时间线,但我还是会抱着怀疑的态度去查。宁可损失一点线上分数,也不能一次性把大量未来信息带进模型,否则就算比赛拿了好名次,这套方案放到实际业务里也完全不可用。
2.3 训练集和测试集分布一致性检查
另一个让很多人翻车的点是训练集和测试集分布不一致。数据竞赛中,测试集往往来自不同的时间窗口或不同的用户群体,如果建模时不考虑这一点,很容易出现本地验证分数很高、线上分数却掉得厉害的情况。
一个简单的检查方法是用模型区分训练集和测试集,也就是把“是训练集还是测试集”当成一个新的二分类标签,用已有特征去训练一个分类器,看分类的 AUC 有多高。如果 AUC 显著高于 0.5,说明两个数据集的分布有明显差异,这时候就要考虑是不是某些特征在时间推移中发生了变化,例如收入水平整体上升、车辆均价整体变化等。
我当时也做了这个检查,结果发现测试集中的一些用户特征分布和训练集确实存在偏移,主要体现在个别匿名特征上。处理方式是在特征工程阶段,对这些分布偏移严重的特征做特殊处理:对于名称匿名度较高、业务含义不明的字段,直接删除,避免模型学到的规律在测试集失效;对于含义明确的字段,则尝试做分箱或者标准化,降低量纲变化带来的影响。这个思路不是每次都能带来提升,但至少能防止模型在未知数据上“飘”得太离谱。
3. 特征工程实战:从原始数据到建模数据
3.1 手工特征与业务语义的组合
我一直认为,在金融风控领域,手工构造特征的价值远高于自动特征搜索工具,原因是风控数据中的业务逻辑很清晰,好的业务特征往往自带强解释性。
举例来说,贷款金额和车辆价格这两个字段单独看都有一定区分度,但它们组合出来的“首付比例”或者“贷款成数”才是风控真正关心的指标。贷款成数越高,说明用户首付越低,一旦车辆贬值,用户的还款意愿可能快速下降,因为车都亏钱了,为什么不直接弃贷呢?所以当时我构造了贷款金额 / 车辆价格这个比例特征,并且在后续特征重要性分析中,它排得非常靠前。
同样思路,我还构造了月供收入比的替代变量:虽然数据集里不一定会直接给你收入,但往往会有和收入相关的匿名评分字段,此时把贷款金额按期限分摊成月供,再除以相关的评分或收入代理变量,就能得到一个“每个月还款压力有多大”的组合特征。这个特征在业务上非常直观,也从结果上证明了对模型增益很大。
3.2 历史行为特征的聚合与统计
用户历史借贷和还款行为数据经常以“一事一行”的形式出现,也就是一个用户可能有多条历史记录。比如同一用户在不同机构借过多笔贷款,或者同一辆车在多个平台被抵押过。直接拿原始明细数据丢给模型是不现实的,因为每个用户的记录条数不一样。
正确做法是按用户做聚合统计,生成“每用户一行”的宽表特征。我常用的聚合方式包括:历史贷款笔数的计数、历史逾期次数总和、逾期月份的最大值、平均借款金额、最大借款金额、最近一次借款距离现在的天数,以及各种 ratio 类特征,比如逾期笔数占历史总笔数的比例。
聚合统计时有一个细节:不是所有聚合函数都有用,要结合业务含义去选。比如 max 类特征可以代表用户最坏的历史情况,mean 可以反映平均信用水平,而 count 则可以反映用户的借贷活跃度。把一堆 count、mean、max、min 全堆上去,特征数量会爆炸,而且在树模型中,很多相关性极高的特征会在信息增益上相互竞争,反而降低单棵树的稳定性。所以我一般会先用 IV 或者特征重要性做一轮粗筛,把明显无效或高度冗余的特征剔除。
3.3 缺失值、异常值与编码处理
金融数据集里的缺失值通常不是随机缺失。比如一个用户没有逾期记录,那和逾期天数相关的字段可能全为空;又比如部分用户征信报告查询次数异常多,某些字段可能出于合规原因被隐藏。所以在处理缺失值时,我基本不用简单的均值填充,而是倾向于把“是否缺失”本身做成一个特征,同时再用一个特殊值填充原字段。
以树模型为例,LightGBM 和 XGBoost 都原生支持缺失值处理,它们会在节点分裂时自动学习缺失值应该往哪个方向走,所以很多人干脆不填充,直接把原始数据丢进去。这确实是可行的,但我个人还是会做一层处理:对缺失率特别高的字段,直接删除;对缺失率适中且含义明确的字段,填 -999 或使用一个远低于正常范围的常数,让树模型可以捕捉到“这个用户在这个字段上是特殊人群”的信号。
异常值处理方面,信贷数据里的异常值往往集中在极少数高风险人群,比如某个字段是身份证号或者用户 ID 的哈希值,这类字段绝不能当成数值特征使用,只能当成类别特征。对于真正的连续型数值特征,如果出现极端大或极端小的值,我基本不做硬截断,因为树模型对异常值并不敏感,强行截断有时反而损失高分段的信息量。
4. 模型构建与调参优化:从基线到进阶
4.1 基线模型与快速验证框架
在特征工程还没完全成形的时候,我会先用一个简单的基线模型把整个训练和预测流程跑通,这非常关键。所谓跑通,不仅仅是代码能执行,还包括数据划分、特征对齐、结果输出等环节都不出错。
我通常用 Logistic Regression 作为基线,一方面训练速度快,另一方面可以输出概率作为排序分数,方便快速观察 AUC。逻辑回归本身对特征缩放比较敏感,所以跑基线之前需要做标准化或者归一化,但如果后续主力模型是树模型,这一步就不用太纠结。
数据划分上,竞赛场景里为了快速验证,我经常采用 5 折交叉验证,把训练集分成 5 份,轮流用 4 份训练、1 份验证,得到平均 AUC。这样做能比较稳定地评估特征变化带来的效果。交叉验证的代码模板我基本每次比赛都会复用,改一改数据路径和特征列就行,省去大量重复劳动。
4.2 树模型三件套与参数策略
进入正式建模阶段,我常用的是 XGBoost、LightGBM 和 CatBoost 三个树模型,它们对表格数据的表现非常稳定,也是各类竞赛里最主流的选择。三个模型各有特点:XGBoost 的正则化做得比较完备,对噪声数据相对稳健;LightGBM 训练速度快,内存占用少,而且对类别特征有原生支持;CatBoost 则在处理高基数类别特征和防止过拟合方面有独到优势。
团队早期的策略是三个模型各跑一版,分别调参,然后做加权融合。但这次我想强调的是,调参并不需要像网上教程那样做大规模网格搜索。网格搜索在某些场景下确实是万金油,但计算成本太高。我的做法是先固定学习率(一般用 0.02 到 0.05),然后手动调整树的数量和树的深度。
比如对 LightGBM,我习惯先设num_leaves=31、max_depth=-1、learning_rate=0.05,然后训练 3000 轮,同时开启 early stopping。这样既能看到模型在验证集上的表现曲线,又能自动确定最佳迭代次数。之后根据验证集表现逐步调整num_leaves、min_data_in_leaf、feature_fraction和bagging_fraction。这些参数看起来复杂,但实际核心就两个目的:控制模型复杂度和增加随机性,防止单棵树学得过细导致过拟合。
4.3 样本不平衡优化与自定义目标函数
关于样本不平衡,竞赛里最常见的处理方式有三类:第一类是采样,对多数类样本进行降采样,或者对少数类样本做 SMOTE 过采样;第二类是调整样本权重,让少数类样本在损失函数中占更大比重;第三类是修改模型的目标函数,比如用 Focal Loss 替代普通的二分类对数损失。
但在这里我想分享一个实际经验:对于以 AUC 为评估指标的任务,完整的样本不处理,往往也能取得不错的效果。原因在于 AUC 只看排序,类别不平衡本身并不直接影响排序质量,反而是在阈值选择或精确率召回率优化时,不平衡的影响才明显。因此我当时先跑了一版不做任何采样的模型,发现本地 AUC 已经接近 0.76,然后尝试对少数类样本做 5 倍权重放大,结果 AUC 反而有小幅下降,因为权重变化改变了树分裂时对少数类的偏置,导致整体排序受到干扰。
当然这不代表样本权重完全没用。如果你后续要做概率校准,或者需要输出一个更贴近业务含义的违约概率值,那确实需要调整先验分布。不过竞赛场景里排名就是看排序指标,所以我的结论是:先跑通建模流程,观察 AUC 变化,再决定要不要处理样本不平衡,而不是一上来就盲目采样。
4.4 模型融合的正确姿势
等到三个树模型都训练完毕,并且各自在交叉验证上都达到了相对稳定的分数,下一步就是模型融合。模型融合能提升分数的原理在于,不同模型在特征空间中的决策边界不同,彼此的误差存在互补性,融合之后可以抵消一部分单一模型的偏差。
最简单的融合方式是概率平均值,就是把三个模型预测出的违约概率做加权平均,权重可以按各自验证集的 AUC 做归一化。这种方法的实现非常简单,一般能让线上分数微涨 0.001 到 0.003。更进阶一点的是 Stacking,也就是用第一层模型的输出作为第二层模型的输入特征,让第二层模型自动学习如何组合它们。但 Stacking 在竞赛中容易出现局部过拟合,需要配合更严格的时间序列切分或者 K 折训练逻辑来避免信息泄漏。
我个人的建议是,如果你的时间和精力有限,优先做概率加权融合;如果还有余力,再尝试 Stacking,并且一定要确保第二层模型的训练数据来自第一层模型在验证集上的预测结果,而不是训练集上的预测结果。这一条我在刚开始学融合时栽过跟头,后来才彻底搞清楚为什么会过拟合。
5. 风控业务里常常踩的坑与排查实录
5.1 分数分布偏移与线上不稳定
竞赛进入后期,相信很多人都遇到过这种情况:本地交叉验证分数稳定提升,但提交到线上之后分数不升反降。遇到这种情况,不要急着怀疑模型代码有 bug,大概率是线上数据和本地验证数据的分布出现了偏差。
我遇到过一次非常典型的例子。当时我加了一个基于用户历史逾期天数的 max 类特征,本地验证 AUC 提升了 0.005,原本挺高兴,结果线上分数掉了一截。事后排查发现,这个特征在训练集中缺失率非常低,但在测试集中缺失率很高,说明我构造特征时没有充分对齐训练集和测试集的数据覆盖范围。后来我把缺失率异常高的分支处理掉,并对缺失部分单独编码,线上分数才恢复回来。
从那之后,我养成了一个习惯:每次新增特征,都必须分别统计该特征在训练集和测试集中的缺失率、均值、标准差,如果差异过大,要么删掉,要么做分箱处理来抹平差异。这个检查成本很低,但能帮你省去大量反复提交浪费的次数。
5.2 过拟合信号:本地分数和线上分数差距拉大
另一个高频问题是过拟合。树模型在特征数量达到几百甚至上千时,很容易在训练集上做到近乎完美的成绩,但验证集和线上效果却跟不上。这其实是高维特征加上模型复杂度过高共同导致的。
我初期也掉进过这个坑,当时为了提高 AUC,我把所有能想到的交叉特征都生成了一遍,特征数量从 100 个猛增到 1800 个,结果本地 5 折交叉验证的分数确实涨了 0.004,但线上提交之后不但没涨,反而下降了一点点。经验告诉我,这不是特征本身的问题,而是太多无效特征引入了噪声,让模型在训练集上过度拟合了。
解决办法有两步。第一,在特征工程完成后,跑一遍 LightGBM 的特征重要性,把重要度为 0 的特征删掉,通常能砍掉一半以上的无关特征。第二,在模型层面增加正则化参数,比如调大lambda_l2,或者调大min_data_in_leaf,限制每片叶子上最少的数据量,让模型没那么容易学到极端模式。做完这两步,线上分数基本能够恢复甚至微涨。
5.3 类别特征编码细节与脏数据
处理表格数据时,类别特征往往是容易出错的地方。竞赛数据里常见的坑包括:类别编码不连续,字段值里有隐藏的空格,以及测试集中出现训练集没见过的类别。
针对类别编码不连续的问题,我一般会先统一转成字符串再做标签编码,避免把编码当数值用。对于测试集中出现新类别的问题,树模型天然能处理,只要在训练时构造一个“未知”类别,或者直接让模型在分裂时把新类别分配到缺失值分支,都能规避明显的报错。
还有一个容易被忽视的细节:直接使用 pandas 的factorize做标签编码时,如果训练集和测试集分开运行,两边的编码映射可能会错位。正确做法是先合并训练集和测试集,统一做编码,再拆分回原训练集和测试集。这一点看似基础,但我就曾在多人协作的项目里见过因为编码不一致导致的线上分数暴跌。
5.4 特征重要性和模型解释性的结合
风控模型上线之后,除了预测准确性,还要考虑可解释性。银行、汽车金融公司等资方通常需要给监管方或客户一个说法,为什么这笔贷款被拒绝。虽然竞赛不需要提交解释文档,但过程中养成用 SHAP 分析特征的习惯,对你理解数据和模型都有帮助。
SHAP 可以给出每个样本中每个特征对预测结果的贡献方向。比如你会发现某个用户的违约概率高,主要贡献来自他历史逾期次数多、贷款成数高、且车辆价格偏低。这种分析不仅帮你验证特征是否合理,还能在竞赛方案文档里增加说服力。我一般会在模型基本稳定之后,对验证集样本跑一次 SHAP 分析,挑出 Top 20 个特征,看它们的贡献方向是否符合业务认知。
如果某个特征贡献方向完全反常识,比如贷款金额越高违约概率越低,这并不一定代表逻辑错误,可能是因为贷款金额和用户资质有相关性,高贷款金额的用户往往征信好。但这类反直觉发现需要进一步验证,必要时宁可删除这个特征,避免模型学到虚假相关性。
6. 参赛过程中的团队协作与时间管理
这类赛事通常可以组队,团队协作的效率直接决定了最终能走多远。我所在的队伍是三人组,一个负责特征工程和数据分析,一个负责模型训练和参数调优,我主要负责代码框架搭建和最终结果验证。团队分工明确之后,最需要注意的就是代码风格统一和数据管道的统一。
具体来说,我们约定所有特征处理逻辑都写在同一个feature_engineering.py文件里,每一版特征都保存一份对应的特征列表文件,方便追溯。模型训练脚本也统一封装成函数,输入训练集路径和参数配置,输出预测结果和验证分数。这样即使某个人临时有事,其他人也能快速接手他的部分。
时间安排上,比赛周期通常一个月左右。前一周重点做数据探索和特征工程,第二周到第三周重点调模型和做特征迭代,最后几天集中做模型融合、最终验证和可能的方案文档整理。千万不要一上来就花大量时间调参,越到后期才越会发现,特征工程带来的提升远大于参数微调。
另外一个小建议是坚持记录每次实验的变更。我习惯在每天结束时简单记录一下当天尝试了哪些新特征、改了哪些参数、验证分数变化是多少。这个习惯在长周期比赛中非常重要,否则过几天就会忘记上一次的分数是因为什么涨上来的,又开始重复劳动。
7. 从竞赛代码到真实风控系统的迁移思考
竞赛结束之后,如果你真的打算把这份代码用到实际项目中,有几个差距必须正视。第一是数据量级。竞赛往往只给你几十万条或者几百万条历史样本,而真实业务的样本量可能是千万级别,甚至每天还在增长。这会直接影响训练时间、内存占用以及特征落地的实时性。
第二是数据分布漂移。真实业务中,用户群体、宏观经济环境、产品政策都在变化,模型不可能永远有效,所以真实风控系统需要建立完善的监控体系,定期看特征分布和分数分布,当分布发生明显偏移时,要触发重新训练或者模型下线的流程。
第三是业务约束。真实风控系统不仅要追求区分度,还要满足通过率、坏账率、客诉率等多项业务指标,模型的分数通常还要经过人工规则、额度策略、反欺诈策略的多重叠加,不可能像竞赛里那样一个模型打天下。
不过话说回来,竞赛中锻炼的数据敏感度和建模思路,放到真实项目中依然适用。你能从一堆匿名特征里还原业务含义,能判断哪些特征可能是未来变量,能快速定位线上和线下分数差异,这些都是真金白银买不来的经验。我甚至觉得自己第一次参与真实风控模型项目时,很多思路和方法就是那段时间在竞赛里磨练出来的。
如果你也想走这条路,我的建议很直接:不要光看别人的方案,一定要自己完整跑通一遍。从数据读入、清洗、特征加工、模型训练到结果输出,每一步都亲手写一遍代码。中途遇到不懂的,优先去翻官方文档和靠谱的社区帖子。等你独立跑完一版,再回来看别人分享的高分方案,你会突然发现自己能看懂很多之前完全看不明白的操作了。
那次的参赛经历对我影响挺大,也让我更坚定地在风控算法这条路上走下去。后来我又陆续参加了几场同类赛事,慢慢沉淀出了一套适合自己的建模模板,也在真实项目里反复验证了这套方法的可靠性。如果这篇文章能帮你少走几个弯路,或者让你对车辆贷款违约预测这件事有了更完整的认识,那今天写下的这些内容就值了。
本文还有配套的精品资源,点击获取