news 2026/9/11 12:31:30

Python时间序列预测:ARIMA模型从定阶到预测实战全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python时间序列预测:ARIMA模型从定阶到预测实战全解析

Python做时间序列预测,绕不开ARIMA模型;说到ARIMA,最经典的入门案例就是洗发水销售预测。今天咱们不聊虚的,把原理、定阶、建模、预测、排查一路走下来,代码直接贴,大家复制就能跑起来。这篇东西更适合两类人看:一类是做数据分析或运营,手头有销售、流量这类按月或按天统计的指标,想用Python把预测做出来;另一类是刚接触机器学习但只做过回归、分类,对时间序列有点懵,想找一个最小闭环练手项目的人。用洗发水预测当例子有个好处:数据量小、结构简单、趋势明显,能让你把注意力放在ARIMA的建模逻辑上,而不是被几十个字段和复杂预处理劝退。

1. 先搞清楚ARIMA在解决什么问题

1.1 时间序列预测和普通预测的核心区别

很多初学者会问:时间序列预测不也是训练一个模型、然后预测一个数吗?和线性回归有什么区别?最大的区别在于样本的独立性假设。

普通回归默认样本之间是独立的,比如预测房价,每个房子的特征和价格之间是独立关系,打乱顺序不影响结果。时间序列数据则完全不是这样——今天卖了多少瓶洗发水,和昨天、前天、上个月都有关系,甚至和去年同月都有关系。这种“前后依赖”关系一旦被打乱,训练出来的模型基本没有意义。

所以时间序列预测的核心,不是找一堆特征和标签的对应关系,而是把时间线上的依赖结构拆出来,然后基于这种结构去外推未来。ARIMA就是做这件事的最经典、最成熟的方法之一。它能从历史数据里提取出三样东西:趋势、周期性扰动、随机噪声,然后用这三样去组合出未来的值。

1.2 AR、I、MA三件套到底在干嘛

ARIMA的全称是Autoregressive Integrated Moving Average,翻译过来是“自回归积分滑动平均”。名字很拗口,但拆开看就简单了:

  • AR(p) 自回归部分:用过去p期的数值来预测当前值。可以理解成“惯性”——这个月销量高,下个月大概率也不会太低。公式上是当前值等于常数项加上前p期数值的加权和,再加上一个随机误差。
  • I(d) 差分部分:对原始序列做d阶差分,让它变平稳。一阶差分就是每个月的销量减去上个月的销量,相当于不看绝对水平,只看“变化量”。差分的作用是把趋势脱掉,比如销量一直在涨,涨本身就带有规律,但建模时我们更关注“新增量”的变化。
  • MA(q) 移动平均部分:用过去q期的预测误差来修正当前预测。你可以把它理解成“踩坑修正”——上个月预测误差大,说明有外部冲击,这个月要带上这个冲击的余波。

其实ARIMA的通俗解释就是:先用差分把不平稳的序列“熨平”,再用自回归抓住惯性,用滑动平均修正误差。三者结合起来,就是ARIMA(p,d,q),p、d、q分别是三部分的阶数。

1.3 为什么拿洗发水销售当案例

洗发水销售数据是时间序列入门里非常经典的一个公开数据集,一般包含36个月的月度销量。这个选择有几个原因:

一是数据量小,只有36条,适合用来跑通流程,不会像生产环境的海量数据一样,光预处理就耗掉半天。二是趋势明显,早期销量低、后期销量高,能非常直观地体现“不平稳”和“差分”的作用。三是业务背景容易理解,销售预测本身就是时间序列应用最广的场景之一,从洗发水到空调、从股票到流量,本质上都是一套思路。

当然,我也要提前泼一盆冷水:正因为样本少,这个案例更适合学习,不适合直接代表高精度业务预测。真实业务数据里,缺失值、异常值、节日效应、渠道变化等因素都会影响模型效果,那些才是真正的难点。这些在后面的常见问题部分会展开讲。

