数据预处理基础:缺失值处理、异常值检测、数据标准化与归一化

做数据分类项目,我有个铁打的习惯:拿到原始数据后,绝不直接扔进模型。你想想看,脏数据、缺数据、量纲不一致的数据,模型根本学不到东西。我记得刚入行时接过一个金融反欺诈项目,原始表里 30% 的年龄字段是空的,还款金额还有几个天文数字……结果模型 AUC 只有 0.51,跟抛硬币没区别。后来老老实实把预处理做了一遍,AUC 直接跳到 0.82。

预处理说白了就三件事:缺的补上、坏的揪出、尺度拉齐。下面我按这个顺序一个一个讲。

核心流程:缺失值处理 → 异常值检测 → 数据标准化/归一化。这三步顺序不能乱,尤其是标准化要在异常值处理之后做,否则异常值会把均值和标准差拉偏。

缺失值处理——别让空值毁了模型

缺失值分三种:完全随机缺失、随机缺失、非随机缺失。实战中我们不用纠结分类,直接用策略处理就行。我个人把策略分三级:

策略适用场景缺点
直接删除缺失比例 < 5%,且样本充足丢失信息,样本太少时不能用
均值/中位数填充数值型,数据分布较对称会降低方差,影响模型对极端值的敏感度
KNN 填充特征间有相关性,数据量中等计算量大,特征维数高时效果差

我项目中用得最多的是中位数填充。为什么不是均值?因为异常值经常存在,均值会被拉偏。比如收入字段,几个大老板的年薪能到千万级别,均值一下子五六百万,普通人反而被掩盖了。中位数就稳得多。

代码实现很简单:

import pandas as pd
import numpy as np

# 中位数填充
df['age'].fillna(df['age'].median(), inplace=True)

# 还可以用 groupby 分组填充(比如按性别填充年龄)
df['age'] = df.groupby('gender')['age'].transform(lambda x: x.fillna(x.median()))

小贴士:用 groupby 填充能保留组内差异,比全局填充更精细。我曾经在一个电商用户画像项目里,按城市级别分组填充收入,模型效果提升了 3 个百分点。

异常值检测——把刺头找出来

异常值分两种:全局异常(明显超出正常范围)和局部异常(在群体内奇怪)。我常用的检测方法就三个:

  • Z-Score 法:假设数据服从正态分布,阈值一般取 3。超过 3 个标准差就算异常。
  • IQR 法:四分位距法,用 Q1 - 1.5×IQR 到 Q3 + 1.5×IQR 做界限。不依赖正态假设,更鲁棒。
  • 孤立森林:基于随机森林思想,适合高维数据。我接口里一般用它做兜底。

我建议先用 IQR 法快速筛查。之前在银行信用评分卡项目里,就用 IQR 筛出了几个“借款金额”在 0.01 元的脏样本,其实是测试数据混进去了。

def detect_outliers_iqr(df, column):
    Q1 = df[column].quantile(0.25)
    Q3 = df[column].quantile(0.75)
    IQR = Q3 - Q1
    lower = Q1 - 1.5 * IQR
    upper = Q3 + 1.5 * IQR
    return (df[column] < lower) | (df[column] > upper)

# 标记异常
df['is_outlier'] = detect_outliers_iqr(df, 'amount')

注意:异常值不一定要删除。如果确认是录入错误,直接删;如果是真实罕见事件(比如大额交易),你可能需要保留并单独建模。我曾经因为一刀切删了全部异常值,导致欺诈检测模型召回率暴跌 20%,教训深刻。

数据标准化与归一化——把特征拉到同一把尺

为什么需要这一步?你想想看,收入范围 0~10 万,年龄范围 0~100,两个特征直接一起算距离,年龄那点差异根本不够看。模型会认为收入更重要,但实际上未必。

标准化和归一化是两个不同东西,别搞混。

方法公式输出范围适用场景
Min-Max 归一化(x - min)/(max - min)[0, 1]数据有明确上下界,如像素值
Z-Score 标准化(x - μ)/σ无固定范围,均值为0数据接近正态,或者有异常值存在
Robust 标准化(x - median)/IQR无固定范围数据包含很多异常值

我个人习惯:分类算法用归一化(比如 KNN、神经网络),树模型不用做(比如随机森林、XGBoost,它们只关心分裂点)。线性回归、SVM 等距离敏感的算法,优先用 Z-Score 标准化。

from sklearn.preprocessing import StandardScaler, MinMaxScaler

# Z-Score 标准化
scaler = StandardScaler()
df_scaled = scaler.fit_transform(df[['income', 'age']])

# Min-Max 归一化
scaler = MinMaxScaler()
df_norm = scaler.fit_transform(df[['income', 'age']])

实战经验:如果你用 pipeline,一定要先填充缺失值、再检测异常值、最后做缩放。我见过有人先归一化再填充缺失值,结果填充的均值被缩放影响,完全乱掉了。

知识体系总览

数据预处理核心流程 原始数据 1. 缺失值处理 删除 / 均值填充 / KNN填充 2. 异常值检测 Z-Score法 / IQR法 / 孤立森林 3. 标准化 / 归一化 MinMax / Z-Score / Robust 预处理后数据 ⚠ 注意:缺失值处理 → 异常值检测 → 标准化,顺序不可颠倒。先标准化会放大异常值影响。

避坑指南——我踩过的几个坑

讲几个真实教训,你遇到了直接跳过:

  1. 不要对布尔型或分类型特征做标准化。比如性别(0/1),标准化后变成 -0.5 到 0.5,完全失去可解释性。
  2. 测试集和训练集要使用相同的 scaler。也就是说,先用训练集 fit,然后用同一个 scaler 去 transform 测试集。我曾经直接在测试集上重新 fit,导致分布偏移,模型评估结果虚高。
  3. 填充缺失值后记得检查数据分布。如果填充值太多,分布会出现一个尖峰,模型可能学出偏差。

特别提醒:如果数据带有时间序列属性,千万不要用未来数据填充过去。比如用整月的均值填充某一天的空缺,那就泄露了未来信息。正确做法是按时间窗口滑动填充。

嗯,数据预处理这部分,说难不难,说简单也不简单。说白了就是把原始数据整理成模型能理解的语言。后面几章搭建分类接口时,你会反复用到这些方法。先在心里画好这张流程图的顺序,写代码时就稳了。