特征工程入门实战:用 scikit-learn 处理缺失值、类别变量和数值标准化
特征工程入门实战:用 scikit-learn 处理缺失值、类别变量和数值标准化
站内搜索
直接问 AI

特征工程入门实战:用 scikit-learn 处理缺失值、类别变量和数值标准化

在很多机器学习项目里,模型本身并不是最先决定效果的部分。真正拉开差距的,往往是特征工程:缺失值怎么处理、类别变量怎么编码、数值特征要不要标准化、训练集和测试集之间有没有数据泄漏。

这篇文章用 scikit-learn 的 PipelineColumnTransformer 跑通一个小型特征工程流程。读完以后,你可以把缺失值、类别变量和数值标准化放进同一个可复用的训练流程里。

如果你还在纠结应该选哪个模型,可以先读 机器学习算法怎么选。本文关注的是另一个高频问题:机器学习特征工程怎么做。

一、什么是特征工程

特征工程就是把原始数据变成模型能稳定学习的输入。它不是简单地“清洗数据”,而是让数据结构更适合模型。

  • 缺失值处理:例如年龄缺失、收入缺失、类别未知
  • 类别编码:把城市、专业、设备类型这类文本类别转成模型可读形式
  • 数值缩放:让不同单位的数值特征处在可比较的尺度上
  • 特征组合:把已有字段组合成更有含义的新字段
  • 防止数据泄漏:测试集信息不能提前参与训练阶段的统计

同一个模型,输入特征不同,效果可能完全不一样。特征工程的目标不是制造复杂字段,而是保留真实有用的信息,并让训练过程可复现。

二、为什么不要手动处理完整数据后再拆分

很多初学者会先对完整数据做均值填充、标准化、类别编码,然后再拆分训练集和测试集。这是一个常见的数据泄漏风险。

正确做法是:只在训练集上学习填充值、标准化参数和类别编码规则,然后把同一套规则应用到测试集。scikit-learn 的 Pipeline 正是为这个流程设计的。

三、准备一个混合特征数据集

下面用一个很小的示例数据集模拟真实表格数据。它同时包含数值特征、类别特征和缺失值。

import pandas as pd


data = pd.DataFrame({
    "study_hours": [2.0, 5.5, 1.0, 7.0, None, 3.5, 6.0, 8.0],
    "sleep_hours": [6.0, 7.5, None, 8.0, 5.0, 6.5, 7.0, 8.5],
    "major": ["CS", "Math", "Art", "CS", "Business", "Math", None, "CS"],
    "uses_planner": ["yes", "yes", "no", "yes", "no", "no", "yes", None],
    "passed": [0, 1, 0, 1, 0, 0, 1, 1],
})

X = data.drop(columns="passed")
y = data["passed"]

这个例子不代表真实教育评价,只用于演示机器学习特征处理流程。真实项目里要避免把敏感变量、偏见变量或不合规数据直接放进模型。

四、用 ColumnTransformer 分开处理数值和类别特征

数值特征和类别特征不能用同一种处理方式。数值特征适合用中位数填充和标准化,类别特征适合用最常见类别填充和 one-hot 编码。

from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler


numeric_features = ["study_hours", "sleep_hours"]
categorical_features = ["major", "uses_planner"]

numeric_transformer = Pipeline([
    ("imputer", SimpleImputer(strategy="median")),
    ("scaler", StandardScaler()),
])

categorical_transformer = Pipeline([
    ("imputer", SimpleImputer(strategy="most_frequent")),
    ("encoder", OneHotEncoder(handle_unknown="ignore")),
])

preprocessor = ColumnTransformer([
    ("num", numeric_transformer, numeric_features),
    ("cat", categorical_transformer, categorical_features),
])

handle_unknown="ignore" 很重要。它表示测试集中如果出现训练时没见过的新类别,编码器不会直接报错,而是用一组空的 one-hot 编码处理。

五、把特征工程和模型放进同一个 Pipeline

接下来把预处理器和模型连接起来。这样训练、预测、交叉验证时都会自动使用同一套处理流程。

from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score
from sklearn.pipeline import Pipeline


model = Pipeline([
    ("preprocess", preprocessor),
    ("classifier", LogisticRegression()),
])

scores = cross_val_score(model, X, y, cv=4, scoring="accuracy")
print("CV accuracy:", scores)
print("Mean accuracy:", scores.mean())

在真实项目中,你可以把 LogisticRegression 换成随机森林、梯度提升树或其他模型,但前面的预处理结构可以继续复用。

六、完整可运行代码

下面是整合后的完整版本,可以直接保存为 feature_engineering_demo.py 运行。