2. 数据准备与平稳性检验

2.1 数据长什么样,怎么读进来

网上流传的shampoo-sales数据集通常是一个CSV文件,只有两列:月份和销量。月份通常是文本格式,例如“1-01”到“3-12”,或者直接就是序号;销量单位是“千件”。我习惯先读进来,再统一转成Pandas的时间索引,这样后面画图、切分、预测都方便。

import pandas as pd import numpy as np import matplotlib.pyplot as plt # 读取数据,假设文件放在当前目录 df = pd.read_csv('shampoo-sales.csv') print(df.head()) print(df.info())

读进来之后,建议给列改名,并把月份解析成时间序列索引:

df.columns = ['Month', 'Sales'] # 数据集默认从2001年1月开始,共36个月 df['Month'] = pd.date_range(start='2001-01-01', periods=len(df), freq='M') df.set_index('Month', inplace=True) plt.figure(figsize=(10, 4)) plt.plot(df['Sales']) plt.title('Shampoo Sales by Month') plt.show()

画完图你会看到明显的上升趋势,尤其是后半段。这种带趋势的序列直接建模是不行的,因为均值在随时间变化,统计性质不稳定,预测结果很容易失真。

2.2 平稳性到底是什么意思,为什么非要它平稳

平稳性听起来很数学,其实可以这样理解:一个平稳时间序列的统计性质不随时间变化。具体表现是均值稳定、方差稳定,没有明显的趋势和周期性。你拿任何一段子序列去看,它的波动范围都差不多。

为什么ARIMA需要平稳?因为AR和MA模型本质上是在用过去的“稳定模式”外推未来。如果序列本身有上升趋势,那不同时间段的数据均值就不同,用早期数据推断晚期数据自然不靠谱。就像一个人跑步速度忽快忽慢,你很难根据前5分钟的平均速度预测最后5分钟的位移。

还有一个更实际的原因:非平稳序列容易产生“伪回归”现象,模型表面上看拟合得很好,R方很高,实际上只是同时抓住了两个上升趋势,并没有真实的因果关系。

2.3 用ADF检验判断,不够平稳就差分

判断是否平稳,最常用的工具是ADF检验(Augmented Dickey-Fuller Test)。它的原假设是“序列存在单位根,即不平稳”,所以当p值小于0.05时,我们有理由拒绝原假设,认为序列是平稳的。

from statsmodels.tsa.stattools import adfuller adf_result = adfuller(df['Sales']) print('ADF统计量:', adf_result[0]) print('p值:', adf_result[1]) print('临界值:', adf_result[4])

在实际运行这个案例时,原始序列的p值通常会远大于0.05,说明确实不平稳。接下来做一阶差分,把每个月的增量提取出来:

df['Sales_diff'] = df['Sales'].diff().dropna() adf_result_diff = adfuller(df['Sales_diff'].dropna()) print('差分后p值:', adf_result_diff[1])

一阶差分后,p值基本会小于0.05,说明差分序列平稳了。这时候d定为1。我见过不少初学者拿到任何数据都先做一阶差分,也不管原本是不是平稳,这是不对的。差分是有代价的:每差一次,就会损失一条样本,同时也会让原始水平的解释变弱。所以正确的做法是:先检验,不平稳再差,差到1阶或2阶为止,不要为了“看起来更像随机噪声”无限差分。

3. 定阶:p、d、q怎么确定

3.1 看ACF和PACF图的直觉方法

d确定之后,接下来要定p和q。最传统的方法是看自相关函数ACF图和偏自相关函数PACF图。

  • ACF(自相关函数)描述的是序列和它滞后k期之间的相关系数。
  • PACF(偏自相关函数)描述的是剔除了中间滞后项影响后,序列和滞后k期之间的偏相关系数。

判断经验是:

