资讯详情

个贷违约预测实战:从数据清洗到模型评估的机器学习完整流程

📅 2026/9/12 3:26:19 | 华诺云谱 👁 阅读
个贷违约预测实战:从数据清洗到模型评估的机器学习完整流程
简介这是一份机器学习课程个贷违约预测大作业的完整项目源码与配套文档面向需要完成类似课程设计或入门信用风险预测的学习者。压缩包共59个文件包含Python算法源码、CSV训练与测试数据、Markdown/PDF/Word格式的实验报告、PNG结果图表以及模型权重文件整体体积142.31MB覆盖数据探索、模型构建、评估与汇报全流程。已有2870人学习下载适合作为课程大作业参考或实战练习。项目以AUC为评测指标实现多层感知机、决策树概率树和基于距离-概率转换的自定义模型并加入软聚类进行描述性分析。目录结构清晰附有实验报告与演示文稿可帮助读者快速理解每个模块的作用便于对照复现与二次改进。1. 个贷违约预测机器学习课程大作业里最值得拆开看的源码个贷违约预测是机器学习课程大作业里出现频率最高的选题之一因为它的数据形态足够典型表格、类别特征、严重的不平衡标签以及一套必须讲清楚的评估指标。做这个题目重点不在调出一个99%准确率的模型——那往往是数据泄漏而在于你能不能把「原始表格 → 清洗分箱 → 模型训练 → 阈值决策」这条链路完整落地。标题里这个 zip 源码包本质上装的就是这么一条流水线。这篇博文就按这条链路的顺序把每个环节的代码怎么写、参数怎么定、坑在哪里讲清楚。新手可以照着拼出自己的作业有经验的人也能借这份源码梳理一遍自己的个贷建模框架。2. 个贷违约预测的数据清洗与特征工程从原始表到 WOE 分箱拿到个贷数据第一步永远是打开数据看结构而不是急着训练。个贷原始表里常见的字段有用户 id、申请金额、期限、利率、收入、年龄、历史逾期次数、征信查询次数、负债率等等。这里有一个所有大作业都会踩的坑直接把原始数值塞进模型。个贷特征里存在大量离群值和缺失值而且很多字段是「越界即异常」比如收入填了 0、年龄填了 200、逾期次数为负。先清洗再谈建模。2.1 缺失值与异常值的处理策略处理缺失值不能一上来就 fillna(0)要看字段的业务含义。个贷数据里收入缺失往往意味着申请人没有稳定工作这本身就是一个强风险信号而性别缺失可能就是单纯漏录。常见做法是把缺失单独编码成一档让模型自己学习这一档的风险水平而不是用均值去抹平。import pandas as pd import numpy as np df pd.read_csv(loan_data.csv, encodinggbk) # 年龄过滤业务上不可能的取值 df df[(df[age] 18) (df[age] 80)] # 收入0 视为缺失单独标记 df.loc[df[income] 0, income] np.nan df[income_is_missing] df[income].isna().astype(int) # 缺失率低于 5% 的字段用中位数填充高于 5% 的保留缺失标记 miss_rate df.isna().mean() for col in df.columns: if miss_rate[col] 0.05 and df[col].dtype in (float64, int64): df[col] df[col].fillna(df[col].median()) print(df.isna().mean().sort_values(ascendingFalse).head(10))这段代码做了三件事用业务阈值过滤异常年龄把收入为 0 转成缺失并单独加一列标记缺失率低的字段用中位数填充。逻辑说明income_is_missing这一列的价值在于把「数据缺失」本身变成模型可用的信息维度很多个贷场景里缺失与否比具体值更有区分度。miss_rate的计算是为了区分处理力度避免对高缺失列做粗暴填充。参数上年龄的 18/80 边界可以按数据集的实际分布调整缺失率 5% 的阈值不是硬规定如果样本量过万5% 意味着几百条记录删除或填充的影响都不大但超过 10% 时建议单独建模或引入缺失指示列。异常值的另一类典型是「多头借贷」特征比如近一个月查询次数达到几十次。这类离群值常见做法是做截断winsorize把超过 99 分位数的值压到 99 分位数的位置防止个别极端样本主导梯度更新。还有金额类特征个贷金额跨度大直接送进模型会让损失函数被大数值样本带着走一般要做 log1p 变换后再入模。2.2 连续变量分箱与 WOE 编码分箱是逻辑回归评分卡的标准前置步骤现在用 XGBoost 的人常常跳过它但课程大作业里保留分箱环节有一个实际好处它逼着你把特征和违约率的关系看清楚。比如年龄分箱后你大概率会看到 30 岁以下和 55 岁以上违约率偏高25-40 岁区间风险较低这种规律在连续原始值上不容易直观察觉。def woe_bin_continuous(df, col, target, bins5): # 用等频分箱保证每箱样本量接近 df[bin] pd.qcut(df[col], qbins, duplicatesdrop) grouped df.groupby(bin, observedTrue).agg( total(target, count), bad(target, sum) ) grouped[good] grouped[total] - grouped[bad] grouped[bad_rate] grouped[bad] / grouped[total] # 避免除零加一个小平滑项 grouped[woe] np.log( (grouped[bad] / grouped[bad].sum() 1e-5) / (grouped[good] / grouped[good].sum() 1e-5) ) grouped[iv] ( (grouped[bad] / grouped[bad].sum() - grouped[good] / grouped[good].sum()) * grouped[woe] ) return grouped result woe_bin_continuous(df, age, is_default, bins6) print(result[[total, bad_rate, woe, iv]])这段代码实现的是最基础的等频 WOE 分箱。逻辑说明bad_rate是每个分箱内的违约占比它随分箱的变化趋势决定了这个变量有没有区分度woe是该箱坏客户占比与好客户占比之比的对数数值符号说明这一箱相对整体是偏风险还是偏安全iv是每个分箱的 IV 贡献所有分箱 IV 之和就是该变量的总 IV。参数上bins5是起点样本量够大可调到 8-10但每箱样本量最好不低于总样本的 5%1e-5是平滑项防止某箱没有坏样本时 log 里出现 0。IV 的参考标准业界比较统一小于 0.02 基本无预测力0.02 到 0.1 较弱0.1 到 0.3 中等大于 0.3 需要警惕是否是强泄漏特征比如用事后信息构造的标签衍生变量。2.3 特征筛选别把几十个弱变量全塞进模型个贷大作业常见的特征数在 20 到 60 个之间不是越多越好。逻辑回归对特征共线性敏感树模型虽然不敏感但冗余特征会稀释有效特征的权重还拖慢训练。常用做法是走一遍 IV 筛选 相关性去重先按 IV 排序去掉低于 0.02 的特征再对相关性高于 0.7 的特征对做人工取舍。当然「人工取舍」在大作业里就是指你至少要看一眼这两个特征是不是同一个业务信息的两种表达比如「月收入」和「年收入」明显是一回事保留 IV 高的那个即可。# 相关性去重示例 corr_matrix df[numeric_cols [is_default]].corr() upper corr_matrix.where(np.triu(np.ones(corr_matrix.shape), k1).astype(bool)) high_corr_pairs [(i, j) for i in upper.index for j in upper.columns if upper.loc[i, j] 0.7] print(high_corr_pairs)这段代码取出相关系数矩阵的上三角找出所有相关性超过 0.7 的特征对。逻辑说明np.triu只保留上三角避免每对特征被重复输出针对找出的特征对人工判断保留哪个一般优先保留 IV 更高、业务含义更直接的字段。参数说明相关性阈值 0.7 是经验值如果想更保守可以调到 0.6特征数量很紧张时可以放宽到 0.8。提示分箱之后原特征必须先转成 WOE 值再喂给逻辑回归不能把分箱标签1、2、3…5直接当数值用——分箱序号之间没有等距关系直接进模型等于强行规定了一个不存在的线性顺序。3. 分类器选型与训练逻辑回归、XGBoost 和 LightGBM数据清洗和特征工程做完就到了建模环节。课程大作业里最稳妥的打法不是只用一个模型而是先训练一个逻辑回归作为基线再上梯度提升树对比。个贷场景下逻辑回归的优势是参数可以直接转成评分卡刻度答辩时能讲清楚「每增加一次逾期分数扣多少」XGBoost 和 LightGBM 的优势是能自动捕捉非线性关系精度通常更高但解释性弱一些。3.1 样本切分与类别不平衡处理个贷违约率通常在 3% 到 10% 之间直接训练的话模型会倾向于把所有样本都预测为「不违约」。应对手段有两个层面一是评估指标不用 accuracy而是看 AUC、KS 和 Recall二是在训练时做处理。这里要特别提醒过采样SMOTE在大作业里可以做但必须只在训练集上做千万不能在交叉验证或测试集上做否则相当于让模型「看过答案」。from sklearn.model_selection import train_test_split X df[feature_cols] y df[is_default] X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy ) X_train, X_valid, y_train, y_valid train_test_split( X_train, y_train, test_size0.25, random_state42, stratifyy_train ) print(fTrain: {X_train.shape}, Valid: {X_valid.shape}, Test: {X_test.shape}) print(fTrain 违约率: {y_train.mean():.4f})这段代码做了标准的三段切分训练、验证、测试。逻辑说明stratifyy保证了切分后各组违约率与全量一致这在个贷这种不平衡数据里尤其重要否则可能出现测试集里连一个违约样本都没有的情况。random_state42固定了随机种子保证每次跑分结果可复现这是课程作业答辩时的基本素养。参数说明test_size0.2切出 20% 做最终测试剩余 80% 中再切 25% 做验证——注意这里的 25% 是相对剩余部分的实际验证集占比是 0.8×0.2520%。三段切分比两段好的地方在于验证集用于调参测试集只碰一次防止你对测试集反复调参导致「测试集泄漏」。3.2 逻辑回归训练与评分卡刻度逻辑回归在个贷预测里不是用来拿最高分的而是用来建立可解释基线。训练之前要做的关键一步是特征标准化。个贷字段量纲差异巨大金额是万级、次数是个位级不标准化的话 L2 正则对量纲小的特征惩罚更重导致系数失真。from sklearn.linear_model import LogisticRegression from sklearn.preprocessing import StandardScaler from sklearn.pipeline import Pipeline lr_pipeline Pipeline([ (scaler, StandardScaler()), (lr, LogisticRegression(C1.0, class_weightbalanced, max_iter1000, random_state42)) ]) lr_pipeline.fit(X_train, y_train) train_pred lr_pipeline.predict_proba(X_train)[:, 1] valid_pred lr_pipeline.predict_proba(X_valid)[:, 1] print(fLR Valid AUC: {roc_auc_score(y_valid, valid_pred):.4f})逻辑说明Pipeline把标准化和模型串在一起避免对验证集单独做标准化时出现统计量泄漏class_weightbalanced让损失函数按样本数量的反比重新加权默认情况下违约少数类会获得更大的权重相当于内置了一次类别不平衡处理。C1.0是正则强度的倒数调小如 0.1会让权重范数更小模型更平滑适合特征间相关性较高的场景。参数上逻辑回归的max_iter设到 1000 是为了确保收敛数据量大时默认 100 次可能不够。输出predict_proba的第二列即为违约概率分数后续所有阈值操作都在这个分数上进行。3.3 XGBoost 与 LightGBM 的核心参数与训练模板梯度提升树是这类表格数据的默认首选。个贷预测用 LightGBM 更多一些因为处理几十万行数据速度更快内存占用小而且对缺失值有原生支持。下面给出一个可以直接套用的训练模板重点在参数取舍而不是堆参数。import lightgbm as lgb lgb_params { objective: binary, metric: auc, learning_rate: 0.05, num_leaves: 31, max_depth: 5, min_child_samples: 100, subsample: 0.8, colsample_bytree: 0.8, reg_alpha: 0.1, reg_lambda: 1.0, n_jobs: -1, random_state: 42, verbose: -1 } d_train lgb.Dataset(X_train, labely_train) d_valid lgb.Dataset(X_valid, labely_valid, referenced_train) model lgb.train( lgb_params, d_train, num_boost_round500, valid_sets[d_valid], callbacks[lgb.early_stopping(100), lgb.log_evaluation(50)] ) valid_pred model.predict(X_valid, num_iterationmodel.best_iteration) print(fLGB Valid AUC: {roc_auc_score(y_valid, valid_pred):.4f})这段代码是 LightGBM 训练的最小可用模板。关键参数说明learning_rate0.05配合num_boost_round500和早停比默认的大学习率加少轮数的组合稳定得多——学习率越小每棵树对残差修正的步子越慢越不容易过拟合但需要更多棵树训练时间线性增长num_leaves31控制树的复杂度叶子数翻倍会使模型容量大幅增加个贷这种千到百万级样本的任务 31 是合理起点min_child_samples100限制了每片叶子的最小样本量是防止过拟合最重要的参数subsample和colsample_bytree分别做行采样和列采样类似随机森林的思路0.8 对个贷这种特征数中等的任务性价比很高。reg_alpha和reg_lambda是 L1/L2 正则L1 还有特征选择的效果。逻辑说明referenced_train让验证集的统计量对齐训练集分布避免验证集在分裂点计算时使用自己的分布信息early_stopping(100)表示验证集 AUC 连续 100 轮不提升就停止最终模型取best_iteration对应的轮数——这里有个容易忽视的细节训练完成后打印的 AUC 用的是best_iteration的预测不是最后一轮的。3.4 XGBoost 对比与选型结论XGBoost 和 LightGBM 在个贷场景下的差异没有想象中大AUC 差距通常在 0.005 到 0.02 之间。选哪个更多取决于环境XGBoost 的历史更久稳定性和文档完善度更好适合需要严格复现的作业环境LightGBM 的训练速度快得多特征数量上百或者样本量过百万时优势明显。课程大作业里更合理的策略是两个都跑一遍选验证集 AUC 高的那个作为最终模型并在报告里写明两个模型的结果对比。预计算法使用默认参数通常就能达到基线 80% 的效果剩下 2% 的提升来自特征而不是参数微调。参数调优可以用网格搜索但对课程作业来说手调 3-4 个参数num_leaves、min_child_samples、learning_rate、subsample就足够了。维度逻辑回归XGBoostLightGBM训练速度快中快解释性高系数可直接解释低需 SHAP/特征重要性辅助低缺失值处理需预先填充原生支持原生支持对特征缩放要求需要标准化不敏感不敏感典型 Valid AUC 区间0.70-0.780.75-0.840.75-0.85适合答辩展示点评分卡刻度特征重要性训练效率逻辑说明这张表是选型依据而非定论。逻辑回归的 AUC 下限低的原因是个贷特征和违约率之间多为非线性关系线性模型天然吃亏梯度提升树能捕捉到比如「收入高但查询次数也多」这种交叉模式。但要注意逻辑回归低不代表没价值——它的输出天然是 0-1 之间的概率转换成分数后稳定性更好银行系评分卡至今仍在用。4. 模型评估与阈值优化AUC、KS 与业务成本模型训练完真正的考验在评估环节。课程大作业里最常见的错误是只看 AUCAUC 高只能说明模型排序能力强不代表你选了一个好阈值。个贷业务关心的是「在某个审批通过率下违约率能压到多少」这需要做阈值优化而不是直接用 0.5。4.1 混淆矩阵与业务指标换算先看混淆矩阵但要用业务语言重新翻译一遍。真正贷后关心的指标是精确率违约预测得准不准和召回率违约的抓到了多少而审批部门关心的是通过率和整体坏账率。这些指标全部由混淆矩阵衍生但要在答辩时讲出它们在业务上的含义。from sklearn.metrics import confusion_matrix, classification_report threshold 0.3 y_pred_class (valid_pred threshold).astype(int) cm confusion_matrix(y_valid, y_pred_class) tn, fp, fn, tp cm.ravel() approve_rate (tp fp) / len(y_valid) bad_rate_among_approved tp / (tp fp) if (tp fp) 0 else 0 capture_rate tp / (tp fn) print(f通过率: {approve_rate:.2%}) print(f通过客户实际违约率: {bad_rate_among_approved:.2%}) print(f全部违约客户中被拦截比例: {capture_rate:.2%})逻辑说明这段代码把阈值设到 0.3高于多少视为「预测违约」。三个派生指标比准确率有意义得多approve_rate对应审批通过比例业务上不可能为了零违约把所有人都拒掉bad_rate_among_approved是放进来的人里真正违约的比例这是风控最关心的损失源capture_rate是拦截覆盖率代表风控的有效性。这三个指标是互相制衡的阈值定得越高拦截率越高但通过率越低误杀好客户越多。4.2 KS 曲线与最优阈值选择业界做评分卡时最常看的是 KS 值它衡量的是模型把好坏客户分开的最大能力。KS 的定义是好客户累计占比与坏客户累计占比之差的最大值个贷模型一般要求 KS 大于 0.3 才可上线。课程作业里跑出 KS 在 0.35 以上答辩时就可以拿出来讲了。def compute_ks(y_true, y_score): df pd.DataFrame({y: y_true, score: y_score}) df df.sort_values(score, ascendingFalse).reset_index(dropTrue) total_bad df[y].sum() total_good len(df) - total_bad df[cum_bad] df[y].cumsum() / total_bad df[cum_good] (1 - df[y]).cumsum() / total_good df[ks] (df[cum_bad] - df[cum_good]).abs() max_ks df[ks].max() best_idx df[ks].idxmax() return max_ks, df.loc[best_idx, score] ks_value, best_threshold compute_ks(y_valid, valid_pred) print(fKS: {ks_value:.4f}, 最优阈值: {best_threshold:.4f})这段代码实现了 KS 的计算逻辑。逻辑说明先把样本按预测分从高到低排序cum_bad是坏客户累计占比cum_good是好客户累计占比两者之差最大处对应的分数就是区分能力最强的切分点。代码返回的best_threshold是一个常用参考阈值——它表示在这个分数之上拒绝能同时最大化好坏客户的分离程度。实现上的细节(1 - df[y])利用了标签是 0/1 的特性直接计算出每行的好客户标记。提示KS 最优阈值和业务最优阈值不是一回事。KS 阈值是纯统计意义的最优点但如果此时通过率只有 20%业务上大概率不可接受。实际落地时通常在 KS 阈值附近的一个区间内如 0.25~0.4结合通过率和坏账率两个业务指标来定最终阈值。4.3 阈值网格搜索按业务口径挑阈值与其手动试阈值再算指标不如写成阈值网格搜索一次跑完画出曲线。这里我用一个数值示例来演示阈值、通过率、违约拦截率、误杀率之间的制衡关系。假设验证集有 10000 个样本其中违约 500 个模型对每个样本输出违约概率我们按不同阈值计算业务指标。thresholds np.arange(0.1, 0.71, 0.05) grid_results [] for thr in thresholds: pred_class (valid_pred thr).astype(int) tn, fp, fn, tp confusion_matrix(y_valid, pred_class).ravel() approve_rate (tp fp) / len(y_valid) bad_rate tp / (tp fp) if (tp fp) 0 else 0 recall tp / (tp fn) precision tp / (tp fp) if (tp fp) 0 else 0 grid_results.append({ threshold: round(thr, 2), approve_rate: approve_rate, default_rate: bad_rate, recall: recall, precision: precision }) result_df pd.DataFrame(grid_results) print(result_df.to_string(indexFalse))这段代码在 0.1 到 0.7 之间按 0.05 步长扫描阈值每个阈值下都算一套业务指标。逻辑说明approve_rate随阈值升高而降低因为更多样本被判为违约被拒recall随阈值升高而降低因为阈值高意味着只有分数极高的样本才被判违约大量低分违约样本漏过precision通常随阈值升高而升高因为高分样本中违约占比更高。参数说明步长 0.05 用于快速预览锁定区间后可以细化到 0.01 再跑一轮。最终选阈值的原则是找出「通过率下降斜率开始变陡」的拐点——再往上调阈值通过率损失很大但坏账率降幅很小说明模型已经把能分清的部分分完了。5. 源码组织与复现验证拿到 zip 包后怎么在 30 分钟内跑通你拿到或者即将提交的这个个贷违约预测源码 zip结构一般逃不出这几块数据加载与清洗、特征工程、模型训练、评估可视化、主入口。拿到源码包之后第一件事不是看代码而是看requirements.txt或environment.yml确认依赖版本。个贷项目最常见的运行失败原因不是代码逻辑错误而是 LightGBM 版本不一致导致模型文件无法加载或者 pandas 版本升级后append方法被移除。建议直接按源码附带的依赖列表新建一个虚拟环境不要用全局环境。5.1 一个可复现的最小运行验证流程不知道源码入口时按下面的顺序快速检查文件结构先找main.py或train.py再看有没有README.md或requirements.txt最后确认数据文件路径是否硬编码。mkdir -p loan_default_env python -m venv loan_default_env source loan_default_env/bin/activate # Windows 下为 loan_default_env\Scripts\activate pip install -r requirements.txt python main.py这段命令创建虚拟环境并运行主程序。逻辑说明虚拟环境隔离依赖避免你机器上已有的 scikit-learn 版本和源码要求的不一致。python main.py运行前建议先确认源码里数据路径是相对路径还是绝对路径——很多课程作业源码里直接写死了C:\Users\xxx\Desktop\data.csv换机器跑必然报错遇到这种情况把数据放到项目根目录下的data/文件夹再把源码里的路径改成os.path.join拼接即可。5.2 复现时最常见的三类报错排查第一类是编码错误中文数据读进来报UnicodeDecodeError原因是原始 CSV 是 GBK 编码pd.read_csv默认用 UTF-8 解码。解决方法是读文件时显式指定encodinggbk更稳妥的做法是用encodinggb18030它是 GBK 的超集兼容性更好。第二类是LightGBM的特征名称或数据类型错误比如缺失值全部填充成空字符串再转 float 失败报错信息里会直接点名某个特征名顺着报告回数据预处理阶段修。第三类是内存不足几十万行数据加上 onehot 编码后特征矩阵膨胀MemoryError时优先检查是不是有哪个高基数类别特征被 onehot 了换成 LightGBM 的类型特征categorical_feature可以省下大量内存。# 保存最终模型与特征列表确保复现一致 import joblib joblib.dump(model, artifacts/lgb_model.pkl) joblib.dump(feature_cols, artifacts/feature_cols.pkl)保存模型和特征列表是源码交付的最后一步。逻辑说明模型文件和特征列表必须配套保存因为重新训练时 if 特征的顺序变了模型预测会静默出错而不是报错。建议把lr_pipeline和 LightGBM 模型分开存同时把最终的特征列顺序、阈值一起写进一个 JSON 配置文件。joblib对包含大量 numpy 数组的模型效率高于 pickle尤其是 LightGBM 的 booster 对象而特征列表直接用 joblib 或 json 都可以。另有一个容易忽视的文件——模型训练过程中固定random_state后训练和验证划分结果也要导出存成 CSV这样答辩时可以精确复现「模型看到的每一条样本」。最后一件事把valid_pred和y_valid输出到predictions.csv包含三列样本 id、预测违约概率、真实标签。这样你可以用任意工具绘制 KS 曲线和 PR 曲线不用重新训练就能反复验证阈值选择是否合理。这份文件也是答辩时老师最爱要的交付物——纸面上的分箱代码和训练代码都不如一条完整的预测结果更有说服力。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

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

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

你可能需要的服务

订阅华诺云谱资讯周报

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