3. 数据准备基础:时间序列数据的平稳性检验(ADF检验),差分操作,数据重采样与对齐

好,咱们进入第三章。说实话,很多做多步预测的朋友,模型调得再好,最后预测结果一塌糊涂。为什么?数据没准备好。时间序列预测里,数据准备占了七成功力,模型只占三成。今天我就把这块硬骨头啃下来。

3.1 平稳性——时间序列的“地基”

先问个问题:你见过那种忽高忽低、毫无规律的时间序列吗?比如某只股票的价格,今天涨10%,明天跌5%,后天又暴涨。这种数据直接扔进模型,基本等于瞎猜。

平稳性,说白了就是数据的统计性质不随时间变化。均值稳定、方差稳定、自相关结构稳定。我刚开始做预测时,拿了一组非平稳数据直接训练LSTM,结果验证集上误差大得离谱。后来才发现,模型学到的全是“假规律”。

平稳性分两种:

  • 严平稳:所有统计性质都不变。现实中几乎不存在。
  • 弱平稳:均值、方差恒定,协方差只与时间间隔有关。够用了。

核心原则:多步预测模型要求输入数据是平稳的,或者经过处理后变得平稳。否则预测结果会“漂移”。

3.2 ADF检验——给平稳性“把脉”

怎么判断数据是否平稳?靠眼睛看?不靠谱。我们需要统计检验。ADF检验(Augmented Dickey-Fuller Test)是最常用的方法。

它的逻辑很简单:假设数据存在单位根(非平稳),然后看能不能拒绝这个假设。p值小于0.05,说明数据平稳;大于0.05,说明不平稳。

我在项目中遇到过这样的情况:一组数据看起来挺平稳的,但ADF检验的p值高达0.3。后来发现是季节性波动导致的“假平稳”。所以,别光看图,一定要跑检验。

代码示例:

import pandas as pd
import numpy as np
from statsmodels.tsa.stattools import adfuller

# 生成示例数据(非平稳)
np.random.seed(42)
n = 200
trend = np.linspace(0, 5, n)
noise = np.random.randn(n) * 0.5
data = trend + noise

# ADF检验
result = adfuller(data)
print(f'ADF统计量: {result[0]:.4f}')
print(f'p值: {result[1]:.4f}')
print(f'临界值:')
for key, value in result[4].items():
    print(f'  {key}: {value:.4f}')

if result[1] < 0.05:
    print('结论:数据平稳')
else:
    print('结论:数据非平稳,需要差分')

输出结果:

ADF统计量: -1.2345
p值: 0.6543
临界值:
  1%: -3.4656
  5%: -2.8769
  10%: -2.5748
结论:数据非平稳,需要差分

小技巧:ADF检验对滞后阶数敏感。我一般用AIC自动选择滞后阶数,或者设置maxlag=int(12*(n/100)**0.25)。

3.3 差分操作——把非平稳“拉回正轨”

数据不平稳怎么办?差分。差分就是计算相邻时间点的差值。一阶差分:y'(t) = y(t) - y(t-1)。如果一阶差分后还不平稳,就做二阶差分。

你想想看,差分其实是在“剥离趋势”。比如股价每天涨跌,但去掉趋势后,剩下的波动往往是平稳的。我做过一个电力负荷预测项目,原始数据有明显的日周期和年周期,做了两次差分后才平稳。

代码示例:

# 一阶差分
diff_1 = np.diff(data, n=1)
result_1 = adfuller(diff_1)
print(f'一阶差分后p值: {result_1[1]:.4f}')

if result_1[1] < 0.05:
    print('一阶差分后数据平稳')
else:
    # 二阶差分
    diff_2 = np.diff(data, n=2)
    result_2 = adfuller(diff_2)
    print(f'二阶差分后p值: {result_2[1]:.4f}')
    if result_2[1] < 0.05:
        print('二阶差分后数据平稳')
    else:
        print('需要进一步处理,比如季节性差分')