特征ACF表现PACF表现适合模型
AR(p)拖尾(逐渐衰减)在p阶后截尾(突然为0)AR(p)
MA(q)在q阶后截尾拖尾MA(q)
ARMA(p,q)拖尾拖尾ARMA(p,q)

画图代码很简单:

from statsmodels.graphics.tsaplots import plot_acf, plot_pacf # 对差分序列画图 fig, axes = plt.subplots(2, 1, figsize=(10, 8)) plot_acf(df['Sales_diff'].dropna(), lags=12, ax=axes[0]) plot_pacf(df['Sales_diff'].dropna(), lags=12, ax=axes[1]) plt.show()

不过说实话,现实数据往往没有教科书那么标准。洗发水这个例子更是如此,ACF和PACF的截尾特征不是特别清晰,不同人看图会定出不同阶数。所以我的习惯是:把图当作初步参考,再用信息准则做一次自动搜索,两边交叉验证。

3.2 用AIC自动网格搜索,省心还严谨

AIC(赤池信息准则)是一个同时衡量拟合效果和模型复杂度的指标。它不是越大越好,而是越小越好。AIC本质上会对参数多的模型施加惩罚,避免我们一味追求“拟合完美”而把模型搞得越来越复杂。

我常用的做法是在一个合理范围内暴力搜索所有p、q组合,选出AIC最小的那个:

from statsmodels.tsa.arima.model import ARIMA from itertools import product import warnings warnings.filterwarnings('ignore') best_aic = float('inf') best_order = None best_model = None # p、q都取0到4,d按前面检验固定为1 for p_val, q_val in product(range(0, 5), range(0, 5)): try: model = ARIMA(df['Sales'], order=(p_val, 1, q_val)) model_fit = model.fit() if model_fit.aic < best_aic: best_aic = model_fit.aic best_order = (p_val, 1, q_val) best_model = model_fit except: continue print('最佳阶数:', best_order) print('最佳AIC:', best_aic)

这段代码把p和q都限制在0到4之间,已经是比较务实的范围。洗发水销售数据的最佳结果常见是ARIMA(1,1,2)或ARIMA(2,1,1)之类,AIC会落在500到600之间。如果你把p、q范围放大到0到10,理论上能找到AIC更小的模型,但很容易过拟合,预测新数据效果反而更差。

3.3 定阶时的三条经验法则

第一,不要盲信AIC。AIC值差小于2时,两个模型解释力差别不大,这时应该选参数更少的模型。比如ARIMA(1,1,2)和ARIMA(2,1,2)只差一个AR参数,AIC只差0.5,那我会选前者。

第二,差分阶数d不宜过高。实际经验里d=0或1就覆盖了绝大多数问题,d=2已经算比较极端。差分的目的是把序列变成平稳,不是追求“差分到像白噪声为止”。差过分了,反而会把原本可预测的信息也差掉。

第三,模型阶数要和业务逻辑匹配。如果数据有季节性(比如洗发水销量每年都有固定旺季),那ARIMA里的p、q可能不足以捕捉这种周期,你需要考虑SARIMA或者在定阶时结合业务判断。这节的网格搜索只能给你一个统计上的“建议”,最终判断还是要看残差检验和实际预测效果。

4. 模型拟合与预测实操

4.1 划分训练集和测试集,顺序不能乱

时间序列建模和普通机器学习最大的流程差异,就在于数据划分。我们不能用随机打乱的方式切分数据集,因为时间顺序本身就是信息的一部分。常见做法是拿前一段训练,后一段验证预测效果。

洗发水数据一共36个月,我习惯拿前30个月训练、后6个月测试,这样既保留足够训练样本,又能检验模型在稍长周期上的表现:

train = df['Sales'].iloc[:-6] test = df['Sales'].iloc[-6:] print('训练集样本数:', len(train)) print('测试集样本数:', len(test))

这里有个容易混淆的概念:机器学习里训练集通常要覆盖更丰富的样本分布,时间序列也一样,训练集必须包含一个完整的业务周期,比如有旺季也有淡季。如果训练集中恰好只包含上升段,那模型学到的主要是趋势,测试集中一旦出现波动,误差会非常大。

