第四章 数据预处理(上):数据清洗与缺失异常值处理

各位同学,咱们今天聊聊数据预处理。说实话,这步看着不起眼,但在我做过的几十个评分卡项目里,80%的模型翻车都出在数据预处理上。你想想看,模型再牛,喂进去的是垃圾,吐出来的能是黄金吗?

核心观点:数据预处理不是体力活,是决定模型上限的关键环节。我见过太多人急着调参,结果数据里一堆坑,最后模型上线就崩。

数据预处理 数据清洗 缺失值处理 异常值处理 去重 格式统一 逻辑校验 均值/中位数 众数填充 模型预测 盖帽法 分箱法 截尾处理 目标:干净、完整、合理的建模数据

4.1 数据清洗:别让脏数据毁了你的模型

数据清洗,说白了就是给数据「洗澡」。我刚开始做风控那会儿,接过一个项目,数据量看着挺大,结果一跑模型,AUC只有0.52。查了半天,发现是重复数据占了30%。你说气不气人?

4.1.1 去重处理

重复数据在申请评分卡里特别常见。比如同一个客户申请了两次,或者系统抽风多录了一条。我个人习惯分三步走:

  1. 完全重复检测:所有字段一模一样,直接删掉一条
  2. 关键字段重复:身份证号、手机号相同,但其他字段不同——这时候要保留最新的那条
  3. 业务逻辑去重:比如同一个人同一天申请多次,只保留第一次

我的小技巧:去重之前,先看看重复数据的占比。如果超过5%,建议先排查数据采集环节是不是出了问题。我曾经遇到过一个项目,重复率高达40%,最后发现是接口调用逻辑写错了。

# Python 示例:去重处理
import pandas as pd

# 完全去重
df_clean = df.drop_duplicates()

# 按身份证去重,保留最新记录
df_clean = df.sort_values('apply_time').drop_duplicates(subset='id_card', keep='last')

# 查看重复率
dup_rate = (df.duplicated().sum() / len(df)) * 100
print(f'重复率:{dup_rate:.2f}%')

4.1.2 格式统一

格式问题看着小,坑起来真要命。我记得有一次,一个同事跑模型一直报错,查了两天才发现——身份证号有的带X,有的带x,有的中间有空格。你说冤不冤?

我一般会做这几件事:

  • 字符统一:全部转大写或小写,去掉首尾空格
  • 日期格式:统一成 YYYY-MM-DD,别搞什么 2024/01/01 或 01-01-2024
  • 数值格式:去掉千分位逗号,统一小数位数
  • 分类变量:把「男」「M」「男性」统一成「男」

注意:格式统一一定要在去重之后做。为什么?因为格式不统一的时候,你根本看不出来哪些是重复数据。比如「张三」和「张三 」在去重时会被当成两条不同的记录。

4.2 缺失值处理:填还是不填,这是个问题

缺失值处理,是数据预处理里最考验经验的环节。你想想看,一个字段缺失率80%,你硬填进去,那不是自欺欺人吗?

4.2.1 先判断缺失类型

我个人习惯,拿到数据先看缺失率。然后分三种情况:

缺失率 处理方式 我的经验
< 5% 直接删除或简单填充 删掉影响不大,但要注意样本量
5% - 30% 均值/中位数/模型填充 这是最常见的区间,要仔细处理
> 30% 考虑删除该变量 或者做成「是否缺失」的哑变量

4.2.2 均值/中位数/众数填充

这是最基础的方法,简单粗暴。但要注意:

  • 均值填充:适合正态分布的数据,比如收入。但如果有异常值,均值会被拉偏
  • 中位数填充:适合偏态分布,比如贷款金额。我一般优先用中位数,因为它更稳健
  • 众数填充:适合分类变量,比如学历、职业

关键点:填充之前,一定要先分组。比如按「是否逾期」分组后,分别计算好客户和坏客户的中位数,然后各自填充。你想想看,好客户和坏客户的收入能一样吗?

# 分组填充示例
# 按目标变量分组后,用各组的中位数填充
df['income_filled'] = df.groupby('target')['income'].transform(
    lambda x: x.fillna(x.median())
)

