时间序列预测是机器学习中最常见也最具挑战性的任务之一。无论是销售预测、流量预估、股票分析还是能源负荷预测,时间序列数据无处不在。与常规表格数据不同,时间序列数据具有时序依赖性、季节性和趋势性等独特特征,需要专门的建模方法。本文将从经典统计方法到现代深度学习模型,系统讲解时间序列预测的核心技术,并给出完整的 Python 实战代码。

一、时间序列数据的核心特征
在动手建模之前,必须先理解时间序列数据的几个核心构成要素。任何时间序列都可以分解为以下四个组成部分:
- 趋势(Trend):数据在长期内呈现的上升或下降走势,例如电商年交易额持续增长。
- 季节性(Seasonality):数据中重复出现的周期性波动,例如冰淇淋销量每年夏天达到高峰。
- 周期性(Cyclic):非固定周期的波动,通常与经济周期相关。
- 残差/噪声(Residual):剔除趋势和季节性后的随机波动。
使用 Python 的
1 | statsmodels |
库可以轻松进行时间序列分解:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 import pandas as pd
import matplotlib.pyplot as plt
from statsmodels.tsa.seasonal import seasonal_decompose
# 加载数据
df = pd.read_csv('sales.csv', parse_dates=['date'], index_col='date')
# 加法模型分解(当季节性波动幅度不随趋势变化时使用)
decomposition = seasonal_decompose(df['value'], model='additive', period=12)
# 乘法模型分解(当季节性波动幅度随趋势按比例变化时使用)
# decomposition = seasonal_decompose(df['value'], model='multiplicative', period=12)
fig, axes = plt.subplots(4, 1, figsize=(14, 10))
decomposition.observed.plot(ax=axes[0], title='原始数据')
decomposition.trend.plot(ax=axes[1], title='趋势')
decomposition.seasonal.plot(ax=axes[2], title='季节性')
decomposition.resid.plot(ax=axes[3], title='残差')
plt.tight_layout()
plt.savefig('decomposition.png', dpi=150)
平稳性检验:ADF检验
许多经典模型(如 ARIMA)要求数据是平稳的,即均值和方差不随时间变化。Augmented Dickey-Fuller(ADF)检验是最常用的平稳性检验方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 from statsmodels.tsa.stattools import adfuller
def adf_test(series, title=''):
result = adfuller(series.dropna(), autolag='AIC')
labels = ['ADF统计量', 'p值', '使用滞后阶数', '观测数', '临界值']
print(f'--- {title} ---')
for i, label in enumerate(labels[:4]):
print(f'{label}: {result[i]:.6f}')
for key, val in result[4].items():
print(f' 临界值({key}): {val:.4f}')
if result[1] <= 0.05:
print('=> 序列是平稳的(拒绝原假设)')
else:
print('=> 序列非平稳(不能拒绝原假设)')
adf_test(df['value'], '原始序列')
# 差分使其平稳
df['value_diff1'] = df['value'].diff()
adf_test(df['value_diff1'], '一阶差分后')
如果原始序列非平稳,通常通过差分(
1 | diff() |
)来消除趋势,必要时还需进行季节性差分。
二、经典统计方法:ARIMA 与 SARIMA
ARIMA(AutoRegressive Integrated Moving Average)是时间序列预测的经典模型,包含三个核心参数
1 | (p, d, q) |
:
- p(自回归阶数):使用过去多少期的值来预测当前值。
- d(差分阶数):对数据做几次差分使其平稳。
- q(移动平均阶数):使用过去多少期的预测误差来修正当前预测。
SARIMA 在 ARIMA 基础上增加了季节性参数
1 | (P, D, Q, m) |
,其中
1 | m |
是季节周期长度。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28 from statsmodels.tsa.statespace.sarimax import SARIMAX
from sklearn.metrics import mean_absolute_error, mean_squared_error
import numpy as np
# 划分训练集和测试集
train_size = int(len(df) * 0.8)
train, test = df['value'][:train_size], df['value'][train_size:]
# 构建 SARIMA 模型
# order=(p,d,q), seasonal_order=(P,D,Q,m)
model = SARIMAX(
train,
order=(1, 1, 1),
seasonal_order=(1, 1, 1, 12),
enforce_stationarity=False,
enforce_invertibility=False
)
results = model.fit(disp=False)
print(results.summary())
# 预测
forecast = results.forecast(steps=len(test))
# 评估
mae = mean_absolute_error(test, forecast)
rmse = np.sqrt(mean_squared_error(test, forecast))
mape = np.mean(np.abs((test - forecast) / test)) * 100
print(f'MAE: {mae:.2f}, RMSE: {rmse:.2f}, MAPE: {mape:.2f}%')
自动参数选择:pmdarima
手动选择 ARIMA 参数既繁琐又依赖经验。
1 | pmdarima |
库提供了自动搜索最优参数的功能:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24 import pmdarima as pm
# 自动搜索最优 SARIMA 参数
auto_model = pm.auto_arima(
train,
seasonal=True,
m=12, # 季节周期
d=None, # 自动确定差分阶数
D=None, # 自动确定季节性差分阶数
start_p=0, start_q=0,
max_p=3, max_q=3,
start_P=0, start_Q=0,
max_P=2, max_Q=2,
trace=True, # 打印搜索过程
error_action='ignore',
suppress_warnings=True,
stepwise=True # 使用步进搜索加速
)
print(f'最优参数: order={auto_model.order}, seasonal_order={auto_model.seasonal_order}')
# 预测
forecast_auto = auto_model.predict(n_periods=len(test))
print(f'Auto ARIMA MAE: {mean_absolute_error(test, forecast_auto):.2f}')

