☰
零基础入门贷款违约预测:Python风控源码包与特征工程全解析
2026/9/28 1:53:34 网站建设 项目流程

简介:面向零基础入门金融风控的学习者,一个基于Python的贷款违约预测项目包含完整源码与项目说明,可直接用作金融风控方向的课程设计或期末大作业。项目经导师指导并获97分高分,覆盖数据探索、特征工程、模型训练与集成融合等多个核心环节,适合希望快速上手完整赛题方案的初学者参考。资源共10个文件,压缩包约380KB,主要包含6个Python脚本(分别封装LightGBM、XGBoost、CatBoost与集成模型等算法)、1份EDA探索性分析Notebook、1份Word版项目说明手册,以及特征重要性可视化图等,目录结构清晰便于按模块阅读。目前已有131人学习下载。下载即可运行,配合说明文档可以快速理解特征构造思路、模型参数调优与结果评估逻辑,既能用于课程设计报告撰写,也能支撑期末项目答辩演示。

1. 零基础入门贷款违约预测:这份 Python 风控源码包里到底有什么

做金融风控的期末大作业,最怕的不是模型不会调,而是从零搭一套完整流程:数据清洗、特征工程、三四个模型对比、融合、写报告,每一步都是一整夜的坑。这份零基础入门金融风控的贷款违约预测源码包,把这条路直接铺平了——解压之后你能看到gen_feas.py特征生成、lgb.py/xgb.py/catboot.py三个单模型、ensemble.py融合脚本、EDA.ipynb探索分析,外加一份 97 分项目说明手册。它不是只有训练脚本的玩具代码,而是把特征、建模、评估、文档串起来的完整工程。适合三类人:要交课程设计的本科生、想快速入门金融风控建模的转行者、需要一套 baseline 跑对比实验的研究生。

2. 先把特征工程做厚:EDA 与 gen_feas.py 里的衍生特征套路

2.1 EDA.ipynb 先看什么:分布、缺失与违约率的单调性

拿到贷款数据第一步不是建模,而是先理解数据长什么样。这个项目的EDA.ipynb是我见过比较务实的探索流程,核心就四件事:看形状和类型、看数值分布、看缺失情况、按违约标签分组看特征差异。其中最关键的一步是看每个特征分箱后违约率的变化趋势——如果某个特征从低分段到高分段违约率是单调递增的,这个特征进模型基本稳了;如果忽高忽低没有规律,多半是噪音或者分箱方式不对。

# EDA.ipynb 里的核心检查片段 import pandas as pd import matplotlib.pyplot as plt df = pd.read_csv('loan_data.csv') print(df.shape) print(df.dtypes.value_counts()) # 按贷款金额分箱,观察违约率变化 df['amount_bin'] = pd.qcut(df['loan_amount'], q=5, duplicates='drop') grouped = df.groupby('amount_bin', observed=True)['is_default'].mean() print(grouped) # 缺失率超过 40% 的列列出来 missing_ratio = df.isnull().mean() print(missing_ratio[missing_ratio > 0.4])

这段代码的逻辑是用qcut把连续变量切成五档,然后算每档的违约率均值。observed=True这个参数是 pandas 2.x 处理分类分箱时防止警告用的,老版本可以不加。qcut和cut的差别在于前者按分位数切,样本量更均匀,后者按固定数值区间切,适合有业务含义的边界。看违约率曲线时我会额外画一张折线图,比打印数值直观得多。

2.2 gen_feas.py 拆解:缺失值插补与衍生特征的一段可复用代码

gen_feas.py是这套源码里含金量最高的文件,因为期末作业的分数差距基本都拉在这里。贷款风控场景里有一批固定套路衍生特征:负债收入比(每月债务支出除以月收入)、信用历史长度、利率与信用分的交叉项,以及"无抵押记录"这种有意义的缺失。这些特征在业务上都有明确解释——负债收入比衡量还款压力,信用历史长度衡量信用积累,都是银行审批时真正会看的指标。

# gen_feas.py 核心片段:贷款场景的衍生特征与缺失值处理 import numpy as np import pandas as pd def gen_features(df): # 负债收入比:每月债务支出 / 月收入,分母加极小值防除零 df['dti'] = df['monthly_debt'] / (df['monthly_income'] + 1e-6) # 信用历史长度:以月为单位 df['credit_history_months'] = ( df['report_date'] - df['first_credit_line_date'] ).dt.days / 30 # 有意义的缺失单独成类:没有房贷记录的人,标记为 0 df['has_mortgage'] = np.where(df['mortgage_amount'].isnull(), 0, 1) df['mortgage_amount'] = df['mortgage_amount'].fillna(0) # 利率与信用分的交叉:两者对违约有交互作用 df['rate_score_ratio'] = df['loan_rate'] / (df['credit_score'] + 1e-6) return df