4.2.3 模型预测填充

这个方法我特别喜欢,尤其是在做评分卡的时候。说白了,就是用其他字段来预测缺失的那个字段。

具体怎么做?

  1. 把数据分成两部分:有缺失值的和没缺失值的
  2. 用没缺失值的数据训练一个模型(比如随机森林、XGBoost)
  3. 用训练好的模型预测缺失值

避坑指南:我曾经犯过一个错误——用目标变量(y)来预测缺失值。结果模型过拟合得一塌糊涂。记住,预测缺失值时,只能用特征变量,不能用目标变量。否则就是数据泄露。

# 模型预测填充示例
from sklearn.ensemble import RandomForestRegressor

# 分离有缺失和没缺失的数据
not_missing = df[df['income'].notna()]
missing = df[df['income'].isna()]

# 训练模型
features = ['age', 'education', 'work_years', 'debt_ratio']
model = RandomForestRegressor()
model.fit(not_missing[features], not_missing['income'])

# 预测缺失值
df.loc[df['income'].isna(), 'income'] = model.predict(missing[features])

4.3 异常值处理:别让极端值带偏了模型

异常值,说白了就是那些「不正常」的数据。比如一个20岁的人年收入1000万,你觉得正常吗?

4.3.1 盖帽法

盖帽法,也叫截尾处理。就是把超过某个阈值的值,强行拉回到阈值上。

我一般用百分位数来定阈值:

  • 下界:1%分位数或5%分位数
  • 上界:99%分位数或95%分位数

举个例子,收入的99%分位数是50万,那所有超过50万的收入,都改成50万。这样做的好处是保留了样本量,不会因为删掉异常值而损失数据。

注意:盖帽法的阈值不是固定的。我建议你先画个箱线图看看数据分布。如果异常值特别多,可能是数据本身有问题,而不是真正的异常。

# 盖帽法示例
def cap_outliers(df, column, lower_percentile=0.01, upper_percentile=0.99):
    lower = df[column].quantile(lower_percentile)
    upper = df[column].quantile(upper_percentile)
    df[column] = df[column].clip(lower, upper)
    return df

# 对收入字段做盖帽处理
df = cap_outliers(df, 'income', 0.01, 0.99)

4.3.2 分箱法

分箱法,就是把连续变量变成离散变量。比如把年龄分成「18-25」「26-35」「36-45」「46-55」「55+」这几个区间。

为什么这么做?有两个好处:

  1. 降低异常值影响:极端年龄和正常年龄被分到同一个箱子里,影响就小了
  2. 提升模型稳定性:评分卡里用的就是分箱后的WOE值,这是标准做法

我的经验:分箱不是随便分的。我一般会结合业务逻辑和统计检验。比如年龄分箱,我会看看每个箱子里好客户和坏客户的比例,确保分箱有意义。如果某个箱子里全是好客户,那这个箱子可能分得太细了。

# 分箱法示例
import numpy as np

# 等距分箱
df['age_bin'] = pd.cut(df['age'], bins=[18, 25, 35, 45, 55, 100], 
                       labels=['18-25', '26-35', '36-45', '46-55', '55+'])

# 等频分箱(每个箱子里样本量差不多)
df['income_bin'] = pd.qcut(df['income'], q=5, labels=['Q1', 'Q2', 'Q3', 'Q4', 'Q5'])

4.4 本章小结

数据预处理这块,说白了就是「磨刀不误砍柴工」。我见过太多人急着建模,结果数据预处理草草了事,最后模型效果不好,回头又花大把时间调参。其实问题根本不在模型,在数据。

记住三个原则:

  • 先清洗,再处理:去重和格式统一是第一步
  • 先分析,再填充:缺失值处理前,先看看缺失率和缺失模式
  • 先理解,再截尾:异常值处理前,先理解业务含义

好了,这一章的内容就到这儿。数据预处理的下半部分,咱们会聊特征工程和变量筛选,那才是真正考验功力的地方。

专注资料整理