简介:这份资源面向具备一定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 图。
从那以后我每次拿到新的时间序列数据,都强制走一遍「先画图看振幅 → 试加法分解 → 试乘法分解 → 跑滚动预测对比」这个流程,不再凭感觉选模型。希望帮到你。
本文还有配套的精品资源,点击获取