跳转至

P4-7.2 预处理(preprocessing)

Section ID: P4-7.2 Version: v2026.07.20

在 P4-7.1 里,我们看的是 要保留什么输入。现在要进入下一步:不是把保留下来的输入原样扔给 model,而是先整理成 model 更容易读取的形式。这个阶段就是预处理(preprocessing)。

这一节的核心不是复杂的库语法,而是不要只把预处理理解成 数据清扫,而要把它理解成 把输入表达改造成 model 能处理的形式

这一节会说明 预处理(preprocessing)缺失值处理(imputation)尺度(scale)调整类别编码(encoding) 的基本含义。后面的章节会沿着这个抓手继续当前判断,而输入表达变换的基本含义,也会以这一节和 概念词汇表 为基准再次接回。

还有一个重要原因。人们常常先学算法,再把预处理当成后面补上的辅助工作。但实际情况更接近相反。如果输入表达没有先整理好,后面要学的线性回归(linear regression)、逻辑回归(logistic regression)、k-NN、SVM 这些算法的性质也很难真正读清。

这一节不是某个特定算法的附录,而是整理 算法到底接收什么输入 的共同基础章节。

从学术上看,预处理也不是外围工作。在数据挖掘(data mining)、模式识别(pattern recognition)、机器学习(machine learning)的教材和工具文档里,预处理通常被当作一个独立阶段:把 原始数据(raw data) 移到 特征空间(feature space)模型输入(model input)。预处理不是贴在 model 前面的杂务,而是 构造可学习表达的阶段

这个视角之所以重要,原因很简单。model 学的不是现实本身,而是经过预处理后得到的输入表达。因此,预处理怎么做,会直接影响 boundary、distance、optimization 和 interpretability。

本节范围

这一节回答下面这些问题。

  • 预处理(preprocessing)是在改变什么?
  • 为什么缺失值、尺度、类别值如果原样放着会出问题?
  • 哪些 model 对尺度(scale)更敏感,哪些相对没那么敏感?
  • 在 train data 和 test data 里,预处理应该怎样分开处理?

这一节先收束 怎样把保留下来的输入改造成可计算、可比较的表达。各种 encoder 和 scaler 的细部设置,会在 P4-9.1、P4-9.2 的超参数和调优语境里再接回来;降维的目的和直觉,则会在 P4-18.1、P4-18.2 继续展开。

用预处理(preprocessing)留下的判断标准

  • 能把预处理解释成 把原始输入改造成更合适表达的阶段
  • 能说明为什么缺失值修正、尺度调整、类别编码是必要的。
  • 能区分预处理里的 fittransform,并说明为什么不能对 test data 做 fit
  • 能在入门层面理解为什么 pipeline 和 ColumnTransformer 在实务里经常出现。

学习背景

Part 4 前面的章节是按下面这个流程接过来的。

  • P4-4: 应该怎样划分数据
  • P4-5: 为什么泛化(generalization)很难
  • P4-6: 应该用什么标准评价
  • P4-7.1: 应该保留什么输入

走到这里之后,自然会出现下一个问题。

保留下来的输入,到底要以什么形式交给 model?

这正是预处理章节所在的位置。在课程结构里,这一节承担下面这些作用。

课程位置 预处理章节的作用
特征选择之后 整理保留下来的输入应该改成什么表达
模型选择之前 为理解不同 model 更偏好什么输入表达做准备
算法入门之前 连接为什么距离、边界、优化会对输入表达敏感

预处理就是连接 输入设计理解 model 的章节。

从课程结构上看,这一节也是 Module 2 和 Module 3 性质发生变化的边界。Module 2 里讲的数据划分、泛化、评价指标,处理的是 为了公平比较 model,必须先整理什么。而 Module 3 处理的是 实际要用什么输入和什么 model 组合来开始实验。预处理就正好落在这个转折点上。

经过这一节之后,读者就会从单纯把数据收集起来的状态,走向 先构造可比较的输入表达,再建立 model 候选 的流程。所以 P4-7.2 会成为 P4-8 模型选择(model selection)、P4-9 超参数调优(hyperparameter tuning)、以及 P4-10 之后算法章节的共同基础。

主要学习内容

第一次听到预处理时可以先这样抓

第一次听到 preprocessing 这个词,可能会觉得有点难。但非常短地说,它就是 把 model 不容易直接读的输入,先改造成能读的输入 的准备工作。

人是可以直接读下面这张表的。

age income city
29 3200 Seoul
6100 Busan
35 Incheon

但很多 model 很难把这张表原样拿来算。

  • 这种值,计算就会断掉。
  • 320029 的数值尺度差太大。
  • SeoulBusan 这样的字符串,很难直接参与计算。

所以通常可以把预处理理解成下面这几件事。

  1. 用某种规则处理空值。
  2. 把数值型的比较基准对齐,让比较更公平。
  3. 把字符串或类别改造成可计算的表达。

经过这个过程,同样的数据大致会变成下面这种输入形式。

age_filled income_scaled city_search city_busan city_incheon
29 ... 1 0 0
35 ... 0 1 0
35 ... 0 0 1

也就是说,预处理不是在制造新的正确答案,而是在做 把原来的输入重写成 model 能计算的格式 这件事。

这一节会把这个想法拆成三个代表场景来读。

  • 缺失值处理: 空着的值要怎么处理
  • 尺度调整: 数值轴大小差异要怎么对齐
  • 编码: 字符串和类别要怎样改造成可计算的形式

