4. 数据探索与预处理:加载原始信贷数据、缺失值处理、异常值检测与处理、数据类型转换

在构建额度定价模型之前,原始信贷数据通常包含大量噪声、缺失值和异常分布。本章节将系统性地完成数据加载、质量探查、缺失值填充、异常值识别与修正,以及数据类型对齐,为后续特征工程与 LightGBM 训练奠定干净、一致的数据基础。

4.1 加载原始信贷数据

原始信贷数据通常以 CSV 或 Parquet 格式存储,包含用户基本信息、历史借贷行为、还款记录、多头借贷等字段。加载时需指定数据类型与日期解析,避免后续类型转换错误。

import pandas as pd
import numpy as np

# 加载原始信贷数据
df = pd.read_csv(
    'credit_raw_data.csv',
    parse_dates=['apply_date', 'due_date'],
    dtype={
        'user_id': 'int32',
        'age': 'float32',
        'income': 'float32',
        'credit_score': 'float32',
        'loan_amount': 'float32',
        'interest_rate': 'float32',
        'overdue_days': 'int32',
        'is_default': 'int8'
    },
    low_memory=False
)

print(f'数据维度: {df.shape}')
print(f'字段列表: {list(df.columns)}')
加载要点:使用 parse_dates 参数将日期列转为 datetime 类型;使用 dtype 字典显式指定数值列类型,可减少内存占用并避免隐式类型推断错误。

4.2 数据探索与缺失值处理

加载后首先进行数据概览,识别各字段的缺失比例、基本统计量与分布特征。缺失值处理策略需结合业务含义与缺失机制:随机缺失(MAR)可采用中位数/众数填充;非随机缺失(MNAR)需单独标记或使用模型预测。

字段 缺失比例 数据类型 处理策略
income 12.3% 连续数值 按行业+地区分组中位数填充
credit_score 8.7% 连续数值 使用 LightGBM 回归预测填充
overdue_days 5.1% 离散数值 填充 0(表示无逾期)
employment_type 3.2% 类别 填充众数“受雇”
education_level 1.5% 有序类别 填充中位数等级“大专”
# 缺失值概览
missing_ratio = df.isnull().mean().sort_values(ascending=False)
print('字段缺失比例:\n', missing_ratio[missing_ratio > 0])

# 分组中位数填充 income
df['income'] = df.groupby(['industry', 'region'])['income'] \
                 .transform(lambda x: x.fillna(x.median()))

# 众数填充 employment_type
mode_emp = df['employment_type'].mode()[0]
df['employment_type'].fillna(mode_emp, inplace=True)

# 标记缺失来源(可选)
df['income_was_missing'] = df['income'].isnull().astype(int)
缺失值处理避坑:避免全局统一填充(如全填均值),这会破坏字段间的条件分布关系。优先使用分组统计量或模型预测填充;对于缺失比例超过 50% 的字段,建议直接删除或构造“是否缺失”二值特征。

4.3 异常值检测与处理

信贷数据中的异常值可能来自数据录入错误、极端欺诈行为或特殊客群。异常值检测需结合统计方法与业务规则,避免误删正常的高风险样本。

4.3.1 基于 IQR 的异常值检测

对于连续数值字段(如 income、loan_amount、overdue_days),使用四分位距(IQR)方法识别离群点:

def detect_outliers_iqr(series, multiplier=1.5):
    Q1 = series.quantile(0.25)
    Q3 = series.quantile(0.75)
    IQR = Q3 - Q1
    lower_bound = Q1 - multiplier * IQR
    upper_bound = Q3 + multiplier * IQR
    return (series < lower_bound) | (series > upper_bound)

# 检测 income 异常值
outlier_mask = detect_outliers_iqr(df['income'], multiplier=2.0)
print(f'income 异常值数量: {outlier_mask.sum()} ({outlier_mask.mean():.1%})')

# 对异常值进行截断处理(Winsorize)
df.loc[df['income'] < df['income'].quantile(0.01), 'income'] = df['income'].quantile(0.01)
df.loc[df['income'] > df['income'].quantile(0.99), 'income'] = df['income'].quantile(0.99)

4.3.2 基于业务规则的异常值处理