三、Prophet:Facebook 的开源预测利器
Prophet 是 Facebook 开源的时间序列预测工具,特别适合处理包含强季节性和多节假日效应的商业数据。它采用加法模型,将趋势、季节性、节假日效应和噪声分别建模后叠加。
Prophet 的核心优势在于:
- 无需手动调参,默认参数在大多数场景下就能给出不错的结果
- 能自动处理缺失值和异常值
- 支持自定义节假日效应
- 同时建模日、周、年多种季节性
- 输出丰富的可解释性组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35 from prophet import Prophet
import pandas as pd
# Prophet 要求列名必须为 ds 和 y
prophet_df = df.reset_index().rename(columns={'date': 'ds', 'value': 'y'})
train_p = prophet_df.iloc[:train_size]
test_p = prophet_df.iloc[train_size:]
# 创建模型并添加节假日
model = Prophet(
yearly_seasonality=True,
weekly_seasonality=True,
daily_seasonality=False,
changepoint_prior_scale=0.05, # 控制趋势灵活度
seasonality_prior_scale=10 # 控制季节性强度
)
# 添加中国节假日(自定义)
holidays = pd.DataFrame({
'holiday': 'spring_festival',
'ds': pd.to_datetime(['2023-01-22', '2024-02-10', '2025-01-29']),
'lower_window': -7, # 节前7天开始影响
'upper_window': 7, # 节后7天结束影响
})
model = Prophet(holidays=holidays)
model.fit(train_p)
# 预测
future = model.make_future_dataframe(periods=len(test_p), freq='D')
forecast_p = model.predict(future)
# 查看预测组件
fig = model.plot_components(forecast_p)
fig.savefig('prophet_components.png', dpi=150)
Prophet 的
1 | changepoint_prior_scale |
是最关键的超参数:值越大,趋势越灵活(可能过拟合);值越小,趋势越平滑(可能欠拟合)。推荐在 0.01 到 0.5 之间网格搜索。
四、深度学习方法:LSTM 时序建模
当数据量较大、非线性关系复杂时,深度学习模型往往能超越传统统计方法。LSTM(长短期记忆网络)通过门控机制有效缓解了 RNN 的梯度消失问题,成为时间序列建模的经典深度学习方案。
数据预处理与滑动窗口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31 import torch
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader
from sklearn.preprocessing import MinMaxScaler
# 归一化(LSTM对输入scale敏感)
scaler = MinMaxScaler(feature_range=(-1, 1))
scaled_data = scaler.fit_transform(df[['value']].values)
# 滑动窗口构建数据集
class TimeSeriesDataset(Dataset):
def __init__(self, data, window_size=30, pred_len=1):
self.data = data
self.window_size = window_size
self.pred_len = pred_len
def __len__(self):
return len(self.data) - self.window_size - self.pred_len + 1
def __getitem__(self, idx):
x = self.data[idx:idx + self.window_size]
y = self.data[idx + self.window_size:idx + self.window_size + self.pred_len]
return torch.FloatTensor(x), torch.FloatTensor(y)
WINDOW = 30 # 用过去30天预测未来1天
dataset = TimeSeriesDataset(scaled_data, window_size=WINDOW)
train_dataset = torch.utils.data.Subset(dataset, range(0, train_size - WINDOW))
test_dataset = torch.utils.data.Subset(dataset, range(train_size - WINDOW, len(dataset)))
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=1, shuffle=False)
LSTM 模型定义与训练
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47 class LSTMModel(nn.Module):
def __init__(self, input_dim=1, hidden_dim=64, num_layers=2, output_dim=1, dropout=0.2):
super().__init__()
self.hidden_dim = hidden_dim
self.num_layers = num_layers
self.lstm = nn.LSTM(
input_dim, hidden_dim, num_layers,
batch_first=True, dropout=dropout
)
self.fc = nn.Linear(hidden_dim, output_dim)
def forward(self, x):
# x shape: (batch, seq_len, input_dim)
out, (hn, cn) = self.lstm(x)
# 取最后一个时间步的输出
out = self.fc(out[:, -1, :])
return out
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = LSTMModel(hidden_dim=64, num_layers=2, dropout=0.2).to(device)
criterion = nn.MSELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001, weight_decay=1e-5)
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(
optimizer, mode='min', factor=0.5, patience=5
)
# 训练循环
EPOCHS = 50
for epoch in range(EPOCHS):
model.train()
train_loss = 0
for batch_x, batch_y in train_loader:
batch_x, batch_y = batch_x.to(device), batch_y.to(device)
optimizer.zero_grad()
outputs = model(batch_x)
loss = criterion(outputs, batch_y)
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()
train_loss += loss.item()
train_loss /= len(train_loader)
scheduler.step(train_loss)
if (epoch + 1) % 10 == 0:
print(f'Epoch [{epoch+1}/{EPOCHS}], Loss: {train_loss:.6f}')
训练完成后进行预测并反归一化:
1
2
3
4
5
6
7
8
9
10
11
12
13
14 model.eval()
predictions = []
with torch.no_grad():
for batch_x, _ in test_loader:
batch_x = batch_x.to(device)
pred = model(batch_x)
predictions.append(pred.cpu().numpy())
predictions = np.array(predictions).reshape(-1, 1)
predictions = scaler.inverse_transform(predictions)
y_true = scaler.inverse_transform(scaled_data[train_size:])
lstm_mae = mean_absolute_error(y_true[:len(predictions)], predictions)
print(f'LSTM MAE: {lstm_mae:.2f}')

