2、数据预处理基础:缺失值处理、异常值检测、数据平滑

各位同学,咱们直接进入正题。数据预处理这事儿,说难不难,说简单也不简单。我见过太多人,模型调了半天没效果,最后发现是数据本身就有问题。说白了,垃圾进垃圾出,你喂给模型一堆脏数据,它怎么可能给你好结果?

这一章我们聚焦三个核心问题:缺失值怎么补?异常值怎么揪?数据太毛刺怎么平滑? 这三个问题,我在实际项目中几乎每次都会遇到。尤其是做时序预测,数据质量直接决定了模型的天花板。

核心观点:预处理不是可有可无的步骤,而是模型性能的基石。花 70% 的时间做数据清洗,剩下 30% 调模型,这是资深工程师的常态。

数据预处理基础 缺失值处理 删除法 均值/中位数填充 前向/后向填充 插值法 模型预测填充 异常值检测 3σ 原则 IQR 四分位法 箱线图可视化 Z-Score 标准化 DBSCAN 聚类 数据平滑 移动平均 指数平滑 Savitzky-Golay 小波去噪 低通滤波 三者顺序:先处理缺失值 → 再检测异常值 → 最后做数据平滑

2.1 缺失值处理:别让空值毁了你的模型

时序数据里出现缺失值,太常见了。传感器掉线、网络波动、人为漏记……原因五花八门。我刚开始做工业时序项目时,遇到缺失值就直接把整行删掉,结果模型训练出来跟抽风一样。后来才明白,删除法只适合缺失比例极低的情况(比如小于 5%)。

那怎么办?我给大家总结了几种常用方法,按推荐程度排序:

方法 适用场景 优点 缺点
前向填充 (ffill) 缓慢变化的数据(如温度) 简单、保持趋势 突变场景会失真
后向填充 (bfill) 数据末尾缺失 利用未来信息 不适用于实时预测
线性插值 近似线性变化 平滑、合理 非线性数据误差大
均值/中位数填充 缺失比例小 计算快 降低方差
KNN 填充 多维特征相关 精度高 计算量大

我的个人习惯:对于金融时序数据,我一般先用前向填充,再用线性插值做二次修正。工业传感器数据,我更喜欢用中位数填充——因为传感器偶尔会有毛刺,均值容易被极端值带偏。

来看一段代码,演示几种常见填充方法:

import pandas as pd
import numpy as np

# 构造带缺失的时序数据
dates = pd.date_range('2024-01-01', periods=10, freq='D')
values = [100, np.nan, 103, 105, np.nan, 108, 110, np.nan, 115, 118]
df = pd.DataFrame({'date': dates, 'value': values})
df.set_index('date', inplace=True)

# 前向填充
df['ffill'] = df['value'].ffill()

# 线性插值
df['linear'] = df['value'].interpolate(method='linear')

# 中位数填充
median_val = df['value'].median()
df['median_fill'] = df['value'].fillna(median_val)

print(df)

注意:千万不要在测试集上做填充!我曾经犯过这个错——用整个序列的均值去填充训练集和测试集,结果造成了数据泄露,模型在验证时表现很好,上线后直接崩了。正确的做法是:只用训练集的统计量去填充测试集

2.2 异常值检测:揪出那些「不听话」的数据点

异常值,说白了就是那些明显偏离正常范围的数据点。比如股票突然暴跌 99%,或者传感器读数瞬间飙到 10000。你想想看,这种数据喂给模型,它学到的全是噪音。

我常用的异常检测方法有三种,咱们一个一个说:

2.2.1 3σ 原则(拉依达准则)

这个方法假设数据服从正态分布。如果某个点距离均值超过 3 个标准差,就认为是异常。简单粗暴,但很有效。不过要注意——如果数据本身不是正态分布,这个方法会误判。我在做电力负荷预测时就吃过这个亏,负荷数据是双峰分布,用 3σ 把正常的高峰时段全标记成了异常。

2.2.2 IQR 四分位法

这个方法不依赖正态分布假设,更稳健。计算 Q1(25% 分位数)和 Q3(75% 分位数),IQR = Q3 - Q1。一般认为小于 Q1 - 1.5*IQR 或大于 Q3 + 1.5*IQR 的点是异常。

代码实现很简单:

