ARTICLE DETAIL

资讯详情

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

指数平滑法与Holt-Winters实战:时间序列预测的经典基线模型

指数平滑法与Holt-Winters实战:时间序列预测的经典基线模型 我一直觉得时间序列预测这个方向现在被很多人带上了一条“非深度学习不可”的路。打开技术社区铺天盖地都是LSTM、Transformer、Kairos这类算法教程好像不用神经网络就做不了预测似的。但实际情况远不是这样尤其当你面对的是带趋势、带季节性的销售数据、运营指标、服务器监控曲线时指数平滑法这套几十年前的老方法依然能在绝大多数场景下打得过一堆花里胡哨的深度模型。这一讲我打算把指数平滑法、Holt方法、Holt-Winters方法从头到尾讲透。这不仅是因为它是时间序列模型发展历程里绕不开的一环更因为它是我在实际项目里用得最多、踩坑最少、解释成本最低的一类模型。从数学原理到参数调优从趋势建模到季节性拆解我会把每个环节的“为什么”都讲清楚而不是只丢给你几个公式。这篇文章适合刚接触时间序列的人打基础也适合那些已经在用深度学习做预测、想回头补一补经典方法的人。1. 时间序列模型演进脉络指数平滑法到底处在什么位置1.1 从朴素预测到统计模型的自然演进要理解指数平滑法的价值得先看看时间序列预测整个演进路径上的几个关键节点。早期的时间序列预测非常朴素大家用的就是移动平均、朴素预测这类方法。朴素预测就是把最近一期的观测值直接当成下一期的预测值移动平均则是把最近N期的均值作为预测值。这些方法简单到几乎没有假设但效果嘛只能说能用谈不上准。后来统计学家们开始思考一个问题历史观测值对未来的影响是不是应该不一样直觉上昨天的数据肯定比三个月前的数据更能说明明天的走势。这个直觉催生了加权移动平均再进一步就催生了指数平滑法。指数平滑法的核心思想是赋予近期观测值更高的权重并且权重随着时间间隔的增大呈指数级衰减。这个思想朴素到不能再朴素但它几乎是一切更复杂时间序列模型的基石。再往后统计学界发展出了ARIMA这套体系用自回归和移动平均的组合来捕捉序列中的线性依赖关系再到后来状态空间模型把观测方程和状态方程引入让模型能够处理更复杂的结构然后就是机器学习时代树模型、支持向量机被用在特征工程后的时间序列预测上最后就是现在大家热捧的深度学习系列LSTM、GRU、Transformer、Kairos等等。你如果把这整条线串起来看会发现指数平滑法处在“朴素方法”和“现代统计模型”之间的那个承上启下的位置。它既有朴素方法那种直观、计算量小、易于解释的优点又初步具备了“建模趋势”和“建模季节性”的能力。说得直白一点掌握了指数平滑法你就等于拿到了打开时间序列预测大门的第一把钥匙后面再学ARIMA、状态空间模型甚至深度学习理解起来都会顺畅很多。1.2 为什么这堂课要专门拆开讲Holt和Holt-Winters市面上很多教程把指数平滑法当作一个简单的知识点草草带过。但实际项目中简单指数平滑只能处理没有趋势、没有季节性的平稳序列这种序列在现实业务里少得可怜。销量数据有增长趋势还有季节性波动服务器负载有周期性的高峰低谷网站流量更是白天高晚上低、工作日高周末低。你要拿一个简单指数平滑去预测这些东西结果基本等于拿均值去猜偏差会大到你怀疑人生。Holt方法在简单指数平滑的基础上引入了一个趋势项能够捕捉序列中的线性增长或下降趋势。Holt-Winters则在Holt的基础上再引入一个季节性项能够处理周期性的波动。这两个扩展看起来只是多了一两个公式但它们的出现让指数平滑法从一个“玩具模型”变成了一个真正能应对复杂业务场景的实用工具。我见过不少做预测的工程师一上来就堆特征、上LSTM结果模型的预测效果还不如一个调好参数的Holt-Winters。原因很简单Holt-Winters对趋势和季节性的归纳方式是结构性的它直接建模这两个成分而不是让模型从数据里“隐式”学习。当你的数据量有限、特征维度不高的时候这种结构性归纳的优势非常明显。这也是为什么在时间序列模型发展历程中Holt-Winters即便到了今天依然在生产环境里占据重要地位。2. 三大核心模型的数学原理与直觉理解2.1 简单指数平滑用“衰减权重”看历史先铺垫最简单的版本。简单指数平滑的公式是[ \hat{y}_{t1} \alpha y_t (1 - \alpha) \hat{y}_t ]其中 ( \hat{y}_{t1} ) 是下一期的预测值( y_t ) 是当前期的实际观测值( \hat{y}_t ) 是当前期的预测值( \alpha ) 是平滑系数取值范围在0到1之间。这个公式看着简单但内涵很深。你把它层层展开会发现它实际上是所有历史观测值的加权平均权重按照 ( \alpha(1-\alpha)^k ) 的形式衰减。距离当前越近的观测值权重越大距离越远的权重越小。这个“近大远小”的权重分配方式比简单移动平均那种“一刀切”的均等权重合理得多。实际应用中( \alpha ) 的选择直接决定了模型的平滑程度和响应速度。( \alpha ) 越大模型对近期数据越敏感预测值会快速跟随实际值的变化( \alpha ) 越小模型越平滑历史数据的惯性作用越强但响应也越迟钝。我在实际项目里通常会先把 ( \alpha ) 设为0.2或者0.3作为初始值然后基于训练数据用最小化误差平方和的方式去搜索最优值。这一步不建议手调写个循环或者直接用现成库的自动优化功能省时省力还更准。2.2 Holt方法把“趋势”这个变量显式建模Holt方法在简单指数平滑的基础上引入了一个趋势项 ( b_t )。它的核心是两个递推公式[ \ell_t \alpha y_t (1 - \alpha)(\ell_{t-1} b_{t-1}) ][ b_t \beta (\ell_t - \ell_{t-1}) (1 - \beta) b_{t-1} ]其中 ( \ell_t ) 是t时刻的水平项level( b_t ) 是t时刻的趋势项trend。预测 ( h ) 期之后的值为[ \hat{y}_{th} \ell_t h \cdot b_t ]这个方法的厉害之处在于它不把趋势当作数据的“偶然抖动”而是当作一个需要实时追踪的状态。你可以把 ( \ell_t ) 理解为序列当前所在的“水位”把 ( b_t ) 理解为水位的“流速”。水位在涨还是在跌涨得多快都由这两个变量分别刻画。( \beta ) 是趋势项的平滑系数控制趋势项对近期变化的反应用多快。( \beta ) 越大趋势项更新越快模型越容易捕捉到趋势的加速或减速( \beta ) 太小趋势项会变得迟钝跟上不数据的变化节奏。Holt方法的局限是它只建模了线性趋势趋势既不会衰减也不会加速。如果数据里带明显的非线性趋势比如指数增长的用户量Holt方法的预测就会出现系统性偏差。这个时候有两种选择一种是对数据先做变换比如取对数后再用Holt另一种是引入阻尼趋势的变体限制趋势项在未来无限延伸的强度。2.3 Holt-Winters方法季节性拆解的灵魂Holt-Winters方法也叫三参数指数平滑法它在Holt的基础上再进一步把季节性项也显式建模出来。完整的三组递推公式是水平项 [ \ell_t \alpha (y_t - s_{t-m}) (1 - \alpha)(\ell_{t-1} b_{t-1}) ]趋势项 [ b_t \beta (\ell_t - \ell_{t-1}) (1 - \beta) b_{t-1} ]季节性项 [ s_t \gamma (y_t - \ell_t) (1 - \gamma) s_{t-m} ]其中 ( m ) 是季节周期的长度比如月度数据一年一个周期时 ( m12 )周数据一周一个周期时 ( m7 )。对于加法季节性预测公式为 [ \hat{y}{th} \ell_t h \cdot b_t s{th-m(k1)} ]对于乘法季节性预测公式为 [ \hat{y}{th} (\ell_t h \cdot b_t) \cdot s{th-m(k1)} ]名字挺唬人但你把它拆开看本质就是“先去掉季节性的影响找到水平项和趋势项再把季节性加回来”。打个比方你想看一个零售店一年的销售趋势但每个月有节日促销、季节变化这些周期性因素。Holt-Winters的做法就是先把月度“折扣力度”算出来把每个月的销量还原成“不打折时的基准销量”在这个基准上找趋势预测的时候再把这个月的“折扣力度”叠回去。( \gamma ) 是季节性项的平滑系数控制季节性模式更新的快慢。( \gamma ) 越大季节性项对近期季节模式的变化越敏感越能适应当前的周期波动( \gamma ) 过小季节性项会保持一个历史平均的“季节模板”对季节性变化缓慢的场景更稳但对突发性季节变化响应不足。3. 加性、乘性、阻尼趋势怎么选才是对的3.1 看数据形态选模型结构Holt-Winters看着完美但用之前必须先做一道选择题季节性到底是加法还是乘法趋势到底要不要阻尼这三个参数搞错了模型再精确也是白搭。加性季节性的意思是季节性波动的大小不随序列整体水平的变化而变化。比如某个产品的销量全年围绕一个均值上下波动高峰期比均值高500件低峰期比均值低500件无论整体销量是1000还是5000这个500的差值基本不变。这种情况下用加性模型是对的。乘性季节性的意思是季节性波动的幅度跟序列整体水平成正比。比如双十一大促期间销量是平日的3倍这个“3倍”就是乘性关系。销量基数越大促销期间的绝对增量越大。电商数据、旅游数据、食品饮料行业的数据绝大多数都是乘性季节性。判断方法也简单。你画一张按年份叠加的月环比曲线或者画出去趋势后的残差图如果残差的波动幅度随时间变化而变大变小说明是乘性关系如果残差的波动幅度基本稳定那就是加性关系。数据量少的时候直接用AIC或者BIC做模型选择更省事。阻尼趋势是指数平滑法家族里一个容易被忽略的变体。阻尼趋势的预测公式在趋势项前加了一个阻尼系数 ( \phi )通常取值在0.8到0.98之间[ \hat{y}_{th} \ell_t (\phi \phi^2 \cdots \phi^h) b_t ]这样做的效果是预测越往远期走趋势的延伸效应越弱预测曲线逐渐趋近于一个水平渐近线。这对于很多真实业务场景是更合理的假设。一个产品不可能永无止境地线性增长市场总有天花板阻尼趋势本质上是在对这个“天花板”做隐含建模。3.2 参数优化的收敛策略与公式推导Holt-Winters模型的参数 ( \alpha, \beta, \gamma, \phi ) 通常通过最小化训练集上的均方误差来求解。常用的方法是L-BFGS-B或者Nelder-Mead这类优化算法。要注意的是这些参数都有边界约束均在0到1之间而且误差曲面在参数空间里往往存在多个局部极小点优化过程对初值比较敏感。我自己实践下来比较稳的策略是先把 ( \alpha, \beta, \gamma ) 的初始值都设为0.2。用L-BFGS-B做一轮初步优化。在初步优化结果附近做一次小范围的网格细化搜索确保没有陷在明显很差的局部极小点里。网格细化搜索听起来土但在时间序列参数优化里效果出奇地好。因为参数空间只有三维或者四维网格搜索的代价完全可接受。我在一个项目里用这个方法把MSE优化掉了将近15%光是这一步优化预测精度就上升了一个台阶。边界处理上要特别小心。如果优化出的 ( \beta ) 或者 ( \gamma ) 逼近边界值1.0说明模型在试图极端快速响应变化这往往是过拟合的信号。我通常会检查这种情况下模型的滚动预测表现如果滚动预测误差明显大于训练集误差就会把边界手动压一下比如限制 ( \beta \leq 0.8 )宁可损失一点训练集的拟合精度也要保证预测的稳定性。4. 完整实操用Python实现Holt-Winters预测4.1 数据准备与初始化理论讲再多不如跑一次代码来得直接。我用一个具有明显趋势和季节性波动的销售数据来演示完整流程这个数据的特点跟电商和零售场景非常接近很适合作为操作参考。import numpy as np import pandas as pd import matplotlib.pyplot as plt from statsmodels.tsa.holtwinters import ExponentialSmoothing from sklearn.metrics import mean_absolute_error, mean_squared_error import warnings warnings.filterwarnings(ignore) # 生成模拟数据线性趋势 乘法季节性 噪声 np.random.seed(42) t np.arange(0, 48) # 48个月 trend 0.5 * t 20 # 基础水平线性趋势 season 1 0.4 * np.sin(2 * np.pi * t / 12) # 乘法季节性幅度40% noise np.random.normal(0, 0.3, sizet.shape) y trend * season noise df pd.DataFrame({month: pd.date_range(2020-01-01, periods48, freqMS), value: y}) train, test df.iloc[:36], df.iloc[36:]这里我把数据分成训练集36个月和测试集12个月。为什么要留12个月因为数据的季节周期是12个月要验证模型能不能预测完整一个春夏秋冬的循环测试集的长度至少要覆盖一个完整周期。4.2 模型训练、参数选择与效果评估用statsmodels库实现Holt-Winters非常简单但参数要理解透彻。# 乘法季节性 加法趋势 阻尼趋势 model ExponentialSmoothing( train[value], trendadd, damped_trendTrue, seasonalmul, seasonal_periods12 ).fit(optimizedTrue, use_boxcoxFalse) # 预测12个月 pred model.forecast(12) # 模型参数输出 print(alpha:, model.params[smoothing_level]) print(beta:, model.params[smoothing_trend]) print(gamma:, model.params[smoothing_seasonal]) print(phi:, model.params[damped_trend]) # 计算误差指标 mae mean_absolute_error(test[value], pred) rmse np.sqrt(mean_squared_error(test[value], pred)) print(MAE:, mae) print(RMSE:, rmse)有个细节值得单独说model.params[smoothing_level]这个属性在不同版本里的键名可能不一样有的版本是model.params[smoothing_level]有的是model.params[smoothing_level]也可以通过model.params看整体结构。跑代码之前先打印一下model.params确认键名可以避免不少兼容性上的小坑。如果你想知道更详细的模型诊断信息可以用model.summary()它会输出对数似然、AIC、BIC等统计量对于模型选择很有参考价值。print(model.summary())我建议把训练集上的model.summary()和测试集上的预测误差对照着看。如果AIC很低但测试集MAE很高说明模型在训练集上过拟合了需要进行参数约束。4.3 预测结果的“可解释性分析”Holt-Winters这种结构化模型最大的优势现在体现出来了。你能把预测拆成水平项、趋势项、季节性项这让“预测结果为什么会是这样”变得一目了然。fitted model.fittedvalues # 拆解各个成分statsmodels 将各分量存在不同属性中 level model.level trend_comp model.trend season_comp model.season你可以把这三条线画出来就能直观看到水平项在缓慢爬升趋势项保持稳定增长季节性项呈现出完美的周期性波动。这种可解释性在业务沟通中太重要了。你拿着一个神经网络的预测结果去跟老板解释老板只会觉得是一个黑盒但你拿着成分拆解图告诉老板“这个月预测值低是因为季节性处于低谷但趋势项显示还在增长”老板立刻就能理解并且采纳你的建议。我在一次复盘会上干过这事效果出奇地好。当时团队在用LSTM做预测模型调参调了一个月线上效果还是不如一个Holt-Winters。后来我把Holt-Winters的成分拆解图拿给业务方看对方很快就明白了预测值背后的驱动因素甚至提出了改进方向——把某个大促活动的日期作为额外因素加进去这在Holt-Winters框架下就是调整季节性项的基准或做事件修正简单直接。5. 常见问题与排查技巧实录5.1 参数优化失败、提示无法收敛怎么办statsmodels做Holt-Winters拟合时偶尔会遇到收敛失败报错或者警告。最常见的原因是初始值设得太差导致L-BFGS-B在错误的区域反复打转。我的做法是自己先给定一个合理的初始参数组合再交给优化器。在ExponentialSmoothing.fit中可以通过start_params指定初始参数。另外把maxiter适当调大比如从默认的1000调到5000很多时候优化器只是迭代次数不够不是真的无法收敛。5.2 预测结果“过平滑”或出现过冲如果你发现预测曲线几乎变成了一条水平直线感受不到趋势和季节性的变化通常是平滑系数太小。alpha太小会让水平项更新得过慢模型对近期的实际变化充耳不闻。反过来如果预测结果在短期出现大幅度波动甚至冲出了合理范围那大概率是beta或gamma设得太大模型对趋势项和季节性项的变化过于敏感把随机噪声也当作真实信号捕捉进去了。这种时候我的经验是先看残差图。如果残差里还存在明显的周期信号说明季节性建模不够如果残差里还存在明显的趋势信号说明趋势项设置有问题。通过残差分析可以快速定位是哪个成分出了问题。5.3 季节性周期设错了怎么办季节性周期seasonal_periods是Holt-Winters里最容易踩的坑。很多人拿着月度数据就填12拿着日数据就填7但真实业务往往更复杂。举个例子我做过一个外卖平台的单量预测数据是小时级的。单量有明显的以24小时为周期的波动还有以7天为周期的周内波动。这种情况下单靠一个Holt-Winters模型是无法同时建模两个周期的。Holt-Winters只支持单一季节性周期这是一个硬约束。面对这种场景我有两个方案都在实际项目中验证过效果不错。第一个方案是先用STLSeasonal-Trend decomposition using Loess把周内季节性和日内季节性中的一层先拆掉把残差序列再喂给Holt-Winters。也就是两阶段建模STL负责拆一层季节性Holt-Winters负责处理剩余的规律。第二个方案是直接换模型。当序列存在多季节性时更合适的选择是TBATS或者 Prophet甚至直接用带季节性编码的树模型。这也是我经常强调的一点Holt-Winters很强大但没必要拿锤子去拧螺丝工具选型要和问题本身匹配。还有一个容易出错的地方是季度数据的季节性周期设置。季度数据一年4个季度如果你误把4写成了12模型会认为一年有12个季度导致季节性模式完全错乱。检查方法很简单画个季节性分解图看看提取出来的季节性成分是否真的在按你设定的周期重复。如果看起来不像那肯定是seasonal_periods写错了。5.4 预测远期时不确定性的理解Holt-Winters的预测是点预测没有内置的置信区间计算能力。但在业务汇报里只给一个点预测往往不够业务方更关心的是“最差能差到多少”。我一般用残差的标准差来做一个近似区间估计。residuals train[value] - fitted std_resid np.std(residuals) # 近似95%置信区间 lower pred - 1.96 * std_resid upper pred 1.96 * std_resid当然真正的预测区间应该考虑参数估计的不确定性以及随机噪声的方差异质性用一个固定的残差标准差来近似会有些粗略。但用来做业务沟通已经足够了。5.5 常见问题速查表问题现象可能原因处理方式模型提示收敛失败初始参数不佳手动设置start_params增大maxiter预测曲线太平滑趋势不变化平滑系数过小调大alpha和beta或用优化器自动搜索预测短期波动过大且不合理平滑系数过大手动限制参数边界降低beta和gamma季节性模式无法匹配实际情况seasonal_periods设置错误核对周期画季节性分解图检查序列有多个季节性周期单一周期模型限制使用两阶段拆解或换TBATS/Prophet预测结果长期无法收敛到合理范围趋势假设不合理尝试阻尼趋势或者对数据取对数处理优化参数接近边界1.0模型过拟合训练噪声手动约束参数检查滚动预测表现数据中存在缺失值模型无法处理NA先用插值或前向填充补齐再建模预测值出现负数但业务上不合理加性模型导致改为乘性模型或对数据做log变换模型对节假日等事件不敏感模型缺乏事件建模能力事后残差修正或用梯度提升模型补充6. 经典方法的现代应用Holt-Winters在你项目里的定位6.1 别急着上LSTM先让Holt-Winters给你一个基线我见过太多项目组一上来就奔着LSTM去数据量只有几百条特征工程还没做就想让深度学习模型“自学成才”。结果往往是模型训练时间长、解释性差、线上效果不稳定。其实这里有一个更好的套路选一个预测问题后先花半天时间用Holt-Winters做一版基线结果。有了基线你的工作就会变得非常主动。基线的误差如果已经足够满足业务需求那根本不需要上深度学习如果不够你至少知道了误差的下界在哪里再上LSTM、GRU或者Kairos时你能很清楚地知道深度模型的“增量价值”到底有多大。没有基线做参照你很难说清楚深度模型效果好是模型本身的功劳还是数据预处理、特征工程的功劳。在机器学习里这叫“先建立朴素基线再做复杂建模”的思路。时间序列预测里Holt-Winters就是那个“朴素但绝不简单”的基线。6.2 数据量少、可解释性要求高的场景它是首选深度学习模型的训练需要大量数据这个“大量”在时间序列场景里往往意味着你要有几年的历史数据才能喂饱一个LSTM。但Holt-Winters不一样它对数据量的要求很低甚至可以说几十个观测点它就能跑出相对像样的结果。除了数据量需求少可解释性更是Holt-Winters的看家本领。在金融、供应链、能源这些行业决策者通常需要知道“预测结果是怎么来的”。你给他一个黑盒预测他不敢用你给他一个成分拆解告诉他这个增长是季节性驱动还是趋势驱动他就敢拍板。这种“被信任”的模型在生产环境里的价值远远超过了一点点的精度提升。6.3 和深度学习方法如何互补两段式预测思路我自己在实际项目里经常会用两段式预测的思路先用Holt-Winters抓出趋势和季节性的主干再用深度学习模型拟合残差里的复杂非线性部分。这种方法既保留了Holt-Winters的可解释性和稳定性又利用了深度模型的学习能力。举个例子我做过一个云资源容量预测的项目。CPU使用率有明显的日周期性还有缓慢的增长趋势。我用Holt-Winters做一轮预测把预测残差拿出来一看残差里还有一些突发的、非周期性的抖动。这些抖动跟业务发布、代码变更有关光靠历史模式无法预测但可以通过一个特征工程加LSTM的小模型来捕捉部分规律。两段式方案上线后整体预测误差比单独用任一模型都要低。做预测这事真正高效的思路从来不是把模型内部做得多复杂而是把问题拆解成不同层面选对每个层面的模型。6.4 系列课程的承上启下下一讲怎么走这一讲结束后你应该对指数平滑法这套经典方法的来龙去脉有了比较完整的认识。简单指数平滑解决的是平稳序列的预测问题Holt解决了趋势问题Holt-Winters解决了趋势加季节性的问题。它们共同构成了时间序列模型发展历程中从简单到复杂的第一条完整主线。后续如果你继续深入会接触到ARIMA这套以差分和自相关为核心的体系它和指数平滑法虽然数学体系不同但在实践里解决的问题高度重合很多场景下两个模型的预测结果可以互相验证。再往后就是状态空间模型你可以把Holt-Winters看成一种特殊的状态空间模型的雏形理解了这套哲学再去学状态空间模型会轻松很多。真正的进阶方向是让每个模型在你手里成为工具按需取用而不是被某一个模型框住思路。
返回列表