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 知识体系总览

下面这张图,把数据预处理标准化的核心逻辑串起来了。你可以把它当作团队内部的“操作手册”。

数据预处理标准化流程 数据清洗 缺失值处理 特征工程 去重 格式统一 异常检测 逻辑校验 中位数填充 KNN插补 众数填充 前向填充 标准化 编码 特征衍生 选择 核心:固定流程、固定参数、固定种子、固定版本

3.5 避坑指南

最后,分享几个我亲身踩过的坑,希望能帮你省点时间:

  • 我曾经在训练集和测试集上分别做了标准化,结果模型预测效果极差。后来才发现,应该用训练集的均值和标准差去标准化测试集。
  • 我曾经在特征工程里用了全局变量,结果多线程跑的时候数据互相污染。现在所有特征工程函数都是纯函数,不依赖外部状态。
  • 我曾经因为换了pandas版本,groupby的排序行为变了,导致特征衍生结果不一致。现在我会在项目里固定所有依赖的版本号。
我的建议:把数据预处理标准化当作“基础设施”来建设。花一天时间把流程固化好,后面能省一个月的时间去排查那些“莫名其妙”的结果波动。