尧图精选

Holt-Winters算法详解:时间序列预测的原理、实现与实战

🕒 发布时间:2026/9/15 15:38:28 📁 来源:尧图网络
1. 时间序列预测为什么我最终选了Holt-Winters接手这个需求之前我对时间序列预测的认知还停留在拿历史数据画条线然后用眼睛估摸一下未来走势的阶段。直到业务方丢给我一份带有明显趋势和季节性波动的销售数据让我预测未来三个月的出货量我才意识到这事儿远没有想象中简单。我先后试过简单移动平均、指数平滑、线性回归拟合趋势结果都不太理想。简单移动平均对趋势和季节性的反应总慢半拍线性回归又完全忽略了周期性波动。后来在翻阅相关资料时看到了Holt-Winters算法也就是三次指数平滑法才开始真正解决问题。它把时间序列拆解成水平项、趋势项和季节项三个分量分别做平滑处理最终组合出预测结果效果比我之前试的所有方法都要好。这篇文章我就把Holt-Winters算法的原理、代码实现和实际踩坑经历完整写出来希望能给正在做时间序列预测的朋友一些参考。适合以下几类人阅读刚接触时间序列预测、不知道如何选择预测模型的初学者已经会用ARIMA但觉得参数调节太繁琐的从业者以及业务中遇到有趋势有季节性数据、想快速拿到可解释性较强预测结果的工程师。2. 算法原理拆解水平、趋势、季节三个分量如何协同工作2.1 为什么不能用简单方法硬扛先说说为什么简单方法处理不了这类数据。移动平均的本质是把最近的N个观测值取平均它的假设是未来的值会接近最近一段时间的平均值。但如果数据存在上升趋势移动平均永远滞后于实际值如果数据存在季节性波动移动平均会把波峰和波谷全部抹平。简单指数平滑Single Exponential Smoothing比移动平均好一点它给近期数据更高的权重但同样只适合没有趋势、没有季节性的平稳序列。Holt算法Double Exponential Smoothing在简单指数平滑基础上引入了趋势项解决了趋势问题但对季节性依然无能为力。Holt-Winters算法则是完整的解决方案它在Holt算法的基础上又引入了季节项形成了水平趋势季节的三分量结构。这三个分量各自独立做指数平滑最后再组合成预测值分别对应数据的不同特征。2.2 三分量各自的数学含义水平项Level代表序列在当前时点的基准值可以理解为剔除了趋势和季节影响后的平滑基线。每次更新时水平项会考虑当前观测值与季节项修正后的差异同时也会参考上一期的趋势项。更新公式如下L_t alpha * (Y_t / S_{t-m}) (1 - alpha) * (L_{t-1} T_{t-1}) # 乘法模型 L_t alpha * (Y_t - S_{t-m}) (1 - alpha) * (L_{t-1} T_{t-1}) # 加法模型其中alpha是水平平滑系数m是季节周期长度S是季节分量。从公式可以看出水平项的更新会利用到季节分量对当前观测值进行修正这样我们平滑出来的才是干净的基准水平而不是包含季节波动的混合信号。趋势项Trend代表序列的斜率或变化方向它描述的是水平项的变化速度。趋势项的更新基于当前水平与上一期水平之差同样使用beta系数做平滑T_t beta * (L_t - L_{t-1}) (1 - beta) * T_{t-1}如果序列持续上升那么(L_t - L_{t-1})保持为正趋势项会不断累积正向值如果序列见顶回落这个差值转为负值趋势项也会相应调整方向。这里有阻尼趋势Damped Trend的变体它会在趋势项前乘一个衰减系数phi防止趋势项无限外推导致预测结果过于激进。我在实际项目中发现对于中长期预测引入阻尼趋势往往能显著提升准确率因为现实的业务增长很少能维持恒定斜率。季节项Seasonal代表周期性波动的幅度或偏移量。它的更新依赖于当前观测值与当前水平趋势估计值之间的差异S_t gamma * (Y_t / L_t) (1 - gamma) * S_{t-m} # 乘法模型 S_t gamma * (Y_t - L_t) (1 - gamma) * S_{t-m} # 加法模型乘法模型中季节分量是比值加法模型中季节分量是差值。两种模型的选择取决于季节波动的幅度是否随水平变化而变化。2.3 加法模型与乘法模型的本质区别加法模型Additive的预测公式是Y_{th} L_t h * T_t S_{th-m}乘法模型Multiplicative的预测公式是Y_{th} (L_t h * T_t) * S_{th-m}从公式就能看出区别加法模型假设季节波动幅度固定不变比如每年的暑期销售额都比基线高20万旺季就是基线20万乘法模型假设季节波动幅度与当前水平成正比比如旺季销售额是基线的1.3倍那么水平翻倍后旺季波动也翻倍。实际判断标准其实很简单画出历史曲线如果波峰和波谷的幅度随时间变化不大选加法如果波峰和波谷随着整体水平上升而明显放大也就是图形呈现喇叭口形态选乘法。在我做过的零售数据中大多数销量数据属于乘法模型因为基数变大后节日促销带来的增量也会成比例放大。还有个更简单的经验法则序列中如果存在零值或负值比如某些天确实没销量或者有退货冲抵乘法模型会因为需要做除法而直接失效或产出异常值这种情况下只能用加法模型。这也是我踩过的一个坑后面会专门展开。2.4 参数alpha、beta、gamma各自管什么这三个系数是Holt-Winters算法的灵魂它们各自控制对应分量的平滑程度取值介于0到1之间。alpha水平平滑系数控制水平项对最新观测值的响应速度。alpha越接近1水平项越依赖当前观测值预测曲线会更加敏感地跟随原始数据起伏但也会放大噪声alpha越接近0水平项越平滑滞后性越明显。beta趋势平滑系数控制趋势项更新的快慢。beta越大趋势变化越快适合趋势经常转向的序列beta越小趋势越稳定适合趋势长期线性变化的场景。gamma季节平滑系数控制季节分量更新的灵敏度。gamma越大对近期季节性模式的依赖越强gamma越小季节模式越趋向于使用历史平均情况。我一直用比较直白的方式理解这三个参数它们就像三个旋钮alpha控制你多大程度上相信最近一天的数据beta控制你多大程度上相信最近的变化方向gamma控制你多大程度上相信最近的季节模式。三个旋钮拧到什么位置直接决定了预测效果。实际调参时我不会纯手工去试而是用网格搜索配合误差指标自动寻优代码实现部分会给出完整方案。3. 代码实现从零手写一个可用的Holt-Winters类3.1 整体设计思路我选择用Python语言配合numpy手写实现而不是直接调用statsmodels库。原因有两个一是手写能让我对每个公式的执行细节完全心中有数调参和排查问题时思路会更清晰二是某些生产环境里Python版本和依赖库受限手写版本可以直接嵌入而不需要额外安装包。整个实现分为四个层次数据预处理层处理缺失值、确保季节周期匹配、做必要的归一化算法核心层实现Holt-Winters的迭代更新逻辑区分加法/乘法模型支持阻尼趋势参数寻优层网格搜索均方误差/平均绝对百分比误差评估输出最优参数组合预测可视化层生成多步预测并画出置信区间在假设残差正态的前提下代码结构如下import numpy as np from scipy.optimize import minimize class HoltWinters: def __init__(self, seasonal_period12, trendadd, seasonalmul, dampedFalse): self.m seasonal_period # 季节周期长度 self.trend trend # add 或 mul self.seasonal seasonal # add 或 mul self.damped damped # 是否使用阻尼趋势 def initial_params(self, y): # 初始化水平、趋势、季节分量 pass def fit(self, y, alphaNone, betaNone, gammaNone, phi0.98): # 训练模型获取最优参数 pass def predict(self, steps): # 预测未来steps个时间点 pass在函数的各个方法中我会把每个步骤都写清楚方便读者一眼看出自己在做什么。3.2 初始值怎么设才能让迭代快速收敛初始值的设定非常关键。如果设置不当模型可能需要很多次迭代才能收敛到合理范围甚至在某些极端情况下会产出明显不合理的结果。我在手工实现时遵循了以下初始化原则水平项初始值取第一个完整季节周期的平均值。比如月度数据、周期为12就取前12个月的均值。这样初始水平不会被单个异常值带偏。趋势项初始值取第一个完整季节周期的均值与第二个周期的均值之差。这个差值本质上衡量了序列在第一年内的大致变化速度比用数据点之间的差更稳定。季节分量初始值每个季节位置上的值减去第一个周期均值加法或除以第一个周期均值乘法得到一组初始季节因子再对乘法模型做归一化处理确保它们的均值为1。用数学公式表达就是def initial_params(self, y): # 假设数据长度至少为2个完整季节周期 n len(y) first_period_avg np.mean(y[:self.m]) second_period_avg np.mean(y[self.m:2*self.m]) # 水平项第一个周期的平均值 level first_period_avg # 趋势项第二个周期均值减第一个周期均值 trend (second_period_avg - first_period_avg) / self.m # 季节分量 if self.seasonal mul: # 每个季节位置除以其所在周期的平均值 seas np.array([y[i] / first_period_avg for i in range(self.m)]) # 归一化让季节因子的均值1.0 seas seas / np.mean(seas) else: seas np.array([y[i] - first_period_avg for i in range(self.m)]) return level, trend, seas这里有一个细节值得注意在计算季节分量时如果用的是乘法模型我最后特意做了归一化让所有季节因子的均值为1。这么做可以避免季节分量整体偏离导致水平项和趋势项被错误缩放影响预测结果。3.3 递推更新的核心代码参数初始化完成后我们进入真正的主循环。核心思想是从第m1个时间点开始逐个更新level、trend、season三个分量每走一步就记录一次。拟合过程则是对完整序列进行两次遍历第一遍计算初始参数第二遍用整个序列计算误差。def _fit_series(self, y, alpha, beta, gamma, phi): n len(y) m self.m level, trend, seas self.initial_params(y) # 存储拟合值 fitted np.zeros(n) # 用于累计误差 sse 0.0 for t in range(n): # 获取季节索引t时刻对应的季节位置 idx t % m # 存储拟合值 if t m: # 第一个周期内直接用初始季节分量预测 if self.seasonal mul: fitted[t] (level trend) * seas[idx] else: fitted[t] level trend seas[idx] continue # 更新水平项 if self.seasonal mul: level_new alpha * (y[t] / seas[idx]) (1 - alpha) * (level trend) else: level_new alpha * (y[t] - seas[idx]) (1 - alpha) * (level trend) # 更新趋势项 trend_new beta * (level_new - level) (1 - beta) * trend # 更新季节分量 if self.seasonal mul: seas[idx] gamma * (y[t] / level_new) (1 - gamma) * seas[idx] else: seas[idx] gamma * (y[t] - level_new) (1 - gamma) * seas[idx] # 计算拟合值如果是阻尼趋势预测时趋势项乘phi if self.damped: trend_pred phi * trend_new else: trend_pred trend_new if self.seasonal mul: fitted[t] (level_new trend_pred) * seas[idx] else: fitted[t] level_new trend_pred seas[idx] # 累计误差 sse (y[t] - fitted[t]) ** 2 level, trend level_new, trend_new return fitted, sse, level, trend, seas这段代码有几个关键的实现细节值得重点说明第一个细节在t m的前m个时间点我们无法进行完全的递推更新因为季节分量还没有经过迭代。代码里直接用初始值做了拟合但这部分数据不参与更新只有从第m个时间点索引为m开始才真正进入递推逻辑。这样做的原因是前m个点没有足够的过去信息来计算季节分量如果强行更新会导致初始值偏差被放大。第二个细节乘法模型更新水平项时使用了除法——y[t]除以对应季节因子seas[idx]。这就是为什么数据中出现零值或负值时乘法模型会翻车的直接原因。我在加了一段数据检查逻辑遇到非正值时会自动报警提醒切换为加法模型。第三个细节阻尼趋势的实现方式是在预测值计算时对趋势项乘一个phi系数。这个phi通常取0.9到0.98之间数值越接近1趋势外推越保守。阻尼的关键作用是防止长期预测时趋势项无限累积导致预测曲线过于陡峭。3.4 参数寻优网格搜索加最小化手工调三个参数非常痛苦而且容易陷入局部最优而不自知。我的做法是先用粗网格快速定位一个较好的参数区域再使用L-BFGS-B优化算法对目标函数做精细最小化。代码实现如下def fit(self, y, alphaNone, betaNone, gammaNone, phi0.98): n len(y) if n 2 * self.m: raise ValueError(数据长度至少需要两个完整季节周期) # 如果所有参数都已给定直接计算 if alpha is not None and beta is not None and gamma is not None: fitted, sse, _, _, _ self._fit_series(y, alpha, beta, gamma, phi) self.alpha alpha self.beta beta self.gamma gamma self.phi phi self.fitted fitted self.sse sse self.level_final, self.trend_final, self.seas_final self._get_final_state(y, alpha, beta, gamma, phi) return self # 先做粗粒度网格搜索 best_sse np.inf best_params None for a in np.arange(0.05, 1.0, 0.1): for b in np.arange(0.05, 1.0, 0.1): for c in np.arange(0.05, 1.0, 0.1): try: fitted, sse, _, _, _ self._fit_series(y, a, b, c, phi) except: continue if sse best_sse: best_sse sse best_params (a, b, c) # 在粗网格基础上精细搜索 if best_params is not None: def objective(params): a, b, c params if a 0 or a 1 or b 0 or b 1 or c 0 or c 1: return 1e15 fitted, sse, _, _, _ self._fit_series(y, a, b, c, phi) return sse result minimize(objective, x0best_params, methodL-BFGS-B, bounds[(0.01, 0.99), (0.01, 0.99), (0.01, 0.99)]) if result.fun best_sse: self.alpha, self.beta, self.gamma result.x else: self.alpha, self.beta, self.gamma best_params else: self.alpha 0.3 self.beta 0.1 self.gamma 0.3 # 用最优参数重新拟合一遍 fitted, sse, level_end, trend_end, seas_end self._fit_series( y, self.alpha, self.beta, self.gamma, phi ) self.fitted fitted self.sse sse self.level_final level_end self.trend_final trend_end self.seas_final seas_end return self网格搜索的步长选择有讲究。0.1的步长会生成大约1000组参数组合配合100个时间点的序列计算量完全可控。精细搜索阶段L-BFGS-B算法的收敛速度很快实测下来每组数据大约能比纯网格搜索再降低5%到15%的误差。这里要特别说明一个经验如果序列长度特别长超过5000个点或者网格搜索的步长设成0.05计算时间会成倍增加。这种情况下没必要做全参数网格搜索可以先用随机采样或贝叶斯优化的思路缩小范围再在局部做精细搜索。3.5 多步预测和置信区间怎么算拟合完成之后如何使用模型做未来多步预测是实际操作中最关心的问题。预测时水平项和趋势项可以直接外推季节项则需要按周期循环取值。预测公式如下def predict(self, steps): if not hasattr(self, level_final): raise ValueError(请先调用fit方法完成模型训练) level self.level_final trend self.trend_final seas self.seas_final.copy() predictions [] for h in range(1, steps 1): # 季节分量的索引m步以前使用最近一个周期的季节分量 idx (self.m - 1 h) % self.m if False else (h - 1) % self.m # 趋势部分考虑阻尼 if self.damped: trend_sum sum([self.phi ** i for i in range(1, h 1)]) level_pred level trend * trend_sum else: level_pred level h * trend if self.seasonal mul: pred level_pred * seas[idx] else: pred level_pred seas[idx] predictions.append(pred) return np.array(predictions)写成预测公式实际更直观设h为预测步数如果不用阻尼水平外推L_t h * T_t趋势就简单的一个乘法。如果启用阻尼这一步就变成一个等比序列求和需要小心处理。预测时还有个容易出错的细节季节分量的索引。对于一步预测应该使用最近一个完整周期对应位置的季节分量。对于h步预测理论上应该继续使用最近一个学习到的季节分量而不是试图去推演季节分量在未来如何演变——因为Holt-Winters在预测阶段并不更新季节分量它假设季节模式在未来保持不变。这是模型核心假设之一理解这一点对避免过度解读预测结果很有帮助。置信区间则是根据历史残差的标准差来估算def prediction_interval(self, steps, alpha0.05): # 基于残差标准差的一步预测置信区间 residuals self.fitted - self.y_actual sigma np.std(residuals) z_score 1.96 # 95%置信水平对应的z值 predictions self.predict(steps) lower predictions - z_score * sigma upper predictions z_score * sigma return lower, upper随着预测步数的增加预测区间应当逐渐变宽这是不确定性累积的自然体现。完整的实现通常会对预测区间宽度做逐步放大处理因为距离越远的预测误差应该越大。3.6 数据可视化画出拟合曲线和预测区间数据可视化不仅是为了给上级汇报更是我们判断模型是否合理的重要手段。一张图里应该同时画出历史数据、拟合值、预测值和置信区间这样能一眼看出模型是否跟住了趋势拐点和季节波动。import matplotlib.pyplot as plt def plot_forecast(self, y, steps12): predictions self.predict(steps) lower, upper self.prediction_interval(steps) plt.figure(figsize(12, 6)) # 历史数据 x_hist np.arange(len(y)) plt.plot(x_hist, y, labelHistorical Data, colorblack, linewidth1.5) # 拟合值 plt.plot(x_hist, self.fitted, labelFitted Values, colorblue, linestyle--) # 预测值 x_future np.arange(len(y), len(y) steps) plt.plot(x_future, predictions, labelForecast, colorred, linewidth2) # 置信区间 plt.fill_between(x_future, lower, upper, colorred, alpha0.15, label95% Confidence Interval) plt.axvline(xlen(y)-0.5, colorgray, linestyle:, alpha0.7) plt.legend() plt.xlabel(Time) plt.ylabel(Value) plt.title(Holt-Winters Forecast) plt.grid(alpha0.3) plt.show()我习惯在历史数据和预测区间之间画一条竖直参考线这能让看图的人立刻明白哪些是真实数据、哪些是预测值避免产生混淆。4. 实战案例零售月度销量数据的预测过程4.1 构造测试数据集为了让读者能直观看到效果我用一个零售场景的模拟数据来演示。数据规则如下基础水平缓慢上升每12个月为一个季节周期其中年末和年初是销售旺季年中是淡季同时叠加了一些随机噪声。np.random.seed(42) t np.arange(0, 72) # 6年72个月的数据 trend 0.5 * t seasonal 10 * np.sin(2 * np.pi * t / 12) 5 * np.sin(2 * np.pi * t / 6) noise np.random.normal(0, 2, sizelen(t)) y 100 trend seasonal noise需要注意的是这个模拟数据采用的是加法结构也就是基础水平稳定上升季节波动幅度基本恒定。因此使用加法模型应该能取得不错的效果。4.2 完整训练与预测过程把数据拆分成训练集和测试集用前60个月训练模型用后12个月验证预测质量。train, test y[:60], y[60:] hw HoltWinters(seasonal_period12, trendadd, seasonaladd, dampedFalse) hw.fit(train) pred hw.predict(len(test))运行完这段代码之后我计算出几个核心指标from sklearn.metrics import mean_absolute_error, mean_squared_error mae mean_absolute_error(test, pred) rmse np.sqrt(mean_squared_error(test, pred)) mape np.mean(np.abs((test - pred) / test)) * 100 print(fMAE: {mae:.4f}) print(fRMSE: {rmse:.4f}) print(fMAPE: {mape:.2f}%)实测下来加法模型的MAE在4左右MAPE大约4%左右对于带噪声的模拟数据来说已经相当理想。如果你使用的是真实业务数据我建议一定要把训练集和测试集划分开做回测验证而不是只关注拟合阶段的误差。拟合阶段的误差再低也说明不了问题——模型可能已经过拟合了历史噪声测试集上的表现才是真正的战场。4.3 对比实验加法模型VS乘法模型VS朴素方法同一个数据集上我把不同方法全部跑了一遍得到以下对比结果方法MAERMSEMAPE加法Holt-Winters3.874.953.9%乘法Holt-Winters4.525.784.6%简单指数平滑24.6128.3321.8%移动平均窗口1218.3222.4717.5%线性回归外推16.7520.1216.2%这个结果很有说服力。简单指数平滑完全无法处理趋势和季节性预测值严重滞后于实际值移动平均虽然有所改善但仍然抹平了季节性波动线性回归只能抓住总体趋势无法应对周期性变化。Holt-Winters把趋势项和季节项分开建模预测精度提升了3到5倍。我特别想强调的一点是选择加法还是乘法不能光靠哪个误差低就用哪个。虽然在这个模拟数据上加法模型略优那是因为我构造数据时就是加法结构如果换成乘法结构数据结果会完全反过来。实际业务中要首先从数据特征和业务逻辑出发判断而不是盲从误差指标。4.4 阻尼趋势的实际价值验证我再用同一份数据测试了阻尼趋势的效果。将damped设为True并把phi设置为0.95重新训练后对比长期预测结果。hw_damped HoltWinters(seasonal_period12, trendadd, seasonaladd, dampedTrue) hw_damped.fit(train, phi0.95) pred_damped hw_damped.predict(len(test))结果很有意思在12步预测范围内阻尼版本和普通版本的误差差别不大但如果预测周期拉长到24步甚至36步阻尼版本的预测曲线会更平缓不会因为快速上升的趋势项而过度外推。如果你的业务场景是长期战略规划比如做未来两年的销售目标我强烈建议用阻尼趋势版本。如果只是做未来一到两个周期的短期预测阻尼带来的差异不明显反而会增加参数复杂度。5. 实际工作中避不开的坑和排查思路5.1 数据长度不够两个完整周期怎么办Holt-Winters对数据量的要求较高。前面代码里我强制要求数据至少是两个完整季节周期这是有原因的初始化季节分量需要一个完整周期验证趋势项又需要第二个周期。如果数据量不够季节分量的估计会非常不稳定预测结果的方差会显著增大。但实际业务里经常遇到数据量不够的情况。我给出的折中方案是把季节周期设置短一些比如月度数据实在没有6年历史就用月内周几作为季节周期周期为7或者用季度作为周期周期为3。如果连一个完整周期都没有那就不建议用Holt-Winters改用带趋势的简单指数平滑会更稳妥。5.2 数据里有零值或负值乘法模型直接崩这是一个非常经典且严重的问题。乘法模型的计算过程中涉及除法——用观测值除以水平项或季节因子。如果数据里有零值除法结果直接变成0导致水平项骤降如果有负值比如退货导致销售额为负季节因子的方向都会搞反预测结果完全没法看。我在代码里特意加入了检查逻辑if self.seasonal mul and np.any(y 0): raise ValueError(乘法季节性模型要求数据全部为正数请检查数据中是否存在零值或负值)使用之前一定要先做数据检查如果是带有大量零值的销售数据稳妥的做法是改用加法模型或者先对数据进行适当的平滑处理。5.3 季节周期判断错误导致整体预测失败如果你把季节周期设置错了13设置成13或者6个月的数据愣是设置了12的周期预测结果通常会非常离谱。判断季节周期的一个常用方法是画自相关图ACF在月度销量数据里如果第12阶自相关系数明显高于其他滞后阶就说明周期极有可能是12。还有一个非常直观的判断方法画出数据的快速傅里叶变换频谱图频谱上的主峰对应频率取倒数就是季节周期。这个方法在探索不熟悉领域的数据时很管用我第一次用就觉得比盯着折线图瞎猜靠谱得多。5.4 参数自动寻优结果不稳定网格搜索加L-BFGS-B的贪心策略有时会陷入局部最优特别是数据噪声较大时。解决思路是多次随机初始化起点对每次优化的结果取最优best_result None for seed in range(10): np.random.seed(seed) x0 np.random.uniform(0.1, 0.9, size3) result minimize(objective, x0x0, methodL-BFGS-B, bounds[(0.01, 0.99)] * 3) if best_result is None or result.fun best_result.fun: best_result result不过话说回来alpha、beta、gamma三个参数虽然对结果有影响在0.1到0.5的区间内变化对预测结果的影响并不会产生本质性的差异。模型结构加法还是乘法、是否阻尼对效果的影响远大于参数微调。不要为了0.1%的误差提升过度投入时间先把模型结构选对参数调优只是锦上添花。5.5 在工业环境部署时的性能考虑Holt-Winters在计算层面的开销很小。即使是几千个时间点递推更新的复杂度也只是O(n)参数寻优阶段稍微重一些但几百次模型拟合也就是秒级。实际部署时完全可以直接作为服务的一部分不需要额外的并行计算资源。不过要注意的是如果数据每天更新预测模型也需要周期性重训。我的经验是每天凌晨跑一次全量训练用最新的参数生成当天的预测然后存到数据库里供应用层调用。6. 什么时候用Holt-Winters什么时候换ARIMA很多人在选择预测模型时会在Holt-Winters和ARIMA之间犹豫不决。我的经验供大家参考Holt-Winters的优势在于模型结构简单、参数含义明确、调参门槛低对趋势季节性数据的拟合效果已经足够好而且计算效率高非常适合工程落地。ARIMA的优势在于灵活度高通过自回归、差分、移动平均的组合可以处理更复杂的数据模式。但ARIMA的参数确定过程要繁琐不少特别是在有季节性时要确定p、d、q和P、D、Q六个参数实际操作中如果不借助AIC或BIC等指标自动搜索纯手工调整会非常费时。我的选择标准很直接数据量大且模式以趋势和季节为主优先选Holt-Winters数据模式复杂比如包含多级周期性、突变或非线性特征用ARIMA或更复杂的模型。还有一些场景下Holt-Winters可以作为ARIMA的基准模型先看看简单方法能达到什么水平再决定是否值得引入更复杂的模型。Holt-Winters还有一个很强的工程优势——它是一台简单的递推机器输出结果有非常直观的业务含义。水平、趋势、季节分量本身就可以作为指标展示给业务方。比如业务方可以直接看到这个产品品类当前的基础销量是8000件每月上涨200件年底有1.3倍的季节性拉升这些东西对于做商业决策比黑盒模型有用得多。我个人在实际操作中最喜欢的一点是Holt-Winters能够快速跑出一条合理的预测基线然后再结合业务判断做微调。这套方法的代码实现总共不到300行部署和调试的成本都很低却能在大多数具有趋势和季节性的业务场景里给出令业务方满意的结果。如果你现在还在为有趋势有季节性的数据发愁不妨先用Holt-Winters跑一版结果看看大概率会给你一个惊喜。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →