3. 数据清洗与预处理:把脏数据变成干净燃料

数据清洗这步,说白了就是给模型喂饭前的洗菜切菜。我见过太多团队,模型结构调得花里胡哨,结果数据里一堆空值和异常点,跑出来的结果根本没法看。嗯,咱们今天就聊聊这块硬功夫。

核心观点:数据清洗占全链路 60% 以上的工作量。别嫌烦,这一步省下来的时间,后面会加倍还给你。

3.1 缺失值处理:空着不是办法

现实中的数据几乎没有完美的。传感器掉线、用户没填、系统故障——缺失值就这么来了。我个人习惯,拿到数据先跑个 df.isnull().sum(),看看哪些列在「装死」。

3.1.1 直接删除:简单粗暴,但有代价

删除是最直接的方式。但什么时候删?我建议你记住两个原则:

  • 行删除:缺失比例 < 5%,且缺失完全随机。比如 10 万条数据里只有 200 条有空值,直接 dropna() 问题不大。
  • 列删除:某列缺失超过 70%,这列基本废了。我曾经在风控项目里遇到一列「配偶收入」,90% 都是空值,留着只会增加噪声。
# 行删除
df_clean = df.dropna()

# 列删除(缺失率 > 70% 的列直接扔掉)
df_clean = df.dropna(thresh=len(df) * 0.3, axis=1)

注意:删除会丢失信息。如果样本本来就少(比如只有 500 条),删掉 50 条就是 10% 的损失。这时候要慎重。

3.1.2 填充:用合理值补位

填充比删除更常用。我一般按这个优先级来:

  1. 固定值填充:适用于类别特征。比如「性别」缺失,填「未知」。
  2. 均值/中位数填充:数值型特征。有异常值时用中位数,没有就用均值。
  3. 众数填充:离散型特征。比如「学历」缺失,填出现最多的那个。
  4. 前后填充:时间序列数据。用前一个值或后一个值填充。
# 均值填充
df['age'].fillna(df['age'].mean(), inplace=True)

# 中位数填充(更抗异常值)
df['income'].fillna(df['income'].median(), inplace=True)

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

# 前向填充(时间序列)
df['temperature'].fillna(method='ffill', inplace=True)

3.1.3 插值:更高级的补位

插值不是简单填一个数,而是根据数据趋势「猜」出缺失值。线性插值、多项式插值、KNN 插值——我用的最多的是线性插值和 KNN 插值。

# 线性插值
df['value'] = df['value'].interpolate(method='linear')

# KNN 插值(需要 sklearn)
from sklearn.impute import KNNImputer
imputer = KNNImputer(n_neighbors=5)
df_imputed = imputer.fit_transform(df)

我的经验:KNN 插值效果好,但计算量大。数据量超过 10 万行时,我会先用线性插值兜底,只在关键特征上用 KNN。

3.2 异常值检测:揪出那些「不听话」的数据

异常值是什么?就是那些明显偏离正常范围的数据点。比如用户年龄填了 200 岁,或者某个传感器突然跳变到正常值的 10 倍。这些点不处理,模型会被它们带偏。

3.2.1 3σ 法则:适合正态分布

3σ 法则基于正态分布。数据落在均值 ±3 个标准差之外的概率只有 0.3%,这些点可以视为异常。

import numpy as np

def detect_outliers_3sigma(data):
    mean = np.mean(data)
    std = np.std(data)
    lower = mean - 3 * std
    upper = mean + 3 * std
    return (data < lower) | (data > upper)

避坑:3σ 对异常值本身很敏感。如果数据里已经有几个极端异常值,均值和标准差会被拉偏,导致漏检。我一般先用 IQR 粗筛一遍,再用 3σ 精筛。

3.2.2 IQR 方法:更稳健的选择

IQR(四分位距)不依赖均值和标准差,抗异常值能力强。它的逻辑是:

  • 计算 Q1(25% 分位数)和 Q3(75% 分位数)
  • IQR = Q3 - Q1
  • 下界 = Q1 - 1.5 × IQR,上界 = Q3 + 1.5 × IQR
  • 超出上下界的视为异常
