个人信贷违约预测实战:从特征工程到模型评估与阈值选择
简介基于机器学习的个人信贷违约预测识别项目包含完整源码与配套训练测试数据集面向计算机相关专业学生和从业者适用于毕业设计、课程大作业或期末项目等场景。压缩包共59个文件大小约142.33MB资源内CSV数据文件提供训练与测试样本py与go文件实现核心算法pth保存模型权重markdown与docx/pdf为说明和实验报告pptx可用于答辩演示代码、数据、文档与演示材料一应俱全。已有178人学习下载。项目评审分达97分经过严格调试可直接运行内含决策树、MLP等多种模型实现并附有成绩截图、实验报告、演示幻灯片与启动脚本既能帮助快速复现个贷违约预测流程也可作为算法对比分析和毕业设计撰写的有力参考方便在此基础上进一步改进、扩展或迁移到其他金融风控场景。1. 从样本不平衡到违约概率个人信贷预测项目到底在解决什么问题银行零售信贷的风控流程里个人信贷违约预测从来不是“把坏人找出来”这么简单。一个客户逾期了他在逾期之前的行为模式和正常客户之间往往只有细微的差别消费频次略降、夜间交易变多、还款金额卡在最低线。机器学习模型要做的是在这些微弱信号彻底恶化之前给出一个可排序、可解释、可干预的违约概率。标题里这个“项目源码训练测试数据集”的压缩包本质上就是一个完整的二分类建模流程从原始数据清洗、特征构造到模型训练、阈值选择最后输出每个样本的违约概率。这也是面试和课程设计里最常见的“高分项目”形态因为它在有限的数据集上同时考验了数据敏感度、算法选型和结果表达能力。实际做这类项目时新手最容易踩的坑是拿全量数据直接训练然后盯着准确率看。准确率在这类任务里几乎没有参考价值因为违约样本通常只占 3% 到 8%模型只要全都预测为“正常”准确率也能到 95% 以上。这个反直觉的结论是理解整个项目的关键个人信贷违约预测的核心评价指标不是准确率而是区分度——也就是模型能不能把违约客户的打分明显低于正常客户。AUC、KS、Lift 这些指标以及 Precision-Recall 曲线下的面积才是真正判断模型有没有用的标准。如果你手里已经拿到了这个项目的数据集先别急着跑代码先确认正负样本比例和特征字段的含义这会直接决定后续所有建模策略。适合读这篇文章的人是已经会用 pandas 和 sklearn 跑通基本流程但想知道信贷场景下如何把模型做到“能说服风控同事”的从业者和学生。2. 信贷违约数据集里的数据结构与标签定义2.1.1 原始字段的两种典型形态个人信贷数据集中最常见的字段形态有两种一种是以“用户静态属性历史行为汇总”为主的表格数据比如年龄、收入、职业、历史贷款次数、近 6 个月逾期次数另一种是包含还款时间序列的流水数据比如每个月的应还金额、实还金额、逾期天数。标题里的训练测试数据集如果是前者通常每一行是一个客户每一列是一个特征如果是后者就需要自己做聚合把时间序列转成统计特征。先用 pandas 把数据读进来看形状和字段类型这一步能帮你判断后面要不要做特征工程。import pandas as pd train_df pd.read_csv(train_data.csv) test_df pd.read_csv(test_data.csv) print(train_df.shape, test_df.shape) print(train_df.dtypes.value_counts()) print(train_df.head()) target_col is_default print(违约样本占比: {:.4f}.format(train_df[target_col].mean()))这段代码中shape告诉你训练集和测试集的行列数dtypes.value_counts()能快速看出字段是数值型还是对象型。违约样本占比用.mean()直接算出正样本比例这是判断不平衡程度的第一依据。如果占比低于 10%后续就要考虑类别权重或采样策略。2.1.2 标签定义里的三个隐藏风险信贷项目里标签的定义方式直接决定模型上限。常见的定义是“逾期 90 天以上记为违约”但这里有两个隐藏风险需要你拿到数据后立即确认。第一个是观察期和表现期是否分离训练集里每个样本的特征应该只包含“申请时点”之前的信息而标签来自申请之后的表现期。如果特征里混入了表现期内的数据就是典型的标签泄漏模型分数会虚高得离谱。第二个是删失问题有些客户在表现期内还没到 90 天但已经逾期 60 天这类样本要不要排除、或者标记成什么都影响模型学习。第三个是坏样本定义过窄或过宽只算“已核销”会让正样本太少把“逾期 30 天”也算进来又会把短期资金紧张的人错杀。处理方式上没有绝对正确答案但一个通用做法是在项目里保留标签定义的配置文件把“观察期 12 个月、表现期 6 个月、逾期 90 天为坏样本”这类规则明确写出来方便后续复现。另外需要检查训练集和测试集的分布是否一致一个简单的验证方法是比较两套数据里同一个特征的均值、缺失率和分位数。如果相差过大说明采样不随机模型上线后的表现会明显低于验证集。特征层面常见的直接可发现问题包括收入字段有大量 0 值、年龄有超过 100 的异常值、贷款金额分布右偏严重。这些都需要在预处理阶段统一处理否则同一个模型在不同时间段的预测稳定性会很差。2.1.3 特征缺失与数据类型清洗的最小流程在信贷数据里“缺失”本身常常就是信息。一个客户没有填写职业信息、没有公司电话这类缺失往往与违约率正相关。所以处理缺失值时不要一律填充可以保留一个is_missing标志位。数值型特征用中位数填充比用均值更稳健因为收入、负债这类字段长尾明显均值容易被极端值带偏。类别型特征中职业、教育程度这类有序类别可以映射成整数居住城市这种无序类别建议做目标编码target encoding或频次编码而不是独热编码炸维度。from sklearn.model_selection import train_test_split feat_cols [c for c in train_df.columns if c ! target_col] for c in feat_cols: if train_df[c].dtype object: train_df[c _missing] train_df[c].isna().astype(int) train_df[c] train_df[c].fillna(UNKNOWN) else: train_df[c _missing] train_df[c].isna().astype(int) train_df[c] train_df[c].fillna(train_df[c].median()) X train_df[feat_cols [c _missing for c in feat_cols if train_df[c].dtype object]] y train_df[target_col] X_train, X_val, y_train, y_val train_test_split(X, y, test_size0.2, random_state42, stratifyy)这里stratifyy保证训练集和验证集里的违约比例一致避免因随机拆分把正样本都切到某一侧。test_size0.2是常见拆分比例如果数据集本身很小比如不到 1 万行可以改成 0.3让验证集有更多样本去评估模型稳定性。缺失标志位_missing的加入是为了让模型自己学习“缺失”这一状态和违约之间的关系通常对 LR 和树模型都有正向作用。3. 机器学习模型选型对比从逻辑回归到梯度提升树的迭代路径3.1.1 逻辑回归为什么会是信贷场景的默认基线信贷风控领域深耕多年后逻辑回归依然是不可替代的基线模型核心原因有三个可解释性、稳定性和监管友好性。每一个特征的权重都能直接转成分数业务人员能看懂“收入每高一个档次分数加多少”这在贷前审批场景里非常重要。另一个原因是逻辑回归对特征的单调性敏感而信贷业务里的很多特征天然是单调的比如收入越高违约率越低、历史逾期次数越多违约率越高这类关系用逻辑回归能表达得很干净。用逻辑回归做基线时特征缩放是必须的因为梯度下降在特征量纲差异过大的时候收敛很慢L1/L2 正则化的惩罚力度也会被量纲影响。from sklearn.linear_model import LogisticRegression from sklearn.preprocessing import StandardScaler from sklearn.pipeline import Pipeline lr_pipeline Pipeline([ (scaler, StandardScaler()), (lr, LogisticRegression(class_weightbalanced, C0.5, max_iter1000)) ]) lr_pipeline.fit(X_train, y_train) val_prob_lr lr_pipeline.predict_proba(X_val)[:, 1]class_weightbalanced让逻辑回归自动调整类别权重正样本少时会给违约样本更高的惩罚系数。C0.5是正则化强度的逆值越小正则化越强在线性模型里适当降低 C 能减少对噪声特征的学习。max_iter1000是为了确保特征标准化后模型能收敛如果数据量大可以调到 2000。Pipeline把标准化和模型打包在一起避免在验证集上重复写变换逻辑也防止数据泄漏——标准化参数只从训练集学习这一要求在 Pipeline 里是天然保证的。3.1.2 树模型在非线性特征交互上的优势逻辑回归的局限性在于它默认特征与 log-odds 之间是线性关系。信贷数据里很多信号是非线性的年龄和违约率是 U 型关系年轻人和老年人的违约率都偏高收入与负债的比值在小范围变化时影响不大但超过某个阈值后违约率陡增。这种结构用逻辑回归需要手动做分箱或构造交乘特征而随机森林和梯度提升树能自动逼近这些非线性边界。在项目里通常对比三个模型逻辑回归、随机森林、XGBoost 或 LightGBM。随机森林对异常值和缺失值更鲁棒但泛化能力往往不如梯度提升树XGBoost 训练慢但精度高LightGBM 训练快且内存占用低是当前信贷竞赛和工业场景里最常见的配置。from sklearn.ensemble import RandomForestClassifier from xgboost import XGBClassifier rf_model RandomForestClassifier( n_estimators300, max_depth8, min_samples_leaf50, class_weightbalanced, random_state42, n_jobs-1 ) xgb_model XGBClassifier( n_estimators500, max_depth4, learning_rate0.05, subsample0.8, colsample_bytree0.8, scale_pos_weightsum(y_train 0) / sum(y_train 1), eval_metricauc, random_state42 ) rf_model.fit(X_train, y_train) val_prob_rf rf_model.predict_proba(X_val)[:, 1] xgb_model.fit(X_train, y_train, eval_set[(X_val, y_val)], verboseFalse) val_prob_xgb xgb_model.predict_proba(X_val)[:, 1]min_samples_leaf50是控制随机森林过拟合的关键参数信贷数据通常有几千到几万行叶节点样本数太小会让模型记住个别样本的噪声。max_depth8和n_estimators300的组合在大多数表格数据上已经是够用的范围继续加深树对 AUC 的提升非常有限但训练时间会成倍增加。XGBoost 里的scale_pos_weight是处理不平衡的正样本权重参数这里直接按负样本数除以正样本数来设置效果比class_weight更直接。colsample_bytree0.8让每棵树只用 80% 的特征增加模型多样性在高维特征场景下能明显压制过拟合。3.1.3 用 KS 和 AUC 判断哪个模型值得留下来模型对比不能只盯一个指标。AUC 衡量的是整体排序能力即随机抽一个违约样本和一个正常样本模型给违约样本打更高分的概率KS 衡量的是模型分数分布的最大分叉程度反映在某个阈值区间内模型能把两类人群分得多开。在实际审批场景里KS 比 AUC 更贴近业务直觉KS 值 0.3 以上说明模型有区分能力0.4 以上说明区分度优秀。下面这段代码一次性输出三个模型的 AUC、KS 和混淆矩阵在项目报告里用来回答“为什么最终选这个模型”这个问题。from sklearn.metrics import roc_auc_score, confusion_matrix import numpy as np def ks_score(y_true, y_prob): fpr_list [] tpr_list [] thresholds np.percentile(y_prob, np.arange(1, 100, 1)) for t in thresholds: pred (y_prob t).astype(int) tp ((pred 1) (y_true 1)).sum() fp ((pred 1) (y_true 0)).sum() fn ((pred 0) (y_true 1)).sum() tn ((pred 0) (y_true 0)).sum() tpr_list.append(tp / (tp fn) if (tp fn) 0 else 0) fpr_list.append(fp / (fp tn) if (fp tn) 0 else 0) return max(np.array(tpr_list) - np.array(fpr_list)) for name, prob in [(LR, val_prob_lr), (RF, val_prob_rf), (XGB, val_prob_xgb)]: auc roc_auc_score(y_val, prob) ks ks_score(y_val, prob) print(f{name}: AUC{auc:.4f}, KS{ks:.4f})ks_score的实现是按分数分位数遍历阈值每一步计算 TPR 和 FPR二者差值的最大值就是 KS。阈值取百分位数的好处是计算量小而且不管分数分布是否均匀都能覆盖到整个区间。验证集的 KS 比训练集低 0.1 以上时说明模型过拟合严重需要减少树的深度或增加min_samples_leaf。另外要注意KS 曲线上最大值对应的那个点往往就是后续选择审批阈值时最值得参考的位置这一点在后面会用到。3.1.4 信贷场景里的模型对比结果表下面这张表是这类项目最常见的对比结果格式在实际交付时可以直接放进项目 README 或实验记录里。不同数据集上的绝对值会有差异但规律基本一致逻辑回归稳定但上限低随机森林居中梯度提升树通常能拿到最高的 AUC 和 KS。需要注意的是精度高并不代表一定要选 XGBoost如果业务方要求每个特征对分数的贡献可解释逻辑回归或带 SHAP 分析的 XGBoost 才是更合适的选择。模型AUCKS训练时间千行数据可解释性备注逻辑回归0.780.42秒级高建议作为基准线随机森林0.810.45分钟级中可用特征重要性近似对异常值鲁棒XGBoost0.840.49分钟级低需配合 SHAP精度上限最高真实项目里最终选哪个要看部署环境。如果你是面试展示这个项目建议保留“LR 做基准、XGBoost 做主力”的双模型结构既展示了对比思维又避免了只用一个黑盒模型的尴尬。如果数据集特征维度不高少于 50 列随机森林和 XGBoost 的差距会缩小这时优先选随机森林部署和调试成本更低。4. 源码项目结构设计与训练测试数据集的组织方式4.1.1 一个能直接跑通的可复用工程结构拿到标题里说的“源码训练测试数据集”最容易让人迷惑的是里面几十个文件和 notebook 之间的关系。一个规范的信贷预测项目源码结构应该达到“拿到手后 5 分钟能跑通、1 小时能改参数重训”的标准。我一般会按下面的方式组织目录这也是竞赛和开源项目里最常见的约定。credit_default_project/ ├── data/ │ ├── raw/ # 原始数据只读不修改 │ │ ├── train.csv │ │ └── test.csv │ └── processed/ # 预处理后的训练测试数据集 ├── src/ │ ├── config.py # 路径、参数、标签定义 │ ├── preprocess.py # 清洗、缺失值、特征编码 │ ├── train.py # 模型训练与评估入口 │ ├── predict.py # 对新样本输出违约概率 │ └── utils.py # KS、AUC、绘图等工具函数 ├── models/ │ └── model_artifacts/ # 保存训练好的模型和特征列表 ├── notebooks/ │ └── EDA.ipynb # 探索性数据分析 └── README.md # 数据集说明、运行步骤、结果摘要这个结构的核心原则有三条原始数据目录只读所有预处理结果写入processed保证同一个原始数据可以复现出完全一致的特征矩阵config.py集中管理所有超参数而不是把参数散落在各个脚本里predict.py只依赖models/model_artifacts/里的产物不需要重新训练就能对新数据推理。这样做的直接好处是你在实验阶段调整的特征处理逻辑不会影响线上预测的输入格式——训练和预测走的是同一套preprocess.py格式天然对齐。4.1.2 训练入口脚本和参数配置train.py是整个源码的核心入口。它要做的事情按顺序是加载config.py里配置的参数、读入训练测试数据集、调用preprocess.py做特征工程、划分训练验证集、训练模型、输出评估指标、保存模型产物。下面是一个精简但完整的实现它把逻辑回归、随机森林、XGBoost 三个模型串行训练并把每个模型的结果写入一个字典方便统一比较。# train.py import json import joblib import pandas as pd from sklearn.model_selection import train_test_split from sklearn.metrics import roc_auc_score from src.config import DATA_RAW_DIR, MODEL_DIR, SEED from src.preprocess import build_features from src.utils import ks_score def main(): train_df pd.read_csv(f{DATA_RAW_DIR}/train.csv) test_df pd.read_csv(f{DATA_RAW_DIR}/test.csv) X_train build_features(train_df.drop(columns[is_default])) y_train train_df[is_default] X_test build_features(test_df) X_tr, X_val, y_tr, y_val train_test_split( X_train, y_train, test_size0.2, random_stateSEED, stratifyy_train ) models build_models() # 返回包含 LR/RF/XGB 的 dict results {} for name, model in models.items(): model.fit(X_tr, y_tr) prob model.predict_proba(X_val)[:, 1] results[name] { auc: roc_auc_score(y_val, prob), ks: ks_score(y_val, prob) } joblib.dump(model, f{MODEL_DIR}/{name}.pkl) joblib.dump(X_train.columns.tolist(), f{MODEL_DIR}/feature_list.pkl) with open(f{MODEL_DIR}/metrics.json, w) as f: json.dump(results, f, indent4) print(json.dumps(results, indent4)) if __name__ __main__: main()脚本里build_features返回的是已经处理好的特征矩阵它同时作用于训练集和测试集这是防止训练测试数据特征不一致的关键。joblib.dump保存模型的同时保存了特征列表预测时先加载特征列表把新数据的列对齐到训练时的顺序再输入模型可以避免训练和预测时因列顺序不同导致的静默错误。metrics.json把三个模型的 AUC 和 KS 落盘后续画图或写报告时直接读文件不需要重新训练。4.1.3 predict 模块如何对单条新样本做违约预测模型训练完成后predict.py的输入是一行新的用户数据输出是违约概率和风险等级。这个模块要能独立运行不依赖训练代码。一个常见的设计是load_model()负责加载模型和特征列表preprocess_single()把原始输入行转成特征向量predict()输出概率。# predict.py import joblib import pandas as pd from src.config import MODEL_DIR model joblib.load(f{MODEL_DIR}/xgb.pkl) feature_list joblib.load(f{MODEL_DIR}/feature_list.pkl) def predict_single(raw_data: dict): df pd.DataFrame([raw_data]) df build_features(df) df df.reindex(columnsfeature_list, fill_value0) prob model.predict_proba(df)[:, 1][0] risk_level high if prob 0.5 else low return {probability: round(prob, 4), risk_level: risk_level}reindex(columnsfeature_list, fill_value0)这行的作用是如果新样本里缺少某个特征或者特征列顺序和训练时不一致会自动按训练时的列顺序补齐缺失的列填 0。这个处理在信贷场景里很重要因为线上实时请求的数据未必包含所有训练特征漏掉一列如果不报错模型分数会有偏差报错则直接导致接口不可用。risk_level的阈值这里先用 0.5 做简单切分实际业务中这个值需要通过阈值选择来确定而不是拍脑袋定 0.5下一章会讲具体做法。4.1.4 训练测试数据集的使用与防止数据泄漏数据集的正确打开方式是理解猜答案的方向。拿到train.csv和test.csv后第一件事是确认测试集里的特征列是否与训练集完全一致包括列名、缺失率、取值分布。常见的问题有三个训练集里的某个类别特征在测试集里出现了新类别比如训练集里城市只有 10 个测试集里多了 1 个新城市测试集的缺失模式与训练集不一致训练集的目标变量is_default在测试集里被错误地保留。这些都会导致训练时 AUC 很高、预测时效果崩盘。处理办法类别特征在build_features里做频次编码时未出现的类别统一映射成 0并在预处理阶段记录训练集的编码映射表预测时加载同一张表。数据泄漏则是另一个容易出现的问题如果你在划分训练测试数据集之前就对全量数据做了标准化或目标编码验证集的统计信息就提前泄漏给了训练过程模型的验证分数会虚高。正确的做法是先切分再在训练集上拟合编码器和标准化器然后 transform 验证集。5. 从概率到审批动作阈值选择与分数映射的三个进阶技巧5.1.1 用业务代价矩阵确定最优违约概率阈值模型输出的违约概率本身不能直接用于审批决策必须通过阈值把它转化成“通过”或“拒绝”的动作。0.5 这个默认阈值在信贷场景里几乎总是错的因为违约样本占比远低于 50%模型预测概率的分布整体偏低。选阈值的正确思路是根据业务代价矩阵拒绝一个正常客户损失的是这笔贷款的利息收入通过一个违约客户损失的是贷款本金。两类错误的代价不同最优阈值应该在代价曲线的最低点。一个快速可行的做法是遍历验证集上每个可能阈值计算总代价取最小值对应的点。cost_fp 0.05 # 误拒绝一个正常客户的代价损失 5% 利息收入 cost_fn 1.0 # 误通过一个违约客户的代价损失全部本金 thresholds np.arange(0.1, 0.9, 0.02) total_cost [] for t in thresholds: pred (val_prob_xgb t).astype(int) fn ((pred 0) (y_val 1)).sum() fp ((pred 1) (y_val 0)).sum() total_cost.append(fn * cost_fn fp * cost_fp) best_idx np.argmin(total_cost) best_threshold thresholds[best_idx] print(f最优阈值: {best_threshold:.2f}, 对应最小总代价: {total_cost[best_idx]:.2f})cost_fp0.05的含义是银行从一笔正常贷款中赚到的利息约为本金的 5%cost_fn1.0代表违约后本金全损。这两个数值根据业务不同差别很大但代价框架是通用的。遍历步长0.02能保证在 0.1 到 0.9 之间有 40 个候选点足够找到平滑曲线上的最小值。需要理解的是这个阈值不是固定不变的——市场环境或客群结构变化时代价矩阵里的cost_fp和cost_fn会变阈值也要重新计算。5.1.2 把模型概率映射成标准化的信用分数信贷业务里模型输出的概率通常会被映射成一个整数分数这样业务人员不用理解概率的含义只需要记住“分数低于 600 拒绝”之类的规则。最常见的映射方式是评分卡逻辑设定基准分和基准违约概率以及 PDOPoints to Double the Odds分数翻倍所需要的分数增量。映射公式是score offset factor * ln(odds)其中odds p / (1 - p)。下面这段代码实现了从概率到分数的转换并生成了分数分布描述。base_score 600 base_odds 50 # 基准分数 600 对应的违约赔率 pdo 20 # 分数每增加 20 分odds 翻倍 factor pdo / np.log(2) offset base_score - factor * np.log(base_odds) def prob_to_score(prob): odds prob / (1 - prob 1e-9) return offset factor * np.log(odds) val_scores prob_to_score(val_prob_xgb) print(f分数范围: {val_scores.min():.0f} - {val_scores.max():.0f}) print(f分数均值: {val_scores.mean():.0f} ± {val_scores.std():.0f})factor20/ln(2)≈28.85这表示违约风险每下降一半分数提高约 28.85 分。offset的作用是把分数平移到业务想要的量纲上。odds50表示正常客户与违约客户的比例为 50 比 1log(50)是对应的对数赔率。这里的1e-9只是为了防分母为 0。分数映射完成后上一节通过代价矩阵选出的阈值best_threshold可以直接用prob_to_score转成审批线比如 0.32 的概率对应 572 分那审批规则就定为“低于 572 分拒绝”。这种方式比直接告诉业务“概率大于 0.32 拒绝”要自然得多。5.1.3 验证阈值稳健性的拒绝推断与跨时间回测确定了阈值和分数规则之后还需要做一步容易被忽略的验证拒绝推断。被拒绝的客户没有真实标签模型在打分时对这部分人群的预测是外推的分数分布可能不准确。一个简单的处理手段是用当前模型给这些被拒绝样本打分取低分段样本人工抽样核实一部分坏账情况把这些信息加入下个版本的训练测试数据集。同时建议做跨时间切片回测把历史数据按月份切块用前 80% 的时间段训练、后 20% 的时间段验证看 KS 和阈值是否会明显衰减。如果衰减明显说明客群在变化阈值需要动态调整。最终交付时把best_threshold、prob_to_score、跨时间回测的 KS 对比图和 SHAP 特征重要性图一起放进项目 README这组材料就是“高分项目”里最能体现工程经验的部分——它意味着你不只是训练了一个分类器而是给出了一个能直接对接审批系统的决策链路。本文还有配套的精品资源点击获取