预处理是在做什么

scikit-learn 的预处理文档说明,它提供了多种函数和 transformer,用来把 raw feature vectors 转换成更适合 downstream estimator 的表达。把这句话改写成入门层面的意思,大概就是下面这样。

预处理是把原始输入改造成 model 更容易处理的输入表达的阶段。

预处理不是在改 label,而是在改输入表达。

如果再用稍微学术一点的话来说,预处理是一组对输入空间(input space)表达进行变换的操作,让学习算法能够更稳定地工作。这里面可以包括缺失值处理、scaling、encoding,有时也会包含 normalization、feature construction、dimensionality reduction 这样更宽的类别。这一节只聚焦于最常见的三类。

这一节会把预处理大致看成三部分。

  1. 缺失值处理(imputation)
  2. 数值变换与缩放(scaling / normalization)
  3. 类别表达变换(encoding)

这三件事的共同点都是 它们在改变输入表达。不过,改的东西并不一样。

预处理类型 它在改变什么 为什么要改
缺失值处理 填空值的规则 为了让计算成为可能
尺度调整 数字的大小和基准 为了稳定比较和优化
编码 把类别值变成数值表达 为了匹配 model 输入形式

不过,如果把实务里的预处理类型看得更宽一点,读者最好还要带着下面这张大地图。

预处理大类 它在做什么 这一节的处理程度
缺失值处理(imputation) 按规则修正或标记空值 详细讲
数值尺度调整(scaling/normalization) 对齐数值轴的大小和基准 详细讲
类别编码(encoding) 把字符串/类别值改成可计算表达 详细讲
异常值处理(outlier handling) 让极端值不要过度扰动学习 只提概念
特征构造(feature construction) 从原始列再造更有用的输入表达 只作为大类提及,本书主要和 P4-7.1 相连
降维(dimensionality reduction) 把很多输入压缩成更小的表达 在 P4-18 再讲
文本/图像等特殊表达变换 把非结构化数据改成数值表达 在后续 Part 作为单独的输入表达处理

这张表的目的,不是把所有预处理技术都塞进一节里,而是先让读者抓住:预处理 这个词并不只是一个狭义的“用平均值填缺失”的动作。

不过,为了在本书里把边界放清楚,一开始到底保留哪些输入、又把什么表达做成新特征候选,主要放在 P4-7.1 的特征设计一侧;这一节则集中在 把保留下来的输入改造成可计算、可比较的形式。也就是说,从大范围看,特征构造也可以算在预处理大类里,但这一节的中心责任是缺失值处理、尺度调整、编码,以及同样规则的复用。

在入门层面,像下面这样记也已经够用了。

  1. 填值的预处理
  2. 对齐数值轴的预处理
  3. 改变表达形式的预处理
  4. 缓和过大数值或减少输入数量的预处理

也就是说,预处理不是一个技术,而是 让输入表达可计算、可比较的多种规则集合

第一次理解预处理时,最快的方法通常还是画出 原始输入的一行,在预处理后会变成什么样

预处理前的一行 预处理后的一行示例
age=无, income=5800, city=Seoul age=用中位数修正, income=尺度调整后的值, city=[0,0,1]
age=35, income=无, city=Incheon age=保留 35, income=中位数修正后再缩放, city=[0,1,0]

这组对比展示出来的核心很简单。

  1. 预处理不会改 label。
  2. 预处理会把一行输入重写成 model 能计算的形式。
  3. 同样的规则必须重复应用到其他行上。

所以,与其说预处理是在 修数据,不如说它更接近 把输入行再翻译一遍

例如,可能会出现下面这些问题。

原始输入状态 为什么 model 会为难 预处理的作用
值是空的 无法直接计算 缺失值修正
数值大小差异过大 某些特征可能会有过大的影响力 尺度调整
值是城市名这样的字符串 很多 model 不能直接算 类别编码
train 和 test 用了不同规则 评价会被扭曲 保持同样的变换规则

因此,预处理既是 整理,也是 表达设计

如果再按输入问题把预处理重新归类,可以像下面这样读。

输入问题 先想到的预处理类型 示例
值是空的 缺失值处理 平均值、中位数、众数、缺失标记
数值大小差太多 尺度调整 标准化、最小-最大缩放
值是字符串或类别 编码 one-hot encoding
某些值跳得太厉害 异常值处理 评估 robust scaling 或 clipping
列太多了 降维/表达压缩 后续章节再讲 PCA 等方法
原始列本身信号太弱 特征构造 生成比率、差值、聚合特征

这样看,预处理就不是一个以 缺失值处理 + 尺度 + 编码 结束的清单,而是根据 当前输入到底有什么表达问题 往不同方向分开的工作。

从理论上看,预处理会同时从下面两个视角去读。

  1. 表达变换(representation transformation)
    把同一件事实换成别的数值表达
  2. 假设匹配(assumption matching)
    让输入更接近 model 或学习过程所期待的条件

例如,标准化(standardization)并不只是把数字排一排,而是在尝试对齐输入的中心和方差,让某些算法更容易工作。one-hot encoding 也不是强行把字符串变成数字,而是在改造成可计算的特征表达。

从学术上,可以把预处理理解成 把放在 model 前面的输入表达做变换,移到学习和推理都可行的空间里去的阶段

这里最重要的是,预处理不是 把数据弄得好看一点,而是在 重新给 model 将要看到的世界坐标化。同样一份客户数据,可能有些值是空的,有些值的数轴特别大,有些值是字符串,根本无法计算。预处理就是把这些东西重新调成 可比较、可计算的表达