这段代码有三个细节值得抄。第一,1e-6加在分母上是防御性编程,防止某个样本月收入恰好是 0 导致除零报错或者产生无穷大值。第二,has_mortgage这个特征非常重要——没有房贷记录不等于没有贷款能力,可能是租房或者无房,直接fillna(0)会把这个信息丢掉,拆成两个特征反而让模型自己学。第三,rate_score_ratio是典型的交叉特征,利率高且信用分低的人违约概率显著上升,单一特征很难捕捉这种联合效应。做特征的时候我一般会把衍生特征都放一起,最后统一做标准化,避免量纲影响树模型的分裂效果。

2.3 特征筛选的粗筛与精筛:相关性矩阵 + IV 排序

特征做多了之后必须做筛选,否则噪音特征会拖低模型表现。这套项目里用的是粗筛加精筛两层:粗筛用相关性矩阵,去掉相关系数超过 0.8 的冗余特征;精筛用 IV 值(Information Value)排序,保留 IV 大于 0.02 的特征。IV 是风控领域衡量特征预测能力的经典指标,0.02 以下基本没有区分度,0.1 以上算中等,0.3 以上算强。

# 特征筛选:先算相关性矩阵去冗余,再算 IV 排序 corr = X.corr().abs() upper = corr.where(np.triu(np.ones(corr.shape), k=1).astype(bool)) high_corr_cols = [col for col in upper.columns if any(upper[col] > 0.8)] print('high correlation cols:', high_corr_cols) # IV 计算:按分箱统计好样本与坏样本分布 def calc_iv(df, feature, target): df = df[[feature, target]].dropna() df['bin'] = pd.qcut(df[feature], q=10, duplicates='drop') grouped = df.groupby('bin', observed=True)[target].agg(['sum', 'count']) grouped['good'] = grouped['count'] - grouped['sum'] grouped['bad_rate'] = grouped['sum'] / grouped['count'] grouped['woe'] = np.log((grouped['bad_rate'] + 1e-6) / (1 - grouped['bad_rate'] + 1e-6)) iv = ((grouped['bad_rate'] - (1 - grouped['bad_rate'])) * grouped['woe']).sum() return iv

相关性矩阵那块代码用了np.triu取上三角,避免重复计算 A-B 和 B-A 两组。IV 计算里woe的全称是 Weight of Evidence,表示每个分箱里坏样本占比相对于好样本占比的对数差异,IV 就是所有分箱 woe 的加权和。注意qcut分箱时如果特征有大量重复值会报错,duplicates='drop'就是处理这个的。实际跑的时候,IV 排序结果和features_importance.png里 LightGBM 的特征重要性排序通常高度一致,如果出现不一致,优先怀疑有没有数据泄漏或者特征被重复计算。

3. 三份模型脚本读透:LightGBM、XGBoost、CatBoost 的调参与差异

3.1 lgb.py 的参数面板:objective、num_leaves 与早停的配合

lgb.py是这套项目的建模主力。LightGBM 在金融风控里是默认首选,原因很实在:训练速度快、对缺失值天然友好、AUC 表现稳。文件里的参数设置是一个标准的竞赛级配置,核心逻辑是给足n_estimators,然后用early_stopping_rounds在验证集 AUC 不再提升时自动截断,这样既不会欠拟合也不用手动试轮数。

# lgb.py 核心训练流程 import lightgbm as lgb from sklearn.model_selection import train_test_split X_tr, X_val, y_tr, y_val = train_test_split( X, y, test_size=0.2, stratify=y, random_state=42 ) model = lgb.LGBMClassifier( objective='binary', metric='auc', learning_rate=0.05, n_estimators=2000, # 给足轮数,靠早停收住 num_leaves=31, colsample_bytree=0.8, subsample=0.8, subsample_freq=1, reg_alpha=0.1, reg_lambda=1.0, random_state=42, ) model.fit( X_tr, y_tr, eval_set=[(X_val, y_val)], eval_metric='auc', early_stopping_rounds=50, categorical_feature=['employment_status'], )