4.2 拟合ARIMA并检查残差

定好阶数后,直接拟合:

model = ARIMA(train, order=best_order) model_fit = model.fit() print(model_fit.summary())

summary里能看到各项系数的显著性。看回归系数旁边的P值,如果P值远大于0.05,说明这项对应的参数可能没必要存在,可以考虑减少阶数。但这也不是绝对的,有时候保留一项能让模型更稳定,尤其是短期预测。

拟合完必须检查残差。残差是模型没有解释掉的那部分信息,如果残差还有明显模式,说明模型没把序列里的结构提取干净。检查方法有两个:

一是看残差图,理想状态下残差应该像随机噪声,在0附近上下波动,没有趋势或周期性。

resid = model_fit.resid plt.figure(figsize=(10, 4)) plt.plot(resid) plt.title('Residuals') plt.show()

二是做Ljung-Box白噪声检验,原假设是残差序列没有自相关,也就是白噪声。p值大于0.05时,说明残差没有显著自相关,模型通过检验。

from statsmodels.stats.diagnostic import acorr_ljungbox lb_test = acorr_ljungbox(resid, lags=[10], return_df=True) print(lb_test)

如果残差检验不过,不要急着出预测结果,回头调p、q或者考虑加季节项。

4.3 预测未来并评估效果

拟合完成后,用训练好的模型预测测试集对应的6个月:

forecast = model_fit.forecast(steps=len(test)) # 转成Series方便对比 forecast_series = pd.Series(forecast, index=test.index) print(forecast_series)

然后和真实值对比,计算RMSE和MAE:

from sklearn.metrics import mean_squared_error, mean_absolute_error rmse = np.sqrt(mean_squared_error(test, forecast_series)) mae = mean_absolute_error(test, forecast_series) print('RMSE:', rmse) print('MAE:', mae)

RMSE的单位和销量一致,洗发水销量本身在100到500之间,RMSE落在几十左右都算可接受。但更重要的是看相对误差,如果RMSE动辄几百,说明预测完全不能用。

画图对比预测值和真实值:

plt.figure(figsize=(10, 5)) plt.plot(train.index, train, label='Train') plt.plot(test.index, test, label='Test') plt.plot(forecast_series.index, forecast_series, label='Forecast') plt.legend() plt.title('ARIMA Forecast vs Actual') plt.show()

最后,可以用全部36个月数据重新训练模型,预测未来12个月,同时拿到置信区间:

final_model = ARIMA(df['Sales'], order=best_order).fit() future_forecast = final_model.get_forecast(steps=12) future_mean = future_forecast.predicted_mean future_ci = future_forecast.conf_int() print(future_mean)

get_forecast相比forecast的好处是能直接给出置信区间,方便在报告中画出上下界,让业务方知道预测的波动范围在哪里。

5. 实际使用中的几个坑

5.1 数据不平稳,一阶差分后还是不平稳怎么办

这类问题在小样本数据里很常见,尤其当序列存在单调增长又突然变缓时,单次差分可能不够。解决办法先试二阶差分,再检验一次。如果二阶差分后平稳,d=2也是可以的。

如果二阶差分还是不理想,考虑对原数据做对数变换或者Box-Cox变换。对数变换能压缩高值部分的波动,让方差更稳定,特别是销量这种可能指数增长的数据。做法是:

import numpy as np df['LogSales'] = np.log(df['Sales']) # 再对LogSales做ADF检验和差分

变换之后模型预测得到的是对数值,最终需要exp还原,但还原后要记得还原的是中位数意义上的预测,和原始量纲会有一点偏差。

5.2 预测结果画出来像一条水平直线,是不是模型坏了

不是模型坏了,这是ARIMA在小样本、无强季节性数据下的常见表现。如果你拟合的是一个接近随机游走的模型(比如p、q很小,AR系数接近1),那么多步预测的期望值会快速收敛到某个均值,画出来就像一条横线。