所以,预处理不是辅助杂务,而是决定 model 能把什么读成同类信息 的阶段。

如果想更短地抓住这个观念,下面这组对照会有帮助。

问题 更接近特征选择的回答 更接近预处理的回答
要把什么留下当输入 采用哪些列 已经留下来的列要怎么改
什么不该交给 model ID、泄漏列、事后信息 在 test 上 fit 出来的规则、错误的编码方式
什么需要先变得可计算 整理输入候选 填值、对齐数轴、改变表达

也就是说,如果说特征选择是在 决定入口,那么预处理就是 把已经进入入口的值翻译成 model 的语言

要真正理解预处理,还得同时抓住:这种翻译通常会按下面的顺序发生。

flowchart LR
  A["原始表<br/>缺失值 / 尺度混杂 / 类别文本"]
  B["先划分数据<br/>训练 / 验证 / 测试"]
  C["只在 训练 上学习预处理规则<br/>填充值 / 缩放统计量 / 类别映射"]
  D["用同一套规则转换各行"]
  E["可直接送入模型的矩阵"]

  A --> B --> C --> D --> E

这个流程的核心有四点。

  1. 不会把原始表直接送进 model。
  2. 先切分数据,规则只在 train 上学习。
  3. 已学到的规则会重复应用到其他行。
  4. 这样得到的结果,才是 model 的输入矩阵(matrix)。

也就是说,预处理不是 稍微修一下值,而是 把原始表重构成 model 能读的矩阵

没有预处理会立刻坏掉什么

理解预处理必要性最快的方法,就是看 没有预处理时会发生什么

原始输入状态 如果不做预处理直接塞进去会发生什么 为什么是问题
有缺失值 某些 model 和计算会直接停住或报错 它不能成为可计算的输入行
数值尺度差异很大 大单位的轴会过度支配距离和优化 特征比较不公平
混有字符串类别 很多 model 无法直接计算 输入表达没法进入数值空间
train 和 test 用不同规则 评价分数可能看起来比实际更好,或者波动很大 比较和可复现性会一起崩掉

也就是说,预处理不是为了把性能再抬高一点的可选项,在很多情况下,它是 让 model 至少能读懂输入的最低条件

如果把同样的话翻成更实务的说法,就是下面这样。

  1. 不能计算的输入,就无法学习。
  2. 比较不公平的输入,会制造错误的边界和距离。
  3. 不能复现的规则,会让部署后无法重新生成同样的输入。

所以,预处理不是贴在 model 后面的化妆,而更像是在 model 前面把输入世界先盖起来的基础工程。

特征选择和预处理是怎样接起来的

如果说 P4-7.1 里的特征选择是在决定 保留什么,那么预处理就是在决定 把保留下来的东西表达成什么样

核心是:即使经过了 选择,输入表达还是要继续处理。保留下来的特征必须再经过缺失值处理、尺度调整、编码等变换,才能真正变成 model 更容易读取的输入形式。

如果把预处理看成不是单一技术,而是 调节输入表达的一组变换,那么立刻就会出现下面这些输入检查问题。

  • 这个值可计算吗?
  • 这个值的大小差异会不会扭曲学习?
  • 这种表达会怎样影响距离(distance)、边界(boundary)、优化(optimization)?
  • 训练时学到的变换规则,在推理时还能不能原样复现?

这些问题之所以重要,是因为后面会遇到的算法,会以不同方式对输入表达敏感。

如果把“同样的输入也会分岔出好几类预处理问题”用表记下来,就没那么容易混乱。

同样输入里出现的问题 最先出现的问题 更接近的预处理判断
值是空的 计算本身可不可以做 缺失值处理
数值大小差太多 哪个列在支配距离或优化 尺度调整
值是字符串 model 能不能直接计算这种表达 编码
train/test 的规则不同 评价和运营里是否在复现同一规则 分开 fittransform

这张表会让读者重新把预处理读成:不是技术名,而是 到底在诊断哪一种输入问题

再往前走一步,预处理的判断顺序通常可以固定成下面这样。

顺序 先看什么 为什么先看
1 train/test 是否先分开了 为了防止学规则时发生泄漏
2 值是不是可计算 因为缺失值和字符串问题要先解决
3 值之间比较是否公平 因为尺度差异会扭曲学习
4 同样规则是否会重复应用 因为评价和部署都必须可复现

抓住这个顺序之后,预处理会更稳定地被理解成 输入表达设计流程,而不是 套工具的检查清单

把预处理前后放在一个场景里看

如果想更具体地理解预处理,最好一次性看到同一张小表是怎么变化的。

customer age income channel label
A 29 3200 search 0
B 6100 ad 1
C 35 direct 0
D 41 5800 search 1

这张表对人来说是能读的,但从 model 的角度看,直接拿来用会很困难。

  • 会打断计算。
  • ageincome 处在不同数轴上。
  • channel 是字符串,难以直接计算。

经过预处理之后,同一张表会大致变成下面这样的输入。

customer age_filled income_filled age_scaled income_scaled channel_search channel_ad channel_direct label
A 29 3200 ... ... 1 0 0 0
B 35 6100 ... ... 0 1 0 1
C 35 5800 ... ... 0 0 1 0
D 41 5800 ... ... 1 0 0 1

