R时间序列预测:加法与乘法分解及GAM非线性趋势实战
简介这份资源面向具备一定R语言基础、希望深入掌握时间序列建模的数据分析与预测从业者聚焦加法与乘法过程两大核心思路并延伸至广义可加模型GAM的非线性拟合。内容围绕趋势、季节性与随机成分的分解展开涉及ARIMA、SARIMA、STL及mgcv包中gam()函数的应用帮助读者理解不同成分间线性相加与相互影响时的建模差异。压缩包内共1个文件为R脚本整体约1KB代码涵盖数据加载、模型构建与结果解释等环节可直接运行对照学习。目前已有618人学习下载适合想通过实操案例打通时间序列加乘模型与GAM非线性建模思路的读者参考。1. 拿到这份 R 时间序列资源先搞清楚加法和乘法到底差在哪很多人第一次接触时间序列建模看到「加法模型」和「乘法模型」这两个词直觉上觉得只是公式里加号变乘号的区别跑一遍代码就完事。但真正上手 R 语言做预测的时候选错分解方式会让残差检验直接翻车——明明 ACF 图看着还行预测出来的置信区间却宽得没法用。这份资源包里有一个时间序列模型加法和乘法过程.R脚本配套一份说明文档核心就是拿同一组数据分别走加法分解和乘法分解两条路再把 GAM广义可加模型引进来处理非线性趋势。它适合已经会写基础 R 代码、但没系统对比过加法与乘法分解差异的从业者也适合做经济、气象、交通领域预测时想快速验证模型选型的场景。资源本身不依赖冷门包forecast、mgcv、tseries这几个常见库就能跑通拿到手改一下数据读取路径就能复现。2. 加法与乘法分解的 R 实现从 ts() 到 STL 的完整链路2.1 为什么先看分解方式再谈模型选型时间序列的经典分解把观测值拆成趋势、季节性和残差三部分。加法模型写成Y T S R乘法模型写成Y T × S × R。这两个式子的选择不是拍脑袋决定的而是看季节波动的幅度是否随趋势水平变化。如果一组数据的季节性振幅常年稳定在某个固定区间加法分解更合适如果振幅随着趋势上升而变大乘法分解才能把残差压成白噪声。我一般会先画一张时序图肉眼判断振幅是否随均值漂移。更稳妥的做法是对数据取对数后再看如果取对数后季节性振幅变得均匀说明原始数据更适合乘法模型。这个判断步骤在资源脚本里对应的是plot()和boxplot()两个函数的组合调用脚本注释里写得很清楚。R 里用ts()函数把普通向量转成时间序列对象关键参数是frequency。月度数据填 12季度数据填 4周数据填 52。这个参数填错后面stl()分解出来的季节性成分周期就全乱了。资源脚本里用的是月度数据frequency 12如果你换成自己的数据这一行必须改。2.2 加法分解的代码实现与参数说明资源包里的加法分解部分主要围绕decompose()和stl()两个函数展开。decompose()是经典分解要求季节性成分在时间轴上重复且振幅不变stl()基于 LOESS 平滑对季节性和趋势的估计更灵活也是我平时更常用的。# 加载必要的库 library(forecast) library(tseries) # 读取数据并转为时间序列对象 # frequency 12 表示月度数据start 指定起始年份和月份 raw_data - read.csv(your_data.csv) ts_data - ts(raw_data$value, start c(2018, 1), frequency 12) # 加法分解type additive 是默认值 add_decomp - decompose(ts_data, type additive) # 查看分解结果中的季节性成分 plot(add_decomp) # 用 STL 做更稳健的加法分解 # s.window periodic 表示季节性成分在每个周期内重复 add_stl - stl(ts_data, s.window periodic, robust TRUE) plot(add_stl)decompose()的type参数接受additive和multiplicative两个值默认是加法。stl()的s.window控制季节性平滑窗口填periodic表示季节性成分不随时间变化填具体数字则允许季节性形态逐年演变。robust TRUE会降低异常值对分解结果的影响数据里有离群点时建议打开。分解完之后脚本会计算残差的统计量包括残差标准差和 Ljung-Box 检验的 p 值。如果 p 值小于 0.05说明残差里还有自相关没被提取干净加法分解可能不够用得考虑乘法或者引入 GAM。2.3 乘法分解与 SARIMA 的衔接方式乘法分解在 R 里的实现路径和加法类似区别在于decompose()的type参数改成multiplicative以及后续建模时需要对数据做对数变换。资源脚本里对乘法分解的处理是先用log()转换原始数据再走加法分解流程最后把预测结果用exp()还原。这是工程上最常用的等价处理方式因为对数变换把乘法关系转成了加法关系。# 乘法分解先取对数再走加法分解 log_ts - log(ts_data) mul_decomp - decompose(log_ts, type additive) # 季节性成分还原到原始尺度 seasonal_original - exp(mul_decomp$seasonal) # 用 SARIMA 建模 # auto.arima 自动选择 p,d,q 和 P,D,Q 参数 sarima_fit - auto.arima(ts_data, seasonal TRUE, stepwise FALSE, approximation FALSE) summary(sarima_fit) # 预测未来 12 个月 sarima_forecast - forecast(sarima_fit, h 12) plot(sarima_forecast)auto.arima()的stepwise FALSE会穷举更多模型组合跑得慢但选出来的参数更靠谱approximation FALSE关闭近似计算同样是为了精度牺牲速度。如果数据量特别大这两个参数可以改回默认值来提速。seasonal TRUE是默认开启的SARIMA 的季节性阶数(P, D, Q)会自动匹配frequency设定的周期。资源脚本里还留了一段手动指定 SARIMA 参数的代码用Arima()函数配合order和seasonal参数。手动调参的场景是自动选出来的模型残差检验不过关需要根据 ACF 和 PACF 图自己定阶。这部分注释写得很细适合拿来对照学习。3. 把 GAM 引入时间序列mgcv 包处理非线性趋势的实操3.1 GAM 在时间序列里的定位与选型理由ARIMA 和 SARIMA 本质上还是线性模型趋势和季节性的关系被限定在差分和滑动平均的框架里。当数据存在明显的非线性趋势——比如经济增长先加速后放缓、气温变化在某几年出现拐点——线性模型的残差会出现结构性模式预测偏差会越来越大。GAM 的价值在于用光滑样条函数拟合趋势项不需要预先假设趋势是直线还是抛物线。资源脚本里用mgcv包的gam()函数把时间索引作为自变量观测值作为因变量趋势项用s()光滑项表示。如果数据有季节性可以把月份作为因子变量加进去或者用周期样条bs cc处理。GAM 的另一个好处是能直接加入外部协变量比如温度、节假日标记这在纯 ARIMA 框架里需要走回归加 ARIMA 误差的复杂路径。选型上的经验是先跑auto.arima()看残差如果残差的 ACF 图在滞后几阶还有明显尖峰且时序图显示趋势不是线性的就切到 GAM 试试。资源脚本里两条路径都有方便对比。3.2 gam() 函数的关键参数与代码拆解library(mgcv) # 构造数据框time 是连续时间索引month 是月份因子 n - length(ts_data) df - data.frame( y as.numeric(ts_data), time 1:n, month factor(cycle(ts_data)) ) # 拟合 GAM趋势用光滑样条季节性用因子变量 gam_fit - gam(y ~ s(time, k 20) month, data df, method REML) summary(gam_fit) # 查看光滑项的拟合曲线 plot(gam_fit, pages 1, shade TRUE) # 预测 new_df - data.frame( time (n 1):(n 12), month factor(1:12, levels 1:12) ) gam_pred - predict(gam_fit, new_df, se.fit TRUE)s(time, k 20)里的k是基函数维度控制光滑曲线的弯曲程度上限。k太小会欠拟合趋势被拉直k太大会过拟合曲线跟着噪声跑。一般设成数据长度的三分之一到一半月度数据 5 年就是 60 个点k 20比较稳妥。method REML是限制最大似然估计比默认的 GCV 更不容易过拟合也是mgcv作者推荐的设置。month作为因子变量进入模型等价于给每个月一个固定截距这处理的是季节性均值差异。如果季节性形态随时间变化可以把month换成s(month, bs cc)周期样条但那样参数解释会复杂一些。资源脚本里两种写法都有注释可以按需切换。predict()的se.fit TRUE会返回预测标准误用来画置信区间。GAM 的置信区间是贝叶斯后验区间解释上和频率学派的置信区间略有差异但在预测场景下够用。3.3 加法 GAM 与乘法 GAM 的切换技巧GAM 默认是加法结构趋势项和季节项相加。要做乘法结构同样走对数变换对y取log()后拟合 GAM预测结果再exp()还原。资源脚本里有一段对比代码分别拟合原始尺度的加法 GAM 和对数尺度的乘法 GAM然后用 RMSE 和 MAE 比较样本外预测精度。# 乘法 GAM对数变换 df$log_y - log(df$y) gam_mul - gam(log_y ~ s(time, k 20) month, data df, method REML) # 预测并还原 log_pred - predict(gam_mul, new_df, se.fit TRUE) mul_pred - exp(log_pred$fit) # 计算样本外误差假设有真实值 actual # rmse_add - sqrt(mean((actual - add_pred)^2)) # rmse_mul - sqrt(mean((actual - mul_pred)^2))对数变换后exp()还原的预测值是有偏的严格来说需要加一个方差修正项。但在预测精度比较的场景下这个偏差通常可以忽略。如果要做区间预测还原后的区间不对称需要用分位数方法处理。资源脚本里对这一点有注释提醒但没有展开实现属于可以自己补强的部分。4. 避坑与排查这份 R 脚本跑不起来时先查这五处4.1 时间序列 frequency 设错导致季节性完全错位现象stl()分解出来的季节性成分周期明显不对月度数据画出来像季度波动。原因ts()函数的frequency参数没改默认是 1或者从 CSV 读取时没有正确指定start和frequency。解决确认数据周期月度填 12季度填 4周填 52。start参数格式是c(年份, 周期序号)比如c(2018, 1)表示从 2018 年 1 月开始。4.2 decompose() 报错「time series has no or less than 2 periods」现象调用decompose()直接报错提示周期数不够。原因数据长度不足两个完整周期。月度数据至少需要 24 个点季度数据至少 8 个点。解决检查数据行数不够就换stl()并调小s.window或者改用不依赖完整周期的 GAM 路径。资源脚本里默认数据是 5 年月度数据共 60 个点够用。4.3 auto.arima() 跑得极慢或内存溢出现象auto.arima()卡住不动或者报内存分配错误。原因stepwise FALSE和approximation FALSE同时打开模型搜索空间爆炸。解决先关掉stepwise用默认的逐步搜索跑一遍看结果如果精度不够再开approximation FALSE但保留stepwise TRUE。数据量超过 1000 个点时建议先降采样或者分段建模。4.4 gam() 的 k 值过大导致过拟合现象GAM 拟合曲线的置信区间极窄但样本外预测误差很大。原因s(time, k ...)的k设得太大光滑项把噪声也拟合进去了。解决用gam.check()查看k是否足够如果 edf有效自由度接近k说明k偏小需要调大如果 edf 远小于k且残差没有模式说明当前k合适。一般从k 10开始试逐步加到残差通过检验为止。4.5 对数变换后预测值还原忘记 exp()现象乘法模型的预测值全是负数或者量级明显不对。原因对log()变换后的数据建模预测结果没有做exp()还原。解决检查预测代码确保predict()之后有exp()调用。如果用了forecast()包的Arima()配合lambda 0参数包会自动还原不需要手动处理。资源脚本里两种写法都有注意区分。5. 用样本外滚动预测验证模型一个我每次都会走的流程资源脚本最后留了一段滚动预测的框架代码但默认是注释掉的。我建议拿到手之后第一件事就是把这段打开跑一遍因为单次划分训练集和测试集的评估结果波动太大滚动预测才能看出模型稳不稳定。具体做法是从第 48 个点开始每次用前 48 个点拟合模型预测第 49 个点然后窗口往后滑一格重复到数据末尾。这样每个模型都能拿到一组样本外预测值再算 RMSE 和 MAE 做对比。# 滚动预测窗口大小 48预测步长 1 window_size - 48 n_total - length(ts_data) predictions - data.frame( actual numeric(0), add_gam numeric(0), mul_gam numeric(0) ) for (i in window_size:(n_total - 1)) { train - window(ts_data, end time(ts_data)[i]) test_val - ts_data[i 1] # 构造训练数据框 train_df - data.frame( y as.numeric(train), time 1:length(train), month factor(cycle(train)) ) # 加法 GAM fit_add - gam(y ~ s(time, k 15) month, data train_df, method REML) pred_add - predict(fit_add, data.frame(time length(train) 1, month factor(cycle(train)[length(train)] 1, levels 1:12))) # 乘法 GAM train_df$log_y - log(train_df$y) fit_mul - gam(log_y ~ s(time, k 15) month, data train_df, method REML) pred_mul - exp(predict(fit_mul, data.frame(time length(train) 1, month factor(cycle(train)[length(train)] 1, levels 1:12)))) predictions[nrow(predictions) 1, ] - c(test_val, pred_add, pred_mul) } # 计算误差指标 rmse_add - sqrt(mean((predictions$actual - predictions$add_gam)^2)) rmse_mul - sqrt(mean((predictions$actual - predictions$mul_gam)^2)) mae_add - mean(abs(predictions$actual - predictions$add_gam)) mae_mul - mean(abs(predictions$actual - predictions$mul_gam)) cat(加法 GAM RMSE:, rmse_add, MAE:, mae_add, \n) cat(乘法 GAM RMSE:, rmse_mul, MAE:, mae_mul, \n)这段代码里window()函数用来截取训练窗口cycle()提取每个观测值对应的月份序号。factor(..., levels 1:12)确保预测时的月份因子水平和训练时一致否则predict()会报错说新数据有未知水平。这个坑我在第一次写滚动预测的时候踩过排查了半小时才发现是因子水平没对齐。跑完滚动预测之后如果加法 GAM 和乘法 GAM 的 RMSE 差距在 10% 以内我会优先选加法模型因为解释成本低、参数少、不容易过拟合。如果乘法模型明显更优再检查对数变换后的残差是否满足正态性假设。资源脚本里没有内置这个判断逻辑需要自己加一段shapiro.test()或者画 QQ 图。从那以后我每次拿到新的时间序列数据都强制走一遍「先画图看振幅 → 试加法分解 → 试乘法分解 → 跑滚动预测对比」这个流程不再凭感觉选模型。希望帮到你。本文还有配套的精品资源点击获取