数据预处理:缺失值处理、异常点清洗、重采样

拿到时序数据,千万别急着往LSTM里塞。

我先泼盆冷水——模型再先进,也救不了垃圾数据。我在好几个项目里都吃过这个亏,数据预处理没做好,后面调参调得怀疑人生。所以这一章,咱们老老实实把数据洗干净。

缺失值处理:别让NaN毁了你的序列

时序数据里出现缺失值,太常见了。传感器掉线、网络波动、存储异常,都可能整出个NaN。你想想看,LSTM虽然能记住长期依赖,但它不认识空值啊。

处理方式分三类:

  • 删除法:如果缺失比例特别低(<5%),直接扔掉。我一般只在序列首尾缺失时用这招。
  • 填充法:这是主流。常用手段包括前向填充、线性插值、均值填充。
  • 预测法:用该序列的其他段训练一个小模型来预测缺失值。杀鸡用牛刀,但确实有效。

我个人习惯先看缺失的分布。如果是连续一大段缺失,删除比填充更安全。你硬填进去的,反而会把模型带偏。

代码示例:缺失值填充

import pandas as pd
import numpy as np

# 生成带缺失的示例数据
date_rng = pd.date_range(start='2023-01-01', end='2023-01-10', freq='h')
ts_data = pd.Series(np.random.randn(len(date_rng)), index=date_rng)
ts_data[ts_data.sample(frac=0.1).index] = np.nan  # 随机制造10%缺失

# 前向填充(我的首选)
ts_data_ffill = ts_data.ffill()

# 线性插值
ts_data_interp = ts_data.interpolate(method='linear')

# 如果只是少量离散缺失,ffill 足够
# 如果缺失段较长,interpolate 更平滑

异常点清洗:别把故障当特征

嗯,这里要注意。异常检测的课程里讲异常清洗,听起来有点矛盾对吧?

其实逻辑是这样:我们训练模型时,需要干净的数据让模型学会"正常"的模式。如果训练数据里混着异常点,模型就会把异常也当成正常的一部分。

我常用的异常检测方法:

  1. 3σ原则:假设数据服从正态分布,超出均值±3倍标准差的就是异常。简单粗暴,但对非正态分布效果差。
  2. IQR(四分位距):超出Q1-1.5IQR或Q3+1.5IQR的算异常。对长尾数据更友好。
  3. 移动窗口Z-score:用滑动窗口算局部均值和标准差,适合时序数据。我在传感器数据清洗时最爱用这个。
我的经验:别一上来就删异常点。先可视化,看看异常到底长什么样。有时候"异常"恰恰是你想检测的目标,比如服务器CPU突然飙高。
避坑指南:我曾经在一个金融时序项目里,直接用全局3σ删了所有异常。结果发现那些"异常"其实是市场开盘时的正常波动。后来改用分时段的局部阈值,效果才正常。

异常点清洗代码

def clean_outliers_iqr(series, multiplier=1.5):
    """用IQR方法清洗异常点"""
    Q1 = series.quantile(0.25)
    Q3 = series.quantile(0.75)
    IQR = Q3 - Q1
    lower_bound = Q1 - multiplier * IQR
    upper_bound = Q3 + multiplier * IQR
    return series.clip(lower_bound, upper_bound)

# 用滚动窗口的Z-score
def rolling_zscore_outliers(series, window=12, threshold=3):
    rolling_mean = series.rolling(window=window).mean()
    rolling_std = series.rolling(window=window).std()
    z_scores = (series - rolling_mean) / rolling_std
    return series[(z_scores.abs() < threshold)]

# 清洗
cleaned = rolling_zscore_outliers(ts_data_interp)

重采样:让时间对齐

实际项目中,数据采集频率可能不稳定。有的传感器5秒采一次,有的1分钟才采一次。LSTM要求输入是固定时间间隔的序列,所以我们必须重采样。

重采样的两种场景:

场景 方法 说明
降采样 聚合(均值/最大值/最小值) 从高频到低频,减少数据量,计算更快
升采样 插值(线性/多项式) 从低频到高频,需要填充数据点

我建议数据已经过缺失处理和异常清洗后再做重采样。顺序别搞反了——先重采样再清异常,可能会把正常点误判成异常。

重采样代码示例

# 从1分钟降采样到5分钟(聚合为均值)
resampled_5min = cleaned.resample('5T').mean()

# 从1分钟升采样到30秒(线性插值)
resampled_30s = cleaned.resample('30S').interpolate(method='linear')

# 如果时间戳不规则,先转为DatetimeIndex
# 然后统一用resample处理
关键提醒:重采样的频率要和对齐后的LSTM输入长度一起考虑。如果一个序列包含10000个点,但时间跨度只有1小时,那重采样频率太密反而浪费算力。

数据预处理整体流程

把上面三步串起来,就是一个完整的数据预处理管线。我个人习惯写成函数链式调用,方便复用。

def preprocess_pipeline(series, resample_rule='5T'):
    # 1. 缺失值填充
    filled = series.interpolate(method='linear')
    # 2. 异常点清洗
    cleaned = rolling_zscore_outliers(filled)
    # 3. 重采样
    resampled = cleaned.resample(resample_rule).mean()
    # 4. 处理重采样后可能产生的新的缺失
    result = resampled.interpolate(method='linear')
    return result

你会看到,第四步又做了一次插值。为什么呢?因为重采样后如果原始数据本身有空档,聚合出来的结果会是NaN。所以流程走完,最后再补一次缺失,保证出来的序列干干净净。

一个小技巧:在清洗异常点时,别直接用NaN替代,而是用clip做截断。这样保留了数据点的位置信息,只是把极端值拉回到合理范围。LSTM对这种截断后的数据更友好。
数据预处理流程 原始时序数据 缺失值处理(填充/插值) 异常点清洗(IQR/Z-score) 重采样(降/升采样) 顺序很重要 先填充 → 再清洗 最后重采样 最终产出 对齐时间戳 无缺失、无异常

沿着这个流程走下来,你的数据基本就喂给LSTM了。我见过太多项目,预处理阶段省了功夫,结果模型训练时梯度爆炸或者收敛得奇慢。磨刀不误砍柴工,把数据收拾利索了,后面的事就顺了。