def detect_outliers_iqr(data):
    Q1 = np.percentile(data, 25)
    Q3 = np.percentile(data, 75)
    IQR = Q3 - Q1
    lower = Q1 - 1.5 * IQR
    upper = Q3 + 1.5 * IQR
    return (data < lower) | (data > upper)
方法 适用场景 优点 缺点
3σ 数据近似正态分布 计算简单,理论成熟 对异常值敏感,非正态分布效果差
IQR 偏态分布、有异常值 稳健,不受极端值影响 对样本量有要求(至少几百条)

3.3 数据标准化与归一化:让特征站在同一起跑线

为什么需要标准化?你想想看,如果「年龄」范围是 0-100,「收入」范围是 0-100 万,模型会天然认为收入更重要——因为它的数值更大。这不是模型想要的。

3.3.1 标准化(Z-score):去均值,除标准差

标准化后的数据均值为 0,标准差为 1。适合数据本身有正有负、分布近似正态的场景。

from sklearn.preprocessing import StandardScaler

scaler = StandardScaler()
df_scaled = scaler.fit_transform(df)

3.3.2 归一化(Min-Max):缩放到 [0, 1]

归一化把数据压缩到 0 到 1 之间。适合数据有明确上下界、且没有极端异常值的场景。

from sklearn.preprocessing import MinMaxScaler

scaler = MinMaxScaler()
df_normalized = scaler.fit_transform(df)
方法 公式 输出范围 适用场景
标准化 (x - μ) / σ 无固定范围(通常 -3 到 3) SVM、PCA、线性回归
归一化 (x - min) / (max - min) [0, 1] 神经网络、KNN、图像处理

我的习惯:不确定用哪个时,先试标准化。它对异常值的容忍度比归一化高。如果模型收敛慢,再换归一化试试。

3.4 重复数据去重:别让模型「记住」同一件事

重复数据会让模型对某些样本过度学习。比如用户 A 的数据被录入了 3 次,模型就会觉得「这类用户很重要」,但实际上只是数据采集出了问题。

# 完全重复的行
df_clean = df.drop_duplicates()

# 基于特定列去重(比如只保留每个用户的最新记录)
df_clean = df.drop_duplicates(subset=['user_id'], keep='last')

注意:不是所有重复都要删。时间序列里,同一时刻的多个传感器读数可能是正常的。先搞清楚「重复」的业务含义,再动手。

3.5 本章知识体系总览

下面这张图把数据清洗与预处理的四个核心模块串起来了。你可以把它当作操作手册——遇到脏数据,按这个流程走一遍。

数据清洗与预处理知识体系 数据清洗与预处理知识体系 原始数据 缺失值处理 删除 | 填充 | 插值 异常值检测 3σ 法则 | IQR 方法 标准化与归一化 Z-score | Min-Max 重复数据去重 完全去重 | 部分去重 干净、规范、可用的数据集 操作建议 • 缺失值:先看比例,再选方法。比例小就删,比例大就填。 • 异常值:IQR 比 3σ 更稳健,推荐优先使用。 • 标准化:不确定时先试 Z-score,模型不收敛再换 Min-Max。 • 去重:先理解业务含义,再决定是否删除。

3.6 实战建议:先搭流程,再调细节

数据清洗没有银弹。我个人的工作流是这样的:

  1. 先跑概览:df.info()、df.describe(),快速了解数据全貌。
  2. 处理缺失值:按 3.1 的优先级来,先删后填。
  3. 检测异常值:用 IQR 方法,标记出来看看是不是真的异常。
  4. 标准化/归一化:根据模型需求选择。
  5. 去重:最后一步,确保数据唯一性。

最后说一句:数据清洗不是一次性工作。模型跑完发现效果不好,回头看看数据——八成是清洗环节出了问题。迭代几次,你的数据管道会越来越稳。