ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

R语言GAM时间序列预测:加法与乘法结构选择及避坑指南

R语言GAM时间序列预测:加法与乘法结构选择及避坑指南 简介这份资源面向具备一定R语言基础、希望深入掌握时间序列建模的数据分析与预测从业者聚焦加法与乘法过程两类核心思路并延伸至广义可加模型GAM的非线性建模场景。压缩包内共1个文件为R脚本整体约1KB可直接在R环境中加载运行便于对照代码理解建模流程。内容围绕时间序列的基本构成展开涉及趋势、季节性与随机成分的线性相加与相互影响两种假设并串联ARIMA、SARIMA、STL分解以及基于mgcv包的gam()拟合等实现路径同时涵盖数据加载、模型选择、参数调优与残差诊断等环节。目前已有618人学习下载适合作为经济、金融、气象等领域预测任务的实操参考帮助读者把抽象模型转化为可复用的代码框架并借助GAM样条项捕捉复杂非线性趋势。1. 时间序列的加法与乘法为什么你的预测总在趋势拐点翻车很多人做时间序列预测时拿到数据第一件事就是往 ARIMA 或 Prophet 里一塞跑出来一条平滑曲线看着拟合得不错一到真实业务里就发现趋势一变预测全废。问题往往不在模型本身而在于你默认了一个假设——这个序列是「加法结构」还是「乘法结构」。这两个词听起来像统计学课本里的老古董但在 R 语言里用 GAM广义可加模型做时间序列分解和预测时它直接决定了你的模型是「能跟着趋势走」还是「一到旺季就低估」。加法模型假设各成分独立叠加观测值 趋势 季节 残差。乘法模型假设成分之间是比例关系观测值 趋势 × 季节 × 残差。现实业务里销售额随趋势增长时季节性波动幅度也在放大这就是典型的乘法结构。如果你硬用加法模型去拟合残差会呈现明显的异方差预测区间在高峰期窄得离谱低谷期又宽得没用。R 语言里的 GAM 通过mgcv包提供了灵活的平滑项可以让你在同一个框架下处理这两种结构甚至混合结构。这篇文章面向的是已经会用 R 做基本数据分析、但想在时间序列上把 GAM 用对的从业者。我会把加法与乘法的选择逻辑、GAM 的平滑项设置、以及实际跑通一套分解加预测的流程讲清楚顺带把那些让我翻过车的参数坑标出来。2. 加法与乘法结构的判定先看残差再谈模型2.1 从业务场景反推结构类型拿到一条时间序列不要急着画图。先问一个业务问题当趋势上升 10% 时季节波动的绝对幅度是保持不变还是也跟着涨 10%如果是前者加法结构更合理如果是后者乘法结构更合理。这个判断不需要任何统计检验靠业务常识就能定调。举个例子某电商平台的日订单量。过去两年整体趋势从每天 1000 单涨到 3000 单同时「双十一」当天的峰值从 5000 单涨到 15000 单。峰值与均值的比例大致稳定在 5 倍左右这就是乘法结构的典型信号。反过来如果是一个成熟产品的日活用户趋势基本平缓周末比工作日固定少 2000 人那加法结构就够了。在 R 里你可以用forecast包的decompose()函数分别跑加法和乘法分解然后对比残差图。乘法分解的残差如果比加法分解更接近白噪声且残差绝对值不随趋势增大而膨胀那就选乘法。library(forecast) # 假设 ts_data 是一个 ts 对象频率为 7周季节 # 加法分解 decomp_add - decompose(ts_data, type additive) # 乘法分解 decomp_mult - decompose(ts_data, type multiplicative) # 对比残差的标准差随趋势的变化 # 如果乘法分解的残差更稳定选乘法 sd_add - sd(decomp_add$random, na.rm TRUE) sd_mult - sd(decomp_mult$random, na.rm TRUE) # 更直观的方法看残差与趋势的相关系数 trend_add - decomp_add$trend resid_add - decomp_add$random cor_add - cor(trend_add, abs(resid_add), use complete.obs) trend_mult - decomp_mult$trend resid_mult - decomp_mult$random cor_mult - cor(trend_mult, abs(resid_mult), use complete.obs) cat(加法残差与趋势相关系数:, cor_add, \n) cat(乘法残差与趋势相关系数:, cor_mult, \n)这段代码的逻辑是如果残差的绝对值和趋势有正相关说明波动幅度随趋势增长加法模型就不合适。参数上type指定分解类型frequency在ts()里设定。注意decompose()只适合季节性固定的序列如果季节模式在变得用stl()或 GAM。2.2 用 GAM 的平滑项同时捕捉趋势与季节GAM 的优势在于它不预设趋势是线性的也不预设季节是固定的。mgcv包里的gam()函数可以用s()指定平滑项用te()指定交互项。对于时间序列我一般这样建library(mgcv) # 构造时间索引和季节索引 n - length(ts_data) time_idx - 1:n season_idx - cycle(ts_data) # 1 到 frequency # 加法结构 GAM gam_add - gam(ts_data ~ s(time_idx, k 20) s(season_idx, k 7, bs cc), family gaussian()) # 乘法结构先取对数再跑加法 GAM gam_mult - gam(log(ts_data) ~ s(time_idx, k 20) s(season_idx, k 7, bs cc), family gaussian())这里的关键参数是k它控制平滑项的最大自由度。k太小会欠拟合趋势被抹平k太大会过拟合把噪声当信号。我一般从 10 到 30 之间试用gam.check()看残差和k的显著性。bs cc是循环平滑专门用于季节索引这种周期性变量不加这个12 月和 1 月之间会出现断点。乘法结构取对数后跑加法 GAM等价于在原始尺度上做乘法分解。预测时记得exp()回来但要注意偏差修正——直接exp()会低估均值因为对数正态分布的均值是exp(mu sigma^2/2)。这个坑我后面会细说。2.3 模型选择的量化依据AIC 与残差诊断加法 GAM 和乘法 GAM 跑完后不能只看图。AIC()可以比较但注意乘法模型是在对数尺度上算的AIC 不能直接和加法模型比。正确做法是把两个模型的预测值都变换回原始尺度算 RMSE 或 MAE用交叉验证比较。# 样本外交叉验证留出最后 30 个点 train_n - n - 30 train_data - ts_data[1:train_n] test_data - ts_data[(train_n 1):n] # 重新拟合加法模型 gam_add_train - gam(train_data ~ s(1:train_n, k 20) s(cycle(train_data), k 7, bs cc)) # 重新拟合乘法模型 gam_mult_train - gam(log(train_data) ~ s(1:train_n, k 20) s(cycle(train_data), k 7, bs cc)) # 预测 pred_add - predict(gam_add_train, newdata data.frame( time_idx (train_n 1):n, season_idx cycle(test_data) )) pred_mult_log - predict(gam_mult_train, newdata data.frame( time_idx (train_n 1):n, season_idx cycle(test_data) )) pred_mult - exp(pred_mult_log) # 计算 RMSE rmse_add - sqrt(mean((test_data - pred_add)^2)) rmse_mult - sqrt(mean((test_data - pred_mult)^2)) cat(加法 RMSE:, rmse_add, \n) cat(乘法 RMSE:, rmse_mult, \n)这段代码的核心是「用样本外误差说话」。参数上k在训练集上重新选不要用全量数据调好的k直接套。cycle()提取季节位置predict()的newdata必须包含和训练时同名的变量。如果乘法模型的 RMSE 明显更低且残差没有异方差那就选乘法。3. 在 R 里跑通 GAM 时间序列从数据到预测的完整链路3.1 数据准备与 ts 对象构造R 里做时间序列第一步是把数据转成ts对象。很多人从 CSV 读进来直接跑结果cycle()返回 NULL季节平滑项直接报错。正确做法是明确频率和起始时间。# 假设 raw_data 是数据框date 列是日期value 列是观测值 raw_data$date - as.Date(raw_data$date) # 按日期排序 raw_data - raw_data[order(raw_data$date), ] # 构造 ts 对象频率为 7周数据 ts_data - ts(raw_data$value, frequency 7, start c(year(min(raw_data$date)), as.numeric(format(min(raw_data$date), %j)))) # 检查 cat(频率:, frequency(ts_data), \n) cat(周期数:, length(ts_data) / frequency(ts_data), \n) head(cycle(ts_data))frequency 7表示每周 7 个观测start参数指定起始年份和年内第几天。如果频率设错比如日数据设成 365cycle()会返回 1 到 365季节平滑项k就得设得很大计算量爆炸且容易过拟合。常见做法是日数据用frequency 7捕捉周内模式或者frequency 365.25捕捉年内模式但后者需要k至少 50 以上我一般先用周频率跑通再考虑年频率。3.2 GAM 平滑项的参数设置与 gam.check 解读gam()函数里最关键的三个参数k、bs、family。k是基函数维度决定平滑曲线的最大弯曲次数。bs是基函数类型tp是薄板回归样条默认cc是循环三次样条用于周期变量cr是三次回归样条计算更快。family指定分布族高斯用于连续值泊松用于计数负二项用于过离散计数。# 完整模型拟合 gam_fit - gam(ts_data ~ s(time_idx, k 25, bs tp) s(season_idx, k 7, bs cc), family gaussian(), method REML) # 用 REML 估计平滑参数 # 检查 gam.check(gam_fit)gam.check()输出四张图残差 vs 拟合值、QQ 图、直方图、响应 vs 拟合值。重点看第一张如果残差呈现漏斗形随拟合值增大而扩散说明方差非恒定加法高斯模型不合适要么换乘法取对数要么换family Gamma。QQ 图如果尾部偏离说明残差非正态预测区间会不准。method REML是我强烈建议的默认的 GCV 在样本量小的时候容易过拟合REML 更稳健。这个参数不设gam()会用 GCV残差诊断经常显示k不够其实换了 REML 就好了。3.3 预测与置信区间对数变换后的偏差修正乘法模型预测时predict()返回的是对数尺度上的值exp()回去之后得到的是中位数不是均值。如果业务要的是均值预测必须加偏差修正。# 乘法模型预测对数尺度 pred_log - predict(gam_mult, newdata data.frame( time_idx (n 1):(n 30), season_idx rep(1:7, length.out 30) ), se.fit TRUE) # 偏差修正exp(mu sigma^2/2) sigma2 - summary(gam_mult)$scale pred_mean - exp(pred_log$fit sigma2 / 2) # 置信区间近似 lower - exp(pred_log$fit - 1.96 * pred_log$se.fit) upper - exp(pred_log$fit 1.96 * pred_log$se.fit) # 注意这个区间是条件均值区间不是预测区间 # 预测区间还要加残差方差 pred_lower - exp(pred_log$fit - 1.96 * sqrt(pred_log$se.fit^2 sigma2)) pred_upper - exp(pred_log$fit 1.96 * sqrt(pred_log$se.fit^2 sigma2))summary(gam_mult)$scale提取的是残差方差估计。偏差修正项sigma2 / 2看起来小但当sigma2大的时候不修正会低估均值 5% 到 10%在库存补货场景里就是真金白银的差距。置信区间和预测区间是两回事前者是均值的区间后者是单次观测的区间。业务上要「明天可能卖多少」用预测区间要「明天平均卖多少」用置信区间。4. 避坑与排查那些让我重跑模型的参数陷阱4.1 现象gam.check 显示 k 不够调大后过拟合原因k的默认值是 10对于趋势变化剧烈的序列10 个基函数不够弯曲。但直接调到 50模型会把噪声也拟合进去样本外误差反而上升。解决先用gam.check()看残差是否还有模式。如果残差已经像白噪声k就够了不用管 p 值。如果残差还有趋势每次加 5直到残差无模式。同时用method REML它比 GCV 更不容易过拟合。我一般从k 20开始趋势项和季节项分开调。4.2 现象乘法模型预测值在低谷期为负原因对数变换后预测再exp()回去理论上不会负。但如果用了family gaussian()且没有取对数直接跑乘法结构比如把季节项写成乘积形式线性预测子可能为负。解决乘法结构必须在对数尺度上建模。log(ts_data)后跑加法 GAMexp()回来。如果原始数据有零或负值先加一个常数再取对数或者改用family Gamma(link log)后者直接在对数链接函数上建模不需要手动变换。4.3 现象季节平滑项 bs cc 报错「A term has fewer unique covariate combinations than specified maximum degrees of freedom」原因k设得比季节周期的唯一值数量还大。比如周频率frequency 7season_idx只有 7 个唯一值k设成 10 就报错。解决k必须小于等于唯一值数量。周频率用k 7月频率用k 12日频率用k 7周内模式或k 365年内模式但计算量大。如果确实需要更大的k改用bs tp并手动构造傅里叶项。4.4 现象预测区间在趋势上升段明显偏窄原因GAM 的predict(se.fit TRUE)只给了平滑项的不确定性没有包含残差方差。而且如果模型是加法结构但数据实际是乘法结构残差方差会随趋势增大区间估计系统性偏窄。解决预测区间用sqrt(se.fit^2 sigma2)不要只用se.fit。同时检查残差是否异方差如果是换乘法结构或family Gamma。我习惯在预测后画残差 vs 拟合值图确认没有漏斗形才发结果。4.5 现象时间索引 time_idx 从 1 到 n预测时新数据的时间索引对不上原因训练时time_idx是 1 到 n预测时新数据的time_idx必须从 n1 开始。如果直接用1:30模型会以为你在预测历史区间。解决预测时构造newdata的time_idx用(n1):(nh)season_idx用cycle()或手动指定。更稳妥的做法是把时间索引存成数据框的一列训练和预测用同一套构造逻辑。5. 进阶技巧用 te() 捕捉趋势与季节的交互效应5.1 什么时候需要交互项加法 GAM 假设趋势和季节是独立作用的趋势上升不影响季节波动的形状。但现实中很多序列的季节模式会随趋势变化。比如一个产品刚上市时只有周末卖得好随着市场成熟工作日销量也上来了季节波动的幅度和形状都变了。这时候s(time_idx) s(season_idx)就不够了需要用te(time_idx, season_idx)张量积平滑来捕捉交互。# 张量积交互模型 gam_te - gam(ts_data ~ te(time_idx, season_idx, k c(15, 7), bs c(tp, cc)), family gaussian(), method REML) # 对比无交互模型 gam_no_te - gam(ts_data ~ s(time_idx, k 15) s(season_idx, k 7, bs cc), family gaussian(), method REML) # 用 AIC 比较同一尺度下 cat(交互模型 AIC:, AIC(gam_te), \n) cat(无交互 AIC:, AIC(gam_no_te), \n)te()的参数k是一个向量分别对应两个变量的基函数维度。bs也是向量对应各自的基函数类型。交互模型的自由度是k[1] * k[2]的量级计算量比加法模型大很多样本量少于 200 时慎用容易过拟合。我一般先跑加法模型如果残差在特定时间段比如旺季还有系统偏差再上交互项。5.2 用 vis.gam 可视化交互效应mgcv自带的vis.gam()可以画三维透视图或等高线图直观看到季节模式如何随趋势变化。# 三维透视图 vis.gam(gam_te, view c(time_idx, season_idx), theta 30, phi 20, color heat, plot.type persp, ticktype detailed) # 等高线图 vis.gam(gam_te, view c(time_idx, season_idx), plot.type contour, color heat)view指定两个变量theta和phi控制视角plot.type选persp或contour。等高线图里如果等高线是平行的说明没有交互如果弯曲或交叉说明季节模式随趋势变了。这个图我每次做完交互模型都会看比 AIC 更直观。5.3 一个我常犯的错误交互项加了但没做预测对比刚用te()的时候我看到 AIC 降了就以为交互模型更好直接拿去预测结果样本外 RMSE 反而更高。后来养成习惯不管 AIC 多低一定做样本外交叉验证。交互模型的方差更大样本量不够时AIC 的惩罚项不足以抵消过拟合。# 样本外对比 train_n - n - 30 gam_te_train - gam(ts_data[1:train_n] ~ te(1:train_n, cycle(ts_data[1:train_n]), k c(15, 7), bs c(tp, cc)), family gaussian(), method REML) pred_te - predict(gam_te_train, newdata data.frame( time_idx (train_n 1):n, season_idx cycle(ts_data[(train_n 1):n]) )) rmse_te - sqrt(mean((ts_data[(train_n 1):n] - pred_te)^2)) cat(交互模型样本外 RMSE:, rmse_te, \n)如果rmse_te比加法模型的样本外 RMSE 还大说明交互项在拟合噪声果断回退。这个习惯让我少了很多「AIC 好看但业务翻车」的情况。5.4 最后的习惯先画图再跑模型我现在拿到任何时间序列第一件事是plot(ts_data)第二件事是plot(decompose(ts_data))或plot(stl(ts_data, s.window periodic))。图上看一眼趋势和季节的形态比任何统计检验都快。加法还是乘法很多时候图上一眼就能定如果季节波动的幅度随趋势明显放大乘法如果幅度稳定加法。GAM 只是把这个判断量化并给出预测区间。希望帮到你。本文还有配套的精品资源点击获取
返回列表