这里真正该看的,不是具体数值,而是结构变化。

  1. 缺失值按规则被填上了。
  2. 数值型被移到了共同的比较轴上。
  3. 类别值被展开成一组可计算的列。
  4. 结果就是,每一行都被整理成同样结构的输入向量(vector)。

也就是说,预处理的结果不是 更干净的表,而是 model 能按同样结构读取的输入矩阵

抓住这个场景之后,读者就更容易把预处理理解成不是 三个零碎技术,而是 把不同类型的值搬进同一计算世界的整合工作

为什么缺失值(missing value)要先处理

现实数据里经常会有空值。

  • 用户没有回答的值
  • sensor 没能读到的值
  • 只有后来才会记录的值
  • 因为采集失败而漏掉的值

如果把这些值原样放着,很多 model 和运算就无法继续。所以在预处理的第一步里,通常会先决定 这些空值要怎么处理

scikit-learn 的 SimpleImputer 文档给出了用平均值(mean)、众数(most frequent)、常数(constant)等策略填补缺失值的例子。

场景 常见的入门策略
数值列 平均值、中位数(median)
类别列 众数,或者 "unknown" 这样的常数
缺失本身可能有意义 把“是否缺失”保留成额外信号

这里重要的不是 无条件填上,而是 为什么要用这个规则去填

例如,收入(income)这一列可能有很多极端值,因此中位数往往比平均值更合适。相反,如果像考试分数这种分布比较平整的列,平均值也许更直观。

如果换成业务场景来看,会更容易理解。

场景 缺失值意味着什么 入门式判断示例
医院预约数据 预约路径没有被记录 可以保留成 "unknown" 这样的单独类别
商城客户数据 收入信息未填写 评估用中位数修正,或补一个缺失信号
传感器数据 测量失败 可能更应该当作采集故障信号,而不只是简单修正

缺失值不是单纯的空格,而是必须连同 为什么会空 一起读的输入状态。

缺失值处理的方式,粗略地也可以分成下面几类。

方式 它在做什么 入门层面的优点 注意点
平均值/中位数修正 用代表值填数值型缺失 实现简单、速度快 可能把分布压得过平
众数/常数修正 用最常出现的值或 "unknown" 填类别缺失 字符串列也容易处理 可能把真实意义过度简化
增加缺失标记 把“原来是空的”单独留成一列 当缺失本身就是信号时有用 列数会增加,解释也会更复杂
删除行 直接删掉有缺失的行或列 规则简单 可能带来很大数据损失

也就是说,缺失值处理不只是 用什么填 的问题,也是在 要不要保留空白痕迹要不要直接丢掉要不要用代表值近似 之间做选择。

缺失值处理错了会发生什么

如果太快就把缺失值理解成 用平均值补上就结束,就会漏掉很多关键差别。

缺失情况 草率处理 更好的初学者判断
单纯输入漏填 不解释原因,直接用平均值填 先看为什么会空
缺失本身可能是行为信号 把缺失痕迹完全抹掉 同时评估缺失标记
sensor 采集失败 修成像正常值一样 先判断它是不是采集故障信号

也就是说,缺失值处理与其说是在遮住空白,不如说更接近于解释 这个空白到底是什么事件

还有一个重要点:一旦把缺失值填上,那个值就会开始看起来像 原本就观测到的值。所以读者最好始终意识到:这个值是观测值,还是修正值。因为预处理不是在原样保存现实,而是在构造可计算的近似表达。

因此,在缺失值处理里,通常最好顺手加上下面这些问题。

  1. 这一列的缺失,是偶然漏填,还是某种行为/采集事件?
  2. 用代表值来填,会不会造成太大的意义扭曲?
  3. 缺失这件事本身,要不要留成额外信号?

少了这三个问题,缺失值处理就很容易只剩技术动作;加上它们之后,才会变成输入解释。

为什么尺度(scale)会制造问题

不是所有数值型特征都在同一个单位上变化。

例如,可以想下面这两列。

  • 年龄(age): 20, 35, 41
  • 月收入(monthly_income): 2,000,000 / 4,500,000 / 8,000,000

这两列都是数字,但取值范围差非常大。有些算法如果原样接收这种差异,就很容易把大数值那条轴看得更重要。

scikit-learn 的预处理文档说明,线性模型(linear models)等很多学习算法会受益于标准化(standardization),而且如果特征方差差异很大,某些特征就可能在目标函数(objective function)里过度支配。

尺度调整不是把数字弄得更漂亮,而是在重新平衡各个特征之间的影响力。

这个解释也会直接连到后面的理论。

  • 在 k-NN 里,距离计算会改变。
  • 在 SVM 里,边界可能会改变。
  • 在线性模型和逻辑回归里,优化往往会更稳定。

尺度调整不是一个零散的小技巧,它也是理解后面算法章节的准备阶段。

如果换成实务场景,可以像下面这样读。

场景 不做尺度调整可能发生什么
贷款审批 像收入这样的大数值轴可能会压过其他特征
用户聚类 距离计算可能被某个大单位列牵着走
异常检测 重要但变化幅度小的列可能被埋掉

哪些 model 对尺度更敏感

并不是所有 model 对尺度都同样敏感。

model 家族 尺度影响
k-NN、SVM、基于距离的方法 通常比较敏感
线性模型、逻辑回归、基于梯度下降的方法 经常敏感
决策树、随机森林等树模型 相对没那么敏感

这个差别会在后面的章节里变得更清楚。

  • P4-10 线性回归(linear regression)
  • P4-11 逻辑回归(logistic regression)
  • P4-12 k-NN
  • P4-13 SVM
  • P4-14、P4-15 树与集成