这背后其实是无条件预测的数学性质:在只有过去信息、没有未来外部变量的情况下,长期预测会回归到序列的均衡水平。短期预测通常还有变化,拉到12个月以上就平了。

要改善这种情况,要么缩短预测步长,只预测3到6个月;要么在拟合时加入趋势项,例如:

model = ARIMA(train, order=best_order, trend='c')

trend='c'表示允许模型包含一个常数漂移项,能帮助保持一定趋势。如果数据有明显季节性,还得换SARIMA,而不是死磕ARIMA。

5.3 残差检验发现还有自相关,怎么排查

残差不是白噪声,说明模型没有把序列中所有的结构都吸干净。这时候按顺序做三件事:

第一,检查使用的p、q阶数是不是太小。网格搜索得到AIC最小的结果,有时会偏向简洁,但残差检验会暴露问题。直接增加p或者q,再看AIC和残差,找到一个两者平衡的点。

第二,确认是否遗漏了季节性。月度销售数据往往有年度周期,ARIMA只能处理非季节性部分。如果ACF在滞后12、24处明显突出,说明有季节性,需要试试SARIMA模型,例如SARIMA(1,1,1)(1,1,1,12)。

第三,考虑增加外生变量。比如洗发水销售可能受促销活动、节假日、同类竞品上市影响,如果这些变量有记录,可以改用ARIMAX或者SARIMAX,把这些外部因素作为回归项放进去。

5.4 自动定阶很慢或者跑出来一个离谱的模型

网格搜索本身很快,但如果p、q范围设太大,比如各取0到20,组合数有400个,每个都拟合一遍,哪怕数据只有36条,也要等一阵子。更重要的是,范围太大容易选到高维模型,导致预测时数值不稳定,置信区间特别宽。

我的经验是:p和q都只搜索0到4,最多0到7;d固定为1或2。如果搜索出来的最佳模型参数非常大,比如ARIMA(7,1,7),先怀疑是数据量不够或者存在异常值,不是模型越复杂越能预测准。

另外,如果在真实业务中遇到这种小样本销售预测,还可以试试用滚动预测(walk-forward validation)来评估。也就是每次只用截止到t期的数据预测t+1期,然后逐步推进,这样更贴近业务端的实际使用方式,而不是一次性预测6个月然后干等。ARIMA的模型更新成本很低,非常适合这种滚动预测方式。


最后说一点个人体会。ARIMA不是预测模型的终点,但对于样本量有限、单变量、有趋势和少量波动的数据,它永远是快速上场、容易解释的首选。我每次做这类预测,都会坚持三步:先画序列图看趋势,再做ADF检验定差分,拟合后一定看残差和实际预测效果,而不是只看AIC。

洗发水这个例子,最大的价值不是让你记住某个特定阶数,而是让你把“平稳性检验—定阶—拟合—残差分析—预测评估”这一整套时序建模框架跑通。后面遇到真实业务数据,不管是转战SARIMA、Prophet,还是LSTM,底层思路都是一样的:预测的前提是先理解时间线的结构,而不是盲目套模型。希望对正在入门时间序列的你有所帮助。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 12:31:15

Refine 实战:useList 排序(sorters)与动态数据列表构建指南

Refine 实战&#xff1a;useList 排序&#xff08;sorters&#xff09;与动态数据列表构建指南 【免费下载链接】refine A React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility. 项目地址: https://gitcode.c…

作者头像 李华
网站建设 2026/9/11 12:30:27

NPU/GPGPU乱序执行设计:面向AI负载的粗粒度OoO实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 12:29:50

MODBUS从帧格式到CRC校验:嵌入式调试实战完整梳理

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 12:26:45

RK3568+OpenHarmony多路显示全栈移植实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 12:24:22

RP2040 PIO本质:硬件状态机编程与确定性时序实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华