第 2 章 数据预处理基础:缺失值填充、异常值清洗、标准化与归一化
做异常检测,数据预处理是死活绕不开的一步。我见过不少团队,模型选得够先进,调参也舍得花时间,结果数据没洗干净,上线效果惨不忍睹。原因很简单——异常检测本质上是在找“少数派”,如果数据本身脏了,模型根本分不清哪些是真实异常、哪些是采集错误。
这一章我们就把预处理的三板斧练扎实:缺失值怎么填、异常值怎么洗、怎么把数据拉到相同的尺度上。每一点我都会把项目里踩过的坑抖出来。
2.1 缺失值填充——别让空值毁了你的模型
我刚开始做时序异常检测时,遇到过一批传感器数据,缺了将近15%的读数。当时偷懒用了简单的删除行处理,结果模型在缺失时段频繁报警。后来一查,那些缺失本身就是传感器间歇性故障造成的,删除反而丢失了“故障模式”的信息。所以碰到缺失值,第一反应不是删,而是问自己:这个缺失代表的含义是什么?
常见的填充策略
| 方法 | 适用场景 | 风险 |
|---|---|---|
| 均值/中位数填充 | 数值型、缺失率 < 5% | 压缩方差,弱化异常信号 |
| 前向填充 (ffill) | 时序数据,短时间缺失 | 连续缺失太长会引入偏差 |
| 后向填充 (bfill) | 时序数据,开始部分缺失 | 可能使用未来信息,需小心 |
| 线性插值 | 近似线性变化的数据 | 非线性段插值失真 |
| KNN 填充 | 多维特征相关性强 | 计算成本高,异常样本影响大 |
import pandas as pd
import numpy as np
# 示例:时序数据填充
df = pd.DataFrame({
'timestamp': pd.date_range('2024-01-01', periods=10, freq='H'),
'value': [12.3, np.nan, 13.1, np.nan, np.nan, 11.9, 12.7, np.nan, 13.0, 12.5]
})
# 前向填充,再后向填充
df['value_filled'] = df['value'].ffill().bfill()
# 残差做线性插值
df['value_interp'] = df['value'].interpolate(method='linear')
print(df)
2.2 异常值清洗——别把“好异常”当成噪声洗掉
这里有个非常容易翻车的地方。异常值不等于脏数据。你想,我们做异常检测,目标就是找异常,结果预处理阶段一刀把异常全砍了,模型还学什么?
正确的思路是:先把明显的采集错误(比如传感器超出量程的时间戳值)清洗掉,然后保留那些“可疑”的点,等模型去判。我一般把清洗分为两级:
- 一级清洗(硬阈值):比如温度传感器不可能低于 -50°C,压力不可能为负,读数为 NaN 或 Inf 的字段。这些直接删除或替换为空值。
- 二级清洗(统计方法):针对余下的数据用 3σ 或 IQR 标记,但标记后不删除,而是作为候选异常列表,供后续规则引擎和模型联合判断。
两种主流方法对比
| 方法 | 原理 | 适用分布 | 陷阱 |
|---|---|---|---|
| 3σ 原则 | 均值 ± 3×标准差外的点为异常 | 近似正态分布 | 异常本身会拉偏均值和标准差,导致漏检 |
| IQR 法 | Q1 - 1.5×IQR 以下 或 Q3 + 1.5×IQR 以上 | 任意分布,更稳健 | 对长尾分布可能过宽,需要调整倍数 |
import numpy as np
# IQR 异常值标记
def mark_outliers_iqr(data, multiplier=1.5):
q1 = np.percentile(data, 25)
q3 = np.percentile(data, 75)
iqr = q3 - q1
lower = q1 - multiplier * iqr
upper = q3 + multiplier * iqr
return (data < lower) | (data > upper)
# 修正 Z-Score (MAD)
def mark_outliers_modified_zscore(data, threshold=3.5):
median = np.median(data)
mad = np.median(np.abs(data - median))
modified_z = 0.6745 * (data - median) / mad
return np.abs(modified_z) > threshold
2.3 标准化与归一化——统一量纲才能比大小
异常检测模型里很多算法都依赖距离或梯度,比如 KNN、孤立森林、基于聚类的方法。如果一个特征的量级是 0~100000,另一个是 0~1,距离计算会被大数完全主导。说白了就是“大数欺负小数”。
标准化和归一化不是一回事,选错了会出问题。我直接给结论:
- 标准化 (Z-Score):减去均值除标准差,输出服从 N(0,1)。适合数据本身有正有负、大致正态的场景。孤立森林、One-Class SVM 都推荐用这个。
- 归一化 (Min-Max):线性压缩到 [0,1]。适合有明确上下界的场景,比如像素值、百分比。注意如果新来的数据超界,需要做剪裁。
- RobustScaler:用中位数和 IQR 做中心缩放。对异常值更鲁棒。我个人在异常检测预处理阶段用这个最多,因为原始数据里肯定还留着未被清洗的异常点,RobustScaler 不会被带偏。
什么时候一定要用标准化,不能用归一化?
嗯,举个例子。你在做多变量时序异常检测,特征包括 CPU 使用率(0%~100%)和内存消耗(GB 级)。如果用 Min-Max 归一化,压缩到 [0,1] 后,两个特征在零点附近的行为会完全扭曲,因为 Min-Max 对分布形状没有任何概念。标准化可以保留原始分布的峰度和偏度信息,模型更容易学出异常模式。
from sklearn.preprocessing import StandardScaler, MinMaxScaler, RobustScaler
data = np.array([[100, 0.01],
[200, 0.02],
[300, 0.03],
[5000, 0.50],
[10000, 1.00]])
# 对比三种方法
scaler_std = StandardScaler()
scaler_minmax = MinMaxScaler()
scaler_robust = RobustScaler()
print("StandardScaler:\n", scaler_std.fit_transform(data))
print("MinMaxScaler:\n", scaler_minmax.fit_transform(data))
print("RobustScaler:\n", scaler_robust.fit_transform(data))
2.4 关于处理顺序,我建议这样做
你可能会问:这三步是按顺序做,还是可以打乱?我的建议是固定的流程:
- 先清洗异常值(硬阈值 + 统计标记,但只删除明显错误,保留可疑点)。
- 再填充缺失值(否则缺失值会影响异常检测的统计指标)。
- 最后做标准化/归一化(彻底清洗后再缩放,避免缩放参数被脏数据污染)。
你想想看,如果在填充缺失值之前就用 Z-Score 标准化,均值会被缺失值拉偏,填充效果会更差。这是链条效应,一步错步步错。
2.5 一个完整的预处理模板
下面是我在项目中常用的函数骨架,你可以直接拿去改:
def preprocess_pipeline(df, numeric_cols, outlier_method='iqr',
fill_method='linear', scale_method='robust'):
# Step 1: 清洗异常值(硬阈值 + IQR 标记,删除硬错误)
for col in numeric_cols:
# 硬阈值示例:去掉负值或无穷大
df = df[(df[col] >= 0) & (np.isfinite(df[col]))]
# Step 2: 标记但保留候选异常,进入后续模型
# 实际项目里我会保留 outlier_mask 作为特征
# Step 3: 填充缺失值
df[numeric_cols] = df[numeric_cols].interpolate(method=fill_method, limit_direction='both')
# Step 4: 标准化
scaler = RobustScaler() if scale_method == 'robust' else StandardScaler()
df[numeric_cols] = scaler.fit_transform(df[numeric_cols])
return df, scaler
一句话总结:数据预处理不是在“修复”数据,而是在“翻译”数据——把原始嘈杂信号翻译成模型能听懂的语言。缺失值、异常值、尺度差异,都是语言不通的噪声。翻译做得好,异常检测模型才能真正发挥功力。