预处理并不是在所有 model 里都以同样权重发挥作用。根据 model 的类型,它会变得更重要,也可能相对没那么关键。

如果做一个简短对照,尺度问题会更清楚。

特征组合 尺度调整前的读法 尺度调整后的读法
income, visits_30d 大数字的 income 轴很容易压倒判断 两个特征会在更相近的基准上比较
sensor_peak, sensor_std 可能只有大单位轴被过度看见 更容易同时读出波动性和大小

也就是说,尺度调整不是 把小数字放大 的技术,而是 当多个特征一起被比较时,调节哪条轴说话太大声 的技术。

因此,尺度调整不能只当成一个独立技术,而要连着 model 到底用什么比较方式 一起看。用距离的 model 往往更敏感,用切分规则的树家族则相对没那么敏感。预处理始终不只是 数据本身的问题,也是 数据和 model 之间相互作用的问题

如果把尺度调整的类型非常粗略地分开,可以看成下面这样。

方式 对齐什么 什么时候常会想到 注意点
标准化(Standardization) 把值调到平均 0、方差 1 附近 线性模型、逻辑回归、SVM、k-NN 可能仍受异常值影响
最小-最大缩放(Min-Max) 把值压进固定范围 想把范围控制在固定区间时 容易被极端值牵引
Robust scaling 按中位数和分位数对齐 异常值很显眼时 解释起来可能没那么直观

目标不是把这张表背下来,而是要抓住:scaling 也不是一个单一技术名,而是在选择 用什么标准重新对齐数轴

如果再像下面这样读,尺度调整也会更不容易混淆。

问题 尺度调整能回答的部分 它回答不了的部分
数值范围差太大,导致比较不公平吗 这个特征本身有没有用
大数字轴在支配距离和优化吗 这个数字里有没有混进泄漏
model 会不会学得更稳定 经常会 性能一定会不会提高

也就是说,尺度调整是用来对齐 比较基准 的工具,不是拿来代替特征选择或泄漏检查的工具。

为什么类别(categorical)值不能直接塞进去

像城市名、会员等级、商品类别这样的类别值,在现实里很自然,但很多 model 不能直接计算字符串本身。

因此,类别值通常要先经过编码(encoding),再改成数值表达。

类别编码,就是把每个类别改造成可计算的数值表示的过程。

原始值 编码后的示例
city = Seoul [1, 0, 0]
city = Busan [0, 1, 0]
city = Incheon [0, 0, 1]

这种方式通常就叫 one-hot encoding。

重要的不是 把字符串翻成了数字,而是 把表达改造成了 model 能计算的形式

这里初学者常见的混淆是:变成数字之后,就自动带有顺序了。但如果像 Seoul=1Busan=2Incheon=3 这样随便给数字,model 就可能误以为这里存在本来并不存在的大小关系。所以,one-hot encoding 更适合被理解成:它保留类别差异,但不强行制造不存在的顺序。

也就是说,编码不是 把字母变成数字,而是在决定 类别之间的差异,要用什么样的数值结构表现出来。如果漏掉这一点,预处理看起来就像格式转换,实际上却会错过它在处理“意义保存”这个问题。

在入门层面,编码方式可以粗略区分成下面这样。

方式 它在做什么 什么时候比较自然 注意点
one-hot encoding 每个类别拆出一列 没有顺序的一般类别值 类别很多时,列数会增加
类顺序编码(ordinal-like) 给类别放上有序值 类别本来就真的有顺序意义时 如果造出了不存在的顺序,就会扭曲
频率/统计型变换 把类别改成别的统计值 高级场景里可能会用 需要和泄漏风险一起检查

这一节虽然以 one-hot encoding 为中心,但关键习惯是先区分 这个类别到底有没有真实顺序

例如,下面这两种情况表面上都是字符串,但读法会不同。

类别示例 性质 更自然的入门判断
bronze/silver/gold 可能真的带有顺序 要谨慎判断该如何反映顺序意义
Seoul/Busan/Incheon 没有顺序的区分类别 更自然的是像 one-hot encoding 这样不带顺序的表达

也就是说,编码不是 消灭字符串的技术,而是 把类别意义保存在可计算结构里的技术

换成业务场景可以这样看。

原始值 为什么难以原样保留 改完之后得到什么
会员等级 gold/silver/bronze 很多 model 无法直接计算字符串 变成按等级拆开的特征列
配送地区 Seoul/Busan/Incheon 地区名本身不容易做数值运算 可以分别读出地区模式
设备类型 ios/android/web 有类别差异,但没有大小比较意义 用可计算向量表达类别区分信号

细部学习内容

在实务里,预处理通常会以什么问题的形式出现

预处理很容易被记成“填平均值”“套 scaler”“选 encoder”这样的单个技术名。但在实务里,人们通常是反过来,从 到底出现了什么输入问题 出发。前面看到的缺失值、尺度、类别表达问题,本质上都只是这个现场问题的不同样子。

也就是说,在现场里,通常会按下面这个顺序思考。

  1. 现在进来的值里,什么地方不稳定?
  2. 这种不稳定会怎样扭曲学习和评价?
  3. 应该用什么预处理规则来降低这种扭曲?

把这个流程画成最简单的图,大概就是下面这样。

