数据为什么不能直接拿来训练:机器学习里的数据预处理到底在做什么

前面几篇一直在讲模型。

线性回归在干什么,逻辑回归在干什么,损失函数在干什么,梯度下降又是怎么一步一步把参数调好的。

但如果你真的开始自己动手做一个机器学习任务,很快就会发现一件事:

模型本身可能没那么难,真正麻烦的,往往是数据。

很多时候,模型效果不好,不是因为算法太差,而是因为你喂给它的数据本身就有问题。

这篇就来讲一个特别容易被初学者忽略,但实际上又特别重要的东西:

数据预处理。


1. 为什么数据不能直接拿来训练

刚开始学机器学习的时候,最容易形成一种错觉:

只要我选个模型,把数据丢进去,训练一下,结果自然就出来了。

理论上确实是这样。
但现实里的数据,往往并没有那么“干净”。

比如你可能会遇到这些情况:

  • 有些样本缺了值
  • 有些数据明显填错了
  • 有些特征的量纲差得特别大
  • 有些类别变量根本不能直接喂给模型
  • 有些记录重复了
  • 有些数据看起来正常,其实会误导模型

也就是说,真实数据通常不是“现成能吃”的,而是需要先处理一下。

你可以把模型想象成一个人。
如果你给它喂的是乱七八糟、格式不统一、甚至本身有错误的数据,那它最后学出来的东西,大概率也不会太靠谱。

所以很多时候,机器学习并不是“先学模型”,而是“先把数据收拾干净”。


2. 数据预处理到底是在做什么

所谓数据预处理,说白了就是一句话:

在训练模型之前,先把数据整理成更适合模型学习的样子。

它不是某一个固定步骤,而是一组常见操作的总称。

比如:

  • 处理缺失值
  • 去掉重复数据
  • 识别异常值
  • 编码类别特征
  • 标准化数值特征
  • 划分训练集和测试集

这些事情做的,本质上都是同一件事:

尽量减少脏数据、乱数据、格式问题对模型训练的干扰。


3. 先说最常见的问题:缺失值

缺失值几乎是最常见的数据问题之一。

比如你拿到一份用户数据,字段可能长这样:

年龄月收入是否购买
2580001
30120000
28NaN1
NaN90000

这里的 NaN 就表示缺失。

那问题来了:
缺失值怎么办?

第一种做法:直接删除

如果缺失的样本很少,而且删掉以后不会影响整体数据分布,那可以直接删。

比如:

  • 10000 条数据里只有 10 条缺失
  • 删掉影响不大

但如果缺失很多,直接删就不一定合适了。

第二种做法:填补

更常见的方式是填补。

比如数值型特征,可以用:

  • 平均值
  • 中位数
  • 众数

来补上。

举个例子,收入这个字段如果有缺失,可以用已有收入的平均值或中位数填进去。

一般来说:

  • 数据比较平稳时,可以考虑平均值
  • 数据偏差比较大、有极端值时,中位数通常更稳

第三种做法:用更复杂的方法估计

更进阶一点,还可以根据别的特征去预测缺失值。
但对入门阶段来说,先理解“删”和“填”就够了。

最重要的是你要意识到:

大多数模型是不能直接处理缺失值的。

所以缺失值通常必须先处理。


4. 重复数据看起来小问题,其实也会影响结果

重复数据也是很常见的问题。

比如你的一份用户表里,本来一个用户只该出现一次,结果因为数据合并或者导出问题,某些记录重复了两遍甚至三遍。

这会带来什么影响?

最直接的影响就是:

某些样本被模型重复看了好几次。

这样一来,模型就会不自觉地把这些重复样本当得更“重要”,从而影响整体训练结果。

所以在数据预处理阶段,一个很基本的动作就是:

先检查数据里有没有重复记录。

如果有,而且确认是无意义的重复,就应该去掉。


5. 异常值为什么要注意

再来看一个很常见的问题:异常值。

比如你在做房价预测,数据大多数是这样的:

  • 80 平米,200 万
  • 90 平米,230 万
  • 100 平米,260 万

结果里面突然冒出来一条:

  • 95 平米,9999 万

这条数据有可能是真的豪宅,也有可能是录入错误。
如果你不处理,它可能会把模型带偏。

因为很多模型,尤其是像线性回归这种对数值比较敏感的模型,很容易受到极端值影响。

所以预处理阶段要做的一件事就是:

识别这些特别离谱的数据,判断它到底是正常极端值,还是错误数据。

这一步不能机械处理。
因为不是所有异常值都该删。

比如:

  • 如果它只是录错了,那应该修正或者删除
  • 如果它确实是真实存在的特殊样本,那可能应该保留

这里最重要的不是“删异常值”,而是:

你要先知道它是不是异常,以及它为什么异常。


6. 类别特征为什么不能直接扔给模型

很多初学者第一次碰这个问题,都会有点懵。

比如你有一个字段叫“城市”:

  • 北京
  • 上海
  • 深圳

或者一个字段叫“学历”:

  • 本科
  • 硕士
  • 博士

这些内容人一眼就能看懂,但模型看不懂。
因为大多数机器学习模型处理的是数字,不是文字。

所以你不能直接把“北京”“上海”“深圳”喂给模型。
你得先把它们变成数值形式。

这一步就叫做:

类别特征编码。


7. 最基础的类别编码:独热编码

最常见的一种方式叫独热编码(One-Hot Encoding)

比如城市这个字段有三个取值:

  • 北京
  • 上海
  • 深圳

那就可以变成三列:

城市_北京城市_上海城市_深圳
100
010
001

这样模型就能处理了。

为什么不能简单写成:

  • 北京 = 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 看几个最基础的预处理操作

下面直接看代码。
这里用 pandasscikit-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)

这样一来,ageincome 里的空值就被补上了。

当然,真实项目里到底用均值还是中位数,要看数据情况。
这里只是先演示最基础的方法。


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. 这篇最好记住的几句话

真实数据通常不能直接拿来训练。

数据预处理的目标,是把数据整理成更适合模型学习的样子。

常见的预处理包括:处理缺失值、去重、识别异常值、类别编码、标准化、划分训练集和测试集。

不同模型对预处理的依赖程度不一样。

模型效果不好时,不一定先怪算法,先看看数据。


更多推荐