人工智能训练师实战_信用风控数据集的数据清洗与Logistic建模

AI23小时前发布 beixibaobao
5 0 0

人工智能训练师实战:信用风控数据集的数据清洗与 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 高使用率 → 高风险

关键发现

  1. HasPastDue 贡献最大——有没有逾期历史是最强的预测信号,系数达 1.056
  2. 年龄是保护性因素——年龄越大越不容易逾期(系数为负)
  3. 债务比在 20-40% 是低风险区间——这类人群债务水平合理
  4. 月收入是保护性因素——收入越高风险越低

七、模型诊断与改进建议

基于以上评估结果,我们的 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

给人工智能训练师的启示

  1. 数据清洗不是"一次性"的工作——它是一个反复迭代的过程,每次建模后发现新问题,都需要回头重新审视清洗策略
  2. 业务理解比技术重要——年龄 107 岁能被发现,不是因为算法好,而是因为你知道"个人信贷的客户不可能 107 岁"
  3. 评估指标要多维度看——准确率 78.89% 看起来一般,但考虑到类别不平衡(如果全猜 0 也能有 94% 准确率),我们的模型实际上学到了有意义的模式
  4. 可解释性在金融场景中是刚需——Logistic 回归虽然精度不如 XGBoost,但它可以清楚地告诉你"为什么这个人的信用评分低",这在银行合规审查中至关重要
© 版权声明

相关文章