flowchart TB
  A["原始输入<br/>缺失 / 尺度差异 / 类别文本"]
  B["诊断输入问题<br/>能计算吗?<br/>比较公平吗?<br/>规则可复用吗?"]
  C["选择预处理规则<br/>填补 / 缩放 / 编码"]
  D["一致的模型输入<br/>训练 和 测试 使用同一规则"]

  A --> B
  B --> C
  C --> D

这张图的核心是:预处理不是 背技术应用顺序,而是 诊断输入问题 -> 选择变换规则 -> 重构一致输入 的流程。

如果把典型场景整理成表,会像下面这样。

业务场景 输入里看见的问题 先想到的预处理判断
电商流失预测 购买金额很大、访问次数很小、来源渠道是字符串 检查数值缩放 + 编码渠道
广告点击预测 device、region、campaign 等类别很多,而且部分值为空 类别缺失处理 + 编码
医院预约爽约预测 预约路径和问诊信息部分缺失 先判断缺失是单纯空白还是额外信号
制造传感器异常检测 sensor 单位彼此差异很大,而且有采集失败区间 分开制定缩放规则和采集失败规则
客户聚类(clustering) 年龄、销售额、访问频率的单位差异很大 距离计算前优先检查尺度调整
贷款审批辅助 解释很重要,且缺失和类别输入并存 防止泄漏 + 保持可解释的变换规则

这张表的目的不是再添加新技术,而是让读者把前面看过的缺失值处理、尺度调整、编码,重新和实际业务里会出现的问题连起来读。

如果再具体一点看,实务例子通常会重新收拢成下面三个判断轴。

实务判断轴 现场会问的问题 连接到的预处理工作
可计算性 model 能直接计算这个值吗 缺失值处理、编码
可比较性 和其他特征放在一起时,大小比较公平吗 尺度调整、归一化
可复现性 训练时用的规则,在线上还能一模一样复现吗 分开 fit/transform、构造 pipeline

因此,预处理不是单纯的准备工作,而是 把现实数据变成可计算、可比较、可复现输入 的设计阶段。

预处理中经常出现的误解

在实务初期,下面这些误解尤其常见。

常见误解 为什么有问题 更准确的理解
预处理只要清洗好就结束 会漏掉 model 和输入表达之间的相互作用 预处理是在设定 model 将要读取的坐标
所有数值型都必须一律 scaling 连像树模型这样没那么敏感的家族也会被硬套同一规则 不同 model 家族的敏感度不同
缺失值先填上就解决了 缺失本身可能就是信号 先解释为什么会缺
只要把字符串变成数字,编码就结束 可能造出不存在的顺序并导致扭曲 要同时看可计算性和意义保存

为什么 train data 和 test data 必须使用同样规则

预处理中最常见的错误之一,就是把 train data 和 test data 混在一起。

scikit-learn 的 common pitfalls 文档强烈建议下面这些做法。

  • 先把数据划分成 train/test
  • 只在 train data 上做 fitfit_transform
  • 对 test data 只做同一规则下的 transform
  • 用 pipeline 会更容易守住这个边界

test data 是拿来评价的,不是拿来学习预处理规则的。

如果简化来看,大致就是下面这样。

flowchart TD
  A["训练 划分"]
  B["拟合预处理<br/>学习填充值 / 缩放 / 编码规则"]
  C["转换 训练"]
  D["测试 划分"]
  E["转换 测试<br/>只能复用已学到的规则"]

  A --> B --> C
  B --> E
  D --> E

这张图最核心的是:fit 只会在训练数据上发生一次。

这个差别实际上会制造性能幻觉。

错误流程 为什么有问题
先看全体数据算平均值,再切 train/test test 信息混进了训练规则
先看全体数据整理编码类别,再做评价 结果可能比部署前的真实情况更乐观
连 test data 也一起参与缩放基准计算 评价可能会显得比实际更好

也就是说,即使不改 model 结构,预处理里的泄漏(leakage)也足以扭曲评价数字。

如果更直接地看这个差别,可以像下面这样读。

流程 表面结果 实际解释
只在 train 上学习平均值、尺度、类别规则 验证分数也许更保守,但更可信 更接近部署前的真实情况
用全体数据学规则后再评价 分数可能更漂亮 这可能是 test 信息混入造成的幻觉

也就是说,预处理规则到底是在哪里学出来的,和 model 选择一样,是非常重要的评价条件。

为什么 pipeline 和 ColumnTransformer 经常出现

在实务里,数值列和类别列经常要分开处理。

  • 数值列: 缺失值修正 + 尺度调整
  • 类别列: 缺失值修正 + one-hot encoding

scikit-learn 的 Pipeline 文档展示了怎样把 fittransform 这些步骤连起来,而 ColumnTransformer 文档则展示了如何对不同的列组应用不同变换。

工具 入门层面的意思
Pipeline 把变换阶段和 model 学习阶段串成一条线
ColumnTransformer 按列的种类分开应用不同预处理规则

也就是说,预处理不只是知道单个技术就够了,还必须进一步走向 可以重复应用同样规则的结构

从课程结构上看,这个结构也很重要。因为后面的 model selection 和 tuning 章节里,实际比较的往往不是 只有 model,而是 预处理 + model 的组合。

应该先抛出哪些预处理问题

比起背技术名,预处理首先更重要的是迅速诊断:当前输入到底在制造什么问题。

输入状态 最先抛的问题 优先检查的预处理
值是空的 为什么会空,空本身是不是信号 缺失值处理、缺失标记评估
数值尺度差很大 距离或优化是不是被某一列拖着走 标准化、归一化评估
混有字符串/类别 model 能不能直接计算这个值 one-hot encoding 等类别变换
train/test 可能规则不同 规则在哪里学、在哪里重用 分开 fit/transform、pipeline
线上输入质量经常波动 部署后还能不能复现同样变换 按列固定规则、结构化 pipeline

