☰
GAM时间序列预测翻车:加法与乘法过程辨析及R语言建模避坑
2026/10/29 14:12:17 网站建设 项目流程

简介:这份资源面向具备一定R语言基础、希望深入掌握时间序列建模的数据分析与预测从业者,聚焦加法与乘法两类过程,并延伸至广义可加模型(GAM)的非线性建模思路。内容围绕趋势、季节性与随机成分的组合方式展开,涉及ARIMA、SARIMA、STL分解以及mgcv包中gam()函数的应用,帮助读者理解不同模型框架的适用场景与选择依据。压缩包内共1个文件,为一份R脚本,整体约1KB,代码涵盖数据加载、模型构建与结果解释等环节,可直接运行对照学习。目前已有618人学习下载,适合作为时间序列建模的实操参考,便于读者在真实数据上复现加法与乘法过程,并借助GAM处理复杂非线性趋势,提升预测与诊断能力。

1. 时间序列里的加法与乘法:为什么你的 GAM 预测总在季节峰值上翻车

很多做时间序列的人第一次接触 GAM,都是被它那句“自动拟合非线性趋势”吸引进来的。可真正把mgcv::gam()套到带明显季节性的数据上,十有八九会遇到同一个翻车现场:趋势拟合得挺漂亮,一到每年旺季的尖峰就被系统性低估,淡季又被高估。你以为是平滑参数没调好,其实根子在于——你默认用了加法结构,而数据生成过程很可能是乘法的。

时间序列模型里的“加法”和“乘法”,说的不是算术,而是成分之间怎么组合。加法过程假设趋势、季节、残差是相加关系,季节波动的绝对幅度全年恒定;乘法过程假设它们相乘,季节波动的幅度随趋势水平同比放大。R 语言里无论是decompose()的type="additive"/"multiplicative",还是 SARIMA 的差分与对数变换,本质上都在处理这个选择。而 GAM 作为广义可加模型,它的可加性又是另一层含义——是“平滑项相加”,不是“成分相加”。这两层“加法”混在一起,正是大多数人踩坑的起点。这篇笔记就按“先分清两种过程 → 用 GAM 分别建模 → 避坑 → 进阶验证”的顺序,把这条链路走通,适合已经会用 R 跑回归、但被季节数据折磨过的从业者。

2. 加法与乘法过程:先分清数据生成机制再谈建模

2.1 加法过程与乘法过程的判别标准

加法过程的数学形式是 $y_t = T_t + S_t + e_t$,趋势项 $T_t$、季节项 $S_t$、残差 $e_t$ 直接相加。它的关键特征是季节振幅不随趋势变化——不管趋势在高位还是低位,季节波峰波谷的绝对落差基本一致。乘法过程是 $y_t = T_t \times S_t \times e_t$,季节因子是一个围绕 1 波动的比例,趋势越高,季节的绝对波动越大。

判别方法不用太玄学,画一张图基本就能看出来。把数据按时间画出来,观察季节波峰到波谷的垂直距离:如果这个距离在趋势上升段和下降段差不多,偏加法;如果趋势越高距离越大,偏乘法。更严谨一点,可以对数据取对数再画一次,如果取对数后季节振幅变得均匀,那原始数据就是乘法过程——这正是对数变换能把乘法转成加法的原理。

R 语言里decompose()和stl()都提供type参数,type="additive"对应加法,type="multiplicative"对应乘法。但要注意,decompose()要求周期至少两个完整循环,且它对趋势的估计用的是移动平均,端点会缺失。实际项目里我更倾向先做判别,再决定后续建模路径,而不是直接丢给decompose()了事。

2.2 用 R 快速判别:一张图加一个方差比

下面这段代码做两件事:画出原始序列与对数序列的对比图,并计算分段的季节振幅比,给出一个粗略的量化判据。

# 假设 ts_data 是 ts 对象,频率为 12(月度数据) library(forecast) # 1. 原始序列与对数序列对比 par(mfrow = c(2, 1)) plot(ts_data, main = "原始序列", ylab = "值") plot(log(ts_data), main = "对数序列", ylab = "log(值)") # 2. 分段季节振幅比:把序列切成前后两半,各算季节振幅 n <- length(ts_data) half <- floor(n / 2) amp_front <- diff(range(tapply(ts_data[1:half], cycle(ts_data)[1:half], mean))) amp_back <- diff(range(tapply(ts_data[(half + 1):n], cycle(ts_data)[(half + 1):n], mean))) cat("前半段季节振幅:", round(amp_front, 2), "\n") cat("后半段季节振幅:", round(amp_back, 2), "\n") cat("振幅比(后/前):", round(amp_back / amp_front, 3), "\n")

逻辑说明:cycle()返回每个观测所处的季节位置,tapply()按季节位置求均值得到季节轮廓,diff(range(...))就是该段的季节振幅。参数说明:frequency必须设对,月度是 12,季度是 4,周度是 52,设错会导致cycle()分组全乱。判读标准:振幅比接近 1 偏加法,明显大于 1(比如 1.5 以上)且对数序列振幅变均匀,就按乘法处理。这个比值只是辅助,最终还要结合业务判断——比如销售额、流量这类量纲随规模增长的指标,天然偏乘法。