import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler


data = pd.DataFrame({
    "study_hours": [2.0, 5.5, 1.0, 7.0, None, 3.5, 6.0, 8.0],
    "sleep_hours": [6.0, 7.5, None, 8.0, 5.0, 6.5, 7.0, 8.5],
    "major": ["CS", "Math", "Art", "CS", "Business", "Math", None, "CS"],
    "uses_planner": ["yes", "yes", "no", "yes", "no", "no", "yes", None],
    "passed": [0, 1, 0, 1, 0, 0, 1, 1],
})

X = data.drop(columns="passed")
y = data["passed"]

numeric_features = ["study_hours", "sleep_hours"]
categorical_features = ["major", "uses_planner"]

numeric_transformer = Pipeline([
    ("imputer", SimpleImputer(strategy="median")),
    ("scaler", StandardScaler()),
])

categorical_transformer = Pipeline([
    ("imputer", SimpleImputer(strategy="most_frequent")),
    ("encoder", OneHotEncoder(handle_unknown="ignore")),
])

preprocessor = ColumnTransformer([
    ("num", numeric_transformer, numeric_features),
    ("cat", categorical_transformer, categorical_features),
])

model = Pipeline([
    ("preprocess", preprocessor),
    ("classifier", LogisticRegression()),
])

scores = cross_val_score(model, X, y, cv=4, scoring="accuracy")
print("CV accuracy:", scores)
print("Mean accuracy:", scores.mean())

运行命令如下:

python3 -m venv .venv
source .venv/bin/activate
pip install scikit-learn pandas
python feature_engineering_demo.py

七、常见特征工程错误

  • 先处理完整数据再拆分:容易让测试集信息泄漏到训练阶段
  • 对类别变量直接编号:比如把城市编码成 1、2、3,模型可能误以为它们有大小关系
  • 忽略训练和线上数据差异:线上出现新类别时,如果没有 handle_unknown,预测流程可能报错
  • 删除太多缺失样本:缺失本身可能包含信息,直接删除会改变数据分布
  • 没有保存预处理流程:训练时怎么处理,预测时也必须一样处理

八、特征工程和模型调参哪个更重要

如果数据很粗糙,优先做特征工程;如果数据结构已经合理,再做模型调参。很多项目里,补上正确的缺失值处理、类别编码和防泄漏流程,比盲目换模型更有效。

一个实用顺序是:

  1. 先做可复现的数据拆分。
  2. 用 Pipeline 固定预处理流程。
  3. 训练一个简单基线模型。
  4. 检查错误样本和重要特征。
  5. 最后再比较不同模型和参数。

九、特征工程审计表

特征工程最容易出问题的地方不是代码能不能运行,而是训练、验证、线上推理是否使用了同一套可追踪规则。下面的表可以作为每个表格机器学习项目的检查清单。

环节 检查内容 推荐做法 失败信号
拆分顺序 是否先拆分训练/验证/测试,再拟合 imputer、scaler 和 encoder 把所有预处理步骤放进 PipelineColumnTransformer 在完整数据上先算均值、方差或类别集合,再做测试集拆分
类别变量 新类别、低频类别和缺失类别是否有明确处理策略 使用 handle_unknown="ignore",并记录低频类别合并规则 线上出现新城市、新设备或新专业时预测流程直接报错
数值缩放 哪些模型依赖尺度,哪些模型不依赖尺度 线性模型、KNN、SVM、神经网络优先标准化;树模型可作为对照 换一个单位后模型输出明显变化,但训练记录没有说明原因
可复现性 是否保存预处理对象、模型版本、字段顺序和训练随机种子 把完整 pipeline 序列化,并为输入 schema 写单元测试 训练脚本和预测服务各写一套预处理逻辑,字段顺序不一致

十、常见问题 FAQ

所有数值特征都要标准化吗?

线性模型、KNN、SVM 和神经网络通常更需要标准化。树模型对特征尺度不敏感,但标准化也不会总是有害,关键是保持训练和预测流程一致。

one-hot 编码会不会让特征太多?

如果类别数量很少,one-hot 编码通常没问题。如果一个字段有几千或几万个类别,就要考虑合并低频类别、目标编码、哈希技巧或其他表示方法。

缺失值应该用平均值还是中位数?

数值分布比较偏、异常值较多时,中位数通常更稳。类别特征常用最常见类别填充,也可以单独填成 Unknown

十一、下一步阅读

特征工程能让模型看到更稳定的信息,但它不能解决所有问题。如果训练集效果很好、验证集效果明显下降,下一步应该读 过拟合和欠拟合怎么解决

发表回复

向下探索