这张表会帮助读者把预处理读成:不是 套工具的清单,而是 对输入问题的回应规则

一个好的预处理说明,通常至少要能被概括成下面这几句话。

  1. 输入里到底有什么问题?
  2. 为什么这个问题会妨碍 model 计算?
  3. 用什么规则把它改了?
  4. 同样规则是怎样在 train 和 test 上一致复用的?

如果这四句话说不出来,预处理很可能只剩技术名,而判断理由是空的。

再补上一句话,会更完整。

  1. 这个规则在哪些 model 家族里尤其重要?

例如,尺度调整在 k-NN、SVM、线性模型里更敏感,而编码在大多数不能直接读字符串的 model 里几乎是必需的。也就是说,预处理说明虽然从输入问题开始,但最后一定要接到 model 的连接上,密度才会够。

案例及示例

案例 1. 即使是同一份客户数据,没有预处理规则时比较也会崩掉

一个电商团队正在做客户流失预测实验。人最先看的标准,是 最近购买金额访问次数来源渠道会员等级 这样的输入。

问题在于,这些值彼此并不统一。购买金额是几千量级的数字,访问次数可能只有个位数,来源渠道是字符串,而有些客户的收入或地区信息还是空的。这样直接送进 model 的话,有些列根本无法计算,有些列则会在距离计算或优化里获得过大的影响力。

这时,预处理就成了 把输入改造成 model 能处理表达的规则。你要先决定缺失值按什么标准来填,数值型要不要做尺度调整,类别型要怎样编码,然后把同样规则应用到训练数据和测试数据上。尤其重要的是,不能在测试数据上 fit,而只能复用训练数据上学出来的规则。

能验证的结果会体现在预处理前后对比和数据切分流程里。比如,尺度调整前后的性能差异、字符串列有没有编码、训练数据上算出来的中位数有没有原样用在测试数据上。检查这些之后,就能说明为什么预处理不是单纯清扫,而是输入表达设计。

如果把这个案例画成流程图,就更容易看出:预处理不是清洗一次数据,而是 按列状态立规则,再重复使用同一规则的过程

flowchart TD
  A["客户数据表<br/>混合了缺失值 / 数值 / 类别"]
  B["缺失值规则<br/>用 训练 统计量填补"]
  C["数值规则<br/>缩放选定列"]
  D["类别规则<br/>编码渠道 / 等级"]
  E["同一套变换规则<br/>复用到 验证 / 测试"]
  F["可比较的模型输入"]

  A --> B
  A --> C
  A --> D
  B --> E
  C --> E
  D --> E
  E --> F

案例及示例

用一个小例子读出预处理场景

考虑下面这份数据。

age income city label
29 3200 Seoul 0
41 6100 Busan 1
5800 Seoul 0
35 Incheon 1

读者在这里最先会看到的问题有下面这些。

  1. 空着的值该怎么办?
  2. ageincome 的大小差得很远,要不要调?
  3. city 是字符串,要怎样表达?

比较自然的入门判断可以是下面这样。

预处理判断
age 可以用中位数修正缺失
income 先用中位数修正缺失,再评估尺度调整
city 用众数修正或原样保留后做 one-hot encoding

这个例子并不是在强迫只有一个正确答案。重要的是:不同列要抛不同问题

如果想象把同样数据送给不同 model,判断会更清楚。

model 候选 预处理中最先该注意的点
逻辑回归 数值型尺度和类别型编码
k-NN 距离计算前的尺度平衡
决策树 编码需要,但尺度相对没那么敏感

因此,预处理不是 只看数据就结束的章节,它也是 要和 model 候选一起再读一遍的章节

这一点重要的原因是,即使是同一份数据,真正被拿来比较的实验单位常常是 预处理 + model 的组合。也就是说,说自己在比较逻辑回归,很多时候实际比较的是 缺失值处理 + 尺度调整 + 编码 + 逻辑回归 这个整套组合。

练习与示例

用 Python 例子按顺序看缺失值、尺度、编码

下面这个例子是为了说明概念,用纯 Python 简化出来的预处理流程。

问题场景:

  • 预处理不是一下子结束的魔法,而是会依次经过缺失值处理、尺度调整、编码的过程

输入:

  • 混有缺失值的行列表 rows

期待输出:

  • 缺失值填补后的结果
  • 做过尺度调整的数值型
  • 做过编码的类别值

要确认的概念:

  • 预处理是把数据改造成可计算输入矩阵的顺序工作
  • 分开输出每一步的结果,会更清楚看到发生了什么变化
# 这个例子通过处理缺失值、类别编码和尺度调整来预处理模型输入。
rows = [
    {"age": 29, "income": 3200, "city": "Seoul"},
    {"age": 41, "income": 6100, "city": "Busan"},
    {"age": None, "income": 5800, "city": "Seoul"},
    {"age": 35, "income": None, "city": "Incheon"},
]

def median(values):
    sorted_values = sorted(values)
    n = len(sorted_values)
    mid = n // 2
    if n % 2 == 0:
        return (sorted_values[mid - 1] + sorted_values[mid]) / 2
    return sorted_values[mid]

age_fill = median([row["age"] for row in rows if row["age"] is not None])
income_fill = median([row["income"] for row in rows if row["income"] is not None])