参数逻辑说明:objective='binary'指定二分类任务,metric='auc'让训练过程的评估指标用 AUC 而不是默认的 logloss,这是因为贷款违约场景下正负样本通常不平衡,AUC 对阈值不敏感,更能反映排序能力。learning_rate=0.05配合 2000 棵树是常见组合,学习率越低需要的树越多,但精度不一定线性提升。num_leaves=31是 LightGBM 的核心参数,控制单棵树的叶子数,数值太大容易过拟合,太小欠拟合,一般从 31 起步调。reg_alpha和reg_lambda分别是 L1 和 L2 正则,项目里两个都加了,说明作者被过拟合坑过。categorical_feature显式声明类别列,这个参数特别关键——如果不告诉 LightGBM 哪些是类别特征,它会把类别编码当成连续值切分,效果会差一截。

3.2 xgb.py 与 catboot.py:两套框架的写法差异与效果对比

xgb.py和catboot.py是三模型对比的另外两块拼图。XGBoost 是经典方案,参数习惯和 LightGBM 不同:用max_depth控制树深而不是num_leaves,学习率叫eta而不是learning_rate。CatBoost 在文件里拼写成了catboot,但实质就是 CatBoost,它最大的卖点是能自动处理类别特征,不需要手动做 one-hot 编码,对高基数类别特征特别友好。

参数维度lgb.pyxgb.pycatboot.py
树生长方式leaf-wise,按损失增益分裂level-wise,按层分裂对称树,每层所有节点同规则
类别特征处理需显式指定 categorical_feature需手动编码或指定自动识别并处理
主要调参对象num_leaves, learning_ratemax_depth, etadepth, learning_rate
默认缺失值处理自动跳过并按分布归边自动学习缺失方向自动处理
训练速度最快,大数据集优势明显中等最慢,但精度稳定

三个模型的写法差异不算大,都是 sklearn 风格的Classifier接口,但底层逻辑差别明显。XGBoost 按层生长,每层所有叶子一起分裂,对参数不敏感但树更深;LightGBM 按叶子生长,只分裂损失增益最大的叶子,收敛快但也更容易过拟合,所以需要有num_leaves和正则兜底。CatBoost 的对称树每层用完全相同的分裂规则,配合 ordered boosting 减少预测偏移,在小数据集上往往是最稳的。实操中我一般会让三个模型各自跑一遍,看验证集 AUC 谁高就用谁做主要提交,另外两个留着做融合素材。

3.3 三个模型跑完怎么横向对比:AUC 与 KS 一把尺子

三个模型脚本都跑完之后,不能只看 Accuracy,要拿同一把尺子量:AUC 和 KS。AUC 衡量的是模型把违约用户排在正常用户前面的概率,KS 衡量的是好坏样本累计分布的最大差距。风控报告里这两个指标是标配,答辩时老师一定会问。

# 模型对比:计算 AUC 与 KS from sklearn.metrics import roc_auc_score, roc_curve for name, pred in [('lgb', pred_lgb), ('xgb', pred_xgb), ('cat', pred_cat)]: auc = roc_auc_score(y_val, pred) fpr, tpr, _ = roc_curve(y_val, pred) ks = max(tpr - fpr) print(f'{name}: AUC={auc:.4f}, KS={ks:.4f}')

KS 的计算方式是用roc_curve返回的 TPR 减去 FPR 取最大值,代表模型区分好坏样本的最大能力。金融风控里 KS 大于 0.3 就算可用,大于 0.5 已经相当强了。跑对比的时候有个细节:三个模型的预测概率分布不完全一样,CatBoost 的概率普遍偏保守,直接对比绝对值没有意义,重点看 AUC 排序。如果某个模型 AUC 明显低于其他两个,先检查是不是特征处理不一致,比如 XGBoost 没有做类别特征编码、或者categorical_feature没传对。

4. ensemble.py 的融合逻辑:加权平均与 Blending 的落地代码

4.1 为什么单模型到顶之后要融合:偏差方差与排名差异

单模型调参调到一定程度,AUC 基本就卡住了,这时候再花时间调参收益很低,真正能往上顶一步的是模型融合。背后的原因不玄学:三个模型用不同的方式学习数据,误差来源不一样,LightGBM 偏向局部最优,CatBoost 处理类别特征能力强,XGBoost 在正则上更稳健,把它们的结果加权平均,相当于把各自的误差互相抵消一部分。前提是模型之间的相关性不能太高,如果两个模型的预测结果完全相同,融合就没有意义。实操中我会先看三个模型预测值的相关系数矩阵,相关系数低于 0.9 才有融合价值。

4.2 ensemble.py 的加权融合代码:用验证集权重把 0.89 顶到 0.91

