数据预处理基础:数据清洗与特征工程的第一步

数据预处理,说白了就是给原始数据「洗澡」。我做了这么多年特征工程,见过太多「垃圾进、垃圾出」的案例。你想想看,模型再牛,喂进去的数据有问题,结果能好到哪去?

这一章我们重点聊三个核心环节:数据清洗、标准化/归一化、离散化。嗯,都是基本功,但恰恰是这些基本功决定了项目的成败。

核心观点:数据预处理占整个特征工程工作量的 60%-80%。别急着调模型,先把数据收拾干净。

数据预处理知识体系 数据预处理 数据清洗 标准化与归一化 数据离散化 缺失值处理 异常值处理 Z-score Min-Max 等宽 等频 聚类 💡 核心原则:先清洗,再变换,后离散。顺序错了,效果天差地别。 缺失值 → 异常值 → 标准化/归一化 → 离散化

1. 数据清洗:缺失值处理

缺失值,每个做数据的人都会遇到。我记得刚入行时,拿到一个客户数据集,30% 的年龄字段是空的。当时我直接删了那些行,结果模型上线后,年轻用户群体预测准确率惨不忍睹。

处理缺失值,常见策略就三种:

  • 删除法:直接删掉有缺失的行或列。适合缺失比例小(<5%)的情况。
  • 填充法:用均值、中位数、众数或模型预测值填充。
  • 标记法:把缺失本身作为一个特征,比如加一列 is_null 标志。

我的经验:数值型字段我习惯用中位数填充,因为不受异常值影响。类别型字段用众数,或者单独分一类叫「未知」。

import pandas as pd
import numpy as np

# 示例数据
df = pd.DataFrame({'age': [25, 30, np.nan, 35, 40, np.nan, 28],
                   'income': [50000, 60000, 55000, np.nan, 70000, 65000, 48000]})

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

# 均值填充
df['income'].fillna(df['income'].mean(), inplace=True)

print(df)

2. 数据清洗:异常值处理

异常值,说白了就是那些「离谱」的数据点。比如用户年龄填了 200 岁,或者月收入填了 10 个亿。我做过一个电商项目,有个用户的订单金额是平均值的 500 倍,后来发现是测试数据混进来了。

怎么发现异常值?常用方法:

  • 3σ 原则:超出均值 ±3 倍标准差的数据,视为异常。
  • 箱线图法:超出 Q1-1.5IQR 或 Q3+1.5IQR 的数据。
  • 业务规则:比如年龄不能大于 120,金额不能为负数。

⚠️ 注意:别一棍子打死所有异常值。有些异常值其实是「宝藏」,比如金融欺诈检测中,异常交易恰恰是我们要找的目标。

# 使用 IQR 方法检测异常值
Q1 = df['income'].quantile(0.25)
Q3 = df['income'].quantile(0.75)
IQR = Q3 - Q1

lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR

outliers = df[(df['income'] < lower_bound) | (df['income'] > upper_bound)]
print(f"检测到 {len(outliers)} 个异常值")

3. 数据标准化与归一化

为什么要做标准化?你想想看,如果特征 A 的取值范围是 0-1,特征 B 是 0-100000,很多模型(比如 SVM、KNN、神经网络)会天然偏向 B,因为它的数值大。这不公平。

两种主流方法:

方法 公式 输出范围 适用场景
Z-score 标准化 z = (x - μ) / σ 无固定范围,均值为0,标准差为1 数据近似正态分布,或存在异常值
Min-Max 归一化 x' = (x - min) / (max - min) [0, 1] 数据有明确边界,无异常值

我个人习惯:如果后续要用 PCA 或线性模型,优先 Z-score。如果是神经网络,Min-Max 更常见。当然,没有标准,得看数据说话。

from sklearn.preprocessing import StandardScaler, MinMaxScaler

# Z-score 标准化
scaler_z = StandardScaler()
df_z = scaler_z.fit_transform(df[['age', 'income']])

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

避坑指南:我曾经在时间序列预测中,不小心对整个数据集做了标准化,导致数据泄露——测试集的信息混进了训练集。正确的做法是:先拆分训练集和测试集,再用训练集的参数(μ、σ 或 min、max)去变换测试集。

4. 数据离散化

离散化,就是把连续值切成几段。为什么要这么做?有些模型(比如决策树)对连续值不太友好,而且离散化后特征更稳定,不容易受异常值干扰。

三种常见方法:

  • 等宽离散化:把数据范围等分成 k 段。比如 0-100 分成 [0,20), [20,40), ..., [80,100]。简单粗暴,但数据分布不均匀时效果差。
  • 等频离散化:每段包含相同数量的样本。适合长尾分布的数据。
  • 聚类离散化:用 KMeans 等聚类算法,把数据聚成 k 类,每类作为一个区间。效果最好,但计算量大。
import pandas as pd
from sklearn.cluster import KMeans

# 等宽离散化
df['age_bin'] = pd.cut(df['age'], bins=4, labels=['青年', '壮年', '中年', '老年'])

# 等频离散化
df['income_bin'] = pd.qcut(df['income'], q=4, labels=['低', '中低', '中高', '高'])

# 聚类离散化
kmeans = KMeans(n_clusters=3, random_state=42)
df['cluster'] = kmeans.fit_predict(df[['income']])

我的建议:等宽适合数据分布均匀的情况;等频适合偏态分布;聚类适合对精度要求高的场景。我一般先用等频试试,不行再换聚类。别一上来就上最复杂的。

5. 本章小结

数据预处理,说白了就是给模型喂「干净饭」。缺失值、异常值、标准化、离散化,每一步都影响最终效果。我见过太多人跳过这些步骤直接调参,结果浪费大量时间。

记住这个顺序:先清洗(缺失值 → 异常值),再变换(标准化/归一化),最后离散化。顺序错了,效果天差地别。