filled_rows = []
for row in rows:
    filled_rows.append({
        "age": row["age"] if row["age"] is not None else age_fill,
        "income": row["income"] if row["income"] is not None else income_fill,
        "city": row["city"],
    })

ages = [row["age"] for row in filled_rows]
incomes = [row["income"] for row in filled_rows]

age_mean = sum(ages) / len(ages)
income_mean = sum(incomes) / len(incomes)

age_std = (sum((x - age_mean) ** 2 for x in ages) / len(ages)) ** 0.5
income_std = (sum((x - income_mean) ** 2 for x in incomes) / len(incomes)) ** 0.5

cities = sorted({row["city"] for row in filled_rows})

processed_rows = []
for row in filled_rows:
    city_vector = [1 if row["city"] == city else 0 for city in cities]
    processed_rows.append({
        "age_z": round((row["age"] - age_mean) / age_std, 2),
        "income_z": round((row["income"] - income_mean) / income_std, 2),
        "city_onehot": city_vector,
    })

print("age_fill   :", age_fill)
print("income_fill:", income_fill)
print("cities     :", cities)
print()
print("processed rows:")
for row in processed_rows:
    print(row)

执行结果如下。

1
2
3
4
5
6
7
8
9
age_fill   : 35
income_fill: 5800
cities     : ['Busan', 'Incheon', 'Seoul']

processed rows:
{'age_z': -1.43, 'income_z': -1.61, 'city_onehot': [0, 0, 1]}
{'age_z': 1.39, 'income_z': 1.03, 'city_onehot': [1, 0, 0]}
{'age_z': 0.0, 'income_z': 0.76, 'city_onehot': [0, 0, 1]}
{'age_z': 0.0, 'income_z': -0.18, 'city_onehot': [0, 1, 0]}

这个例子一次展示了三件事。

  • 缺失值被填了
  • 数值型被移到了可比较的尺度上
  • 类别值被改造成了可计算的向量

也就是说,预处理不是把数据弄得更漂亮,而是在把它变成 可计算的输入矩阵(matrix)

用 Python 例子看尺度如何改变距离计算

如果直接去算距离(distance),就会更直观地看出为什么尺度调整重要。

问题场景:

  • 在基于距离的 model 里,哪条轴的取值范围更大,会扭曲“谁更接近”的判断

输入:

  • 原始点 point_a, point_b, point_c
  • 做完尺度调整后的点 scaled_a, scaled_b, scaled_c

期待输出:

  • 原始距离 raw_ab, raw_ac
  • 调整尺度后的距离 scaled_ab, scaled_ac

要确认的概念:

  • 同样的样本,在尺度调整前后,距离关系可能会改变
  • 预处理不是把 model 输入变好看,而是在对齐计算基准
# 这个例子通过处理缺失值、类别编码和尺度调整来预处理模型输入。
point_a = {"age": 20, "income": 2000}
point_b = {"age": 40, "income": 2200}
point_c = {"age": 21, "income": 8000}

def distance(p, q):
    return ((p["age"] - q["age"]) ** 2 + (p["income"] - q["income"]) ** 2) ** 0.5

raw_ab = round(distance(point_a, point_b), 2)
raw_ac = round(distance(point_a, point_c), 2)

scaled_a = {"age": 0.0, "income": 0.0}
scaled_b = {"age": 1.0, "income": 0.03}
scaled_c = {"age": 0.05, "income": 1.0}

scaled_ab = round(distance(scaled_a, scaled_b), 2)
scaled_ac = round(distance(scaled_a, scaled_c), 2)

print("raw distance A-B   :", raw_ab)
print("raw distance A-C   :", raw_ac)
print("scaled distance A-B:", scaled_ab)
print("scaled distance A-C:", scaled_ac)

执行结果如下。

1
2
3
4
raw distance A-B   : 201.0
raw distance A-C   : 6000.0
scaled distance A-B: 1.0
scaled distance A-C: 1.0

这个例子展示的是:在原始距离计算里,收入(income)轴几乎压倒了一切;而在尺度调整之后,年龄(age)和收入就可能以更相近的权重被读取。

因此,在 k-NN、SVM、线性模型这些章节里,预处理说明会再次变得重要。

细部学习内容补充

在 scikit-learn 里经常看到的预处理工具名

没必要把所有名字都背下来,但至少知道有哪些类别,后面章节的例子会更容易读。

类别 常见名字
缺失值处理 SimpleImputer
标准化 StandardScaler
范围缩放 MinMaxScaler, MaxAbsScaler
对异常值没那么敏感的缩放 RobustScaler
类别编码 OneHotEncoder
pipeline Pipeline, make_pipeline
按列变换 ColumnTransformer

这里要留下的标准不是 API 记忆,而是先理解每个工具到底是为了解决 什么表达问题 才出现的。

检查清单

  • 你现在遇到的输入问题,能不能先分到缺失值、尺度、类别表达中的哪一类?
  • 你有没有把 fittransform 分开,避免 test data 混进预处理规则学习?
  • 你能不能把预处理解释成不是 model 前的清扫,而是 可复现的输入表达设计
  • 你能不能说明预处理是把输入改成更合适表达的阶段,而且缺失值、尺度、类别表达是不同问题?
  • 你能不能说明为什么 fit 只能在训练数据上做,而测试数据只能用同样规则做 transform
  • 你能不能说明 pipeline 和 column transformer 是让预处理规则可重复使用的结构?

出处与参考资料