资讯详情

基于随机森林算法的贷款违约预测模型研究:从特征工程到模型评估全链路

📅 2026/10/10 12:24:45 | 华诺云谱 👁 阅读
基于随机森林算法的贷款违约预测模型研究:从特征工程到模型评估全链路
简介这份资源是围绕随机森林算法构建贷款违约预测模型的高分项目源码包评审得分98分适合计算机相关专业学生用于课程设计、期末大作业或项目实战练习也可作为机器学习入门者理解集成学习落地流程的参考案例。压缩包共12个文件约5.8MB以csv数据文件、ini配置文件和py脚本为主另含xls表格与项目配置项覆盖数据读取、特征处理、模型训练与基准对比等环节目录结构清晰便于按模块查阅与复现。项目以信用评分数据为基础通过随机森林完成违约风险分类并保留基准结果文件方便对照分析模型表现与调参思路。已有74人学习关注适合希望快速掌握随机森林建模流程、积累完整项目经验的读者参考借鉴。1. 贷款违约预测为什么随机森林成了风控建模的默认起点信贷业务里有一个很朴素的诉求在放款之前判断这笔钱大概率能不能收回来。过去靠人工审批和评分卡现在数据维度多了行为数据、征信衍生变量、多头借贷指标一股脑进来逻辑回归的线性假设就有点撑不住了。随机森林算法在这个场景里几乎是默认起点——它能吃非线性关系、对缺失值和异常值不敏感、不用做特征缩放还能直接吐出特征重要性给风控策略同学一个可解释的抓手。这篇笔记就围绕「基于随机森林算法的贷款违约预测模型研究」这个方向把从数据到模型到评估的完整链路拆开讲清楚。适合正在做信贷风控建模的算法同学、想复现一个完整二分类项目的学生以及需要快速搭一个基线模型再迭代的工程师。我不会只讲概念重点放在参数怎么设、坑在哪、结果怎么验证。2. 数据准备与特征工程把原始借贷表变成模型能吃的矩阵2.1 先搞清楚违约标签怎么定义做贷款违约预测第一步不是调模型而是把「违约」这个标签定义清楚。常见做法是看账龄表现期比如放款后 6 个月或 12 个月内是否出现逾期超过 30 天、60 天或 90 天。不同定义直接决定正负样本比例和模型难度。我一般会先确认三件事表现期窗口多长、逾期天数阈值多少、观察点怎么切。如果标签定义模糊后面所有指标都是空中楼阁。假设我们有一张模拟的借贷流水表字段包括用户 ID、放款金额、利率、期限、历史逾期次数、负债收入比、查询次数等。标签列default_flag为 1 表示违约0 表示正常。下面这段代码演示如何从原始表构造建模样本并处理类别不平衡。import pandas as pd import numpy as np from sklearn.model_selection import train_test_split # 读取模拟借贷数据 df pd.read_csv(loan_data.csv) # 标签定义逾期超过30天记为违约 df[default_flag] (df[overdue_days] 30).astype(int) # 剔除表现期不足的样本避免标签不确定 df df[df[observation_days] 180].copy() # 处理缺失值数值型用中位数填充类别型单独处理 num_cols [loan_amount, interest_rate, debt_ratio, query_count] for col in num_cols: df[col] df[col].fillna(df[col].median()) # 类别不平衡检查 print(违约样本占比, df[default_flag].mean()) # 划分训练集和测试集stratify保证标签分布一致 X df.drop(columns[user_id, default_flag, overdue_days]) y df[default_flag] X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, random_state42, stratifyy )这段代码的关键点有三个。第一observation_days 180是为了保证每个样本都有足够的表现期否则标签可能是错的。第二缺失值用中位数填充是随机森林场景下的稳妥选择因为它不依赖距离度量。第三stratifyy在违约样本占比很低时尤其重要否则训练集里可能没几个正样本。参数test_size0.3可以根据数据量调整数据量小于一万条时建议用 0.2 到 0.25。2.2 特征工程里最容易被忽略的衍生变量原始字段往往不够用风控建模里真正拉开差距的是衍生特征。常见的有几类额度使用率、近 3 个月查询次数、历史最大逾期天数、收入负债比、账龄等。这些特征背后是业务逻辑不是随便加减乘除。比如「近 3 个月查询次数」反映的是借款人是否在多头借贷这个指标在违约预测里通常排在前几位。# 衍生特征构造 df[credit_utilization] df[used_amount] / (df[total_limit] 1) df[query_3m_ratio] df[query_count_3m] / (df[query_count] 1) df[income_debt_ratio] df[monthly_income] / (df[monthly_debt] 1) df[age_bucket] pd.cut(df[age], bins[0, 25, 35, 45, 55, 100], labelsFalse) # 对偏态严重的金额类特征做对数变换 for col in [loan_amount, monthly_income]: df[col] np.log1p(df[col])这里1是为了避免除零np.log1p处理右偏分布。age_bucket把连续年龄分箱是因为年龄和违约的关系往往不是线性的分箱后树模型更容易切分。注意分箱边界要根据业务经验或等频分箱确定不要拍脑袋。2.3 特征筛选别把噪音喂给模型特征不是越多越好。我见过有人把几百个字段全塞进去结果模型在测试集上 AUC 反而掉了。随机森林虽然能处理冗余特征但太多无关变量会稀释真正重要的特征还会拖慢训练速度。常见做法是先用方差过滤去掉常量列再用随机森林自带的特征重要性做一轮初筛。from sklearn.ensemble import RandomForestClassifier # 先跑一个初步模型看特征重要性 rf_temp RandomForestClassifier(n_estimators100, random_state42, n_jobs-1) rf_temp.fit(X_train, y_train) # 输出重要性排序 importances pd.Series(rf_temp.feature_importances_, indexX_train.columns) importances importances.sort_values(ascendingFalse) print(importances.head(15)) # 剔除重要性低于阈值的特征 low_importance_cols importances[importances 0.005].index.tolist() X_train_sel X_train.drop(columnslow_importance_cols) X_test_sel X_test.drop(columnslow_importance_cols)阈值 0.005 不是固定的要看特征总数和重要性分布。如果所有特征重要性都差不多说明特征区分度不够得回去检查特征构造逻辑。这一步的产出是一个精简后的特征矩阵后面建模都用它。3. 随机森林建模参数怎么调、模型怎么训3.1 为什么选随机森林而不是 XGBoost 或逻辑回归这个问题我被问过很多次。逻辑回归可解释性最强但需要手动做 WOE 分箱和变量筛选非线性关系捕捉能力弱。XGBoost 和 LightGBM 在 Kaggle 上表现更好但参数多、调参成本高而且对缺失值和异常值更敏感。随机森林的优势在于开箱即用、对数据质量容忍度高、不容易过拟合、特征重要性直观。对于刚起步的风控团队或者需要快速出基线的场景随机森林是性价比最高的选择。当然如果数据量超过百万级、追求极致 AUC后面还是要上梯度提升树。3.2 核心参数逐个拆n_estimators、max_depth、min_samples_leaf随机森林的参数不多但每个都影响很大。我一般按这个顺序调参数作用常用范围调参建议n_estimators树的数量100-500先设 200观察验证集曲线是否平稳max_depth树的最大深度5-20违约预测建议 8-12太深容易过拟合min_samples_leaf叶子节点最小样本数5-50正样本少时设大一点比如 20max_features每次分裂考虑的特征数sqrt/log2默认 sqrt 通常够用class_weight类别权重balanced违约样本少于 10% 时必设下面是一个完整的训练代码包含交叉验证和参数搜索。from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import GridSearchCV, StratifiedKFold from sklearn.metrics import roc_auc_score, classification_report # 基础模型 rf RandomForestClassifier( n_estimators200, max_depth10, min_samples_leaf20, max_featuressqrt, class_weightbalanced, random_state42, n_jobs-1 ) # 交叉验证评估 cv StratifiedKFold(n_splits5, shuffleTrue, random_state42) param_grid { n_estimators: [100, 200, 300], max_depth: [8, 10, 12], min_samples_leaf: [10, 20, 30] } grid GridSearchCV(rf, param_grid, cvcv, scoringroc_auc, n_jobs-1, verbose1) grid.fit(X_train_sel, y_train) print(最佳参数, grid.best_params_) print(最佳AUC, grid.best_score_) # 用最佳模型预测 best_rf grid.best_estimator_ y_pred_proba best_rf.predict_proba(X_test_sel)[:, 1] print(测试集AUC, roc_auc_score(y_test, y_pred_proba))class_weightbalanced会自动按类别频率反比加权这在违约样本占比 5% 到 15% 时效果明显。max_depth10是我在多个模拟项目里比较稳的起点再深就容易记住训练集噪音。min_samples_leaf20保证每个叶子节点有足够样本避免模型对个别样本过拟合。GridSearchCV的scoringroc_auc是因为违约预测更关心排序能力而不是绝对概率准确度。3.3 训练完先别急着上线看这三个诊断指标模型训完AUC 只是第一关。我一般还会看三个东西KS 值、PSI 和特征重要性稳定性。KS 值衡量的是模型区分正负样本的最大能力风控里通常要求 KS 大于 0.3。PSI 用来监控训练集和测试集分布是否一致大于 0.1 就要警惕。特征重要性排序如果和业务经验严重不符比如「用户 ID 尾号」排进前十那说明数据泄露了。from scipy.stats import ks_2samp # KS值计算 def calc_ks(y_true, y_prob): df_ks pd.DataFrame({y: y_true, prob: y_prob}) df_ks df_ks.sort_values(prob, ascendingFalse) df_ks[cum_bad] df_ks[y].cumsum() / df_ks[y].sum() df_ks[cum_good] (1 - df_ks[y]).cumsum() / (1 - df_ks[y]).sum() return np.max(np.abs(df_ks[cum_bad] - df_ks[cum_good])) print(KS值, calc_ks(y_test, y_pred_proba)) # PSI计算 def calc_psi(expected, actual, bins10): breakpoints np.percentile(expected, np.linspace(0, 100, bins 1)) expected_perc np.histogram(expected, breakpoints)[0] / len(expected) actual_perc np.histogram(actual, breakpoints)[0] / len(actual) return np.sum((expected_perc - actual_perc) * np.log((expected_perc 1e-6) / (actual_perc 1e-6))) print(PSI, calc_psi(y_pred_proba, best_rf.predict_proba(X_train_sel)[:, 1]))KS 值低于 0.25 时先别调模型回去检查特征和标签定义。PSI 超过 0.1 说明测试集分布和训练集差异大可能是时间切分没做好。这两个指标比 AUC 更贴近业务实际。4. 避坑与排查建模路上最容易翻车的五个地方4.1 标签泄露AUC 高得离谱上线就崩现象训练集 AUC 0.99测试集 AUC 0.98但上线后违约识别率惨不忍睹。原因特征里混入了标签衍生变量比如「是否已催收」「当前逾期状态」这类放款后才知道的字段。解决建模前逐列过一遍凡是放款后产生的字段一律剔除。我一般会维护一个「可用特征白名单」只从白名单里取数。4.2 时间切分没做随机切分骗了自己现象随机划分训练测试集AUC 很漂亮但按时间切分后掉 10 个点。原因随机切分让未来数据泄露到了训练集而真实场景是用过去预测未来。解决按时间字段切分比如用前 8 个月训练后 2 个月测试。如果数据量够做滚动时间窗口验证。4.3 类别不平衡处理过头模型变成「全预测正常」现象用了 SMOTE 过采样后模型在测试集上把大部分样本都预测为违约。原因过采样比例没控制好或者采样只在训练集做但没配合class_weight。解决优先用class_weightbalanced实在需要过采样就控制在正负比 1:3 以内并且只在训练折内做不能提前对全量数据采样。4.4 特征重要性当因果用策略跑偏现象模型说「查询次数」最重要业务方直接把查询次数多的人全拒了结果误杀大量优质客户。原因特征重要性只反映相关性不反映因果。解决重要性排序用来做特征筛选和模型解释但策略制定要结合业务逻辑和分层分析不能一刀切。4.5 模型文件太大部署时内存爆了现象n_estimators500、max_depth20训出来的模型几百 MB线上加载慢、预测延迟高。原因树太多太深。解决风控场景n_estimators控制在 200 以内max_depth不超过 12用joblib压缩保存。如果还大考虑用 LightGBM 替代。import joblib # 压缩保存模型 joblib.dump(best_rf, rf_model.pkl, compress3) # 加载 loaded_model joblib.load(rf_model.pkl)compress3能显著减小文件体积加载速度影响不大。上线前记得在目标环境做一次预测延迟测试。5. 模型评估与上线从 AUC 到实际业务指标5.1 别只看 AUC看分层捕获率AUC 是全局排序指标但业务关心的是「抓出多少坏人」。我一般会看 top 10% 分数段里违约样本的捕获率。比如按预测概率从高到低排序取前 10% 的样本看里面包含多少真实违约客户。这个指标直接对应审批策略里的拒绝比例。def capture_rate(y_true, y_prob, top_ratio0.1): df_eval pd.DataFrame({y: y_true, prob: y_prob}) df_eval df_eval.sort_values(prob, ascendingFalse) n_top int(len(df_eval) * top_ratio) top_samples df_eval.head(n_top) return top_samples[y].sum() / df_eval[y].sum() print(Top 10%捕获率, capture_rate(y_test, y_pred_proba)) print(Top 20%捕获率, capture_rate(y_test, y_pred_proba, 0.2))如果 top 10% 捕获率低于 30%说明模型排序能力不够需要回去加特征或换模型。这个指标比 AUC 更能说服业务方。5.2 概率校准让预测值接近真实违约率随机森林输出的概率往往偏保守predict_proba给出的 0.3 不一定代表真实违约率 30%。如果业务方要用概率做定价或额度决策就需要做校准。常见方法有 Platt 校准和 Isotonic 回归。from sklearn.calibration import CalibratedClassifierCV # 用Isotonic回归校准 calibrated_rf CalibratedClassifierCV(best_rf, methodisotonic, cv5) calibrated_rf.fit(X_train_sel, y_train) y_calibrated calibrated_rf.predict_proba(X_test_sel)[:, 1] print(校准后AUC, roc_auc_score(y_test, y_calibrated))校准后 AUC 可能略降但概率更可靠。如果只是做排序决策可以跳过这一步如果要输出违约概率给定价系统校准是必须的。5.3 上线后的监控PSI 和特征漂移模型上线不是终点。我一般会每周算一次线上样本和训练样本的 PSI每月看一次特征分布漂移。PSI 超过 0.1 就触发预警超过 0.25 就要考虑重新训练。特征漂移可以用 KS 检验逐列监控。def monitor_drift(train_df, online_df, features): drift_report {} for col in features: psi calc_psi(train_df[col].values, online_df[col].values) drift_report[col] psi return pd.Series(drift_report).sort_values(ascendingFalse) # 假设online_df是线上采样数据 # drift monitor_drift(X_train_sel, online_df, X_train_sel.columns) # print(drift.head(10))监控频率取决于业务变化速度。信贷场景建议至少每月一次促销活动期间加密到每周。6. 一个能直接复用的调参技巧用 OOB 分数替代交叉验证随机森林有一个别人不太常用的特性袋外样本Out-of-Bag分数。每棵树训练时约 36.8% 的样本没被抽到这些样本可以用来做验证相当于自带交叉验证。用 OOB 分数调参比 GridSearchCV 快得多尤其适合快速迭代。# 开启OOB评分 rf_oob RandomForestClassifier( n_estimators300, max_depth10, min_samples_leaf20, class_weightbalanced, oob_scoreTrue, random_state42, n_jobs-1 ) rf_oob.fit(X_train_sel, y_train) print(OOB分数, rf_oob.oob_score_) # 用OOB分数快速比较不同参数 for depth in [6, 8, 10, 12, 15]: rf_tmp RandomForestClassifier( n_estimators200, max_depthdepth, min_samples_leaf20, class_weightbalanced, oob_scoreTrue, random_state42, n_jobs-1 ) rf_tmp.fit(X_train_sel, y_train) print(fdepth{depth}, OOB{rf_tmp.oob_score_:.4f})oob_scoreTrue时fit结束后可以直接读oob_score_它用的是准确率。如果想看 OOB 的 AUC需要手动取oob_decision_function_再算。这个技巧我在特征筛选阶段用得最多能在几分钟内筛掉明显不好的参数组合然后再用交叉验证精调。还有一个细节n_estimators增大到一定程度后 OOB 分数会趋于平稳我一般会画一条 OOB 分数随树数量变化的曲线找到拐点就停。比如 200 棵树之后 OOB 只涨了 0.001那就没必要加到 500。这个习惯帮我省了不少训练时间也避免了模型文件过大导致的部署问题。最后说一个我踩过的坑OOB 分数在类别极度不平衡时波动很大因为每棵树的袋外样本里正样本可能只有几个。这种情况下 OOB 只能做粗筛最终决策还是要看分层交叉验证的 KS 和捕获率。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