字段 业务规则 异常处理方式
age 18 ≤ age ≤ 100 超出范围设为 NaN,后用中位数填充
interest_rate 0% ≤ rate ≤ 50% 超出范围截断至边界值
overdue_days ≥ 0 负值设为 0
loan_amount ≥ 100 小于 100 视为录入错误,删除该样本
异常值处理原则:先业务规则,后统计方法。对于明显违背业务常识的值(如年龄 200 岁),直接修正或删除;对于统计离群但业务合理的值(如高收入企业家),保留或截断而非删除。
数据预处理流程 数据预处理流程 原始信贷数据 缺失值检测与填充 异常值检测(IQR + 业务规则) 数据类型转换与编码 干净训练数据集

4.4 数据类型转换

LightGBM 对输入数据类型有明确要求:数值型特征必须为 float32 或 float64;类别型特征需转换为 int32 编码或直接使用 LightGBM 的 categorical_feature 参数。错误的类型会导致训练报错或模型性能下降。

4.4.1 数值字段类型统一

# 将连续数值字段统一为 float32
float_cols = ['income', 'credit_score', 'loan_amount', 'interest_rate',
              'debt_ratio', 'age', 'overdue_days']
for col in float_cols:
    df[col] = df[col].astype('float32')

# 将 ID 字段转为 int32
df['user_id'] = df['user_id'].astype('int32')

4.4.2 类别字段编码

对于有序类别(如 education_level),使用 LabelEncoder 保留顺序信息;对于无序类别(如 employment_type),使用 OrdinalEncoder 或直接传入 LightGBM 的 categorical_feature 列表。

from sklearn.preprocessing import LabelEncoder

# 有序类别:LabelEncoder
edu_order = ['小学', '初中', '高中', '大专', '本科', '硕士', '博士']
le_edu = LabelEncoder()
le_edu.fit(edu_order)
df['education_encoded'] = le_edu.transform(df['education_level']).astype('int32')

# 无序类别:OrdinalEncoder(或直接保留原始编码)
df['employment_encoded'] = df['employment_type'].astype('category').cat.codes.astype('int32')

# 记录类别字段名,供 LightGBM 使用
categorical_features = ['employment_encoded', 'region_encoded', 'industry_encoded']

4.4.3 日期字段特征提取

日期字段不能直接输入模型,需提取数值特征:

# 提取日期特征
df['apply_year'] = df['apply_date'].dt.year.astype('int32')
df['apply_month'] = df['apply_date'].dt.month.astype('int32')
df['apply_dayofweek'] = df['apply_date'].dt.dayofweek.astype('int32')
df['loan_term_days'] = (df['due_date'] - df['apply_date']).dt.days.astype('float32')

# 删除原始日期列
df.drop(columns=['apply_date', 'due_date'], inplace=True)
类型转换检查:在训练前使用 df.dtypes.value_counts() 确认所有字段均为数值类型(int32/float32),无 object 或 datetime 残留。LightGBM 不支持缺失值编码为 -999 等特殊值,应使用 NaN 表示缺失。
数据类型转换流程 数据类型转换流程 原始类型 object / int64 / float64 类型转换 astype / LabelEncoder 目标类型 float32 / int32 LightGBM 要求所有特征为数值类型 类别特征通过 categorical_feature 参数传入,无需独热编码

4.5 数据预处理检查清单

在进入特征工程之前,使用以下清单确认数据已准备就绪:

  • 缺失值检查:所有字段缺失比例 < 5%,且已按业务策略填充或标记。
  • 异常值检查:连续字段的极端值已截断或修正,无违反业务规则的值。
  • 类型检查:所有特征为 float32 或 int32,无 object/datetime 类型残留。
  • 类别编码:类别字段已编码为整数,并记录 categorical_features 列表。
  • 内存检查:使用 df.info(memory_usage='deep') 确认内存占用合理。
# 最终数据质量检查
assert df.isnull().sum().sum() == 0, '存在未处理的缺失值!'
assert all(df.dtypes.isin(['float32', 'int32', 'int8'])), '存在非数值类型字段!'
print('数据预处理完成,准备进入特征工程阶段。')
print(f'最终数据维度: {df.shape}')
预处理对 LightGBM 的影响:干净的数据能减少树模型的无效分裂,提升训练速度与泛化能力。缺失值处理不当会导致模型学习到错误的分布;异常值未处理会使树模型过度拟合极端样本,降低额度定价的稳健性。