2.3 为什么 GAM 的“可加”和过程的“加法”不是一回事

这是最容易混淆的地方。GAM 的可加性指的是模型形式:$g(\mu) = \beta_0 + f_1(x_1) + f_2(x_2) + \dots$,多个平滑函数相加。它描述的是预测变量对响应变量期望的影响如何叠加,跟时间序列成分是相加还是相乘没有直接关系。

换句话说,你完全可以用一个“可加”的 GAM 去拟合一个“乘法”过程的时间序列,前提是你对响应变量做了对数变换,或者把季节项建成随趋势变化的交互平滑。反过来,如果你把乘法过程的数据直接喂给加法形式的 GAM,模型会试图用一条平滑曲线去同时解释趋势和放大的季节波动,结果就是趋势被季节带偏,季节峰值被平滑掉。理解这一层,后面选建模路径才不会迷。

3. 用 mgcv 把加法与乘法过程分别建成 GAM

3.1 加法过程的 GAM 建模:趋势平滑加周期因子

加法过程最直接的做法,是把趋势建成时间索引的平滑项,季节建成周期因子或周期平滑项,两者相加。

library(mgcv) # 构造数据框:time 为连续时间索引,month 为季节因子 df <- data.frame( y = as.numeric(ts_data), time = as.numeric(time(ts_data)), month = factor(cycle(ts_data)) ) # 加法 GAM:趋势用薄板样条,季节用因子 fit_add <- gam(y ~ s(time, k = 20) + month, data = df, method = "REML") summary(fit_add) plot(fit_add, pages = 1, shade = TRUE)

逻辑说明:s(time, k = 20)是趋势平滑项,k是基函数维度,决定平滑的最大弯曲能力;month是季节因子,估计 12 个月各自的截距偏移。method = "REML"是平滑参数估计方法,比默认的 GCV 更稳定,不容易过拟合。参数说明:k不能设得比数据能支撑的自由度还大,月度数据 10 年也就 120 个点,k = 20已经够用,设到 50 会触发k.check()警告。plot()里shade = TRUE画出置信带,pages = 1把多个平滑项画在一页。

加法 GAM 的优点是解释性强,趋势和季节的贡献可以分开看。缺点是它假设季节振幅恒定,如果数据其实是乘法的,month的系数会是一个折中值,旺季低估、淡季高估。

3.2 乘法过程的 GAM 建模:对数变换与变系数平滑

乘法过程有两条主流路径。第一条是对数变换后套加法 GAM,因为 $\log(T \times S \times e) = \log T + \log S + \log e$,乘法变加法。第二条是保持原始尺度,把季节项建成随趋势变化的变系数平滑,用by参数实现。

# 路径一:对数变换 + 加法 GAM df$log_y <- log(df$y) fit_log <- gam(log_y ~ s(time, k = 20) + month, data = df, method = "REML") # 预测时记得指数还原 pred_log <- predict(fit_log, newdata = df, type = "response") pred_orig <- exp(pred_log) # 路径二:变系数平滑,季节平滑随趋势水平变化 fit_var <- gam(y ~ s(time, k = 20) + s(time, by = month, k = 10), data = df, method = "REML")

逻辑说明:路径一里log_y把乘法转成加法,exp()还原时要注意,如果type = "response"已经做了链接函数反变换,再exp()会重复。gam()默认高斯族用恒等链接,type = "response"不改变尺度,所以这里exp()是必要的。路径二里s(time, by = month)是变系数平滑,为每个月拟合一条随时间的平滑曲线,相当于允许季节形态随时间演变,能捕捉乘法过程里季节振幅随趋势放大的特征。参数说明:by变量必须是因子,k = 10控制每条曲线的复杂度,月份多的时候要适当降低k避免过拟合。

两条路径怎么选?数据量小、季节形态稳定,用对数变换更稳;数据量大、季节形态本身在演变,用变系数平滑更灵活。我一般先跑对数变换版本作为基线,再用AIC()或k.check()对比变系数版本,看提升是否值得多出来的复杂度。

3.3 用 AIC 与残差诊断对比两种结构

建完模型不能只看summary()的 p 值,要做残差诊断和模型对比。

# 残差诊断 par(mfrow = c(2, 2)) gam.check(fit_add) gam.check(fit_log) # AIC 对比(注意:对数模型和原始尺度模型的 AIC 不可直接比) AIC(fit_add, fit_var) # 若要比对数模型,需在对数尺度上比 fit_add_log <- gam(log_y ~ s(time, k = 20) + month, data = df, method = "REML") AIC(fit_add_log, fit_log)

逻辑说明:gam.check()输出残差图、QQ 图、残差与拟合值图,以及k.check的基函数维度检验。如果k.check的 p 值显著,说明k设小了,平滑项被截断。参数说明:AIC 对比必须在同一响应尺度上进行,fit_add和fit_var都是原始尺度可以比,fit_add_log和fit_log都是对数尺度可以比,跨尺度比是常见错误。残差如果呈现明显的季节自相关,说明模型没吃干净季节结构,要考虑加 AR 项或改用gamm()。

