ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

车贷违约预测实战:随机森林与AdaBoost双模型全流程解析

车贷违约预测实战:随机森林与AdaBoost双模型全流程解析 简介针对车贷违约预测这一信用风控应用这份Python实战资源提供了从数据加载到模型评估的完整基线方案适合机器学习初学者和金融数据岗位的入门者学习参考。压缩包共2个文件、整体约7.34MBcsv文件包含199717条客户贷款记录覆盖客户编号、信用评分、贷款与资产比例、账户贷款次数等49个业务字段py文件集成随机森林与AdaBoost两种模型完整实现数据清洗、特征值与目标值划分、数据集分割、模型构建、模型保存和预测评估并打印精确率、召回率、F1-score与准确率。两种模型当前准确率分别约为0.819和0.822便于对比集成策略差异代码按模块化步骤组织可直接运行或二次修改。目前已有1062人学习下载适合课程设计、算法对比实验或车贷风控建模入门。1. 车贷违约预测项目拆解随机森林与 AdaBoost 双模型从数据到上线一次跑通做车贷违约预测这个项目时我最深的感受是它不像图像分类那样有现成的预训练模型能抄作业而是要把表格数据里的每个字段都当成“线索”来处理。这个项目给了一份结构化的车贷记录数据集要求用 Python 训练模型预测客户是否违约官方指定的两个模型是随机森林Random Forest和 AdaBoost。前者通过并行训练多棵决策树投票来降低方差后者通过串行提升错分样本的权重来降低偏差两条技术路线在风控场景里各有各的胜负手。项目里最反直觉的一点是模型 AUC 冲到 0.99 不一定是好事大概率是你把“未来信息”偷偷喂给了模型。这篇笔记我会从数据处理讲到双模型训练、参数调优、评估对比最后落到概率校准和阈值调整适合正在学机器学习但没碰过真实信贷数据的从业者。2. 数据清洗与特征工程拿到车贷数据集先做这三件事2.1 数据清洗缺失值、重复样本和异常值的处理顺序车贷数据集最常见的格式是每行一个客户列包含年龄、月收入、工作年限、贷款金额、首付比例、贷款期限、历史逾期次数、征信查询次数等字段最后一列是是否违约的标签。这里有一个细节模型训练前必须先把数据拆成训练集和测试集再做缺失值处理顺序反了就会出现数据泄漏。我一般会先用 pandas 快速摸一遍数据底细代码大概是这样的。import pandas as pd df pd.read_csv(car_loan_data.csv) # 先看每列的缺失比例和数据类型 missing_ratio df.isnull().mean().sort_values(ascendingFalse) print(缺失比例 TOP10:\n, missing_ratio[missing_ratio 0]) # 重复样本检查按客户ID去重 df df.drop_duplicates(subset[customer_id]) # 连续型特征的异常值月收入为负数或超过 100 万直接按缺失处理 df.loc[df[monthly_income] 0, monthly_income] None df.loc[df[monthly_income] 1_000_000, monthly_income] None代码逻辑是先把每一列的缺失率打出来确认哪些字段需要重点处理。drop_duplicates按客户ID去重防止同一客户重复出现在训练集里导致模型记住特定样本。月收入为负或极端值属于录入错误置为 None 之后交给后面的填充策略。这里有个经验不要一开始就对所有缺失列统一填中位数要先看缺失率超过 50% 的列直接丢弃更省事比如通讯地址、配偶姓名这类字段对违约预测几乎没有区分度。2.2 特征工程连续变量分箱与类别变量编码车贷数据的特征是典型的混合类型年龄、收入、贷款金额是连续值职业类型、居住城市、婚姻状态是类别值。直接丢给随机森林和 AdaBoost树模型能处理数值但类别特征必须编码。我一般用的是“连续变量分箱 类别变量 label 编码”这样既保留树模型的非线性能力又避免 one-hot 造成维度爆炸。from sklearn.preprocessing import LabelEncoder # 连续变量分箱年龄、工作年限、贷款金额 df[age_bin] pd.cut(df[age], bins[18, 25, 35, 45, 60], labels[0, 1, 2, 3]) df[work_year_bin] pd.cut(df[work_year], bins[0, 1, 3, 5, 10, 40], labels[0, 1, 2, 3, 4]) df[loan_amount_bin] pd.cut(df[loan_amount], bins[0, 50000, 100000, 200000, 500000], labels[0, 1, 2, 3]) # 类别变量用 LabelEncoder 统一编码 cat_cols [job_type, city, marital_status, education_level] for col in cat_cols: df[col] LabelEncoder().fit_transform(df[col].astype(str)) # 删除原始连续变量避免重复特征 df df.drop(columns[age, work_year, loan_amount])这里的逻辑是树模型本身对连续值有切分能力分箱的意义在于减少异常值对树切分点的干扰也能让模型更容易捕捉“年龄段”这类业务含义。LabelEncoder给类别赋的整数值本身没有大小含义但树模型做的是基于值的二分切分所以不影响效果。如果你用的是逻辑回归这里就必须换成 one-hot 或者目标编码了。2.3 训练集与测试集划分风控场景必须按时间切分很多初学者在划分数据集时习惯用train_test_split(random_state42)随机打乱但信贷数据的标签跟时间强相关——某个时间段的宏观政策、利率变动会导致违约率整体波动。正确做法是按申请日期排序用前 80% 的时间段做训练后 20% 做测试。from sklearn.model_selection import train_test_split # 按申请日期排序时间靠前的作为训练集 df df.sort_values(apply_date).reset_index(dropTrue) cut_idx int(len(df) * 0.8) train_df df.iloc[:cut_idx].copy() test_df df.iloc[cut_idx:].copy() # 从特征列中排除标签与ID、日期字段 feature_cols [c for c in train_df.columns if c not in [is_default, customer_id, apply_date]] X_train train_df[feature_cols] y_train train_df[is_default] X_test test_df[feature_cols] y_test test_df[is_default] print(f训练集样本数: {len(X_train)}, 测试集样本数: {len(X_test)}) print(f训练集违约率: {y_train.mean():.4f}, 测试集违约率: {y_test.mean():.4f})这里的关键在于违约率在训练集和测试集之间的差异本身就是重要信号。如果测试集违约率明显高于训练集说明该时间段的风控政策在收紧模型上线后要定期监控分布漂移。按时间切分还能避免一个隐形问题——同一客户多笔贷款如果被随机分到两边测试集就失去了“验证未知客户”的意义。3. 随机森林模型实现从决策树投票到参数调优3.1 从决策树到随机森林为什么风控场景首选它随机森林的本质是 bagging 加随机特征选择。它训练多棵决策树每棵树用有放回抽样得到的子集训练每次切分时只随机挑选一部分特征进行最优选择。这样做的结果是单棵决策树可能过拟合但多棵树投票后方差被大幅拉低。在车贷违约这种标签不平衡、特征噪声不小的场景里随机森林的鲁棒性比单棵决策树和线性模型都强。它还有一个额外优势——能输出特征重要性风控审核时可以解释“为什么拒绝这个客户”。3.2 训练与调参n_estimators 先放大再收max_depth 控制过拟合先用默认参数跑一遍基线再做网格搜索。这里我给出一套在车贷数据集上表现稳定的参数范围你可以根据自己的数据量调整。from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import GridSearchCV from sklearn.metrics import roc_auc_score, classification_report # 基线模型n_estimators200 是经验值过大收益递减 rf_base RandomForestClassifier( n_estimators200, max_depth10, min_samples_leaf20, class_weightbalanced, random_state42, n_jobs-1 ) rf_base.fit(X_train, y_train) y_pred_proba rf_base.predict_proba(X_test)[:, 1] print(f基线随机森林 AUC: {roc_auc_score(y_test, y_pred_proba):.4f}) # 网格调参重点调 max_depth 和 min_samples_leaf param_grid { max_depth: [6, 8, 10, 12], min_samples_leaf: [10, 20, 40], n_estimators: [200, 400] } grid GridSearchCV( RandomForestClassifier(class_weightbalanced, random_state42, n_jobs-1), param_grid, scoringroc_auc, cv3, verbose1 ) grid.fit(X_train, y_train) print(f最优参数: {grid.best_params_}) print(f调参后 AUC: {grid.best_score_:.4f})代码逻辑分两层第一层是快速验证数据是否可用如果基线 AUC 低于 0.7问题多半在特征工程而不在模型先去查特征泄漏。第二层用网格搜索精调max_depth限制树的深度防止过拟合min_samples_leaf保证叶子节点至少有 20 个样本避免模型学到个例。class_weightbalanced是处理类别不平衡的关键参数它让每一类样本在损失函数中的权重与其频率成反比。cv3在风控场景下要注意如果数据量不大3 折交叉验证可能让每一折的违约样本数过少导致验证指标波动很大。我建议换成分层采样交叉验证StratifiedKFold保证每一折的违约比例与全量一致。3.3 特征重要性找出违约的最大贡献因子随机森林训练完成后一定要看特征重要性排序。这个步骤既是为了验证特征工程合理性也是为了给业务解释。比如“历史逾期次数”如果排在首位那模型学到的规律就符合常识如果“客户ID”排在第一那一定是泄漏了。import matplotlib.pyplot as plt feature_importance pd.Series( grid.best_estimator_.feature_importances_, indexfeature_cols ).sort_values(ascendingFalse) # 只看前15个特征 feature_importance.head(15).plot(kindbarh, figsize(10, 8)) plt.title(Random Forest Feature Importance) plt.tight_layout() plt.show() # 打印前10个特征及对应重要性分数 print(feature_importance.head(10))在车贷数据集上我见过的合理特征重要性排序是历史逾期次数、月收入、负债率、贷款金额、征信查询次数。如果模型给出的排序与此差异很大比如“居住城市”重要性出奇地高就要警惕类别编码是否引入了排序信息。另外特征重要性只反映对切分增益的贡献不反映正负方向如果要解释“收入越高违约率越低”还需要配合 SHAP 值验证。4. AdaBoost 模型实现串行提升与学习率调参4.1 boosting 与 bagging 的本质区别AdaBoost 如何咬住难样本AdaBoostAdaptive Boosting是 boosting 家族的代表算法。它的训练逻辑是第一棵树用均匀权重训练预测完成后把分错样本的权重提高分对样本的权重降低下一棵树重点学习上一轮的“错题集”最后把所有树的加权结果加起来作为最终预测。和随机森林的并行投票不同AdaBoost 是串行的因此对噪声样本非常敏感——如果数据集里存在标签错误AdaBoost 会花大力气去拟合这些错误样本。在车贷数据里人工标注的违约标签偶尔会有错漏所以用 AdaBoost 前要尽量确认标签质量。4.2 训练与调参learning_rate 和 n_estimators 的组合是胜负手AdaBoost 最核心的两个参数是learning_rate学习率和n_estimators弱学习器数量。学习率控制每棵树对最终结果的贡献权重学习率越低需要的树越多但泛化性通常更好学习率过高则容易过拟合训练集中的噪声。from sklearn.ensemble import AdaBoostClassifier from sklearn.tree import DecisionTreeClassifier # AdaBoost 默认用决策树桩max_depth1这里加深到3以增强单棵树能力 ada_base AdaBoostClassifier( estimatorDecisionTreeClassifier(max_depth3, class_weightbalanced), n_estimators200, learning_rate0.5, random_state42 ) ada_base.fit(X_train, y_train) y_pred_ada ada_base.predict_proba(X_test)[:, 1] print(fAdaBoost 基线 AUC: {roc_auc_score(y_test, y_pred_ada):.4f}) # 学习率与树数量的联合搜索 param_grid_ada { learning_rate: [0.1, 0.3, 0.5, 0.8], n_estimators: [50, 100, 200, 300] } grid_ada GridSearchCV( AdaBoostClassifier( estimatorDecisionTreeClassifier(max_depth3, class_weightbalanced), random_state42 ), param_grid_ada, scoringroc_auc, cv3 ) grid_ada.fit(X_train, y_train) print(fAdaBoost 最优参数: {grid_ada.best_params_}) print(fAdaBoost 调参后 AUC: {grid_ada.best_score_:.4f})注意这里我用了estimatorDecisionTreeClassifier(max_depth3)而不是默认的深度 1 决策树桩。因为车贷特征间的交互关系较复杂深度 3 的树能捕捉到“收入低但负债率也低”这类组合条件。class_weightbalanced放在基学习器里同样有效AdaBoost 的样本权重更新机制会与类别权重叠加。一个重要的调参经验learning_rate从 0.3 起步n_estimators从 100 起步。如果最终 AUC 在测试集上比随机森林低 2 个百分点以内不用急着换模型先确认两个模型的输入特征是否完全一致。有时只是随机森林对类别不平衡的容忍度比 AdaBoost 更高AdaBoost 更适合在做过 SMOTE 过采样之后使用。4.3 随机森林与 AdaBoost 对比哪个更适合作车贷模型的主模型维度随机森林AdaBoost训练方式并行训练多棵树投票取多数串行训练每轮提升错分样本权重对噪声敏感度较低单棵树过拟合会被投票稀释较高噪声样本会被反复放大主要风险高维稀疏特征下容易学到无关模式标签错误时严重过拟合调参重点max_depth、min_samples_leaf、n_estimatorslearning_rate、n_estimators、基学习器深度类别不平衡处理class_weight 或样本加权需配合采样或权重调整特征重要性解释基于基尼增益稳定且直观基于权重衰减不太稳定我的判断标准是如果训练数据的标签是通过自动化规则生成的比如“逾期超过30天”就被标记为违约错误率低可以用 AdaBoost 冲一冲上限如果标签来自人工审核难免有主观误判就用随机森林作为主模型AdaBoost 做交叉验证对照。两者在车贷数据集上的 AUC 通常差距在 1 到 3 个百分点以内真正拉开差距的是后处理环节。5. 车贷违约预测的五个常见坑从特征泄漏到类别不平衡5.1 特征泄漏AUC 冲到 0.99 先别高兴先查特征里有没有“未来信息”现象模型训练完成测试集 AUC 达到 0.98精确率 0.97效果好得不像真实信贷数据。原因特征列里混入了与标签直接相关的信息。最常见的两类一是把“当前是否逾期”这种与违约定义重叠的字段当作特征二是把“违约金金额”或“催收次数”放进了特征列表这些字段本质上是在违约发生之后才会产生的。解决构建特征列时先列黑名单凡是“在预测时点之后才能获得”的字段一律排除。检查手段很简单——逐个特征做单变量 AUC如果某个特征的独立 AUC 超过 0.9九成是泄漏。从那以后我每次建模前都会强制跑一遍单变量 AUC 检查最高不超过 0.8 才放心进入下一步。5.2 类别不平衡模型“永远不违约”准确率却高达 90%现象分类报告里违约类的召回率是 0.0但整体准确率 0.9 以上模型把每个客户都预测为不违约。原因车贷数据集中违约样本通常只占 5% 到 10%。决策树分裂时哪怕一个叶子节点被少数类样本污染基尼系数的增益也不足以抵消分裂代价所以模型学会“一刀切”地预测多数类。解决优先用class_weightbalanced而不是 SMOTE。SMOTE 会生成过采样样本但在信贷数据上合成的违约样本可能不符合真实分布比如收入与负债的组合在现实中不存在反而增加假阳性率。class_weight不做样本生成只在损失函数层面调整权重模型学到的仍是真实样本的分布。如果class_weight效果不够再考虑RandomUnderSampler对多数类做欠采样。5.3 归一化泄漏先 fit 全量数据再切分测试集信息被偷看了现象模型在训练集和测试集上的表现差异很小但上线后表现大幅下滑。原因做特征缩放时有些人会先对全量数据fit_transform再切分这样测试集的均值和方差已经参与计算等于提前让模型“看到”了测试集的分布。树模型本身不需要归一化但如果你的特征工程里加入了距离类特征或把特征喂给 AdaBoost 之外的模型这个问题就会出现。解决严格遵循“先切分后 fit 训练集transform 训练集和测试集”的顺序。用Pipeline把特征处理和模型训练串起来交叉验证时就不会写错顺序。在车贷项目里我会用sklearn.pipeline.Pipeline把分箱、编码、模型三步打包彻底杜绝这类低级错误。5.4 超参数搜索时用错了评估指标AUC 高不代表赚钱现象网格搜索选了最优 AUC 参数组合实际业务验证时好客户被拒的比例过高坏账成本并没降下来。原因风控场景的收益结构不对称——拒绝一个好客户的损失是利息收入放给一个坏客户的损失是本金两者差距可能是几十倍。AUC 只看排序能力不看阈值位置。解决评估指标应该用“业务成本函数”而不是单看 AUC。比如给好客户定义误杀成本为 100 元坏客户误放成本为 10000 元写一个自定义 scorer 传给GridSearchCV的scoring参数。这样搜索出来的参数组合才是业务最优解。5.5 AdaBoost 过拟合训练集 AUC 0.99测试集 0.78现象AdaBoost 在训练集上表现完美测试集上却比随机森林差尤其当n_estimators设置到 500 以上时。原因AdaBoost 的权重更新机制会不断放大难样本的权重当数据集中存在噪声样本时后期的树几乎等于在拟合这几个噪声点而n_estimators越大放大效应越严重。解决把learning_rate降到 0.1同时用早停法控制树的数量。sklearn 的AdaBoostClassifier没有内置早停但你可以手动在GridSearchCV中把n_estimators搜索范围设为 50 到 200超过这个范围基本都是过拟合区间。训练完成后对比训练集和测试集的 AUC差值超过 0.15 就要考虑降低树的复杂度或者改用随机森林。6. 模型上线前的最后一步概率校准与阈值寻优在随机森林和 AdaBoost 都训练完之后大多数教程会停在打印分类报告这一步但真实上线场景里还差两个动作概率校准和阈值寻优。predict_proba输出的概率值不等于真实违约率树模型给出的概率存在系统性偏差——比如它输出 0.6实际违约率可能是 0.4 或 0.8。这个偏差来源于树的叶子节点内样本比例当每棵树的深度和权重不同时输出的概率分布会偏离真实分布。解决办法是用CalibratedClassifierCV做 Platt Scaling 或 Isotonic Regression。from sklearn.calibration import CalibratedClassifierCV # 对随机森林做概率校准使用 Isotonic 方法 rf_calibrated CalibratedClassifierCV( grid.best_estimator_, methodisotonic, cv3 ) rf_calibrated.fit(X_train, y_train) # 校准后输出概率更接近真实违约率可以直接阈值寻优 y_pred_calib rf_calibrated.predict_proba(X_test)[:, 1] # 在 0.3 到 0.7 之间遍历阈值以业务成本最小为目标选择阈值 best_threshold 0.5 best_cost float(inf) for threshold in [x / 100 for x in range(30, 71)]: y_pred_bin (y_pred_calib threshold).astype(int) cost ( ((y_pred_bin 1) (y_test 0)).sum() * 100 # 误杀好客户成本 ((y_pred_bin 0) (y_test 1)).sum() * 10000 # 漏放坏客户成本 ) if cost best_cost: best_cost cost best_threshold threshold print(f最优业务阈值: {best_threshold}, 最小业务成本: {best_cost})这段代码做的事是先对模型输出概率做校准再用业务成本函数遍历阈值。methodisotonic是非参数校准方法在小样本上表现好但要求测试样本量大如果数据量小用methodsigmoid更稳。cv3保证校准过程不会过拟合。最后提醒一个上线后的习惯概率校准模型需要在生产环境定期重训因为客户群体变化会导致概率分布漂移。从那以后我每次在风控模型上线前都会强制走一遍“训练 - 概率校准 - 阈值寻优 - 按新阈值重新评估成本”的完整流程哪怕只是改了一个特征也照跑不误。希望这篇拆解笔记对你有用把这套流程在你的车贷违约数据上跑一遍再回来对照参数看差别会比只看不练高效得多。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进