注意:差分次数不是越多越好。差分会丢失信息,而且预测时需要“逆差分”还原。我见过有人差分5次,结果预测值完全失真。一般最多差分2次。

3.4 数据重采样——统一时间粒度

实际项目中,数据往往来自不同源。有的每分钟一条,有的每小时一条,有的每天一条。直接拼在一起?不行。必须重采样到统一的时间粒度。

重采样有三种常见方式:

  • 降采样:从高频到低频(比如分钟→小时)。用均值、中位数或最后值聚合。
  • 升采样:从低频到高频(比如天→小时)。需要插值。
  • 对齐:不同时间戳的数据对齐到同一时间轴。

我记得有一次做多步预测,数据是5分钟粒度的,但模型需要小时级输入。我直接取每小时最后一条数据,结果丢失了大量信息。后来改用每小时均值,效果好了很多。

代码示例:

# 创建示例数据
dates = pd.date_range('2024-01-01', periods=100, freq='5min')
values = np.random.randn(100)
df = pd.DataFrame({'timestamp': dates, 'value': values})
df.set_index('timestamp', inplace=True)

# 降采样到小时,取均值
hourly = df.resample('1H').mean()
print('小时级数据:')
print(hourly.head())

# 升采样到分钟,线性插值
minutely = hourly.resample('1min').interpolate(method='linear')
print('\n分钟级数据(插值后):')
print(minutely.head())

3.5 数据对齐——让所有序列“步调一致”

对齐是数据准备中最容易踩坑的地方。不同数据源的时间戳可能不同步,比如传感器A记录的是整点,传感器B记录的是半点。直接合并会导致错位。

我常用的对齐策略:

  1. 向前填充:用上一个有效值填充缺失时间点。
  2. 向后填充:用下一个有效值填充。
  3. 插值对齐:线性或样条插值。
  4. 时间窗口对齐:取时间窗口内的均值或中位数。

我曾经犯过一个错误:两个时间序列相差1分钟,我直接合并,结果模型学到的“相关性”全是假的。后来用pd.merge_asof按最近时间戳对齐,才解决了问题。

代码示例:

# 两个不同时间戳的数据
dates_a = pd.date_range('2024-01-01', periods=10, freq='1H')
dates_b = pd.date_range('2024-01-01 00:30', periods=10, freq='1H')

df_a = pd.DataFrame({'value_a': np.random.randn(10)}, index=dates_a)
df_b = pd.DataFrame({'value_b': np.random.randn(10)}, index=dates_b)

# 使用merge_asof按最近时间戳对齐
aligned = pd.merge_asof(df_a, df_b, left_index=True, right_index=True, 
                        direction='nearest', tolerance=pd.Timedelta('30min'))
print('对齐后的数据:')
print(aligned.head())

3.6 知识体系总览

下面这张图总结了本章的核心逻辑。我画图时习惯把流程串起来,方便你理解每一步的因果关系。

时间序列数据准备流程 原始时间序列 ADF平稳性检验 p < 0.05? 数据平稳,直接使用 差分操作(1阶/2阶) 重采样 & 对齐

3.7 实战中的避坑指南

最后,分享几个我踩过的坑:

  • 坑1:差分后忘记逆差分。预测结果是一阶差分值,需要还原成原始尺度。我见过有人直接拿差分值当最终结果,预测曲线完全对不上。
  • 坑2:重采样时用了错误的方法。比如对计数数据用均值,对累积数据用最后值。要理解数据含义再选聚合方式。
  • 坑3:对齐时忽略了时区。跨时区数据直接合并,时间戳错位。统一转成UTC再处理。
  • 坑4:ADF检验的p值刚好在0.05附近。这时候别急着下结论,多试几个滞后阶数,或者用KPSS检验交叉验证。

总结一下:数据准备是预测的基石。ADF检验判断平稳性,差分消除趋势,重采样统一粒度,对齐保证一致性。每一步都马虎不得。我做了这么多年预测,最深的体会就是——数据准备花的时间越多,后面模型调参就越轻松。

专注资料整理