ensemble.py做的事情就是在验证集上搜索一组最优权重。常见做法是网格搜索或者用 scipy 的优化器,权重范围和步长按模型数量决定。三个模型时权重在 0 到 1 之间穷举,步长设 0.1,一共 66 组组合,跑起来很快。权重调完还有一招是 Blending:不直接加权概率,而是把三个模型的预测概率拼成新特征,再训练一个逻辑回归当第二层模型,效果通常比固定权重好一点,但更吃数据和调参。

# ensemble.py 加权融合:在验证集上找最优权重 import numpy as np from sklearn.metrics import roc_auc_score # 三个模型在验证集上的预测概率 pred_lgb = model_lgb.predict_proba(X_val)[:, 1] pred_xgb = model_xgb.predict_proba(X_val)[:, 1] pred_cat = model_cat.predict_proba(X_val)[:, 1] best_auc, best_w = 0, None for w1 in np.arange(0, 1.05, 0.1): for w2 in np.arange(0, 1.05 - w1, 0.1): w3 = 1 - w1 - w2 blend = w1 * pred_lgb + w2 * pred_xgb + w3 * pred_cat auc = roc_auc_score(y_val, blend) if auc > best_auc: best_auc, best_w = auc, (round(w1, 1), round(w2, 1), round(w3, 1)) print(f'best AUC: {best_auc:.4f}, weights: {best_w}')

权重搜索的逻辑是两层循环枚举 w1 和 w2,w3 由1 - w1 - w2算出,保证三个权重之和为 1。步长 0.1 对三个模型够了,追求更细可以改成 0.05,但收益递减。这里必须强调:权重只能在验证集上搜,不能拿测试集调权重,否则就是对测试集过拟合,交上去分数会很难看。跑完之后把最优权重存下来,对测试集预测时用同一组权重融合。实际经验是融合后 AUC 大概能涨 0.005 到 0.01,不要期待翻天覆地的变化,但期末作业里这 0.01 可能就是 90 分和 97 分的差距。

4.3 融合的边界:什么时候融合反而掉分

融合不是万能的,有三个典型场景会越融越差。第一个是某个单模型特别弱,AUC 比其他两个低 0.02 以上,此时它提供的更多是噪音,权重再小也会拖后腿,直接去掉反而更好。第二个是模型之间相关性太高,比如 LightGBM 和 XGBoost 用同一份特征同一套参数,预测结果相似度 0.95,融合等于重复投票。第三个是验证集和测试集分布差异大,在验证集上搜出的最优权重在测试集上可能完全失效。判断融合是否有效有一个简单方法:在验证集上分别算单模型 AUC 和融合后 AUC,融合后明显更高才值得保留,否则就是过度工程。

5. 期末大作业避坑指南:5 个把分数从 80 拉到 97 的关键检查点

5.1 坑 1:fillna 用全局统计量填充,验证集跟着遭殃

现象:线下验证 AUC 高达 0.93,换到测试集直接跌到 0.85,百思不得其解。原因:在特征工程阶段用了全量数据的均值或中位数填充缺失值,验证集和测试集的信息提前混进了训练集,这就是典型的数据泄漏。解决:先切分数据再填充,或者把填充统计量严格限定在训练集上计算。gen_feas.py里我一般会先做训练集和验证集的切分,再分别写填充逻辑:

# 正确的做法:填充统计量只从训练集计算 train, val = train_test_split(df, test_size=0.2, stratify=df['is_default'], random_state=42) fill_values = train[['monthly_income', 'credit_score']].median() train = train.fillna(fill_values) val = val.fillna(fill_values)

判断有没有泄漏的土办法是抽查:训练集和验证集同一特征的分布如果过于接近甚至完全一致,就要怀疑了。特别是fillna用了均值这种全局统计量,验证集被填进去的值等于训练集均值,模型学到的是"看到这个值就认为是平均用户",测试集上必然翻车。

5.2 坑 2:预测结果提交的是概率还是 0/1 标签

现象:模型 AUC 0.90,但提交后分数极低,或者混淆矩阵怎么看都不对。原因:直接用了predict()而不是predict_proba(),把模型的分类标签当成概率提交了。贷款违约预测的评估标准通常看 AUC 或 KS,这俩依赖预测概率的排序,不是类别。解决:统一用predict_proba()[:, 1]取正类概率,提交文件里保持这个概率列,不要转成 0/1。期末作业的说明文档里如果写了用 AUC 评估,这一步错就全盘皆输。

