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 填充:用合理值补位
填充比删除更常用。我一般按这个优先级来:
- 固定值填充:适用于类别特征。比如「性别」缺失,填「未知」。
- 均值/中位数填充:数值型特征。有异常值时用中位数,没有就用均值。
- 众数填充:离散型特征。比如「学历」缺失,填出现最多的那个。
- 前后填充:时间序列数据。用前一个值或后一个值填充。
# 均值填充
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.6 实战建议:先搭流程,再调细节
数据清洗没有银弹。我个人的工作流是这样的:
- 先跑概览:
df.info()、df.describe(),快速了解数据全貌。 - 处理缺失值:按 3.1 的优先级来,先删后填。
- 检测异常值:用 IQR 方法,标记出来看看是不是真的异常。
- 标准化/归一化:根据模型需求选择。
- 去重:最后一步,确保数据唯一性。
最后说一句:数据清洗不是一次性工作。模型跑完发现效果不好,回头看看数据——八成是清洗环节出了问题。迭代几次,你的数据管道会越来越稳。