个贷违约预测实战:从数据清洗到模型评估的机器学习完整流程
2026/9/12 3:26:09 网站建设 项目流程

简介:这是一份机器学习课程个贷违约预测大作业的完整项目源码与配套文档,面向需要完成类似课程设计或入门信用风险预测的学习者。压缩包共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', encoding='gbk') # 年龄:过滤业务上不可能的取值 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(ascending=False).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, bins=5): # 用等频分箱,保证每箱样本量接近 df['bin'] = pd.qcut(df[col], q=bins, duplicates='drop') grouped = df.groupby('bin', observed=True).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', bins=6) print(result[['total', 'bad_rate', 'woe', 'iv']])

这段代码实现的是最基础的等频 WOE 分箱。逻辑说明:bad_rate是每个分箱内的违约占比,它随分箱的变化趋势决定了这个变量有没有区分度;woe是该箱坏客户占比与好客户占比之比的对数,数值符号说明这一箱相对整体是偏风险还是偏安全;iv是每个分箱的 IV 贡献,所有分箱 IV 之和就是该变量的总 IV。参数上,bins=5是起点,样本量够大可调到 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), k=1).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_size=0.2, random_state=42, stratify=y ) X_train, X_valid, y_train, y_valid = train_test_split( X_train, y_train, test_size=0.25, random_state=42, stratify=y_train ) print(f"Train: {X_train.shape}, Valid: {X_valid.shape}, Test: {X_test.shape}") print(f"Train 违约率: {y_train.mean():.4f}")

这段代码做了标准的三段切分:训练、验证、测试。逻辑说明:stratify=y保证了切分后各组违约率与全量一致,这在个贷这种不平衡数据里尤其重要,否则可能出现测试集里连一个违约样本都没有的情况。random_state=42固定了随机种子,保证每次跑分结果可复现,这是课程作业答辩时的基本素养。参数说明:test_size=0.2切出 20% 做最终测试,剩余 80% 中再切 25% 做验证——注意这里的 25% 是相对剩余部分的,实际验证集占比是 0.8×0.25=20%。三段切分比两段好的地方在于:验证集用于调参,测试集只碰一次,防止你对测试集反复调参导致「测试集泄漏」。

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(C=1.0, class_weight='balanced', max_iter=1000, random_state=42)) ]) 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(f"LR Valid AUC: {roc_auc_score(y_valid, valid_pred):.4f}")

逻辑说明:Pipeline把标准化和模型串在一起,避免对验证集单独做标准化时出现统计量泄漏;class_weight='balanced'让损失函数按样本数量的反比重新加权,默认情况下违约少数类会获得更大的权重,相当于内置了一次类别不平衡处理。C=1.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, label=y_train) d_valid = lgb.Dataset(X_valid, label=y_valid, reference=d_train) model = lgb.train( lgb_params, d_train, num_boost_round=500, valid_sets=[d_valid], callbacks=[lgb.early_stopping(100), lgb.log_evaluation(50)] ) valid_pred = model.predict(X_valid, num_iteration=model.best_iteration) print(f"LGB Valid AUC: {roc_auc_score(y_valid, valid_pred):.4f}")

这段代码是 LightGBM 训练的最小可用模板。关键参数说明:learning_rate=0.05配合num_boost_round=500和早停,比默认的大学习率加少轮数的组合稳定得多——学习率越小,每棵树对残差修正的步子越慢,越不容易过拟合,但需要更多棵树,训练时间线性增长;num_leaves=31控制树的复杂度,叶子数翻倍会使模型容量大幅增加,个贷这种千到百万级样本的任务 31 是合理起点;min_child_samples=100限制了每片叶子的最小样本量,是防止过拟合最重要的参数;subsamplecolsample_bytree分别做行采样和列采样,类似随机森林的思路,0.8 对个贷这种特征数中等的任务性价比很高。reg_alphareg_lambda是 L1/L2 正则,L1 还有特征选择的效果。

逻辑说明:reference=d_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_leavesmin_child_sampleslearning_ratesubsample)就足够了。

维度逻辑回归XGBoostLightGBM
训练速度
解释性高(系数可直接解释)低(需 SHAP/特征重要性辅助)
缺失值处理需预先填充原生支持原生支持
对特征缩放要求需要标准化不敏感不敏感
典型 Valid AUC 区间0.70-0.780.75-0.840.75-0.85
适合答辩展示点评分卡刻度特征重要性训练效率

逻辑说明:这张表是选型依据而非定论。逻辑回归的 AUC 下限低的原因是个贷特征和违约率之间多为非线性关系,线性模型天然吃亏;梯度提升树能捕捉到比如「收入高但查询次数也多」这种交叉模式。但要注意,逻辑回归低不代表没价值——它的输出天然是 0-1 之间的概率,转换成分数后稳定性更好,银行系评分卡至今仍在用。

4. 模型评估与阈值优化:AUC、KS 与业务成本

模型训练完,真正的考验在评估环节。课程大作业里最常见的错误是只看 AUC:AUC 高只能说明模型排序能力强,不代表你选了一个好阈值。个贷业务关心的是「在某个审批通过率下,违约率能压到多少」,这需要做阈值优化,而不是直接用 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', ascending=False).reset_index(drop=True) 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(f"KS: {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(index=False))

这段代码在 0.1 到 0.7 之间按 0.05 步长扫描阈值,每个阈值下都算一套业务指标。逻辑说明:approve_rate随阈值升高而降低,因为更多样本被判为违约被拒;recall随阈值升高而降低,因为阈值高意味着只有分数极高的样本才被判违约,大量低分违约样本漏过;precision通常随阈值升高而升高,因为高分样本中违约占比更高。参数说明:步长 0.05 用于快速预览,锁定区间后可以细化到 0.01 再跑一轮。最终选阈值的原则是找出「通过率下降斜率开始变陡」的拐点——再往上调阈值,通过率损失很大但坏账率降幅很小,说明模型已经把能分清的部分分完了。

5. 源码组织与复现验证:拿到 zip 包后怎么在 30 分钟内跑通

你拿到或者即将提交的这个个贷违约预测源码 zip,结构一般逃不出这几块:数据加载与清洗、特征工程、模型训练、评估可视化、主入口。拿到源码包之后,第一件事不是看代码,而是看requirements.txtenvironment.yml,确认依赖版本。个贷项目最常见的运行失败原因不是代码逻辑错误,而是 LightGBM 版本不一致导致模型文件无法加载,或者 pandas 版本升级后append方法被移除。建议直接按源码附带的依赖列表新建一个虚拟环境,不要用全局环境。

5.1 一个可复现的最小运行验证流程

不知道源码入口时,按下面的顺序快速检查文件结构:先找main.pytrain.py,再看有没有README.mdrequirements.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 解码。解决方法是读文件时显式指定encoding='gbk',更稳妥的做法是用encoding='gb18030',它是 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_predy_valid输出到predictions.csv,包含三列:样本 id、预测违约概率、真实标签。这样你可以用任意工具绘制 KS 曲线和 PR 曲线,不用重新训练就能反复验证阈值选择是否合理。这份文件也是答辩时老师最爱要的交付物——纸面上的分箱代码和训练代码都不如一条完整的预测结果更有说服力。

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

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

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

立即咨询