Lending Club贷款违约预测实战:从数据清洗到模型优化
1. 从零开始:理解Lending Club数据与风控预测
大家好,我是老张,在金融科技和AI模型这块摸爬滚打了十来年。今天想和大家聊聊一个非常经典,也特别有实战价值的项目:用Lending Club的公开数据,来预测一笔贷款会不会违约。这活儿听起来高大上,像是华尔街精英干的,但其实只要你懂点Python,有耐心,跟着我一步步来,完全能自己跑通。这不仅是数据科学入门的绝佳练手项目,更是理解金融风控核心逻辑的窗口。
Lending Club是美国一家知名的P2P借贷平台,它把每一笔贷款的信息,从借款人的信用分数、工作年限,到贷款金额、利率,再到最终的还款状态,都公开了出来。我们拿到的就是这样一个超百万条记录的数据集。我们的目标很明确:根据借款人申请贷款时的各种信息,去预测他未来是否会违约(也就是还不上钱)。这本质上是一个二分类问题:好客户(正常还款)标记为1,坏客户(违约)标记为0。
这个项目为什么值得做?首先,数据是真实的,不是人造的玩具数据,你能遇到真实数据中所有的“坑”:缺失值、异常值、不平衡样本。其次,流程非常完整,从数据清洗、特征工程到模型训练、评估,覆盖了机器学习项目全生命周期。最后,它的结果有直接的商业解读价值——模型预测的违约概率,直接关系到平台是批准贷款还是拒绝,以及该定多高的利率。下面,我就带你从最“脏”的原始数据开始,一步步打造一个靠谱的预测模型。
2. 数据清洗:给数据“洗个澡”,去掉噪音和杂质
拿到原始数据,千万别急着建模。我见过太多新手朋友,一上来就把数据塞进模型,结果要么报错,要么效果奇差。数据清洗这步,就像给食材做预处理,洗菜、切配,虽然繁琐,但决定了最后菜品的质量。Lending Club的数据有150多个字段,不是每个都有用。
2.1 大刀阔斧:处理缺失值与异常值
第一步,我们先处理缺失值。有些字段缺失了一大半,这种字段信息量极低,留着反而会引入噪音。我的经验法则是:如果一列数据缺失率超过50%,就直接剔除。用Pandas实现起来很简单:
import pandas as pd
# 假设df是我们的原始数据框
missing_ratio = df.isnull().mean()
cols_to_drop = missing_ratio[missing_ratio > 0.5].index.tolist()
df_cleaned = df.drop(columns=cols_to_drop)
print(f"剔除了 {len(cols_to_drop)} 个缺失率过高的列。")
第二步,对付异常值。金融数据里经常有些“天文数字”,比如年收入填了几个亿,这明显不符合常识。粗暴地删除这些记录可能会损失信息,但留着会严重扭曲模型的判断。我常用的方法是“掐头去尾”,只保留99%分位数以内的数据。具体来说,对于每个数值型字段,计算其0.5%分位数和99.5%分位数,只保留在这个区间内的数据。
# 筛选数值型列
numeric_cols = df_cleaned.select_dtypes(include=['number']).columns
valid_mask = pd.Series(True, index=df_cleaned.index) # 初始化一个全为True的布尔序列
for col in numeric_cols:
q_low = df_cleaned[col].quantile(0.005)
q_high = df_cleaned[col].quantile(0.995)
# 当前列在正常范围内的掩码
col_mask = (df_cleaned[col] >= q_low) & (df_cleaned[col] <= q_high)
valid_mask &= col_mask # 与总掩码取交集,只有所有列都正常的行才被保留
df_cleaned = df_cleaned[valid_mask].copy()
print(f"异常值处理后,数据从 {len(df)} 行减少到 {len(df_cleaned)} 行。")
这样做虽然会损失大约1%的数据,但能极大提升数据的“健康度”,让后续的模型更稳定。我实测下来,这个步骤对逻辑回归、树模型等算法的效果提升都很明显。
2.2 精雕细琢:分类变量的预处理与目标变量定义
数值型变量处理完了,我们来看看分类变量,比如贷款用途(purpose)、工作年限(emp_length)、州(addr_state)等。对于分类变量,首先要做的是查看其分布,了解每个类别有多少样本。有些类别可能样本极少,比如某个州只有几十条记录,这种类别在建模时可能会带来过拟合问题,需要考虑合并或特殊处理。
categorical_cols = df_cleaned.select_dtypes(include=['object']).columns
for col in categorical_cols:
value_counts = df_cleaned[col].value_counts()
print(f"变量 '{col}' 有 {len(value_counts)} 个不同取值。")
print(value_counts.head(10)) # 查看前10个最常见的类别
print("-" * 30)
接下来是最关键的一步:定义我们的目标变量。原始数据中的贷款状态(loan_status)有十几种,比如“Fully Paid”(已还清)、“Current”(还款中)、“Charged Off”(坏账)、“Default”(违约)等。我们需要将其转化为二进制的0和1。通常,我们将“Fully Paid”和“Current”视为好客户(标记为1),将“Charged Off”和“Default”等视为坏客户(标记为0)。处于宽限期或轻微逾期的,根据业务严格程度,可以归为0或1,这里我们保守一点,将逾期也归为坏客户。
# 定义状态映射规则
status_mapping = {
'Fully Paid': 1,
'Current': 1,
'In Grace Period': 0, # 宽限期,保守起见视为风险
'Late (16-30 days)': 0,
'Late (31-120 days)': 0,
'Charged Off': 0,
'Default': 0
}
df_cleaned['loan_status_binary'] = df_cleaned['loan_status'].map(status_mapping)
# 检查类别分布
print(df_cleaned['loan_status_binary'].value_counts(normalize=True))
你会发现,好客户(1)的比例远高于坏客户(0),这就是典型的类别不平衡问题。在后续建模时,我们需要特别注意这一点,比如在逻辑回归中设置class_weight='balanced',或者在XGBoost中调整scale_pos_weight参数。
3. 探索性数据分析:用图表讲出数据背后的故事
数据洗干净了,目标也明确了,先别急着跑模型。花点时间做探索性数据分析(EDA),就像侦探勘察现场,能发现很多有价值的线索,甚至能直接指导我们如何构造特征。这里我分享几个我觉得最有用的分析角度。
3.1 贷款等级与违约率:风险定价的核心
Lending Club根据借款人的信用状况,将贷款分为A到G共7个等级,A最好,G最差。每个大等级下又有5个子等级(如A1, A2...A5)。我们首先看看贷款等级的分布。
import matplotlib.pyplot as plt
import seaborn as sns
grade_dist = df_cleaned['grade'].value_counts().sort_index()
plt.figure(figsize=(10,6))
sns.barplot(x=grade_dist.index, y=grade_dist.values, palette='Blues_r')
plt.title('不同贷款等级的申请数量分布')
plt.xlabel('贷款等级')
plt.ylabel('申请数量')
plt.show()
你会发现,B级和C级的贷款申请数量最多。这很符合直觉:信用极好的人(A级)可能更容易从银行获得低息贷款,而信用太差的人(F、G级)可能根本无法通过平台审核,所以中间群体构成了主体。
但更关键的是看每个等级的违约率。我们定义一个计算分组违约率的函数:
def calculate_default_rate(df, group_by_col):
result = df.groupby(group_by_col).agg(
good_loans=('loan_status_binary', 'sum'), # 好贷款数
total_loans=('loan_status_binary', 'count') # 总贷款数
)
result['bad_loans'] = result['total_loans'] - result['good_loans']
result['default_rate'] = result['bad_loans'] / result['total_loans']
return result.sort_values('default_rate', ascending=False)
grade_default = calculate_default_rate(df_cleaned, 'grade')
print(grade_default[['total_loans', 'default_rate']])
画成图表,趋势会非常清晰:从A到G,违约率几乎是单调递增的。A级的违约率可能只有2%-3%,而G级的违约率可能超过25%。这强有力地证明了信用评级系统的有效性,也告诉我们“grade”这个特征将是模型里一个非常强的预测因子。
3.2 其他维度的洞察:工作、用途与地域
除了信用等级,我们还可以看看其他因素。比如工作年限(emp_length),直觉上工作越久应该越稳定,违约率越低。但实际计算下来,你会发现差异没有想象中那么大,10年以上工作经验和<1年经验的违约率可能只差一两个百分点。这说明在借贷决策中,当前收入和负债情况可能比单纯的工作年限更重要。
再看贷款用途(purpose)。用于“债务合并”(debt consolidation)的贷款数量通常最多,其违约率处于中等水平。而“小型商业贷款”(small business)的违约率往往最高,因为创业本身风险就大。“教育”(educational)或“婚礼”(wedding)贷款的违约率则通常较低。这些洞察可以帮助我们理解不同借款群体的风险特征。
最后看看地域(addr_state)。不同州的违约率确实有差异,这可能反映了当地的经济状况、法律环境甚至文化因素。例如,某些以农业或传统工业为主的州,经济波动较大,违约率可能偏高。我们可以把这个州级别的信息作为一个分类特征喂给模型,但要注意,如果某些州的样本量太少,需要进行平滑处理(比如用全国平均违约率来修正),防止模型学到噪音。
做完这些分析,你对数据就有了“感觉”。你知道哪些特征可能重要,也知道数据里存在不平衡。带着这些认知进入下一步,你会更有方向。
4. 特征工程:从原始数据中提炼“黄金”
原始数据字段很多,但并非所有字段都对预测违约有用。特征工程的目标就是筛选出信息量大的特征,并处理好它们,让模型能更好地“消化”。这一步是区分普通项目和优秀项目的关键。
4.1 基于IV值的初步筛选:量化特征的预测能力
在金融风控领域,我们常用信息价值(Information Value, IV) 来衡量一个特征对目标变量(好坏客户)的预测能力。它的计算基于WOE(Weight of Evidence)。简单理解,IV值越高,说明这个特征区分好坏客户的能力越强。经验上,IV>0.1的特征就认为有较强的预测能力了。
对于数值型特征(如年收入、贷款金额),我们需要先将其分箱(离散化),然后计算每个箱的WOE和整体的IV。这里有个技巧:直接用等宽或等频分箱可能效果不好,因为数据分布可能不均匀。我更喜欢用决策树分箱,它能根据目标变量的分布自动找到最佳的分割点。
from sklearn.tree import DecisionTreeClassifier
import numpy as np
def calculate_iv_by_tree(data, feature, target, max_depth=4):
"""使用决策树对数值特征进行分箱并计算IV值"""
X = data[[feature]].fillna(data[feature].median()) # 用中位数填充缺失值
y = data[target]
# 训练一棵深度有限的决策树,其分裂点就是我们的分箱边界
tree = DecisionTreeClassifier(max_depth=max_depth, min_samples_leaf=0.05, random_state=42)
tree.fit(X, y)
# 获取树的分裂阈值
thresholds = sorted(tree.tree_.threshold[tree.tree_.threshold != -2]) # -2是叶节点标记
bins = [data[feature].min()] + thresholds + [data[feature].max()]
# 根据分箱计算WOE和IV
data['temp_bin'] = pd.cut(data[feature], bins=bins, include_lowest=True)
grouped = data.groupby('temp_bin', observed=True).agg(
good= (target, 'sum'),
bad= (target, lambda x: len(x) - x.sum())
)
grouped['good_pct'] = grouped['good'] / (grouped['good'].sum() + 1e-10)
grouped['bad_pct'] = grouped['bad'] / (grouped['bad'].sum() + 1e-10)
grouped['woe'] = np.log((grouped['bad_pct'] + 1e-10) / (grouped['good_pct'] + 1e-10)) # 避免除零
grouped['iv_component'] = (grouped['bad_pct'] - grouped['good_pct']) * grouped['woe']
iv_value = grouped['iv_component'].sum()
data.drop(columns=['temp_bin'], inplace=True)
return iv_value
# 遍历数值特征计算IV
numeric_features = df_cleaned.select_dtypes(include=[np.number]).columns.drop('loan_status_binary')
iv_dict = {}
for col in numeric_features:
iv = calculate_iv_by_tree(df_cleaned, col, 'loan_status_binary')
iv_dict[col] = iv
print(f"{col}: IV = {iv:.4f}")
# 筛选IV>0.1的特征
selected_numeric = [k for k, v in iv_dict.items() if v > 0.1]
print(f"\n筛选出的数值特征: {selected_numeric}")
对于分类特征,计算IV更直接,每个类别本身就是一个“箱”。同样地,我们只保留IV值较高的分类特征。经过这一步,我们能砍掉一半以上不痛不痒的特征,大大减轻后续计算的负担。
4.2 处理多重共线性:避免特征“内耗”
筛选出有价值的特征后,还要检查它们之间的相关性。如果两个特征高度相关(比如“贷款总额”和“分期付款额”),它们提供的信息是重复的,同时放入模型不仅浪费算力,还可能让模型变得不稳定(特别是对逻辑回归这类模型)。我们通常计算特征间的相关系数矩阵,并剔除那些相关性过高(例如,绝对值大于0.7或0.8)的特征对中的一个。
# 计算剩余特征的相关性矩阵(数值特征用皮尔逊,分类特征编码后也可计算)
features_for_corr = df_cleaned[selected_numeric + selected_categorical_encoded].select_dtypes(include=[np.number])
corr_matrix = features_for_corr.corr().abs() # 取绝对值相关矩阵
# 找出高相关特征对
high_corr_pairs = []
for i in range(len(corr_matrix.columns)):
for j in range(i+1, len(corr_matrix.columns)):
if corr_matrix.iloc[i, j] > 0.7: # 阈值设为0.7
col_i = corr_matrix.columns[i]
col_j = corr_matrix.columns[j]
high_corr_pairs.append((col_i, col_j, corr_matrix.iloc[i, j]))
print("高相关性特征对:")
for pair in high_corr_pairs:
print(f"{pair[0]} 和 {pair[1]}: {pair[2]:.3f}")
# 通常剔除IV值较低的那个,或者从业务角度选择保留更直观的那个
# 例如,'fico_range_low'和'fico_range_high'高度相关,保留一个即可(如'fico_range_low')
经过IV筛选和共线性剔除,我们最终可能只剩下十几个到二十几个特征。特征数量大幅减少,但每个都是“精兵强将”,模型训练会更快,且更容易避免过拟合。
5. 模型构建与对比:四大算法同台竞技
特征准备好了,终于到了最激动人心的建模环节。我们将使用四种经典的机器学习算法,在同一份数据集上训练和测试,看看谁的表现最好。记住,没有“最好”的算法,只有“最适合”当前数据和问题的算法。
5.1 逻辑回归:稳健的基线模型
逻辑回归虽然简单,但因其可解释性强、计算效率高,一直是金融风控领域的基准模型。它输出的是违约概率,非常直观。在处理我们的不平衡数据时,一定要设置class_weight='balanced',让模型在训练时更关注少数类(坏客户)。
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report, roc_auc_score
# 划分训练集和测试集
X = df_cleaned.drop(columns=['loan_status_binary'])
y = df_cleaned['loan_status_binary']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y) # stratify确保类别比例一致
# 训练逻辑回归模型
lr_model = LogisticRegression(
penalty='l1', # L1正则化,可以起到特征选择的作用
solver='saga', # 支持L1正则化和大数据集
C=0.1, # 正则化强度,值越小正则化越强
max_iter=1000,
class_weight='balanced', # 关键!处理类别不平衡
random_state=42
)
lr_model.fit(X_train, y_train)
# 预测与评估
y_pred_prob_lr = lr_model.predict_proba(X_test)[:, 1] # 预测为正类(违约)的概率
y_pred_lr = (y_pred_prob_lr > 0.5).astype(int) # 以0.5为阈值进行分类
print("逻辑回归分类报告:")
print(classification_report(y_test, y_pred_lr))
print(f"逻辑回归 AUC: {roc_auc_score(y_test, y_pred_prob_lr):.4f}")
逻辑回归的系数还可以直接解释为特征的重要性,例如“fico分数”的系数为负,说明fico分数越高,违约概率越低,这完全符合业务常识。
5.2 树模型三剑客:随机森林、LightGBM与XGBoost
接下来是三个基于树的集成模型,它们通常能获得比逻辑回归更高的预测精度。
随机森林:通过构建多棵决策树并综合其结果,能有效降低单棵树的过拟合风险。它自带特征重要性评估,对异常值也不那么敏感。
from sklearn.ensemble import RandomForestClassifier
rf_model = RandomForestClassifier(
n_estimators=200, # 树的数量
max_depth=10, # 控制树深,防止过拟合
min_samples_split=50,
min_samples_leaf=20,
class_weight='balanced', # 处理不平衡
n_jobs=-1, # 使用所有CPU核心
random_state=42
)
rf_model.fit(X_train, y_train)
y_pred_prob_rf = rf_model.predict_proba(X_test)[:, 1]
LightGBM:微软开发的高效梯度提升框架。它的速度极快,内存消耗小,而且对类别特征有原生支持(无需独热编码)。在大数据集上优势明显。
import lightgbm as lgb
# 创建数据集格式
lgb_train = lgb.Dataset(X_train, label=y_train)
lgb_eval = lgb.Dataset(X_test, label=y_test, reference=lgb_train)
params = {
'objective': 'binary',
'metric': 'auc',
'boosting_type': 'gbdt',
'num_leaves': 31,
'learning_rate': 0.05,
'feature_fraction': 0.8, # 防止过拟合
'bagging_fraction': 0.8,
'is_unbalance': True, # 处理不平衡数据
'verbose': -1
}
lgb_model = lgb.train(params,
lgb_train,
valid_sets=[lgb_eval],
num_boost_round=1000,
callbacks=[lgb.early_stopping(50)]) # 早停防止过拟合
y_pred_prob_lgb = lgb_model.predict(X_test)
XGBoost:另一个强大的梯度提升库,在各类数据科学竞赛中屡获殊荣。它需要更细致的参数调优,但调好后性能往往非常出色。
from xgboost import XGBClassifier
# 计算正负样本比例,用于设置 scale_pos_weight
scale_pos_weight = (y_train == 0).sum() / (y_train == 1).sum()
xgb_model = XGBClassifier(
n_estimators=300,
max_depth=6,
learning_rate=0.05,
subsample=0.8,
colsample_bytree=0.8,
scale_pos_weight=scale_pos_weight, # 处理不平衡的关键参数
eval_metric='auc',
use_label_encoder=False,
random_state=42
)
xgb_model.fit(X_train, y_train, eval_set=[(X_test, y_test)], verbose=False)
y_pred_prob_xgb = xgb_model.predict_proba(X_test)[:, 1]
5.3 模型评估:不止看AUC
模型训练好了,怎么比高低?最常用的指标是AUC(ROC曲线下面积)。AUC越接近1,说明模型整体区分好坏客户的能力越强。我们可以把四个模型的ROC曲线画在一起对比。
from sklearn.metrics import roc_curve, auc
import matplotlib.pyplot as plt
models = {'Logistic Regression': y_pred_prob_lr,
'Random Forest': y_pred_prob_rf,
'LightGBM': y_pred_prob_lgb,
'XGBoost': y_pred_prob_xgb}
plt.figure(figsize=(10, 8))
for name, y_pred_prob in models.items():
fpr, tpr, _ = roc_curve(y_test, y_pred_prob)
roc_auc = auc(fpr, tpr)
plt.plot(fpr, tpr, lw=2, label=f'{name} (AUC = {roc_auc:.3f})')
plt.plot([0, 1], [0, 1], color='navy', lw=2, linestyle='--', label='Random Guess')
plt.xlim([0.0, 1.0])
plt.ylim([0.0, 1.05])
plt.xlabel('False Positive Rate (FPR)')
plt.ylabel('True Positive Rate (TPR)')
plt.title('ROC Curve Comparison of Different Models')
plt.legend(loc="lower right")
plt.grid(True, alpha=0.3)
plt.show()
在我的多次实验中,LightGBM和XGBoost的AUC通常最高,能达到0.72-0.75左右;随机森林次之;逻辑回归作为基线,AUC可能在0.68-0.70。但AUC不是唯一指标。在风控中,我们可能更关心在某个低误杀率(低FPR)下的召回率(TPR)。比如,我们只能容忍1%的好客户被误拒(FPR=1%),那么在这个条件下,哪个模型能抓住最多的坏客户(TPR最高)?这需要查看ROC曲线上具体的点。
6. 模型优化与稳定性检验:让模型真正可靠
得到一个高AUC的模型只是第一步,要让模型能在实际业务中稳定运行,还需要做两件事:优化和稳定性检验。
6.1 超参数调优:释放模型潜力
我们之前训练模型用的都是默认或经验参数,这并不是模型的最佳状态。通过网格搜索(Grid Search) 或随机搜索(Random Search),我们可以系统地寻找最优参数组合。以LightGBM为例:
from sklearn.model_selection import GridSearchCV
# 定义一个较小的参数网格(为了演示,实际可以更大)
param_grid = {
'num_leaves': [20, 31, 40],
'learning_rate': [0.01, 0.05, 0.1],
'n_estimators': [100, 200],
'min_child_samples': [20, 50]
}
lgb_estimator = lgb.LGBMClassifier(objective='binary', is_unbalance=True, random_state=42)
grid_search = GridSearchCV(estimator=lgb_estimator,
param_grid=param_grid,
scoring='roc_auc',
cv=3, # 3折交叉验证
n_jobs=-1,
verbose=1)
grid_search.fit(X_train, y_train)
print(f"最佳参数: {grid_search.best_params_}")
print(f"最佳交叉验证AUC: {grid_search.best_score_:.4f}")
# 用最佳参数重新训练最终模型
best_lgb_model = grid_search.best_estimator_
调优过程可能比较耗时,但AUC提升0.01到0.02是很有可能的,这在风控中意味着巨大的成本节约或风险降低。
6.2 PSI检验:确保模型不会“漂移”
模型上线后,我们最怕的就是“模型漂移”——今天训练好的模型,过了半年,因为经济环境、客户群体变化,预测不准了。群体稳定性指数(Population Stability Index, PSI) 就是用来监控模型稳定性的利器。它通过比较训练集(预期分布)和当前数据集(实际分布)在特征或预测分数上的差异,来判断模型是否稳定。
通常,我们计算模型预测概率分值的PSI。PSI小于0.1说明模型非常稳定;在0.1到0.25之间,需要警惕;大于0.25,则说明分布发生了显著变化,模型可能需要重新训练。
def calculate_psi(expected, actual, bins=10):
"""计算PSI值"""
# 用训练集的分位数点来定义分箱边界,确保一致
breakpoints = np.percentile(expected, np.linspace(0, 100, bins + 1))
breakpoints[-1] = breakpoints[-1] + 1e-8 # 确保最大值被包含
expected_percents, _ = np.histogram(expected, bins=breakpoints)
actual_percents, _ = np.histogram(actual, bins=breakpoints)
# 转换为比例,并避免0值(加一个很小的数)
expected_percents = expected_percents / len(expected) + 1e-10
actual_percents = actual_percents / len(actual) + 1e-10
psi_values = (actual_percents - expected_percents) * np.log(actual_percents / expected_percents)
return np.sum(psi_values)
# 计算训练集和测试集的预测分数
train_scores = best_lgb_model.predict_proba(X_train)[:, 1]
test_scores = best_lgb_model.predict_proba(X_test)[:, 1]
psi_value = calculate_psi(train_scores, test_scores)
print(f"模型预测分数的PSI值为: {psi_value:.6f}")
if psi_value < 0.1:
print("模型稳定性良好。")
elif psi_value < 0.25:
print("模型稳定性一般,建议监控。")
else:
print("模型稳定性差,分布发生显著变化!")
在实际业务中,PSI需要定期(如每月)计算,监控模型性能的衰减。一旦PSI超标,就要启动模型迭代流程。做完这些,你的贷款违约预测模型才算真正具备了上线的潜力。整个过程从数据清洗到稳定性检验,虽然步骤繁多,但每一步都不可或缺,它们共同保证了模型的可信度和实用性。
更多推荐
所有评论(0)