五、多变量预测与多步预测
实际业务中,我们往往需要利用多个相关变量来预测目标值。例如,预测电力负荷时,除了历史负荷数据,温度、湿度和节假日信息都能提供额外信号。
多变量 LSTM 输入构造
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 # 假设有多个特征列
features = df[['value', 'temperature', 'humidity', 'is_holiday']].values
scaler_multi = MinMaxScaler(feature_range=(-1, 1))
scaled_multi = scaler_multi.fit_transform(features)
# 多变量数据集
class MultiVarDataset(Dataset):
def __init__(self, data, target_col=0, window_size=30, pred_len=1):
self.data = data
self.target_col = target_col
self.window_size = window_size
self.pred_len = pred_len
def __len__(self):
return len(self.data) - self.window_size - self.pred_len + 1
def __getitem__(self, idx):
x = self.data[idx:idx + self.window_size] # 所有特征
y = self.data[idx + self.window_size:idx + self.window_size + self.pred_len, self.target_col]
return torch.FloatTensor(x), torch.FloatTensor(y)
# 模型只需修改 input_dim
model_multi = LSTMModel(input_dim=4, hidden_dim=64, num_layers=2).to(device)
多步预测:Seq2Seq 与 Teacher Forcing
当需要一次预测未来多个时间步时,Seq2Seq(编码器-解码器)架构是更合适的选择:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50 class Encoder(nn.Module):
def __init__(self, input_dim, hidden_dim, num_layers=2, dropout=0.2):
super().__init__()
self.lstm = nn.LSTM(input_dim, hidden_dim, num_layers,
batch_first=True, dropout=dropout)
def forward(self, x):
outputs, (hidden, cell) = self.lstm(x)
return hidden, cell
class Decoder(nn.Module):
def __init__(self, output_dim, hidden_dim, num_layers=2, dropout=0.2):
super().__init__()
self.output_dim = output_dim
self.lstm = nn.LSTM(output_dim, hidden_dim, num_layers,
batch_first=True, dropout=dropout)
self.fc = nn.Linear(hidden_dim, output_dim)
def forward(self, x, hidden, cell):
# x shape: (batch, 1, output_dim) - 单步输入
output, (hidden, cell) = self.lstm(x, (hidden, cell))
pred = self.fc(output)
return pred, hidden, cell
class Seq2Seq(nn.Module):
def __init__(self, encoder, decoder, device):
super().__init__()
self.encoder = encoder
self.decoder = decoder
self.device = device
def forward(self, src, trg, teacher_forcing_ratio=0.5):
batch_size = src.shape[0]
trg_len = trg.shape[1]
outputs = torch.zeros(batch_size, trg_len, 1).to(self.device)
hidden, cell = self.encoder(src)
# 解码器第一步输入:序列最后一个值
decoder_input = src[:, -1:, 0:1]
for t in range(trg_len):
decoder_output, hidden, cell = self.decoder(decoder_input, hidden, cell)
outputs[:, t:t+1, :] = decoder_output
# Teacher forcing: 以一定概率用真实值作为下一步输入
teacher_force = random.random() < teacher_forcing_ratio
decoder_input = trg[:, t:t+1, :] if teacher_force else decoder_output
return outputs
Teacher Forcing 在训练时用真实值作为解码器输入,加快收敛速度;推理时则完全依赖模型自身的预测值逐步展开。训练过程中逐步降低
1 | teacher_forcing_ratio |
(如从 0.5 退火到 0.1)能有效缓解暴露偏差问题。
六、Transformer 用于时序预测:Informer 与 PatchTST
近年来 Transformer 架构在时间序列领域也取得了显著进展。原始 Transformer 直接应用于时序数据存在计算复杂度高和缺乏局部归纳偏置等问题,研究者提出了多种改进方案。
| 模型 | 核心创新 | 适用场景 |
|---|---|---|
| Informer | ProbSparse Attention 降低复杂度,Generative Decoder 一步生成多步预测 | 长序列预测 |
| PatchTST | 将序列分段(Patch)后送入 Transformer,增强局部建模能力 | 多变量预测 |
| Autoformer | 自相关机制替代自注意力,自动分解趋势与季节性 | 季节性强的数据 |
| iTransformer | 在变量维度而非时间维度做 Attention | 高维多变量 |
以 PatchTST 为例,其核心思路是将时间序列分割为多个 patch,每个 patch 类似 Vision Transformer 中的图像块,然后通过 Transformer 建模 patch 间关系:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48 import torch
import torch.nn as nn
class PatchTST(nn.Module):
def __init__(self, seq_len, patch_len=16, stride=8, d_model=128,
n_heads=4, n_layers=3, dropout=0.1):
super().__init__()
self.patch_len = patch_len
self.stride = stride
self.num_patches = (seq_len - patch_len) // stride + 1
# 线性投影将 patch 映射到 d_model 维度
self.patch_embedding = nn.Linear(patch_len, d_model)
self.pos_encoding = nn.Parameter(torch.randn(1, self.num_patches, d_model))
encoder_layer = nn.TransformerEncoderLayer(
d_model=d_model, nhead=n_heads, dim_feedforward=d_model * 4,
dropout=dropout, batch_first=True
)
self.transformer = nn.TransformerEncoder(encoder_layer, num_layers=n_layers)
# 预测头
self.head = nn.Sequential(
nn.Flatten(start_dim=1),
nn.Linear(self.num_patches * d_model, 256),
nn.GELU(),
nn.Dropout(dropout),
nn.Linear(256, 1)
)
def forward(self, x):
# x: (batch, seq_len)
batch_size = x.shape[0]
patches = []
for i in range(self.num_patches):
start = i * self.stride
patches.append(x[:, start:start + self.patch_len])
# (batch, num_patches, patch_len)
patches = torch.stack(patches, dim=1)
# 投影 + 位置编码
emb = self.patch_embedding(patches) + self.pos_encoding
# Transformer 编码
out = self.transformer(emb)
# 预测
return self.head(out).squeeze(-1)
七、模型评估与选型策略
时间序列预测的评估比常规机器学习任务更需要谨慎,因为数据的时间顺序不可打乱。以下是正确的交叉验证方案:
滚动预测验证(Rolling Forecast)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 from sklearn.model_selection import TimeSeriesSplit
# 使用 TimeSeriesSplit 做时序交叉验证
tscv = TimeSeriesSplit(n_splits=5, test_size=30)
scores = []
for fold, (train_idx, val_idx) in enumerate(tscv.split(df)):
train_data = df['value'].iloc[train_idx]
val_data = df['value'].iloc[val_idx]
model = SARIMAX(train_data, order=(1,1,1), seasonal_order=(1,1,1,12))
result = model.fit(disp=False)
pred = result.forecast(steps=len(val_data))
mae = mean_absolute_error(val_data, pred)
scores.append(mae)
print(f'Fold {fold+1}: MAE={mae:.2f}')
print(f'\n平均MAE: {np.mean(scores):.2f} ± {np.std(scores):.2f}')
常用评估指标对比
| 指标 | 公式含义 | 优缺点 |
|---|---|---|
| MAE | 平均绝对误差 | 直观、对异常值不敏感 |
| RMSE | 均方根误差 | 对大误差更敏感 |
| MAPE | 平均绝对百分比误差 | 可解释性强,但真实值为0时无定义 |
| SMAPE | 对称MAPE | 有界[0,200%],缓解MAPE偏置问题 |
| MASE | 平均绝对缩放误差 | 与尺度无关,可跨数据集比较,<1表示优于朴素法 |
1
2
3
4
5
6
7
8
9 def smape(y_true, y_pred):
denominator = (np.abs(y_true) + np.abs(y_pred)) / 2
return np.mean(np.abs(y_true - y_pred) / (denominator + 1e-8)) * 100
def mase(y_true, y_pred, y_train, seasonality=1):
# 朴素预测的MAE作为基准
naive_pred = y_train.shift(seasonality).dropna()
naive_mae = mean_absolute_error(y_train[seasonality:], naive_pred)
return mean_absolute_error(y_true, y_pred) / naive_mae
八、实战选型建议与最佳实践
根据数据量和问题复杂度,以下是推荐的技术选型路线:
- 数据量小(<1000条)、有明确季节性:首选 SARIMA 或 Prophet。模型轻量、可解释性强。
- 中等数据量、多变量影响显著:先尝试 LightGBM 配合时序特征工程(滑动统计量、滞后特征),效果往往出乎意料的好。
- 大数据量、复杂非线性关系:LSTM 或 PatchTST。注意做好归一化和窗口大小调优。
- 长序列多步预测:Informer 或 Autoformer,效率优势明显。
特征工程增强:滞后特征与滑动窗口统计量
在使用 LightGBM 等树模型做时序预测时,特征工程是关键:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24 def create_features(df, target_col, lags=[1,7,14,30], windows=[7,14,30]):
df = df.copy()
# 滞后特征
for lag in lags:
df[f'lag_{lag}'] = df[target_col].shift(lag)
# 滑动窗口统计量
for w in windows:
df[f'rolling_mean_{w}'] = df[target_col].shift(1).rolling(w).mean()
df[f'rolling_std_{w}'] = df[target_col].shift(1).rolling(w).std()
df[f'rolling_min_{w}'] = df[target_col].shift(1).rolling(w).min()
df[f'rolling_max_{w}'] = df[target_col].shift(1).rolling(w).max()
# 时间特征
df['month'] = df.index.month
df['day_of_week'] = df.index.dayofweek
df['day_of_year'] = df.index.dayofyear
df['is_weekend'] = (df.index.dayofweek >= 5).astype(int)
return df
feat_df = create_features(df, 'value')
feat_df = feat_df.dropna()
print(feat_df.head())
集成策略:模型融合提升鲁棒性
将统计模型、机器学习模型和深度学习模型进行加权融合,往往能获得比单一模型更好的效果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 # 各模型预测结果
pred_sarima = sarima_forecast
pred_prophet = prophet_forecast
pred_lstm = lstm_forecast
# 简单加权融合(权重可在验证集上搜索)
weights = [0.3, 0.3, 0.4] # SARIMA, Prophet, LSTM
ensemble_pred = (
weights[0] * pred_sarima +
weights[1] * pred_prophet +
weights[2] * pred_lstm
)
# 或使用 Stacking
from sklearn.linear_model import Ridge
stacking_X = np.column_stack([pred_sarima, pred_prophet, pred_lstm])
meta_model = Ridge(alpha=1.0)
meta_model.fit(stacking_X, y_true)
final_pred = meta_model.predict(stacking_X)
总结
时间序列预测没有银弹,模型选择应基于数据特征和业务需求。实践中建议遵循以下原则:先从简单的统计模型建立 baseline,再逐步引入复杂模型;始终使用时序交叉验证而非随机划分;注重特征工程的重要性,好的特征往往比复杂模型更有效;关注模型的可解释性,让业务方理解预测结果的来源。通过本文介绍的方法组合,你能构建一套从经典到前沿的完整时间序列预测工具链,应对绝大多数实际预测场景。
汤不热吧