3. 数据探索与预处理:加载逾期数据集、数据清洗、缺失值处理、异常值检测与处理
数据质量直接决定模型性能的上限。本章将系统性地对原始逾期数据集进行加载、清洗、缺失值填充与异常值处理,为后续特征工程与模型训练打下坚实基础。
3.1 数据集加载与初步探索
我们使用 pandas 加载 CSV 格式的逾期样本数据,并快速了解数据的基本结构与统计特征。
import pandas as pd
import numpy as np
# 加载数据集
df = pd.read_csv('loan_default_data.csv', encoding='utf-8')
# 基础信息查看
print('数据集形状:', df.shape)
print('列名:\n', df.columns.tolist())
print('数据类型:\n', df.dtypes.value_counts())
print('前5行:\n', df.head())
加载后,通过 df.info() 与 df.describe() 快速掌握字段缺失情况与数值分布。
| 检查项 | 方法 | 目的 |
|---|---|---|
| 形状与列名 | df.shape, df.columns |
确认样本量与特征数量 |
| 数据类型 | df.dtypes |
识别数值型、类别型、时间型字段 |
| 缺失统计 | df.isnull().sum() |
定位缺失字段与缺失比例 |
| 数值分布 | df.describe() |
发现异常极值、标准差过大字段 |
实践建议: 加载后先检查
dtypes,确保日期字段被正确解析为 datetime 类型,避免后续时间特征提取出错。
3.2 数据清洗:重复值与格式统一
数据清洗主要处理重复记录与字段格式不一致问题。重复样本会导致模型过拟合于特定模式,降低泛化能力。
# 检查重复行
dup_count = df.duplicated().sum()
print(f'重复行数: {dup_count}')
# 删除重复行
df = df.drop_duplicates()
# 统一字符串格式:去除首尾空格、统一大小写
df['employment_type'] = df['employment_type'].str.strip().str.upper()
# 统一日期格式
df['loan_date'] = pd.to_datetime(df['loan_date'], errors='coerce')
清洗要点:
- 重复行:使用
df.duplicated()检测,drop_duplicates()删除。 - 字符串字段:去除空格、统一大小写、替换特殊符号。
- 日期字段:强制转换为
datetime,无效日期设为NaT后续处理。
3.3 缺失值处理策略
缺失值处理需结合字段含义与缺失比例选择合适方法。以下为常见策略对比:
| 缺失比例 | 推荐策略 | 适用场景 |
|---|---|---|
| < 5% | 删除缺失行 | 样本量充足,缺失随机 |
| 5% - 30% | 均值/中位数/众数填充 | 数值型用中位数,类别型用众数 |
| 30% - 60% | 模型预测填充或KNN插补 | 字段与其他特征相关性较强 |
| > 60% | 删除该字段 | 信息量过低,易引入噪声 |
# 数值型字段:用中位数填充
df['annual_income'].fillna(df['annual_income'].median(), inplace=True)
# 类别型字段:用众数填充
df['employment_type'].fillna(df['employment_type'].mode()[0], inplace=True)
# 时间字段:用前向填充
df['loan_date'].fillna(method='ffill', inplace=True)
# 删除缺失比例超过60%的字段
threshold = 0.6
df = df.dropna(thresh=len(df) * (1 - threshold), axis=1)
注意: 填充缺失值后务必检查填充后的分布是否发生剧烈偏移。对于信用评分、收入等敏感字段,建议保留缺失指示列(
is_missing 特征),供模型学习缺失模式。
3.4 异常值检测与处理
异常值会严重干扰模型训练,尤其在逾期预测中,极端收入或负债值可能导致错误决策。常用检测方法包括 IQR 规则与 Z-Score 方法。
3.4.1 IQR 规则(四分位距法)
def detect_outliers_iqr(series):
Q1 = series.quantile(0.25)
Q3 = series.quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
return (series < lower_bound) | (series > upper_bound)
# 检测 'loan_amount' 字段异常值
outliers_iqr = detect_outliers_iqr(df['loan_amount'])
print(f'IQR 检测异常值数量: {outliers_iqr.sum()}')
3.4.2 Z-Score 方法(适用于近似正态分布)
from scipy import stats
z_scores = np.abs(stats.zscore(df['annual_income']))
outliers_z = z_scores > 3
print(f'Z-Score 检测异常值数量: {outliers_z.sum()}')
| 方法 | 适用分布 | 阈值 | 优点 | 缺点 |
|---|---|---|---|---|
| IQR | 偏态分布 | 1.5×IQR | 不受极端值影响 | 对样本量敏感 |
| Z-Score | 近似正态 | 3 或 3.5 | 计算简单 | 受极端值影响大 |
3.4.3 异常值处理策略
# 策略1:截尾(Winsorize)—— 将异常值替换为边界值
from scipy.stats.mstats import winsorize
df['loan_amount_winsorized'] = winsorize(df['loan_amount'], limits=[0.01, 0.01])
# 策略2:删除异常样本(仅当异常比例 < 1% 时使用)
df_clean = df[~outliers_iqr]
# 策略3:视为缺失值,后续用填充策略处理
df.loc[outliers_iqr, 'loan_amount'] = np.nan
df['loan_amount'].fillna(df['loan_amount'].median(), inplace=True)
推荐流程: 先使用 IQR 检测偏态分布字段(如收入、贷款金额),再使用 Z-Score 检测近似正态字段(如年龄、信用分)。处理时优先截尾或视为缺失,避免直接删除过多样本。
3.5 预处理结果验证
完成所有步骤后,需对预处理结果进行验证,确保数据质量满足建模要求。
# 验证缺失值已处理
print('剩余缺失值:\n', df.isnull().sum().max())
# 验证异常值已处理
print('异常值比例:', outliers_iqr.sum() / len(df))
# 验证数据形状
print('最终数据集形状:', df.shape)
# 保存清洗后数据
df.to_parquet('clean_loan_data.parquet', index=False)
最佳实践: 将预处理逻辑封装为
DataPreprocessor 类,包含 fit() 和 transform() 方法,便于在训练与推理阶段保持处理一致性。后续实时 API 发布时,可直接复用该预处理管线。