简介:这份资源面向计算机相关专业的本科生与研究生,提供一套可直接用于毕业设计、期末大作业或课程设计的贷款违约预测完整方案,帮助解决建模流程不清晰、代码难以复现的问题。压缩包共22个文件,约6.16MB,包含7个Python脚本、11张png图表、2个csv数据集、1个xls数据字典及1个md说明文档,脚本覆盖随机森林、决策树、逻辑回归与提升算法等建模流程,图表则呈现学习曲线、特征重要性与模型对比结果。已有143人学习下载,说明该方案在同类选题中具备一定参考价值。读者可获得从数据读取、特征分析到模型训练与预测的完整代码,注释详尽,新手也能理解;同时附带数据字典与多组可视化结果,便于撰写论文时直接引用与对比分析,部署后即可运行,适合作为高分项目模板使用。
1. 从一份信贷审批台账说起:随机森林怎么把违约概率算出来
银行信贷部门每天面对的核心问题很朴素:这笔钱借出去,对方会不会按时还。传统做法靠评分卡和逻辑回归,特征线性、可解释性强,但遇到多头借贷、收入波动、征信查询密集这类非线性交叉场景,区分度就明显不够。随机森林算法的价值在于,它用多棵决策树投票,天然处理特征交互和缺失值,对异常点不敏感,还能输出特征重要性,正好卡在「预测准」和「讲得清」之间。贷款违约预测模型研究这个方向,适合有 Python 基础、想做一个完整可复现项目的学生和初级数据从业者。它不需要深度学习环境,一台普通笔记本就能跑通全流程,源码结构清晰,是少数能同时写进简历和讲清业务逻辑的题目。
2. 数据到手先别急着建模:违约预测的特征工程与样本处理
2.1 先搞清楚标签和业务口径
贷款违约预测的标签定义直接决定模型上限。常见口径有两种:一是「当前是否逾期超过 90 天」,二是「放款后 12 个月内是否发生首次逾期」。前者是截面标签,后者是观察期标签。做研究项目时,我一般选第二种,因为它更接近真实风控的贷后表现窗口,也能避免用未来信息预测过去。
拿到数据后先做三件事:确认主键唯一性、统计标签分布、检查时间字段。如果正样本占比低于 5%,说明是不平衡数据,后面评估就不能只看准确率。
import pandas as pd df = pd.read_csv("loan_data.csv") # 主键唯一性检查 print("重复主键数:", df["loan_id"].duplicated().sum()) # 标签分布 print(df["default_flag"].value_counts(normalize=True)) # 时间字段范围 df["issue_date"] = pd.to_datetime(df["issue_date"]) print(df["issue_date"].min(), df["issue_date"].max())这段代码的作用是先建立数据可信度。duplicated().sum()返回 0 才说明一条借款记录只出现一次;value_counts(normalize=True)给出正负样本比例;时间范围用来判断能不能按时间切分训练集和测试集。如果时间跨度不足一年,观察期标签就不成立,需要退回截面口径。
2.2 缺失值不是填得越满越好
金融数据里缺失本身有信息。比如「近半年征信查询次数」缺失,往往意味着该用户没有信贷活动,反而可能是低风险。我一般对数值型特征先加一列xxx_isnull标记缺失,再用中位数填充;类别型特征用「Unknown」单独成类。
num_cols = ["income", "loan_amount", "query_count_6m"] cat_cols = ["education", "marital_status", "house_type"] for col in num_cols: df[col + "_isnull"] = df[col].isnull().astype(int) df[col] = df[col].fillna(df[col].median()) for col in cat_cols: df[col] = df[col].fillna("Unknown")astype(int)把布尔标记转成 0/1,方便树模型直接使用。中位数比均值抗极端值,适合收入这类右偏分布。类别缺失单独成类,是因为树模型可以按「是否缺失」这个条件分裂,比强行填众数更保留信息。
2.3 类别特征编码:别一上来就 One-Hot
随机森林对类别特征的处理,常见做法是先用有序编码或目标编码,再看维度决定要不要 One-Hot。学历这种有顺序的用有序编码;职业这种无序且类别多的,用目标编码并加平滑,避免高基数类别过拟合。
from sklearn.model_selection import KFold def target_encode(train, test, col, target, smooth=20): prior = train[target].mean() agg = train.groupby(col)[target].agg(["mean", "count"]) agg["encode"] = (agg["mean"] * agg["count"] + prior * smooth) / (agg["count"] + smooth) train[col + "_te"] = train[col].map(agg["encode"]).fillna(prior) test[col + "_te"] = test[col].map(agg["encode"]).fillna(prior) return train, test平滑参数smooth=20控制先验均值的权重。类别样本数越少,编码值越靠近整体违约率,防止「某个职业只有 3 个人且全部违约」被编码成 1.0。目标编码必须在训练集上计算映射,再应用到测试集,否则就是标签泄漏。
2.4 按时间切分,别用随机切分骗自己
风控场景有时间顺序,随机切分会让未来样本进入训练集,评估结果虚高。正确做法是按放款日期排序,前 70% 做训练,后 30% 做测试。
df = df.sort_values("issue_date") split_idx = int(len(df) * 0.7) train_df = df.iloc[:split_idx].copy() test_df = df.iloc[split_idx:].copy() print("训练集违约率:", train_df["default_flag"].mean()) print("测试集违约率:", test_df["default_flag"].mean())两个违约率接近,说明时间切分没有造成分布剧烈漂移。如果测试集违约率明显更高,可能是经济环境变化,需要在特征里加入时间相关变量,或者缩短观察窗口。
3. 随机森林建模:参数怎么调、阈值怎么定、结果怎么看
3.1 最小可运行训练脚本
先把基线跑出来,再谈优化。下面这段代码用RandomForestClassifier完成训练和预测,重点看class_weight和不平衡处理。
from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import roc_auc_score, classification_report feature_cols = [c for c in train_df.columns if c not in ["loan_id", "issue_date", "default_flag"]] X_train = train_df[feature_cols] y_train = train_df["default_flag"] X_test = test_df[feature_cols] y_test = test_df["default_flag"] rf = RandomForestClassifier( n_estimators=300, max_depth=12, min_samples_leaf=20, class_weight="balanced_subsample", random_state=42, n_jobs=-1 ) rf.fit(X_train, y_train) proba = rf.predict_proba(X_test)[:, 1] print("AUC:", roc_auc_score(y_test, proba)) print(classification_report(y_test, (proba > 0.5).astype(int)))n_estimators=300是树的数量,太少方差大,太多收益递减且训练变慢。max_depth=12限制单棵树深度,防止把训练集噪声学进去。min_samples_leaf=20要求叶子节点至少 20 个样本,这是控制过拟合最有效的参数之一。class_weight="balanced_subsample"让每棵树在自助采样时按类别权重调整,缓解正样本少的问题。n_jobs=-1用满 CPU 核数。
3.2 三个必调参数:树数量、深度、叶子样本数
随机森林的调参空间比深度学习小得多,核心就三个。我一般用GridSearchCV配合时间序列交叉验证,但注意不能用普通 KFold,要用TimeSeriesSplit。
from sklearn.model_selection import TimeSeriesSplit, GridSearchCV param_grid = { "n_estimators": [200, 300, 500], "max_depth": [8, 12, 16], "min_samples_leaf": [10, 20, 50] } tscv = TimeSeriesSplit(n_splits=5) grid = GridSearchCV( RandomForestClassifier(class_weight="balanced_subsample", random_state=42, n_jobs=-1), param_grid, cv=tscv, scoring="roc_auc", n_jobs=-1 ) grid.fit(X_train, y_train) print(grid.best_params_) print(grid.best_score_)TimeSeriesSplit保证每次验证集都在训练集之后,模拟真实上线场景。scoring="roc_auc"比准确率更适合不平衡数据。如果算力有限,可以先固定n_estimators=300,只调max_depth和min_samples_leaf,这两个对泛化影响最大。
3.3 阈值不是 0.5:按业务成本定切割点
模型输出的是概率,不是最终决策。把 0.5 当阈值是默认习惯,但风控里漏判一个坏客户的成本远高于误拒一个好客户。阈值应该按业务成本矩阵来定。
import numpy as np from sklearn.metrics import confusion_matrix thresholds = np.arange(0.1, 0.9, 0.05) for t in thresholds: pred = (proba > t).astype(int) tn, fp, fn, tp = confusion_matrix(y_test, pred).ravel() cost = fn * 5 + fp * 1 # 漏判成本是误拒的5倍 print(f"阈值={t:.2f} 漏判={fn} 误拒={fp} 总成本={cost}")这段代码遍历阈值,按「漏判成本 5 倍于误拒」计算总成本,选成本最低的阈值。实际业务中这个倍数来自催收成本、资金损失和客户流失的综合估算。研究项目里可以设成 3 到 10 之间做敏感性分析。
3.4 特征重要性:用置换重要性而不是默认的杂质重要性
feature_importances_基于杂质下降,对高基数特征有偏。更可靠的是置换重要性,它在验证集上打乱某一列,看 AUC 掉多少。
from sklearn.inspection import permutation_importance result = permutation_importance( rf, X_test, y_test, n_repeats=10, random_state=42, scoring="roc_auc", n_jobs=-1 ) imp_df = pd.DataFrame({ "feature": feature_cols, "importance": result.importances_mean, "std": result.importances_std }).sort_values("importance", ascending=False) print(imp_df.head(15))n_repeats=10表示每列打乱 10 次取平均,减少随机波动。importances_std大说明该特征重要性不稳定,业务解释时要谨慎。排名靠前的通常是征信查询次数、负债收入比、历史逾期次数,这些和风控经验一致,说明模型没有学到伪相关。
4. 避坑与排查:随机森林做信贷模型最容易翻车的五件事
4.1 用随机切分导致 AUC 虚高
现象:交叉验证 AUC 0.85,上线后 KS 只有 0.25。原因:随机切分让未来样本进入训练集,模型记住了时间趋势。解决:改用TimeSeriesSplit或按时间一刀切,训练集时间必须早于测试集。
4.2 目标编码在划分前计算造成标签泄漏
现象:训练集 AUC 接近 1.0,测试集惨不忍睹。原因:目标编码用了全量数据的标签均值,测试集信息渗进训练特征。解决:编码映射只在训练集上fit,再transform测试集;或者把编码放进Pipeline里做交叉拟合。
4.3 把predict当概率用
现象:想按概率排序却调用了predict,得到一堆 0 和 1。原因:predict返回的是阈值 0.5 后的类别,不是概率。解决:用predict_proba[:, 1]取正类概率,再做排序或阈值调整。
4.4 忽略特征量纲和缺失标记的交互
现象:填充中位数后模型表现反而下降。原因:缺失标记列和原特征高度相关,树模型分裂时优先选标记列,原特征的连续信息被浪费。解决:检查缺失标记列的重要性,如果排在前列,考虑只保留标记列或只保留填充后特征,不要两者都放。
4.5 树太多导致推理慢且收益递减
现象:n_estimators从 300 加到 1000,AUC 只涨 0.002,但预测耗时翻三倍。原因:随机森林的方差随树数量增加呈指数衰减,超过一定数量后边际收益极低。解决:画学习曲线,选 AUC 趋于平稳的拐点,通常 200 到 500 之间。线上服务对延迟敏感时,可以配合max_depth一起压缩。
5. 把模型变成能讲清楚的项目:验证、解释与交付技巧
5.1 用 KS 和 Lift 图做业务侧验证
AUC 是排序指标,业务方更关心「抓出多少坏人」和「拒绝多少好人」。KS 统计量衡量好坏样本累计分布的最大差值,Lift 图看头部人群的违约率提升倍数。
from scipy.stats import ks_2samp def ks_stat(y_true, y_proba): return ks_2samp(y_proba[y_true == 1], y_proba[y_true == 0]).statistic print("KS:", ks_stat(y_test, proba)) # Lift 计算 test_df = test_df.copy() test_df["proba"] = proba test_df["decile"] = pd.qcut(test_df["proba"], 10, labels=False, duplicates="drop") lift = test_df.groupby("decile")["default_flag"].mean() / test_df["default_flag"].mean() print(lift.sort_index(ascending=False))ks_2samp返回好坏样本分数分布的最大距离,信贷模型 KS 到 0.4 以上算可用。Lift 按概率分十档,最高档的违约率除以整体违约率,如果 top 10% 的 Lift 达到 3 以上,说明模型在头部有区分力。
5.2 用 SHAP 做单笔解释
随机森林是黑匣子,但 SHAP 可以给出每笔预测的特征贡献。业务方问「为什么拒这个客户」,SHAP 能直接回答。
import shap explainer = shap.TreeExplainer(rf) shap_values = explainer.shap_values(X_test.iloc[:100]) # 正类(违约)的 SHAP 值 shap.summary_plot(shap_values[1], X_test.iloc[:100], plot_type="bar")TreeExplainer对树模型有精确算法,比 KernelSHAP 快几个数量级。shap_values[1]取正类贡献,summary_plot画全局重要性。单笔解释可以用shap.force_plot,但研究项目里全局图足够说明模型逻辑。
5.3 源码交付时把随机种子和依赖锁死
项目源码要能复现,必须固定random_state,并给出requirements.txt。我一般还会加一个config.yaml存参数,避免硬编码。
# config.yaml data: path: "data/loan_data.csv" test_size: 0.3 model: n_estimators: 300 max_depth: 12 min_samples_leaf: 20 class_weight: "balanced_subsample" random_state: 42pip freeze > requirements.txtconfig.yaml让调参不用改代码,requirements.txt锁定版本。这两样东西看起来小,但决定了别人拿到源码能不能一次跑通。我吃过亏,早期项目没锁scikit-learn版本,换台机器predict_proba列顺序变了,排查半天才发现是版本差异。
5.4 一个具体技巧:用 OOB 分数快速判断树数量
随机森林自带袋外样本评估,不用单独切验证集就能看n_estimators是否足够。
rf_oob = RandomForestClassifier( n_estimators=500, max_depth=12, min_samples_leaf=20, class_weight="balanced_subsample", oob_score=True, random_state=42, n_jobs=-1 ) rf_oob.fit(X_train, y_train) print("OOB AUC:", roc_auc_score(y_train, rf_oob.oob_decision_function_[:, 1]))oob_score=True开启袋外评估,oob_decision_function_返回每个样本的袋外概率。如果 OOB AUC 和测试集 AUC 差距在 0.02 以内,说明模型没有严重过拟合。这个技巧适合快速迭代,省去反复切验证集的时间。
做这类项目我最大的习惯是:先把基线跑通,再谈优化;先看业务指标,再看模型指标;先锁随机种子,再调参数。贷款违约预测模型不难,难的是让结果经得起追问。希望帮到你。
本文还有配套的精品资源,点击获取