ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Python贷款违约预测实战:从数据清洗到模型部署全流程

Python贷款违约预测实战:从数据清洗到模型部署全流程 简介这是一套面向金融科技与机器学习入门者的贷款违约预测实践源码围绕信用评分与风险控制场景帮助读者用Python完成从数据分析到模型部署的完整链路。资源包共23个文件约13.77MB其中7个Python源码分别承担数据探索、随机森林、决策树、梯度提升与逻辑回归训练及预测任务2个CSV文件提供训练与测试数据另有11张PNG图像展示学习曲线、特征重要性与树结构并附Excel数据字典、readme说明与LICENSE许可。已有560人学习下载。读者可据此掌握数据清洗、特征工程、模型对比与调优、过拟合识别等关键环节并借助可视化结果直观评估各算法表现快速搭建可复用的违约风险预测方案。1. 贷款违约预测为什么值得用 Python 从头做一遍信贷业务里最怕的不是坏账本身而是坏账来得毫无预兆。一个客户在放款时看着各项指标都正常六个月后却连续三期不还款这种事后复盘往往只能看到一堆「当时其实有信号」的字段。贷款违约预测要解决的就是把这种事后懊悔变成事前概率给定一笔申请或一个存量账户的特征输出一个 0 到 1 之间的违约概率再配合阈值和额度策略决定放不放、放多少。用 Python 做这件事的价值在于链路完整且可复现。从数据清洗、特征工程、类别不平衡处理到逻辑回归、梯度提升树的训练与评估再到把模型固化成能批量打分的脚本整套流程都能用 pandas、scikit-learn、lightgbm 这类常用包串起来。这也是「python 机器学习常用包」被反复搜索的原因——大家真正想要的不是算法推导而是一条能跑通、能改参数、能解释结果的工程路径。这篇面向两类人一类是刚学完机器学习入门课程、想找一个真实结构化数据集练手的同学另一类是手里有信贷数据、需要快速搭出基线模型的从业者。下面按「数据怎么准备 → 特征怎么造 → 模型怎么选和调 → 怎么评估才不骗自己 → 怎么排错」的顺序推进每一步都给可抄的代码和参数说明。2. 数据准备与特征工程把原始字段变成模型能吃的输入2.1 先搞清楚违约标签怎么定义做贷款违约预测第一个翻车点往往不是模型而是标签。同一个数据集里「违约」可能指逾期 30 天、60 天、90 天也可能指核销。定义不同正样本比例能差好几倍模型学出来的东西完全不是一回事。常见做法是明确一个观察窗口和表现窗口比如用放款后前 6 个月的还款行为判断第 7 到第 12 个月是否出现 90 天以上逾期。假设数据里有一个loan_status字段取值包括Fully Paid、Current、Charged Off、Late (31-120 days)等。我一般会把Charged Off和严重逾期归为违约1Fully Paid归为正常0Current这种还没走完周期的样本直接剔除避免标签污染。import pandas as pd import numpy as np df pd.read_csv(loan_data.csv) # 只保留标签明确的样本剔除周期未走完的 df df[df[loan_status].isin([Fully Paid, Charged Off, Late (31-120 days)])].copy() # 定义二分类标签严重逾期与核销记为违约 df[target] df[loan_status].isin([Charged Off, Late (31-120 days)]).astype(int) print(df[target].value_counts(normalizeTrue))这段代码做了两件事过滤掉标签模糊的样本再把多分类状态压成 0/1。isin里的取值要根据你实际数据的枚举来改别照抄。打印出的正样本占比很关键如果低于 5%后面必须处理类别不平衡。2.2 缺失值和异常值别急着用均值填信贷数据里缺失本身可能就是信息。比如「最近一次查询征信距今月数」缺失可能意味着这个人很少借贷反而风险低。所以我不建议无脑用均值填充而是先看缺失比例超过 60% 的字段直接删10% 到 60% 的单独加一个「是否缺失」的指示列再用中位数填数值、用众数填类别。# 统计缺失比例 missing_ratio df.isnull().mean().sort_values(ascendingFalse) print(missing_ratio[missing_ratio 0]) # 高缺失字段直接删除 drop_cols missing_ratio[missing_ratio 0.6].index.tolist() df df.drop(columnsdrop_cols) # 中等缺失字段加缺失指示列再填中位数 for col in missing_ratio[(missing_ratio 0.1) (missing_ratio 0.6)].index: if col in df.columns: df[col _is_missing] df[col].isnull().astype(int) if df[col].dtype ! object: df[col] df[col].fillna(df[col].median())missing_ratio用isnull().mean()一次算出每列缺失占比。阈值 0.6 和 0.1 是我常用的经验值数据质量差可以放宽到 0.7。加_is_missing列是为了让模型自己决定「缺失」这件事有没有预测力这一步在树模型上收益比较明显。异常值方面像年收入出现 9999999 这种多半是占位符。我一般对数值字段做分位数截断把超过 99.5 分位和低于 0.5 分位的值拉回边界而不是直接删样本因为删样本会连带丢掉标签。2.3 类别特征编码与衍生特征类别字段比如贷款用途、房屋所有权、职业直接用 one-hot 会让维度爆炸尤其是职业这种高基数特征。常见做法是低基数取值少于 10 个用 one-hot高基数用目标编码或频率编码。目标编码要注意在训练集上算、验证集上用否则会泄漏标签。from sklearn.preprocessing import OneHotEncoder low_card_cols [c for c in df.select_dtypes(includeobject).columns if df[c].nunique() 10] encoder OneHotEncoder(handle_unknownignore, sparse_outputFalse) encoded encoder.fit_transform(df[low_card_cols]) encoded_df pd.DataFrame(encoded, columnsencoder.get_feature_names_out(low_card_cols), indexdf.index) df pd.concat([df.drop(columnslow_card_cols), encoded_df], axis1)handle_unknownignore保证线上遇到训练时没见过的类别不会报错直接编码成全 0。sparse_outputFalse是为了方便拼回 DataFrame数据量大时改成 True 省内存。衍生特征里最值钱的是比率类贷款金额除以年收入负债收入比、已用额度除以总额度额度使用率、当前负债笔数除以账龄。这些比值比原始金额更能反映还款压力也是信贷风控里公认有效的方向。3. 模型选型与训练从逻辑回归基线到梯度提升树3.1 为什么先跑逻辑回归再上树模型很多人一上来就调 LightGBM结果连基线都没有调完也不知道提升来自哪里。逻辑回归在这个场景有两个不可替代的作用一是它的系数可以直接解释成「这个特征每变化一个单位违约几率变化多少」业务方认这个二是它训练快能快速验证特征和标签有没有问题。from sklearn.model_selection import train_test_split from sklearn.linear_model import LogisticRegression from sklearn.preprocessing import StandardScaler from sklearn.pipeline import Pipeline feature_cols [c for c in df.columns if c not in [target, loan_status]] X df[feature_cols].select_dtypes(include[np.number]) y df[target] X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy) lr_pipe Pipeline([ (scaler, StandardScaler()), (clf, LogisticRegression(max_iter1000, class_weightbalanced, C1.0)) ]) lr_pipe.fit(X_train, y_train)stratifyy保证训练集和测试集的正样本比例一致类别不平衡时必加。class_weightbalanced让少数类权重自动调高等价于按类别频率反比加权。C是正则强度倒数越小正则越强先用 1.0 跑基线。3.2 LightGBM 的关键参数怎么设树模型在结构化信贷数据上通常比逻辑回归强因为它能自动捕捉特征交互和非线性。LightGBM 参数多但真正影响结果的就那几个。下面这套是我在中小规模数据几万到几十万行上常用的起点。import lightgbm as lgb lgb_clf lgb.LGBMClassifier( n_estimators500, # 树的数量配合早停用 learning_rate0.05, # 学习率越小越稳但越慢 num_leaves31, # 叶子数控制模型复杂度 max_depth-1, # 不限制深度靠 num_leaves 约束 min_child_samples50, # 叶子最小样本数防过拟合 subsample0.8, # 行采样比例 colsample_bytree0.8, # 列采样比例 reg_alpha0.1, # L1 正则 reg_lambda0.1, # L2 正则 scale_pos_weight10, # 正负样本权重比按实际比例调 random_state42 ) lgb_clf.fit( X_train, y_train, eval_set[(X_test, y_test)], eval_metricauc, callbacks[lgb.early_stopping(50), lgb.log_evaluation(100)] )num_leaves是 LightGBM 最该调的参数它直接决定模型容量31 是保守起点数据量大可以到 63 或 127。min_child_samples设 50 是为了防止每个叶子只装几个样本导致过拟合正样本少的时候可以降到 20。scale_pos_weight建议设成负样本数除以正样本数比如正样本占 8%就设 11 左右别凭感觉写。early_stopping(50)表示验证集 AUC 连续 50 轮不提升就停能省掉手动定n_estimators的麻烦。eval_metricauc是因为 AUC 对阈值不敏感适合不平衡场景。3.3 类别不平衡的三种处理方式对比信贷违约预测正样本通常只占 5% 到 20%不平衡处理直接影响召回。常见三种做法调整类别权重、过采样少数类、欠采样多数类。我一般优先用权重因为它不改数据分布训练也快。方法做法优点风险类别权重class_weight/scale_pos_weight不改数据实现简单权重过大易过拟合少数类过采样SMOTE 生成少数类样本提升召回明显合成样本可能不真实欠采样随机丢弃多数类训练快丢失大量信息用 SMOTE 时务必只在训练集上做测试集保持原始分布否则评估结果虚高。我见过有人在划分数据集之前就做了过采样导致训练集和测试集里出现同源合成样本AUC 冲到 0.99上线直接崩这是典型的血泪经验。4. 评估与阈值AUC 高不代表模型能用4.1 别只看准确率先看 AUC 和 KS不平衡数据里准确率是骗人的全预测为正常也能有 90% 以上。真正该看的是 AUC 和 KS。AUC 衡量排序能力KS 衡量好坏样本累计分布的最大差距风控里更常用 KS一般要求 0.3 以上才算可用。from sklearn.metrics import roc_auc_score, roc_curve y_prob lgb_clf.predict_proba(X_test)[:, 1] auc roc_auc_score(y_test, y_prob) fpr, tpr, thresholds roc_curve(y_test, y_prob) ks max(tpr - fpr) print(fAUC: {auc:.4f}, KS: {ks:.4f})predict_proba取第 1 列才是违约概率第 0 列是正常概率取错列是新手高频错误。KS 用tpr - fpr的最大值算和 AUC 一起看AUC 高但 KS 低说明模型在中间段排序乱两端还行。4.2 阈值不是 0.5要按业务成本定模型输出概率后得选一个阈值把概率转成「通过/拒绝」。0.5 只是数学默认值业务上完全不合理。假设通过一笔好客户赚 1000放过一个坏客户亏 5000那阈值应该偏向拒绝。可以用成本矩阵算期望损失最小的点。import numpy as np cost_fn 5000 # 放过一个坏客户的损失 profit_tn 1000 # 通过一个好客户的收益 best_thr, best_cost 0.5, np.inf for thr in np.arange(0.05, 0.95, 0.01): pred (y_prob thr).astype(int) fn ((pred 0) (y_test 1)).sum() # 漏掉的坏人 tn ((pred 0) (y_test 0)).sum() # 正确通过的好人 cost fn * cost_fn - tn * profit_tn if cost best_cost: best_cost, best_thr cost, thr print(f最优阈值: {best_thr:.2f}, 期望成本: {best_cost:.0f})这段循环遍历阈值算每个阈值下的总成本取最小。cost_fn和profit_tn必须换成你业务里的真实数字否则算出来的阈值没有意义。实际落地时还会按分数分箱做策略而不是单点阈值。4.3 特征重要性怎么看才不误导LightGBM 的feature_importances_默认按分裂次数算会偏向高基数特征。更稳的是看 gain增益或者用 SHAP 值。我一般先看 gain 排序再挑前几个特征做 SHAP 依赖图确认方向和业务常识一致。importance pd.DataFrame({ feature: X_train.columns, gain: lgb_clf.booster_.feature_importance(importance_typegain) }).sort_values(gain, ascendingFalse) print(importance.head(15))如果某个特征 gain 特别高但业务上说不通比如「客户 ID 后四位」那多半是数据泄漏得查这个字段是不是在放款后才产生的。放款后才知道的字段比如实际还款记录绝对不能进模型这是信贷建模的红线。5. 避坑与排查那些让模型上线就废的细节5.1 现象线下 AUC 0.9线上效果断崖原因训练时用了放款后产生的字段或者做了全局标准化导致测试集信息泄漏。解决逐个核对特征的产生时间点所有「未来信息」字段一律剔除标准化、编码、填充这些操作全部放进 Pipeline只在训练集上 fit。5.2 现象模型对某类客户全部预测为低风险原因这类客户在训练集里样本极少模型没学到。解决检查这类客户的正样本数量少于 50 个就别指望模型能区分要么合并类别要么单独建规则。5.3 现象KS 在验证集 0.35换一版数据掉到 0.15原因特征分布漂移比如收入字段的口径变了。解决上线前做特征稳定性监控用 PSI群体稳定性指标对比训练集和线上样本PSI 超过 0.25 的特征要重新审视。5.4 现象概率输出全是 0.02 到 0.08 之间区分不开原因scale_pos_weight设得过大或者正样本太少导致模型被压平。解决先不加权重跑一版看概率分布再逐步调权重必要时用 isotonic 或 sigmoid 做概率校准。5.5 现象训练报错Input contains NaN原因衍生特征做除法时出现除零产生 inf 或 NaN。解决除法前给分母加一个极小值或者用np.where判断分母是否为 0训练前统一df.replace([np.inf, -np.inf], np.nan)再处理。6. 把模型固化成可复用的打分脚本模型训练完只是半成品真正能用是把它变成一个输入原始特征、输出违约概率的脚本。我习惯把 Pipeline 和编码器一起用joblib存下来线上只调一次predict_proba。import joblib # 保存训练好的模型和特征列顺序 joblib.dump({model: lgb_clf, features: list(X_train.columns)}, loan_model.pkl) def score(new_df): bundle joblib.load(loan_model.pkl) model, features bundle[model], bundle[features] # 对齐特征列缺失的补 0多余的丢掉 new_df new_df.reindex(columnsfeatures, fill_value0) return model.predict_proba(new_df)[:, 1] # 模拟批量打分 batch X_test.head(5).copy() print(score(batch))reindex(columnsfeatures, fill_value0)是保证线上线下特征顺序一致的关键顺序错了模型不会报错但结果全乱这种玄学问题排查起来很痛苦。存模型时把特征列名一起存比只存模型对象靠谱得多。验证脚本是否可靠我一般做两件事一是拿训练时留出的测试集重新打分确认和训练时算的 AUC 一致二是构造几条边界样本比如全 0、极大值看输出是否在 0 到 1 之间且不报错。这两步能挡掉大部分上线事故。最后说个习惯每次调完参数把配置、AUC、KS、阈值和日期记一行到实验日志里。我早期不做记录回头想复现某个 0.02 的提升怎么都想不起来改了哪个参数只能重跑一遍。模型迭代是长期活后悔药就是那本日志。希望帮到你。本文还有配套的精品资源点击获取
返回列表