5.3 坑 3:pandas 的 not in 索引错位,混淆矩阵没法看

现象:用train[~train['id'].isin(val['id'])]切数据后发现行数对不上,模型评估时数组长度报错。原因:DataFrame 的索引没有重置,isin返回的结果是按索引对齐的,如果数据里存在重复索引或者索引不连续,筛选结果会乱掉。解决:数据处理初期先执行reset_index(drop=True),或者切分时改用train_test_split而不是手动isin过滤。

# 避免索引错位的稳妥写法 df = df.reset_index(drop=True) train_idx, val_idx = train_test_split(df.index, test_size=0.2, stratify=df['is_default'], random_state=42) train = df.loc[train_idx].reset_index(drop=True) val = df.loc[val_idx].reset_index(drop=True)

这个坑低级但非常常见,特别是从网上复制代码改的时候最容易踩。混用loc、iloc和布尔索引,索引一旦错位,后面的特征工程和模型训练全在脏数据上进行。

5.4 坑 4:LightGBM 报 bin 数不够,早停轮次白设

现象:LightGBM 训练时直接报错或者跑满 2000 轮不触发早停,训练时间长到离谱。原因:early_stopping_rounds设置了但n_estimators也给了一个大值,LightGBM 认为两者冲突;或者eval_set里的验证集包含太多缺失值,导致每个 bin 的样本数不足。解决:n_estimators给一个足够大的上限比如 2000,但不要手动指定最佳迭代轮数,让早停来定;min_data_in_leaf适当调大减少噪音分裂。还有一个常见配置错误是把early_stopping_rounds放在LGBMClassifier构造函数里而不是fit方法里,后者才生效。这个坑在lgb.py里已经被处理好了,但要理解为什么这么写。

5.5 坑 5:特征重要性图和手工算的对不上,先查重复列

现象:features_importance.png里排第一的特征和自己手工算的分组差异对不上,怀疑报告造假。原因:LightGBM 的feature_importance()默认返回的是split次数,也就是这个特征被用来分裂了多少次,不是信息增益gain,更不是业务上的预测能力排序。解决:想看真正的贡献度要显式指定importance_type='gain',或者用permutation_importance算置换重要性:

# 用 gain 看特征重要性,而不是默认的 split 次数 importance = model.booster_.feature_importance(importance_type='gain') feat_imp = pd.Series(importance, index=X.columns).sort_values(ascending=False) feat_imp.to_csv('feature_importance_gain.csv')

split次数高只说明这个特征频繁参与分裂,gain高才说明分裂带来的信息增益大。报告里写特征分析时,我会把gain排名和业务逻辑对照一遍:负债收入比、信用历史长度这些排名靠前是合理的,如果某个无关痛痒的特征排第一,基本就是泄漏或者数据清洗不干净。

6. 用特征重要性图反推业务结论:把技术报告写成答辩加分项

6.1 特征重要性图的三种读法

features_importance.png不只是用来展示模型做了什么,它是答辩时最有力的素材。拿到这张图先看三件事:排名靠前的特征是还款能力类还是信用记录类、有没有明显不符合业务逻辑的异常特征、top 特征之间是否高度相关。贷款场景里dti(负债收入比)、credit_history_months、loan_rate排在前列都说得通,说明模型学到的规律和信贷审核经验一致。如果某个特征排名异常高但业务上解释不了,优先怀疑数据泄漏,比如把未来信息放进特征里了。

6.2 把技术结论转成答辩话术

技术报告最容易犯的错是通篇写"我用了 LightGBM 参数调到最优",老师听到这种话只想追问到底怎么调的。正确的写法是拿特征重要性做桥,把模型结论翻译成业务判断。一个可以直接抄的表格结构:

特征模型排名业务含义报告话术
dti1每月债务占收入比例还款压力是违约的核心驱动因素,dti 越高违约概率越大,符合信贷审批的负债约束逻辑
credit_history_months2信用记录时长信用历史越长,信息越充分,违约风险越低,模型自动学会了"信用积累"这一风控信念
loan_rate3贷款利率利率与违约风险正相关,高风险用户被定价高利率,模型捕捉到了风险定价的印记

这套写法的好处是每句话都有据可查,既展示了建模能力,又展示了业务理解。我从那次之后每个项目都强制要求自己把 top 特征逐个往业务上靠,能写出合理解释的特征才保留,写不出来的宁可删掉。实践证明这个习惯帮我挡掉了至少三次数据泄漏的翻车,也让报告从"调参记录"变成了"风控分析"。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询