人工智能训练师实战_信用风控数据集的数据清洗与Logistic建模
人工智能训练师实战:信用风控数据集的数据清洗与 Logistic 建模全流程
本文是「人工智能训练师(三级)」数据清洗模块的实战练习复盘。我们将基于一份真实的银行信贷数据集,完整经历从数据探查、质量问题发现、数据清洗、特征工程,到 Logistic 回归建模与模型评估的全流程。每一步都附有真实代码与运行结果,适合正在备考「人工智能训练师」或从事金融风控建模的同学参考。
一、背景与数据集介绍
1.1 任务场景
银行信用卡中心需要构建一个个人信用违约预测模型,用于判断借款人在未来两年内是否会发生 90 天以上的严重逾期(Serious Delinquency)。作为人工智能训练师,我们的职责是在建模之前完成数据清洗,确保输入模型的数据干净、可靠、无噪音。
1.2 数据集概览
本次使用的数据集共有 142,577 条个人信贷记录,包含以下 11 个变量:
| 特征名 | 中文含义 | 数据类型 |
|---|---|---|
SeriousDlqin2yrs |
未来2年是否严重逾期(1=是,0=否) | 目标变量 |
RevolvingUtilizationOfUnsecuredLines |
无担保循环信贷利用率(信用卡额度使用率) | 连续值 |
age |
借款人年龄 | 整数 |
NumberOfTime30-59DaysPastDueNotWorse |
30-59天逾期次数 | 整数 |
DebtRatio |
债务比率(月债务支出/月收入) | 连续值 |
MonthlyIncome |
月收入(美元) | 连续值 |
NumberOfOpenCreditLinesAndLoans |
未结信贷数量 | 整数 |
NumberOfTimes90DaysLate |
90天以上逾期次数 | 整数 |
NumberRealEstateLoansOrLines |
不动产贷款数量 | 整数 |
NumberOfTime60-89DaysPastDueNotWorse |
60-89天逾期次数 | 整数 |
NumberOfDependents |
家属数量 | 整数 |
二、数据探查:你不能跳过的一步
核心原则:在动手清洗之前,必须先理解数据。 跳过这一步等于在黑暗中打扫房间。
2.1 读入数据
import pandas as pd
import numpy as np
df = pd.read_csv('finance数据集.csv')
df = df.drop(columns=['Unnamed: 0']) # 去除无用的索引列
print(f"总行数: {len(df)}, 总列数: {len(df.columns)}")
print(f"列名: {list(df.columns)}")
print(df.dtypes)
输出:
总行数: 142577, 总列数: 11
列名: ['SeriousDlqin2yrs', 'RevolvingUtilizationOfUnsecuredLines', 'age',
'NumberOfTime30-59DaysPastDueNotWorse', 'DebtRatio', 'MonthlyIncome',
'NumberOfOpenCreditLinesAndLoans', 'NumberOfTimes90DaysLate',
'NumberRealEstateLoansOrLines', 'NumberOfTime60-89DaysPastDueNotWorse',
'NumberOfDependents']
2.2 检查目标变量分布
vc = df['SeriousDlqin2yrs'].value_counts()
print(vc)
print(f"坏样本比例: {vc[1]/len(df)*100:.2f}%")
输出:
0 134079
1 8498
坏样本比例: 5.96%
发现 1:严重的类别不平衡。 坏样本(有严重逾期)仅占 5.96%,好样本占 94.04%。这意味着如果模型"偷懒"全部预测为 0,准确率也能达到 94%。但这显然不是我们想要的——我们需要模型能够识别出那 6% 的问题客户。
2.3 描述性统计——发现异常值
print(df.describe().T[['min', 'max', 'mean', '50%']])
关键发现汇总:
| 特征 | 最小值 | 中位数 | 最大值 | 均值 | 疑点 |
|---|---|---|---|---|---|
age |
21 | 52 | 107 | 52.3 | 107 岁还在贷款? |
DebtRatio |
0 | 0.36 | 329,664 | 336.3 | 债务是收入 33 万倍? |
MonthlyIncome |
0 | 5,000 | 3,008,750 | 6,128 | 月收入 300 万美元? |
NumberOfDependents |
0 | 0 | 20 | 0.75 | 20 个家属? |
NumberOfOpenCreditLinesAndLoans |
0 | 8 | 58 | 8.6 | 58 笔未结信贷? |
发现 2:多个特征存在极端离群值。 这些值在业务逻辑上几乎不可能,极可能是数据采集或录入错误。
2.4 缺失值检查
print(df.isnull().sum())
SeriousDlqin2yrs 0
RevolvingUtilizationOfUnsecuredLines 0
age 0
...
NumberOfDependents 0
发现 3:原始数据无缺失值。 但这不代表数据干净——有些"0"值可能实际上应该是缺失值(比如月收入为 0)。
三、数据清洗:去伪存真
3.1 年龄异常值处理
# 个人信贷的合理年龄区间:18-100岁
age_outlier = (df['age'] < 18) | (df['age'] > 100)
print(f"年龄异常值数量: {age_outlier.sum()}") # 输出: 10
df.loc[age_outlier, 'age'] = np.nan
处理原则:超过 100 岁的记录在个人信贷场景中不可信,将其标记为缺失值,后续统一填充。
3.2 月收入极端值处理
income_p99 = df['MonthlyIncome'].quantile(0.99)
print(f"月收入 P99 = {income_p99:.0f}") # 输出: 23,405
income_outlier = df['MonthlyIncome'] > income_p99
print(f"极端收入值数量: {income_outlier.sum()}") # 输出: 1,426
df.loc[income_outlier, 'MonthlyIncome'] = np.nan
处理原则:超过 99 分位数的月收入视为异常(最大值达 300 万),用 P99 截断后标记为缺失值。
3.3 债务比率极端值处理
debt_p99 = df['DebtRatio'].quantile(0.99)
print(f"债务比率 P99 = {debt_p99:.2f}") # 输出: 4,938.24
# 超过 P99 的视为极端值,处理方式见下文
DebtRatio 最大值达到 329,664,意味着"月债务是月收入的 33 万倍"——这在业务上不合逻辑。一种有效策略是将其盖帽处理(winsorize),而非直接删除。
3.4 家属数量和信贷数量异常处理
# 家属数量 > 15 → 可能是录入错误
dep_outlier = df['NumberOfDependents'] > 15
print(f"家属数量 > 15: {dep_outlier.sum()}") # 输出: 1
df.loc[dep_outlier, 'NumberOfDependents'] = np.nan
# 未结信贷 > 50 → 可能混入了商业贷款数据
loans_outlier = df['NumberOfOpenCreditLinesAndLoans'] > 50
print(f"信贷数量 > 50: {loans_outlier.sum()}") # 输出: 15
3.5 统一缺失值填充
# 对之前标记为 NaN 的值,使用中位数填充
numeric_cols = df.select_dtypes(include=[np.number]).columns
for col in numeric_cols:
if df[col].isnull().sum() > 0:
df[col] = df[col].fillna(df[col].median())
为什么用中位数而不是均值? 因为这些特征受到极端值影响严重,中位数比均值更稳健。例如,MonthlyIncome 的均值是 6,128,但中位数是 5,000——因为少数高收入者拉高了均值。
四、特征工程:让模型看懂数据
清洗完数据后,我们还需要做一些特征加工来提升模型效果。
4.1 分箱特征——年龄组
df['AgeGroup'] = pd.cut(df['age'],
bins=[0, 30, 40, 50, 60, 120],
labels=['<30', '30-40', '40-50', '50-60', '>60'])
df = pd.get_dummies(df, columns=['AgeGroup'], drop_first=True)
设计初衷:年龄与信用风险通常不是简单的线性关系。25 岁和 55 岁的贷款人风险特征不同,分桶后模型能捕获这种非线性关系。
4.2 分箱特征——债务收入比
df['DebtRatio_bin'] = pd.cut(df['DebtRatio'],
bins=[0, 0.2, 0.4, 0.6, 0.8, 1.0, float('inf')],
labels=['0-0.2', '0.2-0.4', '0.4-0.6', '0.6-0.8', '0.8-1.0', '>1.0'])
df = pd.get_dummies(df, columns=['DebtRatio_bin'], drop_first=True)
4.3 组合衍生特征——逾期综合评分
这是最关键的特征工程步骤:
# 将所有逾期次数组合成一个综合指标
df['TotalPastDue'] = (
df['NumberOfTime30-59DaysPastDueNotWorse'] +
df['NumberOfTime60-89DaysPastDueNotWorse'] +
df['NumberOfTimes90DaysLate']
)
# 是否有任何逾期记录
df['HasPastDue'] = (df['TotalPastDue'] > 0).astype(int)
# 高信贷利用率标志
df['HighUtilization'] = (df['RevolvingUtilizationOfUnsecuredLines'] > 0.5).astype(int)
设计初衷:三个分别的逾期次数变量在业务上是同一类信息(逾期历史),合并成一个总分能减少稀疏性。HasPastDue 是一个强信号——有没有逾期历史,比"逾期了几次"更能区分风险。
五、Logistic 回归建模
5.1 数据准备
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
X = df.drop(columns=['SeriousDlqin2yrs'])
y = df['SeriousDlqin2yrs']
# 分层采样,保持训练集和测试集的坏样本比例一致
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=42, stratify=y
)
# 标准化连续型特征
scaler = StandardScaler()
original_num_cols = ['RevolvingUtilizationOfUnsecuredLines', 'age',
'NumberOfTime30-59DaysPastDueNotWorse', 'DebtRatio', 'MonthlyIncome',
'NumberOfOpenCreditLinesAndLoans', 'NumberOfTimes90DaysLate',
'NumberRealEstateLoansOrLines', 'NumberOfTime60-89DaysPastDueNotWorse',
'NumberOfDependents', 'TotalPastDue']
X_train[original_num_cols] = scaler.fit_transform(X_train[original_num_cols])
X_test[original_num_cols] = scaler.transform(X_test[original_num_cols])
5.2 模型训练
from sklearn.linear_model import LogisticRegression
lr = LogisticRegression(
max_iter=1000,
random_state=42,
class_weight='balanced', # 自动处理类别不平衡
C=1.0 # 正则化强度
)
lr.fit(X_train, y_train)
关键参数说明:
-
class_weight='balanced':自动给坏样本更高权重,权重 = n_samples / (n_classes × n_per_class),缓解 6% vs 94% 的不平衡问题。 -
C=1.0:L2 正则化的强度,C 越小正则化越强,可以有效防止过拟合。
六、模型评估:数字背后的真相
6.1 整体性能
from sklearn.metrics import classification_report, confusion_matrix, roc_auc_score
y_pred = lr.predict(X_test)
print(classification_report(y_test, y_pred, target_names=['0(没有严重逾期)', '1(有严重逾期)']))
| 类别 | Precision | Recall | F1-Score | Support |
|---|---|---|---|---|
| 0(没有严重逾期) | 0.9801 | 0.7915 | 0.8758 | 40,225 |
| 1(有严重逾期) | 0.1850 | 0.7466 | 0.2965 | 2,549 |
| Accuracy | 0.7889 | 42,774 | ||
| AUC | 0.8475 |
6.2 混淆矩阵解读
预测0(好) 预测1(坏)
实际0(好) 31,840 8,385 ← 假阳性(误报)
实际1(坏) 646 1,903 ← 假阴性(漏报)
-
假阳性(误判为逾期):8,385 人,占测试集的 19.60%
- 这 8,385 个"好人"被模型误判为会逾期,银行可能拒绝给这些人贷款
-
假阴性(漏判逾期):646 人,占测试集的 1.51%
- 这 646 个"坏人"逃过了模型的检测,如果给他们放贷,银行将面临坏账风险
6.3 模型解读:谁在被误判?
假阳性样本的共同特征(被冤枉的好人):
| 特征 | 假阳性样本均值 | 全量均值 |
|---|---|---|
| 循环信贷利用率 | 0.68 | 0.30 |
| 年龄 | 45.1 | 52.4 |
| 30-59天逾期次数 | 0.74 | 0.23 |
| 债务比率 | 298 | 328 |
| 月收入 | 5,224 | 5,681 |
解读:被误判的"好人"有一个共同特点——他们的循环信贷利用率较高(68% vs 30%),并且虽然最终没有严重逾期,但有过一些轻微逾期记录(30-59 天)。模型过分依赖"历史逾期"信号,把有轻微逾期但最终会按时还款的人也标记为高风险。
假阴性样本的共同特征(漏网之鱼的坏人):
| 特征 | 假阴性样本均值 | 全量均值 |
|---|---|---|
| 循环信贷利用率 | 0.31 | 0.30 |
| 年龄 | 51.4 | 52.4 |
| 30-59天逾期次数 | 0.12 | 0.23 |
| 债务比率 | 369 | 328 |
| 月收入 | 5,899 | 5,681 |
解读:漏判的"坏人"看起来"不太像坏人"——他们的逾期记录甚至低于平均水平(0.12 vs 0.23),但他们的债务比率很高(369 vs 328)。这说明存在一类"高负债但表面上按时还款"的风险客户,模型单一使用逾期历史难以捕捉这类风险。
6.4 特征重要性排名
Logistic 回归的一大优势就是可解释性强,我们可以直接查看每个特征的系数:
| 排名 | 特征 | 系数 | 含义 |
|---|---|---|---|
| 1 | HasPastDue |
+1.056 | 有逾期史 → 高风险 |
| 2 | RevolvingUtilizationOfUnsecuredLines |
+0.601 | 信用卡刷得满 → 高风险 |
| 3 | AgeGroup_>60 |
-0.369 | 年龄 >60 → 低风险 |
| 4 | NumberOfTimes90DaysLate |
+0.232 | 90天逾期多 → 高风险 |
| 5 | DebtRatio_bin_0.8-1.0 |
+0.188 | 债务比 80-100% → 高风险 |
| 6 | NumberOfOpenCreditLinesAndLoans |
+0.184 | 信贷数量多 → 高风险 |
| 7 | NumberRealEstateLoansOrLines |
+0.169 | 房贷多 → 高风险 |
| 8 | TotalPastDue |
+0.162 | 总逾期次数多 → 高风险 |
| 9 | DebtRatio_bin_0.2-0.4 |
-0.160 | 债务比 20-40% → 低风险 |
| 10 | age |
-0.155 | 年龄越大 → 风险越低 |
| 11 | MonthlyIncome |
-0.152 | 收入越高 → 风险越低 |
| 12 | HighUtilization |
+0.144 | 高使用率 → 高风险 |
关键发现:
-
HasPastDue贡献最大——有没有逾期历史是最强的预测信号,系数达 1.056 - 年龄是保护性因素——年龄越大越不容易逾期(系数为负)
- 债务比在 20-40% 是低风险区间——这类人群债务水平合理
- 月收入是保护性因素——收入越高风险越低
七、模型诊断与改进建议
基于以上评估结果,我们的 Logistic 回归模型存在以下核心问题及相应的改进方向:
7.1 核心问题
| 问题 | 严重程度 | 表现 |
|---|---|---|
| 正类召回率不足 | ⭐⭐⭐⭐⭐ | 虽然 recall=74.66%,但 precision 仅 18.50%,大量误报 |
| 假阳性过高 | ⭐⭐⭐⭐ | 19.60% 的好人被误判,业务成本高 |
| 线性模型能力有限 | ⭐⭐⭐ | 简单线性边界难以捕获复杂的信用风险模式 |
7.2 改进建议
建议一:尝试非线性模型
Logistic 回归假设特征与目标变量之间是线性关系,但在信用评分领域,这种假设往往不成立。建议尝试:
- XGBoost / LightGBM:能够自动捕获非线性关系和特征交互
- 随机森林:对异常值不敏感,适合信用数据中的噪声场景
建议二:调整分类阈值
当前使用默认的 0.5 作为分类阈值。在信用风控场景中,我们通常更关注召回率(找到所有可能逾期的人),可以适当降低阈值来提升召回率:
from sklearn.metrics import precision_recall_curve
precision, recall, thresholds = precision_recall_curve(y_test, y_prob)
# 选择一个能使 recall > 0.85 且 precision 可接受的阈值
建议三:引入更多外部特征
当前 11 个特征信息量有限,建议补充:
- 征信查询次数:频繁查询征信往往意味着资金紧张
- 工作稳定性:职业类型、在职时长
- 居住稳定性:居住时长、是否自有住房
- 社交网络特征:通讯录联系人信用状况
建议四:SMOTE 过采样
类别不平衡(6% vs 94%)是 precision 偏低的重要原因。可以尝试:
from imblearn.over_sampling import SMOTE
smote = SMOTE(random_state=42)
X_train_balanced, y_train_balanced = smote.fit_resample(X_train, y_train)
但需注意:SMOTE 生成的合成样本可能引入噪声,需谨慎验证。
建议五:特征交叉
人工构造特征交叉项,让 Logistic 回归能间接捕获交互效应:
# 示例:年龄 × 信贷利用率 → 年轻的"卡奴"
df['Age_x_Utilization'] = df['age'] * df['RevolvingUtilizationOfUnsecuredLines']
# 示例:收入 / 债务比率 → 偿债能力
df['Income_Debt_Ratio'] = df['MonthlyIncome'] / (df['DebtRatio'] + 1)
八、总结
本文完整复盘了一次信用风控数据集的数据清洗与 Logistic 回归建模实践,可作为「人工智能训练师(三级)」数据清洗模块的参考。核心要点回顾:
| 步骤 | 关键操作 | 发现/结果 |
|---|---|---|
| 数据探查 | 描述统计 + 缺失值检查 | 类别不平衡(6%:94%)、多个极端离群值 |
| 数据清洗 | P99截尾 + 中位数填充 | 处理 1,426 个月收入极端值、10 个年龄异常值 |
| 特征工程 | 分箱 + 衍生特征 |
HasPastDue 成为最强特征 |
| 建模 | Logistic 回归 + 类别权重 | AUC = 0.8475 |
| 评估 | 混淆矩阵 + 错误分析 | 假阳性 19.60%,假阴性 1.51% |
| 改进 | 5 个方向建议 | XGBoost / 阈值调整 / SMOTE |
给人工智能训练师的启示:
- 数据清洗不是"一次性"的工作——它是一个反复迭代的过程,每次建模后发现新问题,都需要回头重新审视清洗策略
- 业务理解比技术重要——年龄 107 岁能被发现,不是因为算法好,而是因为你知道"个人信贷的客户不可能 107 岁"
- 评估指标要多维度看——准确率 78.89% 看起来一般,但考虑到类别不平衡(如果全猜 0 也能有 94% 准确率),我们的模型实际上学到了有意义的模式
- 可解释性在金融场景中是刚需——Logistic 回归虽然精度不如 XGBoost,但它可以清楚地告诉你"为什么这个人的信用评分低",这在银行合规审查中至关重要