4. 避坑与排查:GAM 拟合时间序列最常见的五个翻车点

4.1 现象:季节峰值系统性低估,残差在旺季为正

原因:数据是乘法过程,却用了加法 GAM,季节因子被平均化,旺季的真实放大效应被压缩。解决:先做 2.2 的振幅比判别,确认乘法后改用对数变换或变系数平滑。判断依据是残差图——如果残差在旺季集中为正、淡季集中为负,基本就是结构选错。

4.2 现象:gam.check()报k值过小,平滑项被截断

原因:s(time, k = ...)的k设得不够,趋势的弯曲能力被限制,模型欠拟合。解决:逐步提高k直到k.check的 p 值不显著,但不要一次设太大,否则计算慢且容易过拟合。经验做法是从k = 10起步,每次加 5,配合method = "REML"让平滑参数自己收缩。

4.3 现象:预测新数据时报因子水平不匹配

原因:训练时的month因子水平和新数据的month水平不一致,比如训练数据只有 1 到 10 月,预测数据有 12 月。解决:在构造因子时显式指定levels = 1:12,保证训练和预测的水平集合一致。这是 R 因子建模的血泪经验,predict()报错十有八九是这个。

4.4 现象:对数变换后预测值还原出现偏差

原因:log()变换后的预测是条件中位数,exp()还原得到的是中位数而非均值,直接当均值用会偏低。解决:如果业务要的是均值预测,还原时要加方差修正项,近似为exp(pred + sigma^2 / 2),其中sigma^2是残差方差。或者干脆在原始尺度上用变系数平滑,绕开还原问题。

4.5 现象:残差自相关严重,AIC 看着好但预测差

原因:GAM 假设观测独立,时间序列的残差自相关被忽略,模型把自相关误当成信号拟合了。解决:改用gamm()加corAR1()相关结构,或先做差分再建 GAM。检查方法是acf(resid(fit_add)),如果滞后 1 或 12 阶超出置信带,就要处理。

5. 进阶:用变系数平滑验证乘法结构并做滚动预测

走到这里,模型基本能跑了。但真正决定这套方案值不值得投入的,是它能不能在滚动预测里稳定。我一般会做两件事:用变系数平滑的显著性验证乘法结构是否真实存在,再用时间序列交叉验证看预测误差。

先看变系数平滑的验证。如果s(time, by = month)的各个平滑项整体显著,且不同月份的曲线斜率方向不一致,说明季节振幅确实随趋势变化,乘法结构成立。可以用summary(fit_var)看每个by水平的 edf(有效自由度),edf 接近 1 说明该月基本是线性,edf 明显大于 1 说明该月季节响应非线性。

# 查看变系数平滑各水平的 edf summary(fit_var) # 时间序列交叉验证:滚动原点预测 library(forecast) ts_cv <- tsCV(ts_data, function(y, h) { df_train <- data.frame( y = as.numeric(y), time = as.numeric(time(y)), month = factor(cycle(y), levels = 1:12) ) fit <- gam(y ~ s(time, k = 20) + month, data = df_train, method = "REML") df_test <- data.frame( time = max(df_train$time) + 1:h, month = factor(cycle(y)[1:h], levels = 1:12) ) predict(fit, newdata = df_test) }, h = 12) # 汇总各步误差 cv_rmse <- sqrt(colMeans(ts_cv^2, na.rm = TRUE)) print(round(cv_rmse, 2))

逻辑说明:tsCV()做滚动原点交叉验证,每次用历史数据训练、预测未来h步,h = 12对应一年。参数说明:tsCV的window参数控制训练窗口长度,不设则用扩张窗口。colMeans(ts_cv^2)按预测步长汇总 RMSE,能看出误差随步长怎么增长。如果 1 步 RMSE 明显小于 12 步,说明模型短期可用、长期衰减快,业务上就要限制预测跨度。

一个具体技巧:把加法 GAM、对数 GAM、变系数 GAM 三条路径的滚动 RMSE 放在一张表里对比,比单看 AIC 靠谱得多。

模型路径1 步 RMSE6 步 RMSE12 步 RMSE适用场景
加法 GAM较低中等偏高季节振幅稳定
对数 GAM低低中等乘法结构明显、数据量中等
变系数 GAM最低低低季节形态随时间演变、数据量大

这张表不是标准答案,是我在几个项目里反复跑出来的经验区间。对数 GAM 通常在乘法数据上全面优于加法 GAM,变系数 GAM 在数据量足够时最好,但数据少于三四个完整周期时容易过拟合,反而不如对数 GAM 稳。

最后说个我自己的习惯:每次建完 GAM,我都会把残差按季节位置分组画箱线图,如果某个季节的残差箱体明显偏离零线,不管 AIC 多好看,我都会回去检查结构选型。这个动作帮我拦下过好几次“指标漂亮但预测翻车”的模型。时间序列建模没有后悔药,结构选错,后面调参都是白费。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询