3. 数据预处理标准化:让数据不再“随机波动”
做算法的人都知道一句话:垃圾进,垃圾出。但很多人没意识到,不稳定的数据预处理,比垃圾数据更可怕。
我见过一个团队,模型上线后准确率忽高忽低。排查了三天,最后发现是数据清洗脚本里有个随机种子没固定。每次跑出来的训练集都不一样,模型自然不稳定。
所以这一章,咱们就聊聊怎么把数据预处理这块“拧紧”。
3.1 数据清洗流程标准化
数据清洗,说白了就是给数据“洗澡”。但每个人洗法不一样,结果就不一样。我个人的习惯是:把清洗流程写成固定的 Pipeline,每次执行顺序、参数完全一致。
3.1.1 清洗步骤固化
下面是我常用的清洗流程,建议你直接抄作业:
| 步骤 | 操作 | 注意事项 |
|---|---|---|
| 1 | 去重 | 基于主键或业务逻辑,保留第一条或最后一条 |
| 2 | 格式统一 | 日期格式、字符串大小写、编码统一 |
| 3 | 异常值检测 | 3σ原则或IQR方法,固定阈值 |
| 4 | 缺失值标记 | 记录缺失位置,不急于填充 |
| 5 | 逻辑校验 | 年龄>0、金额>=0等业务规则 |
这里有个坑:步骤顺序不能乱。比如先做异常值检测再做格式统一,那日期字段里的“2025-01-01”和“2025/01/01”会被当成不同值,异常检测就废了。
3.1.2 清洗参数固定
清洗过程中的参数,比如异常值的阈值、去重保留策略,必须写死在配置文件里,不能靠拍脑袋。
# 清洗参数配置示例
config = {
"dedup_strategy": "keep_first", # 固定策略
"outlier_method": "iqr", # 固定方法
"iqr_multiplier": 1.5, # 固定倍数
"date_format": "%Y-%m-%d", # 固定格式
"encoding": "utf-8" # 固定编码
}
你想想看,如果每次跑脚本都手动调一下阈值,那结果能稳定才怪。
3.2 缺失值处理策略固定
缺失值处理,是数据预处理里最容易“飘”的地方。不同策略会导致模型结果差异很大。我建议:一个项目只用一个策略,并且固定下来。
3.2.1 策略选择矩阵
不同的数据类型和缺失率,适合不同的策略。我整理了一个表格:
| 数据类型 | 缺失率 < 5% | 缺失率 5%-20% | 缺失率 > 20% |
|---|---|---|---|
| 数值型 | 中位数填充 | KNN插补 | 删除特征或建模预测 |
| 分类型 | 众数填充 | 新增“未知”类别 | 删除特征 |
| 时间序列 | 前向填充 | 插值法 | 滑动窗口预测 |
我个人习惯是:数值型优先用中位数,因为中位数对异常值不敏感。均值容易受极端值影响,导致填充值不稳定。
3.2.2 固定随机种子
如果你用KNN插补或随机森林插补,里面涉及随机采样。这时候一定要固定随机种子。
import numpy as np
from sklearn.impute import KNNImputer
# 固定随机种子
np.random.seed(42)
imputer = KNNImputer(n_neighbors=5)
X_filled = imputer.fit_transform(X)
嗯,这里要注意:种子固定了,但不同版本的sklearn实现可能不同。所以最好在项目文档里注明库的版本号。
3.3 特征工程流水线固化
特征工程是算法工程师的“手艺活”。但手艺活如果不标准化,就容易变成“玄学”。
3.3.1 用Pipeline串起来
我强烈建议用sklearn的Pipeline把特征工程步骤串起来。这样做的好处是:训练和预测时用同一套流程,不会出现“训练时做了标准化,预测时忘了做”这种低级错误。
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.compose import ColumnTransformer
# 数值型特征处理
numeric_transformer = Pipeline(steps=[
('imputer', SimpleImputer(strategy='median')),
('scaler', StandardScaler())
])
# 分类型特征处理
categorical_transformer = Pipeline(steps=[
('imputer', SimpleImputer(strategy='constant', fill_value='missing')),
('onehot', OneHotEncoder(handle_unknown='ignore'))
])
# 合并
preprocessor = ColumnTransformer(
transformers=[
('num', numeric_transformer, numeric_features),
('cat', categorical_transformer, categorical_features)
])
# 最终流水线
pipeline = Pipeline(steps=[('preprocessor', preprocessor),
('classifier', LogisticRegression())])
你看,这样写完之后,整个流程就固化了。不管谁来跑,结果都一样。
3.3.2 特征衍生也要标准化
特征衍生是最容易“放飞自我”的环节。我见过有人今天加个交叉特征,明天加个聚合特征,结果模型效果忽上忽下。
我的做法是:把特征衍生规则写成一个函数,入参只有原始数据,出参就是衍生特征。这样每次调用结果一致。
def derive_features(df):
"""固定特征衍生函数"""
df = df.copy()
# 交叉特征
df['feature_a_b'] = df['feature_a'] * df['feature_b']
# 聚合特征
df['feature_c_mean'] = df.groupby('group')['feature_c'].transform('mean')
# 时间特征
df['hour'] = pd.to_datetime(df['timestamp']).dt.hour
return df
3.4 知识体系总览
下面这张图,把数据预处理标准化的核心逻辑串起来了。你可以把它当作团队内部的“操作手册”。
3.5 避坑指南
最后,分享几个我亲身踩过的坑,希望能帮你省点时间:
- 我曾经在训练集和测试集上分别做了标准化,结果模型预测效果极差。后来才发现,应该用训练集的均值和标准差去标准化测试集。
- 我曾经在特征工程里用了全局变量,结果多线程跑的时候数据互相污染。现在所有特征工程函数都是纯函数,不依赖外部状态。
- 我曾经因为换了pandas版本,groupby的排序行为变了,导致特征衍生结果不一致。现在我会在项目里固定所有依赖的版本号。