简介:围绕中原银行个贷违约预测赛题,这套资源以Python实现了从数据清洗、特征工程到模型预测的完整流程,定位清晰,适合金融风控方向学生、竞赛参与者及毕设开发人群做项目复现与思路参考。资源从迁移学习视角切入银行新客群风控难题,代码按数据清洗、特征构建、标签与类别及时间特征加工、主流程运行等模块组织,同时提供公开测试集、样例提交文件与说明文档,便于对照赛题环境开展实验。压缩包共9个文件,核心为6个Python脚本,配合2个CSV数据文件与1个Markdown说明,整体仅311KB,轻量紧凑。该资源已有98人学习,内容包括完整可运行的赛题工程代码、文档与数据集,可在其基础上扩展个人设计,也可用于系统梳理个贷违约预测的主要环节。
1. 用 Python 做中原银行个贷违约预测:一份能跑通全流程的源码包
用 Python 做个人贷款违约预测,网上能搜到的项目不少,但“源码能跑、文档能对上、数据能复现”三者齐全的不多。这份围绕中原银行个贷业务场景的违约预测资源,把源代码、说明文档、脱敏数据集打包在一起,目标很直接:让一个刚接触风控建模的人也能在一台普通笔记本上跑出完整结果。整个流程覆盖数据清洗、特征构造、模型训练、评估对比到结果导出,不依赖生产环境组件。适合两类人:一类是想把教科书里的分类模型落到真实结构化数据上的学生,另一类是正要搭建个人信贷评分流程、需要先跑通基线版本的风控工程师。做完这一遍,你对特征怎么选、标签怎么定、验证集怎么切会有非常具体的体感。
2. 数据与预处理:先摸清字段,再谈清洗和标签
2.1 个贷数据集的字段结构与业务口径
拿到数据第一步不是建模,而是读数据字典。这份资源里的文档说明部分带了字段表,建议你先花十分钟把每个字段的业务含义过一遍。结合银行零售贷款的常见口径,个贷数据通常分三类:客户基础信息(年龄、婚姻、学历、收入),贷款要素(贷款金额、期限、利率、还款方式、担保方式),历史行为(征信查询次数、近 12 个月逾期次数、已有负债收入比)。下面是我见过的典型字段组织方式,你可以对照着去认资源里的实际表:
| 字段 | 类型 | 用途说明 |
|---|---|---|
| customer_id | id | 客户唯一标识,建模时剔除 |
| 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", encoding="utf-8") print("样本量与字段数:", df.shape) print("字段类型分布:") print(df.dtypes.value_counts()) print("缺失率最高的 10 个字段:") print(df.isnull().mean().sort_values(ascending=False).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( C=0.5, class_weight="balanced", max_iter=500, random_state=42 )) ]) lr_pipeline.fit(X_train, y_train)这里做两件关键的事:StandardScaler 标准化,和 LogisticRegression 里的 class_weight="balanced"。标准化对逻辑回归是必须的,因为正则化项对特征尺度敏感,不标准化会让正则惩罚集中在量纲大的字段上。class_weight 是因为个贷违约率通常在 1%~5% 之间,不做均衡处理模型会倾向于把所有样本预测成“正常”,AUC 看着还凑合,但坏客户一个都抓不出来。C 值控制正则强度,0.5 属于比默认稍强的约束,可以防止特征过多时系数膨胀。max_iter=500 是给模型足够的迭代次数收敛,默认的 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, label=y_train) d_valid = lgb.Dataset(X_val, label=y_val) model = lgb.train( params, d_train, num_boost_round=1000, valid_sets=[d_valid], callbacks=[lgb.early_stopping(stopping_rounds=100, verbose=True)] )几个关键参数值得展开说,不建议直接照抄:
| 参数 | 作用 | 我的设置逻辑 |
|---|---|---|
| learning_rate | 每棵树贡献的权重 | 0.05 偏低,配合更多迭代数换取更平滑的收敛 |
| num_leaves | 树复杂度上限 | 31 起步,特征多且非线性强时可以上调到 64 |
| min_data_in_leaf | 叶子节点最少样本数 | 设 50 防止学到只覆盖几十个样本的极端规则 |
| feature_fraction | 每棵树随机抽特征比例 | 0.8 增加树间差异性,减少过拟合 |
| bagging_fraction | 每棵树随机抽样本比例 | 0.8 同样是增加随机性,必须配合 bagging_freq=1 |
| lambda_l2 | L2 正则强度 | 1.0 起步,特征多了可以往上加到 5 |
early_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_weight="balanced" 后还能看,但换成 LightGBM 默认参数训练后,输出混淆矩阵发现坏样本一个都没抓住,坏样本召回率为 0。
原因:个贷数据违约率常在 1%~5% 之间,负样本占据绝对多数。LightGBM 默认用整体准确率作为优化方向,把全部样本判为“正常”就能拿到极高的准确率,所以模型选择了偷懒路径。
解决:在 LightGBM 里用 scale_pos_weight 调节正负样本权重,我没有采用 is_unbalance=True,因为它同时改变采样策略,在某些场景下不稳定。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_rounds=100, verbose=True)]。注意早停依靠验证集评估,因此 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 的模型,换了个时间段直接被打回原形。从那以后我每次建模,都强制把“时间切分、标签时点、特征可用性、验证时点”这四件事写在项目文档最前面,跑数之前逐项打勾。这份资料给出的源码和数据集,足够你把这一整套流程完整走一遍,把该踩的坑提前踩完。希望帮到你。
本文还有配套的精品资源,点击获取