机器学习进阶(5):数据预处理
数据为什么不能直接拿来训练:机器学习里的数据预处理到底在做什么
前面几篇一直在讲模型。
线性回归在干什么,逻辑回归在干什么,损失函数在干什么,梯度下降又是怎么一步一步把参数调好的。
但如果你真的开始自己动手做一个机器学习任务,很快就会发现一件事:
模型本身可能没那么难,真正麻烦的,往往是数据。
很多时候,模型效果不好,不是因为算法太差,而是因为你喂给它的数据本身就有问题。
这篇就来讲一个特别容易被初学者忽略,但实际上又特别重要的东西:
数据预处理。
1. 为什么数据不能直接拿来训练
刚开始学机器学习的时候,最容易形成一种错觉:
只要我选个模型,把数据丢进去,训练一下,结果自然就出来了。
理论上确实是这样。
但现实里的数据,往往并没有那么“干净”。
比如你可能会遇到这些情况:
- 有些样本缺了值
- 有些数据明显填错了
- 有些特征的量纲差得特别大
- 有些类别变量根本不能直接喂给模型
- 有些记录重复了
- 有些数据看起来正常,其实会误导模型
也就是说,真实数据通常不是“现成能吃”的,而是需要先处理一下。
你可以把模型想象成一个人。
如果你给它喂的是乱七八糟、格式不统一、甚至本身有错误的数据,那它最后学出来的东西,大概率也不会太靠谱。
所以很多时候,机器学习并不是“先学模型”,而是“先把数据收拾干净”。
2. 数据预处理到底是在做什么
所谓数据预处理,说白了就是一句话:
在训练模型之前,先把数据整理成更适合模型学习的样子。
它不是某一个固定步骤,而是一组常见操作的总称。
比如:
- 处理缺失值
- 去掉重复数据
- 识别异常值
- 编码类别特征
- 标准化数值特征
- 划分训练集和测试集
这些事情做的,本质上都是同一件事:
尽量减少脏数据、乱数据、格式问题对模型训练的干扰。
3. 先说最常见的问题:缺失值
缺失值几乎是最常见的数据问题之一。
比如你拿到一份用户数据,字段可能长这样:
| 年龄 | 月收入 | 是否购买 |
|---|---|---|
| 25 | 8000 | 1 |
| 30 | 12000 | 0 |
| 28 | NaN | 1 |
| NaN | 9000 | 0 |
这里的 NaN 就表示缺失。
那问题来了:
缺失值怎么办?
第一种做法:直接删除
如果缺失的样本很少,而且删掉以后不会影响整体数据分布,那可以直接删。
比如:
- 10000 条数据里只有 10 条缺失
- 删掉影响不大
但如果缺失很多,直接删就不一定合适了。
第二种做法:填补
更常见的方式是填补。
比如数值型特征,可以用:
- 平均值
- 中位数
- 众数
来补上。
举个例子,收入这个字段如果有缺失,可以用已有收入的平均值或中位数填进去。
一般来说:
- 数据比较平稳时,可以考虑平均值
- 数据偏差比较大、有极端值时,中位数通常更稳
第三种做法:用更复杂的方法估计
更进阶一点,还可以根据别的特征去预测缺失值。
但对入门阶段来说,先理解“删”和“填”就够了。
最重要的是你要意识到:
大多数模型是不能直接处理缺失值的。
所以缺失值通常必须先处理。
4. 重复数据看起来小问题,其实也会影响结果
重复数据也是很常见的问题。
比如你的一份用户表里,本来一个用户只该出现一次,结果因为数据合并或者导出问题,某些记录重复了两遍甚至三遍。
这会带来什么影响?
最直接的影响就是:
某些样本被模型重复看了好几次。
这样一来,模型就会不自觉地把这些重复样本当得更“重要”,从而影响整体训练结果。
所以在数据预处理阶段,一个很基本的动作就是:
先检查数据里有没有重复记录。
如果有,而且确认是无意义的重复,就应该去掉。
5. 异常值为什么要注意
再来看一个很常见的问题:异常值。
比如你在做房价预测,数据大多数是这样的:
- 80 平米,200 万
- 90 平米,230 万
- 100 平米,260 万
结果里面突然冒出来一条:
- 95 平米,9999 万
这条数据有可能是真的豪宅,也有可能是录入错误。
如果你不处理,它可能会把模型带偏。
因为很多模型,尤其是像线性回归这种对数值比较敏感的模型,很容易受到极端值影响。
所以预处理阶段要做的一件事就是:
识别这些特别离谱的数据,判断它到底是正常极端值,还是错误数据。
这一步不能机械处理。
因为不是所有异常值都该删。
比如:
- 如果它只是录错了,那应该修正或者删除
- 如果它确实是真实存在的特殊样本,那可能应该保留
这里最重要的不是“删异常值”,而是:
你要先知道它是不是异常,以及它为什么异常。
6. 类别特征为什么不能直接扔给模型
很多初学者第一次碰这个问题,都会有点懵。
比如你有一个字段叫“城市”:
- 北京
- 上海
- 深圳
或者一个字段叫“学历”:
- 本科
- 硕士
- 博士
这些内容人一眼就能看懂,但模型看不懂。
因为大多数机器学习模型处理的是数字,不是文字。
所以你不能直接把“北京”“上海”“深圳”喂给模型。
你得先把它们变成数值形式。
这一步就叫做:
类别特征编码。
7. 最基础的类别编码:独热编码
最常见的一种方式叫独热编码(One-Hot Encoding)。
比如城市这个字段有三个取值:
- 北京
- 上海
- 深圳
那就可以变成三列:
| 城市_北京 | 城市_上海 | 城市_深圳 |
|---|---|---|
| 1 | 0 | 0 |
| 0 | 1 | 0 |
| 0 | 0 | 1 |
这样模型就能处理了。
为什么不能简单写成:
- 北京 = 1
- 上海 = 2
- 深圳 = 3
因为这样会让模型误以为“上海比北京大一点,深圳比上海再大一点”,但这在很多场景下根本没有意义。
所以对于没有大小顺序的类别特征,更常见的做法就是独热编码。
8. 数值范围差太大时,为什么要做标准化
再看一个特别常见但一开始不容易意识到的问题。
假设你有两个特征:
- 年龄:20 到 60
- 年收入:3000 到 50000
虽然这两个特征都很有用,但它们的数值范围差得特别大。
这会有什么问题?
对于某些模型来说,数值大的特征可能会在训练时显得“更有存在感”,从而影响模型学习。
尤其是:
- KNN
- 逻辑回归
- SVM
- 梯度下降相关模型
这时就经常需要做标准化或者归一化。
标准化是什么
标准化最常见的做法,是把数据变成“均值为 0,标准差为 1”附近的分布。
公式是:
x′=x−μσ x' = \frac{x - \mu}{\sigma} x′=σx−μ
你现在不用深究公式,只要知道效果就行:
把不同量纲、不同范围的特征,拉到比较接近的尺度上。
这样模型训练会更稳定。
9. 不是所有模型都同样依赖标准化
这里有个很重要的提醒。
很多初学者一学到标准化,就容易觉得所有模型都必须做。
其实不是。
像下面这些模型,通常更依赖特征缩放:
- 逻辑回归
- SVM
- KNN
- 神经网络
因为它们对距离、梯度、数值范围比较敏感。
但像下面这些树模型,一般就没那么依赖:
- 决策树
- 随机森林
- XGBoost
因为树模型做划分时,更看重的是特征的相对分割位置,而不是绝对数值大小。
所以数据预处理不是“固定模板全都来一遍”,而是:
看你的数据长什么样,也看你的模型需要什么。
10. 训练集和测试集为什么也算预处理的一部分
很多人会把“划分训练集和测试集”单独看成建模步骤。
这么理解没问题,但它其实也可以算在数据预处理流程里。
原因很简单:
你不能让模型一边训练,一边偷看答案。
如果你把所有数据都拿去训练,然后再用同一批数据测试,结果通常会很好看。
但这种好看没有太大意义,因为模型只是把数据记住了,并不代表它真的有泛化能力。
所以我们通常会把数据分成:
- 训练集:用来训练模型
- 测试集:用来评估模型在新数据上的表现
更规范一点,还会再分一个验证集,用来调参。
这一点特别重要,因为很多机器学习里的“翻车”,不是模型不会学,而是评估方式出了问题。
11. 用 Python 看几个最基础的预处理操作
下面直接看代码。
这里用 pandas 和 scikit-learn 做几个最常见的预处理示例。
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import OneHotEncoder, StandardScaler
# 构造一个简单数据集
data = pd.DataFrame({
"age": [25, 30, 28, None, 40],
"income": [8000, 12000, None, 9000, 50000],
"city": ["北京", "上海", "北京", "深圳", "上海"],
"buy": [1, 0, 1, 0, 1]
})
print("原始数据:")
print(data)
这份数据里已经包含了两个很常见的问题:
- 缺失值
- 类别特征
12. 先处理缺失值
最简单的方式之一,是用均值填补数值型缺失。
data["age"] = data["age"].fillna(data["age"].mean())
data["income"] = data["income"].fillna(data["income"].mean())
print("\n填补缺失值后:")
print(data)
这样一来,age 和 income 里的空值就被补上了。
当然,真实项目里到底用均值还是中位数,要看数据情况。
这里只是先演示最基础的方法。
13. 再处理类别特征
接下来,把 city 这种类别特征做独热编码。
data = pd.get_dummies(data, columns=["city"])
print("\n独热编码后:")
print(data)
处理完以后,原来的 city 会变成多个 0/1 列,比如:
city_北京city_上海city_深圳
这样模型就能读懂了。
14. 再划分训练集和测试集
X = data.drop("buy", axis=1)
y = data["buy"]
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
print("\n训练集特征:")
print(X_train)
print("\n测试集特征:")
print(X_test)
这一步做完之后,模型训练和模型评估就分开了。
这件事非常重要。
因为机器学习真正关心的,不是你能不能把旧数据记住,而是你能不能对没见过的新数据也做出靠谱判断。
15. 最后一步:标准化数值特征
如果你后面打算用逻辑回归、KNN、SVM 这些模型,就可以对数值特征做标准化。
scaler = StandardScaler()
X_train[["age", "income"]] = scaler.fit_transform(X_train[["age", "income"]])
X_test[["age", "income"]] = scaler.transform(X_test[["age", "income"]])
print("\n标准化后的训练集:")
print(X_train)
这里有一个细节特别值得注意:
训练集用的是:
scaler.fit_transform(...)
测试集用的是:
scaler.transform(...)
为什么不能测试集也 fit 一次?
因为那样等于你在测试阶段偷偷使用了测试数据本身的信息,会造成数据泄漏。
所以正确做法永远是:
- 在训练集上
fit - 再用同样的变换去处理测试集
这个细节初学者特别容易忽略。
16. 数据预处理不是“可选项”,而是建模的一部分
很多教程喜欢把重点都放在模型上,好像只要模型选得对,效果自然就会好。
但真实情况通常是:
数据质量往往比模型名字更重要。
你把一堆脏数据喂给一个高级模型,它也学不出太好的结果。
反过来,如果数据处理得足够规范,哪怕只是一个基础模型,效果也可能不错。
所以机器学习里有一句很朴素的话:
Garbage in, garbage out.
输入的东西本身乱七八糟,输出自然也很难好看。
17. 这篇最想让你记住的,不是具体工具,而是这个顺序
很多人学预处理,会把注意力全放在函数和库上,比如:
fillna()get_dummies()StandardScaler()
这些当然要会,但比工具更重要的,其实是思路。
你拿到一份数据以后,脑子里最好先过一遍这几个问题:
- 有没有缺失值?
- 有没有重复数据?
- 有没有明显异常值?
- 有没有类别特征?
- 数值范围差异大不大?
- 训练集和测试集分开了吗?
- 有没有不小心发生数据泄漏?
如果这个思路有了,后面你用什么工具其实都不难。
18. 总结
数据预处理这件事,说到底是在解决一个很现实的问题:
真实世界的数据,通常没有你想象中那么规整。
而机器学习模型,本质上是很“老实”的。
你给它什么,它就学什么。
你给它混乱的数据,它就会学出混乱的规律。
所以在训练模型之前,先把数据整理好,并不是琐碎的辅助工作,而是机器学习流程里非常重要的一部分。
很多时候,一个模型最后表现一般,不是因为算法不够高级,而是因为数据预处理没做好。
19. 这篇最好记住的几句话
真实数据通常不能直接拿来训练。
数据预处理的目标,是把数据整理成更适合模型学习的样子。
常见的预处理包括:处理缺失值、去重、识别异常值、类别编码、标准化、划分训练集和测试集。
不同模型对预处理的依赖程度不一样。
模型效果不好时,不一定先怪算法,先看看数据。
更多推荐


所有评论(0)