恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
Python个贷违约预测实战:从数据清洗到模型评估的完整源码包
首页
资讯中心
/
Python个贷违约预测实战:从数据清洗到模型评估的完整源码包
Python个贷违约预测实战:从数据清洗到模型评估的完整源码包
发布时间:2026/10/11 21:23:26
简介围绕中原银行个贷违约预测赛题这套资源以Python实现了从数据清洗、特征工程到模型预测的完整流程定位清晰适合金融风控方向学生、竞赛参与者及毕设开发人群做项目复现与思路参考。资源从迁移学习视角切入银行新客群风控难题代码按数据清洗、特征构建、标签与类别及时间特征加工、主流程运行等模块组织同时提供公开测试集、样例提交文件与说明文档便于对照赛题环境开展实验。压缩包共9个文件核心为6个Python脚本配合2个CSV数据文件与1个Markdown说明整体仅311KB轻量紧凑。该资源已有98人学习内容包括完整可运行的赛题工程代码、文档与数据集可在其基础上扩展个人设计也可用于系统梳理个贷违约预测的主要环节。1. 用 Python 做中原银行个贷违约预测一份能跑通全流程的源码包用 Python 做个人贷款违约预测网上能搜到的项目不少但“源码能跑、文档能对上、数据能复现”三者齐全的不多。这份围绕中原银行个贷业务场景的违约预测资源把源代码、说明文档、脱敏数据集打包在一起目标很直接让一个刚接触风控建模的人也能在一台普通笔记本上跑出完整结果。整个流程覆盖数据清洗、特征构造、模型训练、评估对比到结果导出不依赖生产环境组件。适合两类人一类是想把教科书里的分类模型落到真实结构化数据上的学生另一类是正要搭建个人信贷评分流程、需要先跑通基线版本的风控工程师。做完这一遍你对特征怎么选、标签怎么定、验证集怎么切会有非常具体的体感。2. 数据与预处理先摸清字段再谈清洗和标签2.1 个贷数据集的字段结构与业务口径拿到数据第一步不是建模而是读数据字典。这份资源里的文档说明部分带了字段表建议你先花十分钟把每个字段的业务含义过一遍。结合银行零售贷款的常见口径个贷数据通常分三类客户基础信息年龄、婚姻、学历、收入贷款要素贷款金额、期限、利率、还款方式、担保方式历史行为征信查询次数、近 12 个月逾期次数、已有负债收入比。下面是我见过的典型字段组织方式你可以对照着去认资源里的实际表字段类型用途说明customer_idid客户唯一标识建模时剔除age数值年龄注意缺失和异常值income数值月收入常用于衍生负债收入比loan_amt数值贷款金额决定额度类特征interest_rate数值贷款利率反映风险定价overdue_12m数值近 12 个月逾期次数强预测特征credit_query_cnt数值近 3 个月征信查询次数default_flag标签是否违约建模目标列业务口径上最需要统一的是“违约”的定义。银行内部通常用逾期天数DPD切分常见的是逾期超过 90 天M3或进入催收定义为违约而不是“只要晚还一天就算”。你在文档里会看到 default_flag 的构造说明我建议你先确认它用的是 M1、M2 还是 M3这决定了样本的坏账率和模型难度也会影响后续所有评估指标。2.2 缺失值、重复样本和异常分布的处理读数据阶段别急着一口气把清洗写完我习惯先跑一个探针脚本把数据规模、缺失率、重复率全部打出来再决定处理策略。因为个贷数据经常是多个业务系统导出的拼接结果字段缺失模式往往带有明显的系统痕迹比如某个月开始新上线的字段前半段全是空值。import pandas as pd df pd.read_csv(data/raw_data.csv, encodingutf-8) print(样本量与字段数, df.shape) print(字段类型分布) print(df.dtypes.value_counts()) print(缺失率最高的 10 个字段) print(df.isnull().mean().sort_values(ascendingFalse).head(10)) print(重复样本数, df.duplicated().sum())这份代码做的事情很朴素但很有必要shape 确认拿到的表不是空表也不是错位表dtypes 检查有没有把数值列读成 object缺失率排名告诉你哪些字段可以直接放弃哪些字段值得花心思补。我见过不止一次“建模建到一半发现主键重复”的情况所以重复样本数量一定要在最开始就确认清楚。接下来是实际清洗我的习惯做法是分类讨论而不是一个 fillna 全表通吃# 类别字段缺失统一填 UNKNOWN保持数据类型稳定 for col in cat_cols: df[col] df[col].fillna(UNKNOWN) # 数值字段先看分布再决定不要无脑填中位数 for col in num_cols: if df[col].isnull().mean() 0.3: df[col] df[col].fillna(df[col].median()) else: # 缺失超过 30% 的数值字段多半来源不稳建议生成缺失标记 df[col _missing] df[col].isnull().astype(int) df[col] df[col].fillna(0) # 完全重复的样本直接去掉 df df.drop_duplicates() # 贷款金额 0 的行视为脏数据 df df[df[loan_amt] 0]这里的关键是区分模型类型。LightGBM 这类树模型原生支持缺失值分裂时会自动把缺失值分到增益更大的一侧但逻辑回归这类线性模型不行缺失值会导致样本直接被丢弃。所以如果你打算主力用 LightGBM数值字段缺失不严重时可以不补交给模型处理但如果你后面要跑逻辑回归做基线就必须先补齐。我在项目里通常是两套都跑所以清洗阶段统一补齐避免两套数据不一致导致对比失真。异常值方面个贷数据最典型的是收入字段出现极端值。常见做法是上下分位数截尾winsorize比如把 99 分位以上的值压到 99 分位而不是直接删除。删除会引入选择偏差截尾则保留排序信息。这段逻辑可以直接写进 01_data_cleaning.py 里跑一次就固化下来。2.3 观察期与表现期标签必须“发生在后”这是我拆过不少风控项目后觉得最值得强调的一步。分类建模新手最容易犯的错是把“当时能拿到的信息”和“事后才知道的信息”混在一起当特征这会导致模型在训练集上表现极好上线后立刻翻车。风控建模的标准做法是切分观察期和表现期观察期提供特征表现期产生标签两者在时间上严格先后错开。-- 以 2023-06-30 为观察期末尾表现期取其后 12 个月 WITH observe AS ( SELECT loan_id, MAX(observe_date) AS observe_end FROM loan_repay_record WHERE observe_date 2023-06-30 GROUP BY loan_id ) SELECT o.loan_id, MAX(CASE WHEN r.dpd_days 90 THEN 1 ELSE 0 END) AS default_flag FROM observe o JOIN repay_record r ON o.loan_id r.loan_id WHERE r.repay_date o.observe_end AND r.repay_date DATE_ADD(o.observe_end, INTERVAL 12 MONTH) GROUP BY o.loan_id;这段 SQL 做的事情是先锁定每个贷款在观察期内的最大日期作为观察点再统计观察点之后 12 个月内是否出现过逾期超过 90 天的情况。dpd_days 在 SQL 里是还款记录表中的逾期天数快照实际项目中可能要用逾期阶段字段替代。先用伪代码把这套逻辑写在文档里再对着资源里的实际字段做映射比拿到数据就开跑要稳妥得多。标签构造还有一个细节表现期设置多长。12 个月是零售信贷里比较常见的口径既能覆盖大部分违约暴露又不至于让样本过期太久。如果你的数据历史较短可以缩短到 6 个月但要注意违约率会偏低模型区分度也会受影响。资源里如果给了不同口径的标签版本建议都跑一遍对比而不是只用一个。3. 建模逻辑回归打底LightGBM 做主力3.1 逻辑回归基线可解释性永远有用个贷风控场景有个特殊性监管和内部审计都要求模型“能说清楚为什么拒绝或通过一个客户”。逻辑回归在这一点的天然优势是——每个特征的系数直接对应风险方向。系数为正代表该特征值越高违约风险越大为负则相反系数大小代表影响强度。这也是为什么即使树模型效果更好银行信贷体系里逻辑回归和评分卡依然是主流。这份资源里同时提供了两种建模路径我的建议是先跑逻辑回归把它当成全流程的“体检工具”特征有没有放反方向、标签有没有构造错都会直接反映在系数上。from sklearn.linear_model import LogisticRegression from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler lr_pipeline Pipeline([ (scaler, StandardScaler()), (lr, LogisticRegression( C0.5, class_weightbalanced, max_iter500, random_state42 )) ]) lr_pipeline.fit(X_train, y_train)这里做两件关键的事StandardScaler 标准化和 LogisticRegression 里的 class_weightbalanced。标准化对逻辑回归是必须的因为正则化项对特征尺度敏感不标准化会让正则惩罚集中在量纲大的字段上。class_weight 是因为个贷违约率通常在 1%5% 之间不做均衡处理模型会倾向于把所有样本预测成“正常”AUC 看着还凑合但坏客户一个都抓不出来。C 值控制正则强度0.5 属于比默认稍强的约束可以防止特征过多时系数膨胀。max_iter500 是给模型足够的迭代次数收敛默认的 100 在某些标准化后的数据上会报“不收敛”的警告。逻辑回归跑完你要看三样东西系数方向和业务常识是否一致、训练集和验证集的 AUC 差距、以及坏样本召回率。如果系数方向反了先别调参回去查特征是不是用了未来数据如果坏样本召回率为零检查 class_weight 有没有生效。这三点过了再进树模型。3.2 LightGBM 参数先理解再抄默认LightGBM 是当前个贷违约预测的主力模型训练快、对缺失容忍度高、能自动捕捉非线性关系。但它的参数自由度也让很多人头疼我见过不少直接把默认参数跑到底、然后抱怨过拟合的人。下面是一组我做信贷项目时常用的起步参数你先跑再根据验证集表现去调import lightgbm as lgb params { objective: binary, metric: auc, learning_rate: 0.05, num_leaves: 31, min_data_in_leaf: 50, feature_fraction: 0.8, bagging_fraction: 0.8, bagging_freq: 1, lambda_l2: 1.0, verbose: -1 } d_train lgb.Dataset(X_train, labely_train) d_valid lgb.Dataset(X_val, labely_val) model lgb.train( params, d_train, num_boost_round1000, valid_sets[d_valid], callbacks[lgb.early_stopping(stopping_rounds100, verboseTrue)] )几个关键参数值得展开说不建议直接照抄参数作用我的设置逻辑learning_rate每棵树贡献的权重0.05 偏低配合更多迭代数换取更平滑的收敛num_leaves树复杂度上限31 起步特征多且非线性强时可以上调到 64min_data_in_leaf叶子节点最少样本数设 50 防止学到只覆盖几十个样本的极端规则feature_fraction每棵树随机抽特征比例0.8 增加树间差异性减少过拟合bagging_fraction每棵树随机抽样本比例0.8 同样是增加随机性必须配合 bagging_freq1lambda_l2L2 正则强度1.0 起步特征多了可以往上加到 5early_stopping 的作用是防止迭代过多导致过拟合每跑完一轮如果验证集 AUC 连续 100 轮没有提升就自动停止并回滚到最优迭代次数。这里有个新版 LightGBM 的坑early_stopping 必须放在 callbacks 列表里传不能再像旧版本那样直接作为 lgb.train 的参数传否则会报 TypeError。我在 5.4 节会详细说这个坑。跑完之后不要只盯着 AUC。个贷场景下我会额外看 KS 和坏样本召回率。AUC 衡量的是排序能力KS 更直观地表示好坏样本分数分布的分离程度信贷业务里一般要求 KS 在 0.3 以上才算有区分度。坏样本召回率则直接回答业务问题模型如果圈定 20% 的客户拒绝掉能覆盖多少真正的坏客户这个指标决定了风控策略的性价比。3.3 两个模型怎么对比对比维度逻辑回归LightGBM可解释性系数直接解释风险方向和强度需借助 SHAP 或特征重要性缺失值处理必须先补齐原生支持可留空非线性关系需手工分箱或做 WOE自动学习但更易过拟合训练速度秒级分钟级需早停典型表现区分度下限基线比 LR 常见提升 0.02~0.05 的 AUC实际项目里这两个模型不是替代关系而是参照关系。即使你最终要上线的是 LightGBM我也建议保留逻辑回归结果它的系数能帮你排查特征逻辑问题它的坏样本召回率能帮你判断树模型到底“多抓到了谁”。如果树模型提升不明显先怀疑特征工程而不是继续调参。4. 源码包怎么用目录、执行顺序与数据替换4.1 拿到源码包先做目录比对这份资源下载下来是一个压缩包里面除了源代码还有文档说明和数据集。我的习惯是先解压、读文档、列目录五分钟内搞清楚里面有哪些文件再开始动手。下面是我在多个类似项目里推荐的文件组织方式你可以拿它去对照资源包的实际结构文件/目录作用你该做什么data/脱敏后的原始数据与数据字典先读字段表别直接跑模型src/清洗、特征、训练、评估脚本按编号顺序看代码output/模型文件、预测结果、评估图表跑完后在这里对结果docs/文档说明先花十分钟读一遍如果资源包里的目录命名和你预期的不同不要慌按文件后缀去认.py 是脚本.csv 是数据.md 或 .docx 是说明.pkl 或 .txt 可能是模型文件。关键是确认一件事数据文件、脚本文件、说明文档三者都存在缺少任何一块都跑不出完整效果。确认齐全之后先把文档读一遍再动手。写文档的人通常会记录版本口径和已知问题这些信息比代码本身更值钱。4.2 按脚本顺序执行从原始表到结果文件我通常把整个流程拆成四个脚本编号即执行顺序每个脚本只做一件事输入输出都是文件而不是内存变量。好处是中间任何一步出问题重跑成本很低不用从头再来。pip install -r requirements.txt python src/01_data_cleaning.py python src/02_feature_engineering.py python src/03_train.py python src/04_evaluate.py四个脚本的职责边界如下01_data_cleaning.py读取原始数据输出清洗后的表。运行完检查行数和缺失率报表确认脏数据被清理干净。02_feature_engineering.py构造衍生特征比如负债收入比、近 6 个月查询次数、额度使用率。输出特征宽表。03_train.py切分训练集和验证集训练逻辑回归和 LightGBM输出模型文件。04_evaluate.py加载模型计算 AUC、KS、坏样本召回率输出评估图表。执行顺序不能乱因为每个脚本的输入是上一个脚本的输出。我最常被问到的问题是“为什么我直接跑了 03_train.py 报找不到文件”——就是因为前面两步的输出没生成。另外注意02 和 03 之间有一个隐含约定特征宽表的主键和标签表要能对上否则 join 完会产生大量空行。如果跑完 03 发现训练的样本量比预期少很多优先去查这两个脚本里有没有做 inner join。4.3 换成自己的数据改配置不碰代码这套源码包复现出来之后大概率你会想换成自己的数据集试一遍。我比较推荐的做法是把路径、字段名、时间口径全部抽到配置区而不是到处修改业务代码。这样换数据时只需要改配置代码一行不动避免改坏原本能跑的流程。# config.py集中管理所有可调项 DATA_PATH data/raw_data.csv LABEL_COL default_flag # 标签列名 # 时间窗口参数决定特征与标签的切分方式 OBSERVE_END 2023-06-30 # 观察期末尾 PERFORMANCE_MONTHS 12 # 表现期长度 # 建模时剔除的字段通常是主键和日期 FEATURE_DROP [loan_id, customer_id, apply_date] # 模型参数训练脚本从这里读取 LR_C 0.5 LGB_LEARNING_RATE 0.05 LGB_NUM_LEAVES 31有三处是换数据时必改的一是 DATA_PATH 路径二是 LABEL_COL 标签列名三是 OBSERVE_END 观察期末尾。前两个好理解第三个是坑最多的。很多人在自己的数据上复现时直接沿用原来的日期导致训练集和验证集时间范围重叠评估结果虚高。如果你不清楚自己的数据哪一天可以充当观察期末尾就先做探索性分析看各字段的时间覆盖情况。还有一类字段必须在建模前剔除主键、客户 ID、申请日期。这些字段要么是唯一值没有区分能力要么是时间字段会带来数据泄漏要么是身份信息根本不参与风控决策。我在 FEATURE_DROP 里把这类字段集中管理就是为了避免每次建模时都要从特征列表里挑一遍。5. 踩坑记录个贷数据上最容易翻车的四个细节5.1 数据泄漏把“未来信息”当特征现象训练时 AUC 虚高 0.1 以上验证集表现也相当漂亮但上线后模型区分度断崖式下跌业务反馈“感觉跟瞎猜差不多”。原因特征里混入了只有在表现期结束后才能拿到的信息。最典型的是把“当前逾期阶段”或者“催收状态”放进了特征列——这些字段在观察期末尾可能还是 0但逾期 90 天的状态到表现期才暴露模型等于直接看到了答案。解决我的习惯是给每个特征写一行“可用时点”说明统一约束在观察期末尾之前生成。筛选技巧很简单如果一个字段的值会随着时间变化且变化方向与坏账强相关先怀疑它是否泄漏。排查手段是看特征重要性和单变量 AUC如果某个特征的 IV 值高得不正常优先检查它的时间语义。5.2 类别不平衡默认参数直接全预测 0现象逻辑回归设置 class_weightbalanced 后还能看但换成 LightGBM 默认参数训练后输出混淆矩阵发现坏样本一个都没抓住坏样本召回率为 0。原因个贷数据违约率常在 1%~5% 之间负样本占据绝对多数。LightGBM 默认用整体准确率作为优化方向把全部样本判为“正常”就能拿到极高的准确率所以模型选择了偷懒路径。解决在 LightGBM 里用 scale_pos_weight 调节正负样本权重我没有采用 is_unbalanceTrue因为它同时改变采样策略在某些场景下不稳定。scale_pos_weight 的取值可以用负样本数除以正样本数得到初始值也可以用网格搜索在 5、10、20 之间试。注意采样策略和权重调整都只作用于训练集验证集必须保持真实分布否则评估指标会失真。5.3 随机切分 vs 时间切分验证集乐观偏差现象用 train_test_split 随机切分验证集 AUC 和 KS 都很好看但上线后效果明显变差回测时发现训练集和验证集中包含了同一时间段的客户。原因随机切分默认假设样本独立同分布但信贷数据天然带有时间结构。同一批客户的申请集中在某几个月随机切分会让训练集和验证集共享相似的宏观环境和市场状态模型学到的“规律”在换个时间段后未必成立。解决固定按时间切分训练集取观察期之前的历史数据验证集取观察期之后的一段连续时间保持验证集在时间上严格晚于训练集。更严谨的做法是 walk-forward 验证多切几个时间段滚动评估我在第 6 章会详细说。5.4 LightGBM 早停回调新版本 API 的兼容坑现象照抄网上老代码把 early_stopping_rounds 直接当作 lgb.train 的参数传入程序直接抛 TypeError提示 unexpected keyword argument。原因LightGBM 从 3.0 开始早停逻辑统一由 callbacks 机制接管旧的传参方式被移除。网上大量教程还是老写法照抄就会踩坑。解决使用 callbacks 列表传入写法是 callbacks[lgb.early_stopping(stopping_rounds100, verboseTrue)]。注意早停依靠验证集评估因此 lgb.train 里必须传 valid_sets否则早停无效。6. 结果怎么验证才算稳时间外样本才是试金石6.1 按时间滚动切分模拟线上真实环境单一时间切分只能证明模型在“某一个时间段之后”有效不能证明它在不同市场环境下都稳定。我在项目上线前会强制走一遍 walk-forward 验证把数据按时间切成多段逐段滚动训练和评估time_points [2022-12-31, 2023-03-31, 2023-06-30, 2023-09-30] for i, cut in enumerate(time_points): train_df df[df[observe_end] cut] val_df df[(df[observe_end] cut) (df[observe_end] time_points[i 1] if i 1 len(time_points) else True)] # 每次循环都重新训练并记录验证集指标 model train_model(train_df) metrics evaluate_model(model, val_df) print(f验证时点: {cut} AUC: {metrics[auc]:.4f} KS: {metrics[ks]:.4f})这段代码的输出是一组随时间变化的指标序列而不是单点值。如果各个时间段的 AUC 和 KS 波动剧烈说明模型对宏观环境敏感需要回到特征工程层面找原因如果指标稳定才能放心进入上线流程。把“验证一次就通过”改成“多个时点都通过”是我吃过亏之后养成的习惯。分数段分析比只看 AUC 更能指导业务。把模型输出的违约概率排序后分成十档统计每一档的真实违约率。正常模型应该是分数越高违约率单调上升如果出现中间档位违约率倒挂说明特征或标签某个环节有问题。这个检查动作我每次都会做。最后说句实在话我最初做违约预测时也吃过数据泄漏的亏一个看起来 AUC 0.85 的模型换了个时间段直接被打回原形。从那以后我每次建模都强制把“时间切分、标签时点、特征可用性、验证时点”这四件事写在项目文档最前面跑数之前逐项打勾。这份资料给出的源码和数据集足够你把这一整套流程完整走一遍把该踩的坑提前踩完。希望帮到你。本文还有配套的精品资源点击获取