ARTICLE DETAIL

资讯详情

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

LendingClub信贷违约预测实战:从数据清洗到模型上线的避坑指南

LendingClub信贷违约预测实战:从数据清洗到模型上线的避坑指南 简介这份资源围绕LendingClub公开历史贷款数据构建了一套完整的智能信贷违约预测模型项目面向金融风控方向的学习者、数据分析初学者及需要实战案例的高校学生与从业者帮助理解从原始信贷数据到违约风险识别的全流程建模思路。压缩包共5个文件约47KB包含ipynb交互式建模笔记、py模型脚本、md说明文档、txt说明文件与docx附赠资料覆盖数据清洗、特征工程、模型训练与评估等关键环节便于边读边跑、快速复现。项目以借款人信用评分、贷款金额、利率、就业年限、年收入及负债等字段为原始材料涉及逻辑回归、随机森林、梯度提升机等算法的对比与调参思路并讨论准确率、精确率、召回率、F1分数等评估指标同时提及数据隐私与合规、模型公平性等现实议题。目前已有59人学习适合作为信贷风控入门与课程设计的参考范例。1. 从 LendingClub 信贷数据说起违约预测模型到底在预测什么很多人第一次拿到 LendingClub 公开信贷数据时第一反应是「字段真多」第二反应是「这玩意儿到底怎么建模」。我见过太多人把 loan_status 直接当标签、把 grade 当特征、把 issue_d 当普通字符串跑出一个 AUC 0.99 的模型上线后直接翻车。问题不在模型在于对「违约」这个定义的理解从一开始就偏了。这个项目的核心是用 LendingClub 平台公开的历史贷款数据——借款人信用评分、贷款金额、利率、就业年限、年收入、负债收入比等字段——构建一个能预测「这笔贷款未来会不会违约」的分类模型。它解决的不是「预测谁会借钱」而是「借出去之后哪些人大概率还不上」。适合两类人一是想入门信贷风控建模的算法工程师二是手里有类似信贷数据、想快速搭一套可解释违约预测流程的数据分析师。下面我按自己实际做过的路径把数据清洗、特征工程、模型选型、阈值调优和踩坑记录一层层拆开。2. 数据加载与标签定义别急着 fit先把 loan_status 拆明白2.1 LendingClub 原始字段里哪些能用、哪些必须丢LendingClub 的公开数据通常是一个几十万到上百万行的 CSV字段在 100 列上下。我一般先做三件事看 loan_status 的取值分布、看缺失率超过 60% 的列、看明显泄漏的列。import pandas as pd import numpy as np df pd.read_csv(loan.csv, low_memoryFalse) # 1. 看标签分布 print(df[loan_status].value_counts(dropnaFalse)) # 2. 缺失率统计 missing df.isnull().mean().sort_values(ascendingFalse) print(missing[missing 0.6].head(30)) # 3. 明显泄漏字段这些字段在放款时不可知或直接由结果反推 leak_cols [ total_pymnt, total_rec_prncp, total_rec_int, recoveries, collection_recovery_fee, last_pymnt_d, last_pymnt_amnt, next_pymnt_d, out_prncp, out_prncp_inv ] df df.drop(columns[c for c in leak_cols if c in df.columns])逻辑说明第一段看 loan_status 有哪些状态LendingClub 常见的有 Fully Paid、Charged Off、Current、Late、In Grace Period 等。第二段找高缺失列像 desc、member_id 这类直接删。第三段最关键——total_pymnt、recoveries 这些是放款后才知道的如果留在特征里模型等于偷看了答案AUC 虚高到 0.98 以上基本就是这个问题。参数说明low_memoryFalse 避免混合类型警告缺失阈值 0.6 是我常用的经验值超过就删低于的再考虑填充。2.2 把多分类 loan_status 变成二分类标签原始 loan_status 有七八种状态但违约预测通常只关心「正常还款」和「坏账」两类。我的做法是Fully Paid 归 0Charged Off 和 Default 归 1Current、Late、In Grace Period 这些中间状态直接剔除不参与训练。# 只保留明确结局的样本 good [Fully Paid] bad [Charged Off, Default] df df[df[loan_status].isin(good bad)].copy() df[target] df[loan_status].isin(bad).astype(int) print(df[target].value_counts(normalizeTrue))逻辑说明中间状态样本的最终结局未知强行打标签会引入噪声。剔除后样本量会减少但标签质量高得多。参数说明normalizeTrue 看正负样本比例LendingClub 坏账率通常在 15%20%属于中度不平衡后面建模要处理。提示如果你拿到的数据里 Default 样本极少可以只保留 Charged Off 作为正类但要在报告里写清楚定义。3. 特征工程利率、就业年限、负债收入比怎么变成模型能吃的数3.1 数值型字段的分箱与 WOE 编码利率 int_rate、负债收入比 dti、年收入 annual_inc 这些连续变量直接扔进逻辑回归效果一般因为非线性关系被压平了。我一般先做分箱再算 WOEWeight of Evidence这是信贷风控里最稳的做法。def woe_binning(df, col, target, bins10): df df.copy() df[bin] pd.qcut(df[col], qbins, duplicatesdrop) grouped df.groupby(bin)[target].agg([count, sum]) grouped.columns [total, bad] grouped[good] grouped[total] - grouped[bad] grouped[bad_rate] grouped[bad] / grouped[total] grouped[woe] np.log( (grouped[good] / grouped[good].sum()) / (grouped[bad] / grouped[bad].sum()) ) return grouped woe_table woe_binning(df, int_rate, target, bins10) print(woe_table[[total, bad_rate, woe]])逻辑说明qcut 按分位数等频分箱避免某些箱样本过少。WOE 衡量每个箱里好坏样本比例的差异值越负说明坏样本越集中。参数说明bins10 是常用起点样本量大可以加到 20样本少就降到 5。分箱后把 WOE 值映射回原字段替换原始数值。3.2 就业年限 emp_length 的清洗与有序编码emp_length 是字符串像「10 years」「 1 year」「3 years」。直接 one-hot 会丢序关系我一般转成 010 的整数。def clean_emp_length(x): if pd.isnull(x): return np.nan if 10 in x: return 10 if 1 in x: return 0 return int(x.split()[0]) df[emp_length_num] df[emp_length].apply(clean_emp_length) df[emp_length_num] df[emp_length_num].fillna(df[emp_length_num].median())逻辑说明10 归 101 归 0其余取数字部分。缺失用中位数填充因为就业年限缺失通常不是随机缺失中位数比均值稳。参数说明如果缺失比例超过 30%建议单独加一个「是否缺失」的指示列而不是只填充。3.3 类别字段的 target encoding 与防泄漏grade、sub_grade、purpose、home_ownership 这些类别字段我优先用 target encoding但必须配合交叉验证折外编码否则泄漏严重。from sklearn.model_selection import KFold def target_encode_cv(df, col, target, n_splits5): df df.copy() df[encoded] np.nan kf KFold(n_splitsn_splits, shuffleTrue, random_state42) for train_idx, val_idx in kf.split(df): means df.iloc[train_idx].groupby(col)[target].mean() df.loc[df.index[val_idx], encoded] ( df.iloc[val_idx][col].map(means) ) global_mean df[target].mean() df[encoded] df[encoded].fillna(global_mean) return df for col in [grade, purpose, home_ownership]: df target_encode_cv(df, col, target)逻辑说明每折用训练部分算类别均值只对验证部分赋值避免当前样本的标签信息进入自己的编码。参数说明n_splits5 是常规选择类别极少时可以把 min_samples_leaf 逻辑加进去比如某类样本少于 50 就用全局均值。4. 模型训练与阈值调优AUC 高不代表能上线4.1 逻辑回归 评分卡的基线方案信贷场景我永远先跑逻辑回归不是因为它准而是因为它可解释、能转评分卡、监管友好。from sklearn.linear_model import LogisticRegression from sklearn.model_selection import train_test_split from sklearn.metrics import roc_auc_score, classification_report from sklearn.preprocessing import StandardScaler feature_cols [int_rate_woe, dti_woe, annual_inc_woe, emp_length_num, grade_encoded, purpose_encoded, home_ownership_encoded] X df[feature_cols].fillna(0) y df[target] X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, stratifyy, random_state42 ) scaler StandardScaler() X_train_s scaler.fit_transform(X_train) X_test_s scaler.transform(X_test) lr LogisticRegression(class_weightbalanced, max_iter1000) lr.fit(X_train_s, y_train) proba lr.predict_proba(X_test_s)[:, 1] print(AUC:, roc_auc_score(y_test, proba)) print(classification_report(y_test, (proba 0.5).astype(int)))逻辑说明class_weightbalanced 自动按类别频率反比加权缓解不平衡。StandardScaler 对逻辑回归必要树模型可跳过。参数说明max_iter1000 防止不收敛阈值 0.5 只是默认实际要用业务成本调。4.2 用业务成本找最佳阈值而不是拍 0.5违约预测里漏判一个坏样本的损失远大于误判一个好样本。我一般设一个成本矩阵然后扫阈值。cost_fn 5 # 漏判坏样本的成本 cost_fp 1 # 误判好样本的成本 thresholds np.arange(0.05, 0.95, 0.01) costs [] for t in thresholds: pred (proba t).astype(int) fn ((pred 0) (y_test 1)).sum() fp ((pred 1) (y_test 0)).sum() costs.append(fn * cost_fn fp * cost_fp) best_t thresholds[np.argmin(costs)] print(最佳阈值:, best_t, 最低成本:, min(costs))逻辑说明遍历阈值算每个阈值下的总业务成本取最小。参数说明cost_fn 和 cost_fp 要根据实际业务填比如一笔坏账平均损失 5000一笔好账利息 1000那比例就是 5:1。这个比例直接决定阈值往哪偏。4.3 树模型做对比与特征重要性交叉验证逻辑回归跑完我会再跑一个 LightGBM 做对比主要看特征重要性和非线性交互。import lightgbm as lgb lgb_model lgb.LGBMClassifier( n_estimators500, learning_rate0.05, max_depth6, scale_pos_weight4, random_state42 ) lgb_model.fit(X_train, y_train) proba_lgb lgb_model.predict_proba(X_test)[:, 1] print(LightGBM AUC:, roc_auc_score(y_test, proba_lgb)) importance pd.Series( lgb_model.feature_importances_, indexfeature_cols ).sort_values(ascendingFalse) print(importance)逻辑说明scale_pos_weight 设成正负样本比例约等于 45。特征重要性用来验证逻辑回归里 WOE 的方向是否合理如果某个字段在两边重要性都高但方向相反就要查数据。参数说明n_estimators500 配合 learning_rate0.05 是稳的组合max_depth6 防止过拟合。5. 避坑与排查那些让 AUC 虚高、上线翻车的细节5.1 现象AUC 0.99 但上线后坏账率没降原因特征里混入了放款后字段比如 total_pymnt、last_pymnt_d模型在训练时偷看了结果。解决逐列核对字段含义凡是放款后产生的字段一律删除再用时间切分验证——用早期数据训练、后期数据测试AUC 会回落到 0.70.8 的正常区间。5.2 现象逻辑回归系数方向反了利率越高违约越低原因多重共线性。int_rate 和 grade 高度相关grade 又和 sub_grade 相关导致系数不稳定。解决先算 VIF超过 10 的字段只保留一个或者直接用 WOE 编码替代原始数值WOE 本身已经单调化处理过。5.3 现象target encoding 后训练集 AUC 0.95验证集 0.65原因没用折外编码类别均值里混入了当前样本的标签。解决必须用 KFold 折外编码且编码后的字段不能再参与同一折的训练。我一般把 target encoding 放在交叉验证管道内部而不是提前对整个数据集做。5.4 现象就业年限缺失填充后模型对短工龄样本预测全偏原因emp_length 缺失不是随机的往往集中在某些贷款用途或收入区间。解决加一个 emp_length_isnull 指示列让模型自己学缺失模式而不是用中位数一刀切。5.5 现象阈值 0.5 时召回率只有 30%坏账漏掉一大半原因正负样本不平衡模型输出的概率整体偏低。解决用业务成本扫阈值或者改用 scale_pos_weight / class_weight 调整再或者直接对概率做校准Platt scaling / isotonic regression让输出概率更接近真实坏账率。6. 进阶技巧用时间切分和概率校准把模型推到可上线最后一章说一个我踩过最深的坑随机切分在信贷数据里是自欺欺人。LendingClub 的数据跨了好几年经济环境在变借款人群也在变。随机切分会让模型学到「未来信息」上线后遇到新客群直接崩。我现在的习惯是按 issue_d 排序用前 70% 时间做训练后 30% 做测试。df[issue_d] pd.to_datetime(df[issue_d], format%b-%Y) df df.sort_values(issue_d) split_idx int(len(df) * 0.7) train_df df.iloc[:split_idx] test_df df.iloc[split_idx:] print(训练集时间范围:, train_df[issue_d].min(), 至, train_df[issue_d].max()) print(测试集时间范围:, test_df[issue_d].min(), 至, test_df[issue_d].max())逻辑说明按时间切分后训练集和测试集的坏账率可能差 23 个百分点这才是真实上线会遇到的分布偏移。参数说明70/30 是常用比例如果数据年份少可以按具体年份切比如用 2015 前训练、2016 后测试。时间切分后AUC 通常会从 0.78 掉到 0.72 左右别慌这才是真实水平。接下来做概率校准from sklearn.calibration import CalibratedClassifierCV calibrated CalibratedClassifierCV(lr, methodisotonic, cv5) calibrated.fit(X_train_s, y_train) proba_cal calibrated.predict_proba(X_test_s)[:, 1] print(校准后 AUC:, roc_auc_score(y_test, proba_cal)) print(校准后平均预测坏账率:, proba_cal.mean()) print(实际坏账率:, y_test.mean())逻辑说明isotonic 校准把模型输出概率映射到真实频率让「预测 20% 坏账」的样本里真的有 20% 坏账。参数说明cv5 用交叉验证拟合校准器避免过拟合。校准后平均预测坏账率和实际坏账率应该接近如果差太多说明校准集分布和测试集分布不一致要重新检查时间切分点。我自己的习惯是任何信贷违约模型不上时间切分和概率校准就不算做完。AUC 只是排序能力校准后的概率才是业务能直接用的数字。希望帮到你。本文还有配套的精品资源点击获取
返回列表