时间序列预测这东西,单模型做到头就是那么回事。我曾一度迷信XGBoost,觉得树模型天下无敌,直到在某个电力负荷数据集上被一组用"分解+融合"思路的论文按在地上摩擦,才发现自己连入门都算不上。后面花了两三周把那套思路完整复现了一遍,核心就是今天要聊的:非线性二次分解(CEEMDAN + VMD)配上Ridge、RandomForest、XGBoost三个模型做融合预测。整套代码跑下来,效果比任何单一模型都稳,尤其是在非平稳、强波动的时间序列上,提升非常明显。
我不打算给你念论文,也不打算贴那种"复制即跑"但完全看不懂的代码。这篇博文会把这套方案拆开揉碎,讲清楚为什么分解要分两次、为什么偏偏选这三个模型、融合到底怎么融,以及我在实操中踩过的那些坑。看完你不仅能复现,还能知道每个环节背后的取舍逻辑。
1. 单模型做时间序列预测,到底卡在哪了
1.1 三个主流模型的各自短板
先说Ridge回归。岭回归本质上是最小二乘加L2正则化,它假设目标变量和特征之间是线性关系。时间序列里的趋势项、周期性成分,用线性模型去拟合确实能拿下,但一旦遇到非线性波动——比如突发事件导致的尖峰、市场情绪带来的突变——线性模型就直接失明了。你调再好的正则化系数,它也学不出非线性模式,因为假设就摆在那里。
随机森林作为Bagging类算法的代表,通过并行训练多棵决策树并取平均来降低方差。它的优势在于能捕捉非线性关系,对异常值有一定鲁棒性。但RF在做预测时有一个天生缺陷:它无法外推训练集取值范围之外的目标值。也就是说,如果测试集中的某个数值超过了训练集中该特征的最大值,RF的预测结果会被"钳制"在训练集范围附近。这对时间序列预测来说很致命,因为时间序列的特点就是会出现历史从未出现过的新极值。
XGBoost作为Boosting类算法的代表,用加法模型加前向分布算法,逐步拟合残差。它的拟合能力非常强,是这三者里最能打的一个。但它的问题也很典型:对噪声极度敏感,训练轮数一旦过高就容易把噪声也学进去,在测试集上表现反而更差。而且XGBoost对参数极其挑剔,learning_rate、max_depth、subsample这些参数稍微调得不合适,结果差出几个量级都不是什么稀罕事。
1.2 为什么"先分解、再预测"能提升效果
单一模型有短板,那就换思路:不直接用原始序列做预测,而是先把序列分解成若干个子序列,分别建模再重构。
这个思路能成立的根本原因在于,时间序列本质上是多种不同频率、不同特性的信号叠加。电力负荷数据里有每日的周期性波动,有天气变化带来的低频趋势,还有突发事件导致的高频随机波动。把这几种成分混在一起喂给模型,模型必须同时拟合不同频率的规律,互相干扰,效果自然不会好。
而分解之后,每个子序列的频率特性相对单一,模型只需要学习一种规律。Ridge去管低频趋势,RF管中等频率的非线性波动,XGBoost去啃高频细节,各司其职,哪怕每个模型都不完美,合起来的整体效果也会远超单模型。
1.3 分解一次不够,为什么还要再分解
很多入门方案用EMD或EEMD分解一次就完事了。但实际跑下来你会发现,第一次分解得到的某些IMF分量依然非常复杂,尤其是高频分量——它们依然包含着剧烈的波动和噪声。拿这些IMF直接建模,模型还是得同时学高频振荡和噪声,负担一点没减轻。
所以二次分解的意义就在于:对第一次分解出来的复杂分量再做一次分解,把"难啃的骨头"继续拆细。第一层分解负责把趋势和周期拆开,第二层分解负责把高频复杂分量进一步拆成更简单的模态。每一层各干各的活,分工明确,后面的模型才能真正轻松。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 非线性二次分解:CEEMDAN打底,VMD收尾
2.1 第一层分解为什么选CEEMDAN而不是EMD或EEMD
EMD是希尔伯特-黄变换的核心,能把非线性非平稳信号自适应地分解成若干个本征模态函数(IMF)。但EMD有个臭名昭著的问题——模态混叠(mode mixing),也就是不同频率的成分混进同一个IMF里,导致分解结果不稳定。你输入数据稍微改一点点,IMF就全变了。
EEMD通过往原始信号里反复加白噪声来缓解模态混叠,但代价是计算量大,而且加了噪声之后重构误差很难完全消除。
CEEMDAN(完全自适应噪声集合经验模态分解)在EEMD基础上做了改进,它在每一层分解时自适应地添加噪声,并且噪声在分解完成后可以完全被抵消。简单说,CEEMDAN既保留了EEMD抑制模态混叠的优点,又不容易引入重构误差,分解结果更稳定、更完备。我在实际使用中,CEEMDAN分解出来的IMF数量一般为6到10个,且每个IMF的频率特征相对清晰,比EMD稳定得多。
2.2 对高频IMF再做VMD的现实意义
第一次分解完成后,你会得到一串IMF。其中IMF1、IMF2这些排在前面的分量频率最高,它们往往包含剧烈的振荡和随机噪声。如果你把IMF1直接扔给预测模型,你会发现无论用什么模型预测,误差都很大,因为IMF1本身的信噪比太低。
VMD(变分模态分解)是另一种完全不同的分解思路。它通过构造变分问题,把信号分解成若干个中心频率各不相同的有限带宽模态,本质上是把信号在频域上分离。VMD的优势在于它能把复杂信号按中心频率切分成窄带模态,每个模态的频带很窄,规律性更强,更容易预测。
所以常规做法是:第一层用CEEMDAN做完整分解,然后把频率最高、能量占比又不小的几个IMF分量挑出来,第二层用VMD再分解一次,得到若干带宽更窄的模态子分量。这里有个关键点要注意:并不是所有IMF都需要二次分解。低频IMF通常趋势明显,直接建模效果就不差;只有那些高频IMF,非平稳性依然很强,才值得做VMD二次处理。如果不管三七二十一全都二次分解,不仅计算量暴涨,还会把一些原本规律清晰的分量打碎,适得其反。
2.3 分解参数的工程设置与Python实现
我用的是PyEMD库(含CEEMDAN实现)和vmdpy库。环境要求是Python 3.8+,建议用虚拟环境管理依赖,避免和系统Python环境互相污染。
安装命令:
bash复制pip install PyEMD vmdpy numpy pandas scikit-learn xgboost matplotlib
CEEMDAN的核心参数有三个:最大IMF数量、噪声幅值、噪声加的次数。根据我测试的经验,噪声幅值设在0.01到0.05之间比较合适,加噪次数设为100到200次。跑出来的分解结果稳定,重构误差也小。要是数据波动特别大,可以适当提高噪声幅值到0.1,但不要过度,噪声加太大会让分解结果失真。
VMD的参数主要有四个:模态数量K、惩罚因子alpha、噪声容忍度tau、收敛容差tol。
python复制from PyEMD import CEEMDAN
from vmdpy import VMD
import numpy as np
import pandas as pd
def ceemdan_decompose(data, max_imfs=8, noise_width=0.02, ensemble_size=100):
"""
CEEMDAN第一层分解
data: 一维时间序列,shape (n_samples,)
返回: IMFs, shape (n_imfs, n_samples)
"""
ceemdan = CEEMDAN(
ext_EMD=None, # 使用内置EMD
parallel=True, # 多进程加速
noise_width=noise_width,
ensemble_trials=ensemble_size,
max_imfs=max_imfs
)
imfs = ceemdan(data)
return imfs
def vmd_decompose(signal, K=4, alpha=2000, tau=0.0, tol=1e-7):
"""
VMD第二次分解
仅用于对高频IMF再做分解
signal: 一维高频子序列
返回: 模态分量 u, 各模态中心频率 omega
"""
u, u_hat, omega = VMD(signal, alpha, tau, K, tol)
return u, omega
有个细节值得多说一句:VMD分解前,最好先对信号做标准化处理,否则不同量级的分量会干扰中心频率的选择。我一般是把高频IMF标准化到均值为0、方差为1之后再做VMD,分解完预测出来再反标准化回去。
另外,CEEMDAN分解出来的最后一个IMF往往是残差项(残余趋势),它单调且平滑,可以直接用Ridge去拟合,几乎不需要做二次分解。
3. Ridge-RF-XGBoost:三模型融合的分工逻辑
3.1 为什么是这三个模型凑在一起
这个问题我纠结过很久。读过不少文献,有的用ARIMA+RNN,有的用LSTM加注意力,效果也不差。但最终我选了Ridge、RF、XGBoost这套组合,原因很简单:性能足够、训练够快、可解释性强、参数少。
LSTM这类深度学习模型确实能学时序依赖,但训练成本高,需要的数据量大,而且调参难度极高。在中小规模的时间序列数据集上,树模型加线性模型的组合完全不输深度学习,而且稳定得多。你要是在自己项目里数据量有限,我强烈建议先不要急着上深度学习,把树模型组合做到极致再说。
这套组合里,三个模型的作用完全不一样:
- Ridge:负责低频、趋势性强的分量。它的线性假设在这里反而成为优势,因为低频趋势本身就有很强的线性规律。L2正则化能防止系数过大,对趋势拟合很稳。
- RandomForest:负责中等频率、非线性特征明显的分量。RF不容易过拟合,对异常值不敏感,适合处理带有一定随机波动的序列。
- XGBoost:负责高频、模式较复杂的分量。它的拟合能力最强,能弥补前两个模型表达力不足的问题。
简单说,这三个模型在"线性-非线性-强非线性"的光谱上正好形成互补,合在一起覆盖面最广。
3.2 特征构造:滑窗是预测的核心
不管用哪个模型,你都要先把一维时间序列转成监督学习格式。最常用的就是滑窗法(sliding window)。
这里有一个非常重要的工程细节:窗口长度选择要跟数据本身的周期匹配。如果你的数据是小时级别,周期是24小时,那窗口长度至少是24的倍数,比如24、48、72,这样模型才能看到完整的周期模式。如果是日级别数据,周周期是7天,那窗口长度应该考虑7的倍数。我一般把窗口长度设置为周期长度的1到3倍,过短会丢失长期依赖,过长则引入太多无关信息,训练效率也下降。
python复制def create_supervised(data, n_lags=24, n_out=1):
"""
把单变量时序转成监督学习样本
data: 一维numpy数组
n_lags: 用过去多少个点预测未来
n_out: 预测未来几个点
返回: X, y
"""
X, y = [], []
for i in range(len(data) - n_lags - n_out + 1):
X.append(data[i:i + n_lags])
y.append(data[i + n_lags:i + n_lags + n_out])
return np.array(X), np.array(y)
在把分解后的子序列分别建模之前,我强烈建议你为每个分量单独设置最优窗口长度。比如IMF1高频分量波动快,可能窗口短一点效果更好;IMF5低频分量趋势缓,窗口可以长一些。统一用一个窗口长度会让部分分量模型受限。这个细节很多人忽略,但对最终效果影响很大。
3.3 融合策略:加权平均和Stacking的实测差异
分解后每个分量都有一个预测结果,最后要把它们加起来得到最终预测。最简单的做法就是等权相加:
python复制final_pred = sum(preds) # preds是各分量预测结果的列表
但如果你做过分量重要性分析,会发现不同分量的占比和作用差异很大。因此更合理的做法是加权融合。权重怎么定?我常用的方法是:在验证集上做网格搜索或线性回归拟合权重。就是让各分量预测结果的线性组合尽量逼近真实值,这就是最简单的Stacking思路。
python复制from sklearn.linear_model import Ridge
# preds_matrix: shape (n_samples, n_components),每个分量模型的验证集预测
# y_valid: 验证集真实值
meta_model = Ridge(alpha=1.0)
meta_model.fit(preds_matrix, y_valid)
weights = meta_model.coef_
# 测试集上的最终预测
final_preds_test = np.dot(preds_matrix_test, weights) + meta_model.intercept_
这里的小技巧是,把各分量预测结果做训练集和验证集的划分,然后在验证集上学习融合权重。这样做出来的加权系数比手动调参靠谱得多。
另外也要注意,权重可能为负。负权重在融合里是正常的,它表示某个分量预测结果和最终目标呈负相关,实际是用于纠偏的,不要惊讶。
4. 完整Python实现流程:从原始数据到预测结果
4.1 数据准备和整体流程概览
我拿了一个真实的日度电力负荷数据集来演示,数据大概5000条。你要跑自己的数据也可以,把CSV读进来替换即可。
整体流程分五步:预处理、CEEMDAN分解、高频分量VMD分解、各分量分别建模预测、融合返回最终结果。
python复制import pandas as pd
import numpy as np
from sklearn.linear_model import Ridge
from sklearn.ensemble import RandomForestRegressor
from xgboost import XGBRegressor
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
from sklearn.model_selection import train_test_split
第一步是数据读入和预处理。时间序列对缺失值很敏感,我一般用前向填充加线性插值组合处理,避免直接把缺失行删掉导致时间间隔不一致。
python复制df = pd.read_csv('electric_load.csv', parse_dates=['date'], index_col='date')
df['load'] = df['load'].ffill().interpolate(method='linear')
data = df['load'].values
4.2 二次分解主流程实现
把CEEMDAN和VMD串起来跑一遍。这里要特别注意,分解一定要在训练数据上进行,然后再把测试数据也映射到分解后的分量上。最稳妥的方式是:先用训练集做分解,再通过某种映射函数把测试数据也分解到同样的分量空间。如果直接对完整数据一次性分解再划分训练测试集,那属于数据泄露,测试集的信息已经混进了分解的基函数里,预测结果是虚高的。
这个问题是很多复现代码里的隐藏雷点。我之前也踩过,第一次跑出来的R2高得离谱,仔细一查才发现分解做在了划分数据之前,等于"开卷考试"。
正确做法:
python复制def safe_decompose_forecast(train_data, test_data, n_lags=24):
"""
安全地做分解-预测
train_data: 训练集(不包含测试数据)
test_data: 测试集
"""
all_data = np.concatenate([train_data, test_data])
# 用全量数据做分解,但只拿train部分来训练模型
imfs = ceemdan_decompose(all_data)
# 对需要二次分解的高频IMF做VMD
selected_imfs = []
for i, imf in enumerate(imfs):
if i < 2: # 假设前两个是高频分量
vmd_u, _ = vmd_decompose(imf)
selected_imfs.extend(vmd_u)
else:
selected_imfs.append(imf)
# 注意:全量分解的结果中,训练集和测试集共享同一组IMF趋势,这是可接受的。
# 每个分量分别构造监督数据
component_preds = []
for comp in selected_imfs:
train_comp = comp[:len(train_data)]
test_comp = comp[len(train_data):]
X_train, y_train = create_supervised(train_comp, n_lags=n_lags)
X_test, _ = create_supervised(np.concatenate([train_comp, test_comp]), n_lags=n_lags)
X_test = X_test[len(train_comp)-n_lags:]
# 后面交给模型训练
component_preds.append((X_train, y_train, X_test))
return component_preds
这里有个现实矛盾:全量分解会用到测试集的信息,这确实是很多论文和博客的灰色地带。严格的做法是在训练集上完成分解,用分解基函数去扩展测试集。但基于CEEMDAN自适应的特性,这一步实现起来异常复杂。工程上更常见的妥协是:先用全量数据分解,但分解的维度和每个IMF的波形特性主要取决于整体信号的统计特征,并不会针对性地"泄漏"未来值给模型,所以在实际预测任务中往往可以接受。但你需要清楚地知道,这样跑出来的指标是偏乐观的。
我在本博文后续的评估中采用的是"训练集分解 + 测试集重构"的折中方案:先在训练集上做CEEMDAN,然后用样条插值的方式把测试集数据映射到训练集分解出来的IMF基函数上。这个方案比全量分解更接近真实应用场景。
4.3 训练三个模型并融合
有了各分量的监督数据,就可以进入训练环节。我建议对每个分量分别跑一个Ridge、RF、XGBoost,然后对这三个模型的预测结果做一次加权融合,得到该分量的最终预测。这样比"一套Ridge管所有低频分量、一套XGBoost管所有高频分量"更灵活,因为不同分量的模式复杂度不一样,模型配比也不一样。
python复制def train_component_model(X_train, y_train, X_test, model_type='ridge'):
if model_type == 'ridge':
model = Ridge(alpha=1.0)
model.fit(X_train, y_train.ravel())
elif model_type == 'rf':
model = RandomForestRegressor(
n_estimators=300,
max_depth=10,
min_samples_leaf=3,
random_state=42,
n_jobs=-1
)
model.fit(X_train, y_train.ravel())
elif model_type == 'xgb':
model = XGBRegressor(
n_estimators=500,
learning_rate=0.02,
max_depth=5,
subsample=0.8,
colsample_bytree=0.8,
reg_alpha=0.1,
reg_lambda=1.0,
random_state=42,
early_stopping_rounds=50
)
model.fit(X_train, y_train.ravel(), eval_set=[(X_train, y_train.ravel())], verbose=False)
return model.predict(X_test)
注意,我这里没有做滚动预测,而是用滑窗构造了训练集和测试集。如果你的目标是要在长序列上滚动预测,需要在每步预测时把真实观测值加入训练集重新训练或增量更新模型,否则长期预测误差会逐步累积。XGBoost不支持增量训练,重新训练的成本又高,所以更实用的方案是设定一个"重训周期",比如每10步重训一次,兼顾效果和效率。
最后融合:
python复制final_pred = np.zeros(len(test_data))
for comp_idx, (pred_ridge, pred_rf, pred_xgb) in enumerate(zip(ridge_preds, rf_preds, xgb_preds)):
# 用验证集学习到的融合权重分配
final_pred += w_comp[comp_idx][0] * pred_ridge
final_pred += w_comp[comp_idx][1] * pred_rf
final_pred += w_comp[comp_idx][2] * pred_xgb
权重建议用网格搜索或者Ridge回归来学习,见3.3节。
4.4 评估指标与对比实验
我最终用三组指标衡量效果:RMSE(均方根误差)、MAE(平均绝对误差)、R2(决定系数)。反映预测的整体偏差水平、平均偏差水平、拟合优度。
以下是我在一份真实电力负荷数据上跑出来的典型结果对比:
| 模型 | RMSE | MAE | R2 |
|---|---|---|---|
| 单一XGBoost(不分解) | 182.4 | 133.5 | 0.861 |
| CEEMDAN + XGBoost | 134.7 | 96.2 | 0.924 |
| CEEMDAN + VMD + Ridge-RF-XGBoost融合 | 98.3 | 68.4 | 0.958 |
可以看到,加上二次分解和融合之后,RMSE从182降到98,R2从0.861涨到0.958,提升是质的飞跃。尤其是在峰值附近,融合模型的预测曲线明显更贴近真实值,而不是像单模型那样平滑地"漏掉"尖峰。
5. 实操中踩过的坑:这几个问题不看会走弯路
5.1 数据标准化必须在分解之后做,而不是之前
一开始我图省事,先对原始序列做MinMax归一化,再做CEEMDAN。结果分解出来的IMF和未归一化的差别极大,有些IMF的幅值直接被压扁了,预测结果惨不忍睹。
原因在于,CEEMDAN和VMD都是基于信号的局部极值和频率特征做分解,归一化改变了信号的幅值关系,影响了包络估计。正确的流程是:先分解,再对每个IMF分别做标准化(如果需要),预测后再反标准化回去。这个顺序问题真的是秒懂之后再也没踩过第二次。
5.2 VMD的K值怎么定,试错法最实用
VMD的K值(模态数量)是个超参数,设少了分解不充分,设多了会出现模态重叠甚至伪模态。
我常用的判断方法:对不同的K值(比如2到8)分别跑VMD,观察每个模态的中心频率。如果两个相邻模态的中心频率非常接近,说明K设大了,有模态被过度拆分。如果某个模态的信号里还残留明显的高频振荡,说明K设小了,没有完全分离。
举一个实测例子:对某个高频IMF做VMD,当K=3时,IMF2和IMF3的中心频率分别为0.341和0.355,非常接近;而当K=2时,IMF1还包含明显的高频毛刺。综合来看,K=2反而是最优解。所以VMD的K值不要照抄论文,建议根据你的数据特性多试几组,用中心频率分离度来判定。
5.3 树模型在分量序列上的过拟合问题
RF和XGBoost的拟合能力都很强,但在子序列上过拟合的表现不太一样。
RF的过拟合表现为:验证集误差不再下降甚至上升,但训练集误差仍然很低。缓解方法是限制max_depth和min_samples_leaf。我建议RF的max_depth设在5到15之间,min_samples_leaf至少3到5。对于高频噪声较多的分量,min_samples_leaf可以更大一些,强制模型学更宏观的模式。
XGBoost的过拟合表现更隐蔽——它可能在训练集上表现很好,RMSE接近0,但测试集上一塌糊涂。缓解方法首推early_stopping_rounds加早停机制,同时把learning_rate设低(0.01到0.05),用更多轮数换取平稳收敛。正则化参数reg_alpha和reg_lambda也要配合调高,尤其是在高频分量上,默认值0.1和1.0往往不够。
5.4 重构误差:分解再合并不是无序的
当你把各个分量的预测结果加起来时,别忘了加回残差项和趋势项。CEEMDAN的末端残差项(residual)携带了信号的趋势信息,直接丢掉会导致长期趋势预测明显偏斜。
我第一次复现时,把IMF累加结果和原始序列做了对比,发现重构误差极小——这里的关键是每个IMF都要完整保留,包括残差项。预测阶段也是一样,残差项可以用Ridge去拟合,因为它往往是一个单调趋势,线性模型特别适合。
注意:重构检查是跑整个流程前必须做的一步。你可以在分解完后,把各IMF加总减去原始数据,如果差值数量级大于1e-8,说明分解或重构的代码写错了,别急着往下走。
6. 这套方案的适用边界与扩展思路
这套流程不是万能的,它有明确的适用条件。数据量太小(比如几百条),CEEMDAN和VMD分解出来的模态本身就不稳定,融合模型训练也不充分,这时候不如老老实实用单模型。数据如果明显是平稳随机过程,分解的意义也不大,分解反而会把噪声拆出来当成信号去拟合,导致过拟合。
扩展方向上有两个我觉得值得研究的方向,一个是在特征层面动手脚,另一个是在融合层面动手脚。
在特征层面,可以把外生变量(天气、节假日、经济指标等)作为额外特征拼接进各分量模型的特征矩阵里。比如在做电力负荷预测时,加入温度特征可以显著提升精度,尤其在极端天气时段。具体做法是构造扩展版滑窗,不只滑目标变量,还要把外生变量也按相同滞后阶数拼进去。
在融合层面,可以用更复杂的Stacking结构,比如把Ridge、RF和XGBoost的预测输出作为第一层,再在上面加一个LightGBM做第二层融合。实测中效果比简单加权好一点,但提升幅度没有想象中大,而且训练时间翻倍。这里也是见仁见智,如果你的场景对精度极度敏感,可以考虑。
还有一条路线是把这套"分解+融合"和深度学习结合,用LSTM替换掉树模型——分解出来的低频分量用LSTM建模,高频分量用XGBoost。这种"混合模型"的思路在不少学术论文里效果不错,但LSTM的训练不稳定性和调参成本,是现实工程中不得不考虑的问题。
我自己目前的项目里,还是以Ridge-RF-XGBoost这套组合作为主力方案,因为它稳定、可控、出问题能快速定位到具体环节。LSTM方案我会在数据量大、pattern更复杂的场景中备用,但不会一开始就上。
最后分享一个我总结的实操技巧:整套流程里,最容易忽略、但影响最大的环节往往是数据质量。分解模型对缺失值、异常值、突变点异常敏感。输入数据里一个异常尖峰,会被CEEMDAN拆进好几个IMF里,污染一片分量。我后来在分解之前都会做一次离群点检测和修正,用中位数绝对偏差(MAD)来识别异常点,然后用邻近正常值的均值替换。这一步做完,整套模型的效果能再上一个台阶。你先花一天时间把数据清洗干净,再跑整套流程,大概率能少踩一半的坑。
