车贷违约预测:随机森林与AdaBoost的完整建模实践 简介车贷违约预测是金融风控领域常见的二分类任务面向Python机器学习入门及进阶学习者资源基于一份含199717条记录的车辆贷款数据集覆盖从数据加载、预处理、特征值与目标值分离、数据集划分到随机森林与AdaBoost两类集成模型构建、保存与评估的完整流程。资源共2个文件压缩包7.34MB内含可运行的Python脚本与CSV训练数据输出精度、召回率、F1-score与准确率等指标实测随机森林准确率0.819、AdaBoost准确率0.822。数据集包含客户编号、信用评分、资产成本、贷款与资产比例、品牌、车厂、地区等49列字段丰富便于理解特征工程与集成学习调优。目前已有1062人学习下载适合希望快速上手车贷违约预测建模、参考完整代码并对比两类集成算法效果的开发者和竞赛爱好者。1. 车贷违约预测先定义违约再谈模型车贷违约预测在风控里是一个典型的二分类问题给定一张包含历史贷款记录、客户画像、还款行为的车贷数据集训练模型预测客户未来是否违约。随机森林和AdaBoost是这个场景里最常被拿出来对比的两套树集成方案。我见过不少团队在公开数据集上把准确率做到95%但一放到新客群上就坏账率升高原因往往不在算法而在训练数据和目标定义。这篇笔记会把从数据清洗、特征工程、随机森林、AdaBoost到阈值调优的完整链路走一遍并指出几个最容易翻车的细节。适合正在做信贷风控建模的算法工程师也适合打算用这类数据集入门集成学习的学生。2. 数据清洗与特征工程把原始表变成直接能喂给树模型的格式车贷数据集常见情况是几百个列几十万行。模型能不能学出信号很大程度上取决于特征矩阵干不干净。随机森林和AdaBoost虽然在一定程度上容忍缺失值和异常值但那不代表你不需要处理。数据清洗至少包括四个动作识别标签定义、处理缺失值、统一类别特征编码、删除目标泄漏特征。这几个动作做在前面能给你后面的调参节省大量时间。2.1 先看数据形态缺失值、类型和标签分布拿到一张car_loan.csv之后不要急着训练先跑下面的代码把数据摸一遍。import pandas as pd import numpy as np df pd.read_csv(car_loan.csv) print(shape:, df.shape) print(df.dtypes.value_counts()) target is_default print(df[target].value_counts(normalizeTrue))这段代码的作用很直接shape告诉你样本量和特征数量如果特征超过200个后面一定要做特征筛选dtypes.value_counts()能一眼看出有多少列是数值型多少列是object型object型通常需要编码value_counts(normalizeTrue)输出的是两个类别的占比这是整个项目最重要的一个数字。如果违约样本占比在5%~15%还算相对乐观。假如低于5%你要在训练时就考虑类别不平衡的影响否则后面得到的准确率再高也都是虚的。我习惯把缺失率也统计出来missing_ratio df.isnull().mean().sort_values(ascendingFalse) print(missing_ratio.head(10))看前10个缺失率高的列缺失率超过60%的列可以直接考虑丢掉除非它有极强的业务含义。缺失率不高的列后面用中位数填充即可。这里尤其注意像“月收入”这种字段往往存在大量0值和空值0值不一定代表收入为0很可能是数据缺失被填成了0需要和业务方确认。2.2 类别特征编码与缺失值填充别让对象列卡住训练随机森林和AdaBoost本身是数值型特征驱动的模型sklearn的实现并不接受字符串输入所以必须先把类别特征转换成数值。这里有个容易踩的坑直接对类别特征用pd.get_dummies()生成几百列稀疏特征会让随机森林的训练速度变慢而且在某些类别数量多的字段上会造成碎片化分裂。相比之下OrdinalEncoder对违约预测这类树模型场景更实用。from sklearn.impute import SimpleImputer from sklearn.preprocessing import OrdinalEncoder cat_cols df.select_dtypes(include[object]).columns.to_list() num_cols df.select_dtypes(include[int64, float64]).columns.to_list() num_cols [c for c in num_cols if c ! target] df[num_cols] SimpleImputer(strategymedian).fit_transform(df[num_cols]) df_clean df.copy() for col in cat_cols: df_clean[col] OrdinalEncoder( handle_unknownuse_encoded_value, unknown_value-1 ).fit_transform(df[col].to_frame())这里有两个关键细节。SimpleImputer用median而不是mean因为车贷收入、年龄这类字段往往存在长尾分布中位数对异常值更稳健而OrdinalEncoder的handle_unknown参数指定了未来新用户在训练集里没出现过的类别时编码成-1而不是报错这一点在线上推理时非常重要。你训练时用的是历史样本线上来的新客户难免会有新职业、新地区编码器必须能处理这种情况。2.3 时间特征与金额特征不要直接丢原始值进模型车贷数据集中通常有放款日期、首次还款日期以及贷款金额、车辆价格、首付比例等字段。日期字段不能直接当成整数塞给树模型比如“2024-05-17”减去“2023-05-17”得到的天数是有业务含义的但原始日期字符串本身不是特征。我一般会把日期拆成几个有意义的衍生字段。df_clean[loan_year] pd.to_datetime(df_clean[loan_date]).dt.year df_clean[loan_month] pd.to_datetime(df_clean[loan_date]).dt.month df_clean[down_payment_ratio] df_clean[down_payment] / df_clean[car_price] df_clean[monthly_payment_ratio] df_clean[monthly_payment] / df_clean[monthly_income] df_clean[loan_amount_log] np.log1p(df_clean[loan_amount])年份和月份能帮模型捕捉不同时期的审批政策差异首付比例、月供收入比是车贷风控里非常经典的风险指标对贷款金额做log变换是为了压缩极端大额贷款的数值范围。树模型对单调变换不敏感log变换的主要价值是为了后续万一做逻辑回归或神经网络时特征尺度更均匀。这些衍生特征在银行客户认购产品预测、个人信用预测这类金融建模任务里也是通用的思路。2.4 特征筛选用单棵决策树先做一次预检当特征量大时全扔给随机森林不是不能用但训练时间和内存都会成问题。我的习惯是先跑一棵很浅的决策树用它的feature_importances_做第一轮筛选。from sklearn.tree import DecisionTreeClassifier X_selected df_clean.drop([target, loan_date], axis1, errorsignore) y df_clean[target] dt DecisionTreeClassifier(max_depth5, random_state42) dt.fit(X_selected, y) feat_imp pd.Series(dt.feature_importances_, indexX_selected.columns) print(feat_imp.sort_values(ascendingFalse).head(30))为什么用单棵决策树而不是随机森林因为单棵树训练快max_depth限制在5层得到的重要性已经够筛掉大量无用特征。把top30到top50的特征保留下来给随机森林和AdaBoost既减少了噪音干扰也大幅缩短了网格搜索的时长。注意这一步只用来粗筛不要用它对不同模型的价值做过高判断因为树模型的特征重要性存在随机性换一组随机种子排名会有波动。跑完特征筛选后才真正进入建模环节。3. 随机森林模型先跑通基线再谈调参3.1 随机森林为什么在车贷违约场景上稳定随机森林属于bagging类集成算法它同时引入样本扰动和特征扰动。每一棵树从训练数据中有放回地抽样每个节点只从随机抽取的特征子集里找最优分裂最后通过投票输出类别。这种结构决定了它对高维稀疏数据、特征间存在非线性关系的数据都有较强的适应能力。车贷违约的影响因素很多比如收入与月供比、历史逾期次数、工作稳定性等它们对违约的影响往往是叠加且非线性的随机森林的随机特征子空间恰好能挖掘这类交互效应。随机森林和决策树的区别在于单棵决策树很容易在训练集上长得过深、记住噪声随机森林用多棵树投票的方式把方差摊平了所以在中小规模的车贷数据集上不容易过拟合。它不要求特征归一化这给数据管线省了很多事。另一个优势是能直接输出特征重要性方便你回头做特征迭代。3.2 训练随机森林跑通基线stratify和AUC代码先跑一个最简版本把基线结果拿到再做调参。from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import train_test_split from sklearn.metrics import roc_auc_score, classification_report X df_clean.drop(target, axis1) y df_clean[target] X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy ) rf RandomForestClassifier( n_estimators200, max_depth8, min_samples_leaf30, random_state42, n_jobs-1 ) rf.fit(X_train, y_train) y_prob rf.predict_proba(X_test)[:, 1] y_pred rf.predict(X_test) print(AUC:, roc_auc_score(y_test, y_prob)) print(classification_report(y_test, y_pred))这里有三个值得说明的参数。stratifyy意思是在切分训练测试集时按照y的类别比例进行分层抽样否则一旦随机切分导致测试集里违约占比特别低你评估出来的指标会和真实效果差很多max_depth8限定了树深度配合min_samples_leaf30让每片叶子至少要有30个样本这能抑制单棵树拟合得过细n_jobs-1则让所有CPU核并行训练跑得快一些。第一次跑完你大概率会看到AUC在0.7~0.85之间具体取决于数据质量。不要急着追求AUC上0.95先对照classification_report看违约类通常记为1的recall和precision。如果recall很低后面要么调class_weight要么下调决策阈值。3.3 三个真正值得调的参数随机森林可以调的参数很多但业务时间有限经验里最值得调的是n_estimators、max_depth和min_samples_leaf。n_estimators不是越多越好超过300以后对结果提升很有限但训练时间线性增加max_depth决定单棵树复杂度一般在6~12之间min_samples_leaf是抗过拟合最有效的旋钮。from sklearn.model_selection import GridSearchCV param_grid { n_estimators: [150, 250], max_depth: [6, 10], min_samples_leaf: [20, 50] } gs_rf GridSearchCV( RandomForestClassifier(random_state42, n_jobs-1), param_gridparam_grid, scoringroc_auc, cv5, n_jobs-1 ) gs_rf.fit(X_train, y_train) print(gs_rf.best_params_) print(gs_rf.best_score_)GridSearchCV会对每个参数组合做5折交叉验证评估指标用roc_auc而不是accuracy这在不平衡数据上更重要。为什么用交叉验证而不用单次训练测试切分因为一次切分的AUC波动很大交叉验证能告诉你模型在多种子样本下的稳定表现。跑这个过程时如果数据量很大建议先缩小参数组合否则几十万的样本跑几十个组合会等得让人心焦。调参完成后把best estimator在测试集上重新评估一遍并保存模型。3.4 用特征重要性验证模型逻辑模型训练完之后不要只留在AUC上。把随机森林的特征重要性打印出来和业务经验对比一遍能发现很多隐藏问题。import pandas as pd importance_df pd.DataFrame({ feature: X_train.columns, importance: gs_rf.best_estimator_.feature_importances_ }).sort_values(importance, ascendingFalse) print(importance_df.head(20))如果排名靠前的特征是“首付比例”“月供收入比”“历史逾期次数”那模型的逻辑基本靠谱。如果排名第一的是某个ID类字段比如“合同编号”“贷款申请流水号”那极有可能是这个字段和目标之间发生了某种意外关联应该把它从特征里去掉。这类ID列在原始数据集里很常见它们对违约预测没有泛化价值只会让树模型钻空子。4. AdaBoost模型与随机森林的差异和融合4.1 AdaBoost算法在车贷数据上适合吗AdaBoost是boosting家族的代表性算法。它逐轮训练弱学习器每一轮都会放大上一个轮被分错的样本权重让新的学习器更关注这些难样本。在车贷违约预测中违约样本往往分布在特征空间的边缘地带比如收入不稳定但贷款金额又高的群体AdaBoost通过不断聚焦这些被漏掉的样本有时能拿到比随机森林更高的AUC。但它也有明显短板对噪声样本和错误标签极度敏感。如果某个样本的特征极端但标签其实是人工录错AdaBoost会把它的权重越推越高不仅拖慢训练还会让最终模型偏离正确边界。因此在用AdaBoost前至少要做一次简单的离群值审计把那些特征和标签明显互斥的样本删掉。你可以看特征分布收入几乎为0却有极高贷款金额且正常还款的样本多半是异常点。4.2 训练AdaBoost基学习器选深度2的决策树sklearn里的AdaBoostClassifier默认基学习器是max_depth1的决策树桩但车贷数据中特征交叉很重要深度1可能欠拟合。我一般会把深度设成2或3这样既保留了boosting的串行纠错能力又不会让单个弱学习器过拟合。from sklearn.ensemble import AdaBoostClassifier from sklearn.tree import DecisionTreeClassifier ada AdaBoostClassifier( estimatorDecisionTreeClassifier( max_depth2, class_weightbalanced, random_state42 ), n_estimators100, learning_rate0.6, random_state42 ) ada.fit(X_train, y_train) y_prob_ada ada.predict_proba(X_test)[:, 1] y_pred_ada ada.predict(X_test) print(AdaBoost AUC:, roc_auc_score(y_test, y_prob_ada)) print(classification_report(y_test, y_pred_ada))这里有个需要注意的地方在较老版本的scikit-learn中基学习器参数名是base_estimator新版本改成了estimator如果你的代码报错先检查一下版本。learning_rate代表每棵新树的贡献在整体预测中被缩小的比例0.6~0.8是车贷场景下相对稳健的区间。设得太小需要更多轮树才能收敛设得太大模型容易变得激进。class_weightbalanced告诉基学习器按样本比例给类别加权这对违约率只有10%左右的数据集很有用。4.3 概率校准软投票之前别跳过随机森林和AdaBoost输出的predict_proba并不是标准概率而是树叶子节点上的经验频率或加权频率。如果两个模型的概率尺度不一致软投票会被某个概率偏高或偏低的模型带偏。我一般会在融合前做一个概率校准。from sklearn.calibration import CalibratedClassifierCV ada_calibrated CalibratedClassifierCV( estimatorada, methodisotonic, cv3 ) ada_calibrated.fit(X_train, y_train) y_prob_cal ada_calibrated.predict_proba(X_test)[:, 1] print(Calibrated AUC:, roc_auc_score(y_test, y_prob_cal))这里用isotonic方法做非参校准在样本量足够时比sigmoid更灵活。cv3表示在训练集内部分三折来生成校准用的概率避免直接用训练集校准导致过拟合。校准后AUC通常不会大幅变化但概率变得更可解释0.7就真的代表大约70%的违约可能。这个校准后的概率在审批策略中可以直接用来设定额度或利率。4.4 融合模型软投票与几何平均随机森林和AdaBoost在特征采样和样本加权机制上差异很大预测错误的样本往往并不完全重叠所以把它们集成起来经常能提高稳定性。先看两个模型在测试集上的AUC如果差距不大就做一次soft voting。from sklearn.ensemble import VotingClassifier voting VotingClassifier( estimators[(rf, gs_rf.best_estimator_), (ada, ada_calibrated)], votingsoft ) voting.fit(X_train, y_train) print(Voting AUC:, roc_auc_score(y_test, voting.predict_proba(X_test)[:, 1]))soft voting会先得到两个模型各自的违约概率然后按平均概率决定最终结果。融合时有一点值得注意如果某个模型的AUC明显高于另一个简单的等权平均反而会拉低成绩。我一般会用网格搜索给两个模型分配权重或者在验证集上比较simple average、几何平均和AUC加权平均三种方式。模型融合不是必选项但它能让你在业务上多一个可解释的容错机制。5. 避坑从数据泄漏到样本不平衡五个高频翻车点如果模型在测试集表现不错放到真实业务却失灵通常不是算法问题而是下面的工程细节出了问题。5.1 先标准化再切分带来的信息泄漏现象无论是随机森林还是AdaBoostAUC在测试集上高得不正常上线后明显下降。原因有些人习惯在pandas里先把所有特征做标准化然后才做train_test_split。标准化用的是全量数据的mean和std这意味着测试集的信息已经进入了训练环节。虽然树模型对尺度不敏感但这在后续尝试逻辑回归或神经网络时泄漏会直接拉低离线评估的可靠性给业务埋雷。解决先切分再对训练集fit再用同一个标准器transform测试集。更通用的做法是把整个流程放进sklearn Pipeline里让交叉验证替你把每一折都重算统计量。5.2 只看准确率模型把违约客户全过滤掉了现象accuracy达到93%但违约类的recall只有不到0.1。原因当违约样本只占5%时模型只要全部预测为不违约准确率就有95%。大多数分类模型默认决策阈值是0.5但在严重不平衡的数据上0.5根本不是最优阈值模型在阈值附近给出的概率大多数都小于0.5于是少数类被吞掉了。解决用AUC、recall、precision和confusion matrix一起来评估。训练时给随机森林或AdaBoost设置class_weightbalanced。更关键的是阈值后调这一点在最后一章会具体讲。不要把注意力放在模型的默认预测类别上要看概率分布和曲线。5.3 随机切分样本忽略了车贷数据的放款时间现象用随机切分得到的测试集表现很好但按放款时间点切分一下模型效果突然下降。原因车贷数据集通常按放款时间顺序累积客户在不同时间的还款行为受宏观环境、审批策略影响。如果直接在全部样本里随机抽20%做测试集同一个用户的不同分期记录可能同时出现在训练和测试中而且时间靠后的样本被提前泄露给了训练集造成时序性泄漏。解决至少做一次按时间排序的验证例如用前80%的放款月份做训练后20%做测试。如果模型在时序切分下表现依然稳定才说明它不是靠捕捉历史数据漏洞取得的高分。按时间切分后的AUC一般会比随机切分低一些这很正常。5.4 特征里混入未来信息现象特征重要性排序中最高的几个特征看起来和违约定义高度重合比如“历史还款次数”“当前逾期天数”。原因给定数据集里可能存在和目标直接相关的衍生变量。比如“近12个月实际还款比例”这个特征如果它是在贷款结束之后才统计的那建模时就等于把答案灌了进去。这类特征叫目标泄漏会让模型在离线评估中几乎满分上线后完全没有同名特征可用。解决训练前逐列检查含义把任何带有还款结果、逾期结果、催收结果语义的字段从特征中去掉只保留申请时点或放款时点之前就已知的信息。如果数据集没有字段说明只能靠业务经验来人工审核每个特征。没有业务解释的特征宁可先删掉。5.5 同一用户的多期记录没有聚合现象模型在单个样本上的预测看起来合理但按客户ID汇总时同一个人的多笔贷款预测结果互相矛盾。原因车贷数据集里可能同一个客户有多笔历史贷款记录如果直接把每一笔贷款当作独立样本模型会学到“这个人以前借过所以信用好”这类假象而且同一客户的样本被同时分进训练集和测试集造成客户层面泄漏。解决如果数据集存在客户ID字段训练前先按客户ID聚合特征比如取最近一次贷款记录或者把多笔记录聚合成“历史贷款笔数”“历史逾期次数”等统计特征。如果业务上要求对每一笔贷款单独预测也要保证同一个客户的所有记录只能出现在训练集或测试集中的一边。6. 用阈值调优和时序验证提升落地价值6.1 用P-R曲线找到风控需要的阈值很多项目到AUC就停了但如果你的任务是“预测是否违约”决策阈值才是最直接影响审批结果的东西。默认0.5的阈值意味着只有模型认为违约概率大于50%才拒绝在违约率只有10%的数据上这通常过于宽松。需要用精确率和召回率的权衡来寻找阈值from sklearn.metrics import precision_recall_curve precision, recall, thresholds precision_recall_curve(y_test, y_prob) f1_scores 2 * precision * recall / (precision recall 1e-6) best_idx f1_scores.argmax() print(最优阈值:, thresholds[best_idx]) print(最大F1:, f1_scores[best_idx])如果风控策略要求违约召回率优先可以挑一个recall不低于0.8的阈值再比较此时的precision能否被业务接受。阈值本质上是业务参数不是模型参数别把它交给GridSearchCV自动决定。调完阈值后记得在干净的测试集上重新跑一次test_status否则阈值是在测试集上调的等于又泄漏了一次。6.2 用三次切分验证模型的时效性最后一个建议是不要只做一次train_test_split而是按放款时间或订单序号切成三段前60%训练中间20%验证最后20%测试。训练时用前60%调参用中间20%选阈值最后20%模拟未来数据来测试。这样的验证曲线更接近真实上线后的表现。我第一次做车贷违约预测时就是靠随机切分拿到了AUC 0.85然后按时间切分掉到0.78才意识到自己前面一直在自欺欺人。后来养成的习惯是每次建模都必须同时跑随机切分和时序切分两组指标数字更难看的那组才是你向风控团队汇报时该用的数。希望这个习惯也能帮到你。本文还有配套的精品资源点击获取