def detect_outliers_iqr(data):
    Q1 = data.quantile(0.25)
    Q3 = data.quantile(0.75)
    IQR = Q3 - Q1
    lower_bound = Q1 - 1.5 * IQR
    upper_bound = Q3 + 1.5 * IQR
    outliers = (data < lower_bound) | (data > upper_bound)
    return outliers

# 使用示例
df['is_outlier'] = detect_outliers_iqr(df['value'])
print(df[df['is_outlier']])

2.2.3 可视化辅助:箱线图

我建议每次做异常检测前,先画个箱线图看看。肉眼观察比任何算法都直观。如果箱线图上出现一堆「小圆圈」飘在外面,那基本就是异常了。

避坑指南:我曾经在一个风电功率预测项目里,用 IQR 方法检测出大量异常值。后来仔细一看,那些「异常」其实是风机在切出风速时的正常停机。所以记住:异常值不等于错误值。有些异常是业务上的正常现象,需要结合领域知识判断。

2.3 数据平滑:让曲线「顺滑」起来

原始时序数据往往有很多毛刺和噪声。直接拿去做预测,模型会拼命去拟合这些噪声,导致过拟合。平滑的目的,就是去掉高频噪声,保留低频趋势。

2.3.1 移动平均(Moving Average)

这是最基础的方法。取一个窗口大小,计算窗口内数据的平均值。窗口越大,曲线越平滑,但也会丢失更多细节。

# 简单移动平均
df['sma_3'] = df['value'].rolling(window=3).mean()
df['sma_5'] = df['value'].rolling(window=5).mean()

# 加权移动平均(越近的数据权重越大)
weights = np.array([0.1, 0.2, 0.3, 0.4])
df['wma'] = df['value'].rolling(window=4).apply(
    lambda x: np.dot(x, weights)
)

我个人习惯用 窗口大小 = 周期长度的 1/4。比如日频数据,如果存在周周期(7 天),窗口就取 7/4 ≈ 2 或 3。太小了平滑效果差,太大了趋势都被抹平了。

2.3.2 指数平滑(Exponential Smoothing)

移动平均有个问题:窗口内的所有点权重一样。但直觉告诉我们,越近的数据应该越重要。指数平滑就是干这个的——它给近期的数据更高的权重,权重按指数衰减。

公式很简单:s_t = α * x_t + (1 - α) * s_{t-1}

其中 α 是平滑系数,取值 0 到 1。α 越大,越关注近期数据,平滑效果越弱;α 越小,历史数据影响越大,曲线越平滑。

# 指数平滑实现
def exponential_smoothing(data, alpha):
    result = [data[0]]
    for i in range(1, len(data)):
        result.append(alpha * data[i] + (1 - alpha) * result[-1])
    return result

df['exp_smooth_03'] = exponential_smoothing(df['value'].dropna().values, 0.3)
df['exp_smooth_07'] = exponential_smoothing(df['value'].dropna().values, 0.7)

我的经验:α 一般取 0.1~0.3 效果比较好。如果数据噪声很大,可以试试 0.05。我见过有人用网格搜索找最优 α,其实没必要——凭经验选一个,然后看平滑后的曲线是否合理就行。做工程不是做学术,够用就好。

2.3.3 什么时候不该平滑?

嗯,这里要注意。不是所有场景都需要平滑。如果你做的是异常检测或者突变点检测,平滑反而会抹掉你关心的信号。我有个同事做金融高频交易策略,一上来就把价格序列做了平滑,结果把交易信号全平滑掉了,模型变成了「睁眼瞎」。

所以,平滑前先问自己:我要预测的是趋势,还是细节? 如果是趋势,放心平滑;如果是细节,谨慎操作。


好了,数据预处理的基础就讲到这里。缺失值、异常值、数据平滑,这三板斧用好了,你的模型已经赢在起跑线上了。记住:好的数据胜过复杂的模型。下次遇到模型效果不好,别急着换算法,先回头看看你的数据干不干净。

本章小结:

  • 缺失值处理:优先前向填充 + 线性插值,避免数据泄露
  • 异常值检测:IQR 法比 3σ 更稳健,但一定要结合业务判断
  • 数据平滑:移动平均和指数平滑是主力,α 选 0.1~0.3 准没错
  • 预处理顺序:先补缺失 → 再揪异常 → 最后平滑
专注资料整理