问题与目标

先划分数据,再只在训练集拟合填充、标准化和编码统计,验证与测试只执行转换。
预处理若在全量数据上拟合,会让测试集信息提前进入训练。即使模型代码正确,评估结果也会过于乐观。
完成标准:使用数值和类别混合数据建立 ColumnTransformer + Pipeline,只在训练集拟合,并在测试集完成一次评估。
核心概念
特征是预测时可获得的输入,标签是要预测的目标。缺失填充、标准化、类别编码和特征选择都可能学习数据统计量,因此必须放进训练流程。
数值列常用中位数填充与标准化;类别列常用众数填充与 One-Hot 编码。handle_unknown="ignore" 允许推理出现训练时未见类别,但仍应监控新类别比例。
特征类型决定处理方式
| 类型 | 示例 | 常见处理 |
|---|---|---|
| 连续数值 | 输入体积 | 缺失填充、标准化 |
| 计数 | 文件数 | 检查非负、必要时变换 |
| 无序类别 | 任务类型 | One-Hot 编码 |
| 有序类别 | 风险等级 | 显式顺序编码 |
| 时间 | 开始时间 | 周期、间隔、按时间切分 |
| 文本 | 错误摘要 | 词袋、TF-IDF 或后续表示模型 |
标识符通常不应直接作为数值特征。用户 ID、任务 ID 可能让模型记住样本而不是学习规律。
标准化、归一化与模型差异
标准化变为均值约 0、标准差约 1,适合逻辑回归、SVM、KNN 等尺度敏感模型。Min-Max 归一化压缩到固定范围,对极端值敏感。树模型通常不需要为了切分而缩放。
三种常见泄漏
- 目标泄漏:输入直接包含标签或其近似结果。
- 预处理泄漏:用全量数据计算均值、词表或特征选择。
- 切分泄漏:同一主体的重复记录跨集合,或未来数据进入过去训练。
错误写法是先对全量数据 scaler.fit_transform(X) 再切分;正确写法把 scaler 放进 Pipeline,让每个训练折只学习自己的统计量。
可运行实现
import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import f1_score
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler
data = pd.DataFrame({
"input_mb": [10, 15, None, 35, 42, 8, 55, 28, 61, 17, 47, 23],
"queue_depth": [1, 2, 1, 5, 7, 0, 9, 3, 10, 2, 8, 4],
"task_type": ["text", "table", "text", "image"] * 3,
"timeout": [0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0],
})
X = data.drop(columns="timeout")
y = data["timeout"]
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.33, stratify=y, random_state=42,
)
preprocess = ColumnTransformer([
("number", Pipeline([
("imputer", SimpleImputer(strategy="median")),
("scale", StandardScaler()),
]), ["input_mb", "queue_depth"]),
("category", OneHotEncoder(handle_unknown="ignore"), ["task_type"]),
])
model = Pipeline([
("preprocess", preprocess),
("model", LogisticRegression(max_iter=1000)),
])
model.fit(X_train, y_train)
print("test F1:", f1_score(y_test, model.predict(X_test)))
print("new:", model.predict(pd.DataFrame([{
"input_mb": 30, "queue_depth": 6, "task_type": "archive",
}])).tolist())
输入是混合类型表格,输出是测试 F1 和新类别样本的预测。样本很少,只用于验证流程,不能代表稳定模型效果。
检查转换后的特征
训练后可以查看 model.named_steps["preprocess"].get_feature_names_out(),确认类别展开、列顺序和最终维度。若特征数量异常增长,检查高基数类别;直接 One-Hot 编码任务 ID 会产生大量稀疏列。
时间数据应模拟真实预测方向,例如用前 70% 时间训练、后 15% 验证、最后 15% 测试。同一用户有多条记录时使用分组切分,不能只靠分层随机抽样。
分组和时间约束的实现
同一个设备反复产生样本时,随机拆行可能让设备特征被模型记住。GroupShuffleSplit 按组留出独立设备:
from sklearn.model_selection import GroupShuffleSplit
groups = data.index.to_series().map(lambda index: f"device-{index // 3}")
splitter = GroupShuffleSplit(n_splits=1, test_size=0.25, random_state=42)
train_index, test_index = next(splitter.split(X, y, groups=groups))
print("group overlap:", set(groups.iloc[train_index]) & set(groups.iloc[test_index]))
交集应为空。如果既有时间约束又有分组约束,应先按实际部署设计自定义切分,不能为了使用某个现成函数而丢掉其中一项约束。
将特征契约变成检查
required_columns = {"input_mb", "queue_depth", "task_type"}
incoming = pd.DataFrame([{
"input_mb": -2, "queue_depth": 6, "task_type": "archive",
}])
missing = required_columns - set(incoming.columns)
if missing:
raise ValueError(f"missing columns: {sorted(missing)}")
if (incoming["input_mb"].dropna() < 0).any():
raise ValueError("input_mb must be non-negative")
Pipeline 能保证转换一致,但不会自动理解业务范围。列名、类型、单位、允许缺失和取值范围都应在模型外层验证。
进一步验证
- 对同一份带设备 ID 的数据分别做随机和分组切分,检查主体重叠。
- 输出 Pipeline 转换后的特征名和维度,确认标识符没有被 One-Hot 展开。
- 用缺列、负数和未知类别三条推理样本验证契约与预处理边界。
常见问题与排查
- 切分前执行
fit_transform:测试统计量已经泄漏,应把转换器放入 Pipeline。 - 标签列混入特征:检查每个字段在预测时是否真的已知。
- 随机切分时间数据:未来记录可能进入训练,应按时间划分。
- 训练和推理列顺序不同:使用带列名 DataFrame 并保存特征契约。
小结
特征工程不是训练前的一次性脚本,而是模型的一部分。把预处理封装进 Pipeline,才能让训练、验证和推理使用同一条数据路径。
License: CC BY-NC 4.0
Updated 2 hours ago
Was this article helpful? Give it a like.
0 comments


