简介:Python实现的ARIMA时间序列预测完整项目,内含可直接运行的源码与配套数据,面向计算机、电子信息工程、数学等专业学生完成课程设计、期末大作业或毕业设计,也适合希望快速掌握时间序列建模流程的初学者。项目基于Anaconda与PyCharm环境开发,采用参数化编程,主要参数集中便于修改,几乎逐行添加保姆级注释,清晰展示ARIMA模型构建、训练与预测的关键代码,方便小白对照学习并二次开发。压缩包共5个文件,包含1个Python脚本、3个CSV数据文件和1个Excel表格,整体仅61KB,轻量便携;数据文件与脚本分离,可替换为自己的时序数据直接进行预测实验。目前已有961人学习下载,由资深算法工程师整理,注释细致、思路清晰,适合毕业设计、课程作业以及初学者快速上手ARIMA预测建模。
1. ARIMA时间序列预测:不是所有序列都适合直接建模
做销售预测、监控指标波动、分析服务器日志趋势的时候,ARIMA 是我第一个上手的模型,也是踩坑最多的模型。这份 Python 源码和数据包把从数据清洗、平稳性检验、定阶到滚动预测的整条链路都串了起来,拿到就能跑,跑完能直接换到自己的序列上,不用再从零拼代码。先说一个反直觉的结论:ARIMA 建模最耗时间的不是调参数,而是平稳性处理和定阶这两个前置环节,整个项目七成的工作量都堆在那里。适合刚开始接触时间序列预测的开发者,也适合想给手头业务数据快速出一版基线预测结果的分析师。
2. 数据清洗与平稳性检验:拿到序列先做的三件事
2.1 平稳性与差分原理:为什么ARIMA强依赖这个前提
ARIMA 全称是自回归积分滑动平均,其中「积分」(Integrated)对应的就是差分这一步。也就是说,模型默认你送入的数据是平稳序列,如果原始数据不平稳,你得先用差分把趋势和季节性成分剥掉,剥完才能进入 p、q 定阶环节。平稳性在工程上的意义更直接:序列的均值和方差不能随时间变化,否则模型学到的统计规律本身就是漂移的,预测出来的数字看着合理,一换时间段就失效。
判断平稳性我一般走两步:先做 ADF 检验看 p 值,再对比差分前后的均值方差曲线。两块都过了才往下走。很多人习惯跳过检验直接差分,或者差分后不做白噪声检查就开训,这两种情况我都翻过车。非平稳序列强行建模,最容易出现的结果是 R² 很高但预测曲线整体滞后,这是典型的伪回归,不是模型效果好。
另外,缺失值处理要放在检验之前。时间序列的缺失不能直接删行,否则索引断裂会让 ACF 计算错位。常见做法是向前填充,业务指标一般用 ffill 就能满足。如果缺失段太长,填充不如插值,这个后面避坑章里再说。
2.2 ADF检验实操:用statsmodels确定差分阶数d
statsmodels 的adfuller是行业里最常用的平稳性检验入口。先看加载数据和检验的代码:
import pandas as pd import numpy as np from statsmodels.tsa.stattools import adfuller df = pd.read_csv('sales_data.csv', parse_dates=['date'], index_col='date') # 缺失值向前填充,适合业务指标类的连续序列 series = df['sales'].astype(float).fillna(method='ffill') result = adfuller(series, autolag='AIC') print(f'ADF statistic: {result[0]:.4f}') print(f'p-value: {result[1]:.4f}') print(f'critical values: {result[4]}')adfuller返回的元组里,第一个是检验统计量,第二个是 p 值,第四个是 1%、5%、10% 显著性水平下的临界值。判断逻辑很简单:p 值小于 0.05 拒绝「存在单位根」的原假设,说明序列平稳;反之不平稳,需要差分。autolag='AIC'表示滞后阶数由 AIC 自动确定,我一般保持默认,手动指定滞后阶数容易引入主观误差。
如果 p 值大于 0.05,进入差分环节:
# 一阶差分后做ADF复检 series_diff1 = series.diff().dropna() result_diff = adfuller(series_diff1, autolag='AIC') print(f'1阶差分后 p-value: {result_diff[1]:.4f}') if result_diff[1] < 0.05: d = 1 else: # 一阶不平稳才考虑二阶差分 series_diff2 = series.diff().diff().dropna() result_diff2 = adfuller(series_diff2, autolag='AIC') print(f'2阶差分后 p-value: {result_diff2[1]:.4f}') d = 2我把差分阶数 d 显式设成变量,后面建模时直接复用。一个容易忽略的细节:diff()之后序列长度会少 1,后面建模时用series[d:]对齐真实样本,避免索引错位导致预测值跟实际日期对不上。业务序列里我基本没见过需要 d=3 的情况,如果二阶差分之后仍然不平稳,先别急着加阶数,回去看一眼数据里是不是有离群点或重复段。
2.3 白噪声排除与对数变换:建模前的最后检查
第三步是排除白噪声。白噪声意味着序列已经没有可被模型利用的自相关结构,差分后的序列如果本身就是白噪声,ARIMA 的 p、q 应该都是 0,硬加参数只会让模型去拟合噪声。这里用 Ljung-Box 检验:
from statsmodels.stats.diagnostic import acorr_ljungbox lb_test = acorr_ljungbox(series_diff1, lags=[10], return_df=True) print(lb_test['lb_pvalue'])p 值大于 0.05 时,残差近似白噪声,序列里没有显著的滞后相关性,这时再做 ARIMA 意义不大;p 值小则说明存在可建模的自相关结构。这个检验在模型训练完以后还要再做一次,两次都通过才算闭环。第一次是确认序列有信号,第二次是确认模型把信号榨干了。
对数变换也要在这个阶段决定。如果序列的波动幅度跟水平值成正比——比如销量从 100 涨到 10000,方差也跟着放大——直接差分得到的是非齐性方差,建模效果不稳定。我一般先画一下原始序列和取对数后的序列,看波动是否被压平。需要时用np.log1p(series)做对数变换,预测完再np.expm1()还原。源码里的示例数据刚好是方差随水平放大的类型,所以数据预处理脚本里对数变换是默认开启的。
3. 定阶与参数选择:ACF/PACF读图、auto_arima与信息准则的配合
3.1 从PACF截尾和ACF拖尾确定候选阶数
差分完成后进入 p 和 q 的定阶。教科书方法是同时看 ACF 和 PACF 两张图:PACF 在滞后 k 阶处截尾,说明 p 可以取 k;ACF 截尾说明 q 取对应阶数。画图代码:
import matplotlib.pyplot as plt from statsmodels.graphics.tsaplots import plot_acf, plot_pacf fig, axes = plt.subplots(1, 2, figsize=(12, 4)) plot_acf(series_diff1, ax=axes[0], lags=20) plot_pacf(series_diff1, ax=axes[1], lags=20, method='ywm') plt.tight_layout() plt.show()读图时有个坑:PACF 在第 1 阶突出、第 2 阶稍微冒头时,很多人直接定 p=2,结果 AIC 比 p=1 高出一截。图只给候选范围,真正做决定的是信息准则。我的读法是先看 PACF 是否有明显截尾,再看 ACF 是否拖尾;如果两张图都是缓慢衰减,说明序列可能还残留趋势,回上一章检查差分阶数。
method='ywm'是 Yule-Walker 方法的改进版,对短序列的 PACF 估计比默认方式稳定。样本量少于 200 时我会显式指定这个参数,否则画出来的 PACF 在低阶处容易抖动,干扰判断。
3.2 用pmdarima自动定阶:参数配置和搜索策略
人工读图定阶对新手不友好,而且业务数据通常没有教科书那么干净的截尾特征。实际项目里我建议先用pmdarima的auto_arima自动扫一遍,拿到候选阶数后再手动验证。这不是偷懒,是把模型选择交给信息准则去优化,人只做方向判断。
from pmdarima import auto_arima stepwise_model = auto_arima( series, # 传入已做平稳化处理的序列 start_p=0, max_p=5, start_q=0, max_q=5, d=1, # 上一章确定的差分阶数 seasonal=False, # 示例数据不带季节周期,先关闭 trace=True, # 打印搜索过程,方便观察收敛方向 error_action='ignore', suppress_warnings=True, stepwise=True, information_criterion='aic' ) print(stepwise_model.order) print(stepwise_model.summary())重点解释几个参数。stepwise=True时不遍历全部 p×q 组合,而是走类似贪心路径:先设一个基准模型,然后尝试上下微调 p、q,保留 AIC 更优的方向,耗时大幅下降。information_criterion='aic'控制选模型依据,换成bic时对参数惩罚更重,结果通常阶数更小。我习惯先用 AIC 选,再用 BIC 复核,两个准则结论不一致时取阶数小的一侧。
seasonal=False这里特意关掉季节性搜索。如果数据里有周度或年度周期,不先做季节分解直接丢给auto_arima,它会把季节阶数混进非季节阶数里,导致 p、q 被拉得很高。源码附带的示例数据不涉及季节性,但换自己的业务数据时如果有明显周期,要同时打开seasonal=True并设置m=,m 是周期长度,月度数据填 12,周数据填 7。
3.3 手动比对AIC/BIC:别把自动结果当成唯一答案
auto_arima给出的阶数不一定适合实际业务解释,两个原因:一是在搜索空间边界上表现不稳定,换一组max_p可能得到不同结果;二是纯靠信息准则选出来的高维参数不一定符合数据生成逻辑。我会把自动结果当起点,再手动比较几组相邻阶数:
from statsmodels.tsa.arima.model import ARIMA import itertools p_range = range(0, 4) q_range = range(0, 4) d = 1 results = [] for p, q in itertools.product(p_range, q_range): try: model = ARIMA(series, order=(p, d, q)) model_fit = model.fit() results.append((p, q, model_fit.aic, model_fit.bic)) except Exception: # 某些阶数组合在数值上不可解,直接跳过 continue results.sort(key=lambda x: x[2]) for p, q, aic, bic in results[:5]: print(f'ARIMA({p},{d},{q}) AIC={aic:.2f} BIC={bic:.2f}')model.fit()输出的aic和bic可以直接用,不用另算。一个工程细节:不同阶数下样本起点会有差异,严格说 AIC 并不可直接比较,但业界普遍接受这种近似,只要阶数差异不大、样本量充足,结论基本可靠。我一般会人为排除 p、q 同时大于 3 的组合,高维模型在业务预测里很难解释,对数据分布变化的鲁棒性也差。
定阶后还要检查系数显著性。model_fit.summary()里每个参数都有 coef 和 P>|z| 两列,如果某个参数的 p 值大于 0.05,说明这阶可能冗余,降一阶重跑。这一步很多人跳过,结果模型 AIC 很低但预测曲线抖动剧烈,多半就是混进了不显著的高阶项。
4. 训练、评估与滚动预测:划分、指标与两种预测方式
4.1 按时间划分训练集与测试集:别把最后一段数据拿去训练
时间序列划分跟普通机器学习完全不同,不能用train_test_split随机切。序列的前后顺序是信息的一部分,随机打乱会把未来信息泄露到训练集里。正确做法是把最后一段连续数据留作测试:
train_ratio = 0.8 split_idx = int(len(series) * train_ratio) series_train = series.iloc[:split_idx] series_test = series.iloc[split_idx:]划分比例没有绝对标准。几百个点以下我留 15%~20% 做测试,超过一千个点可以留 10%。注意测试集要保留原序列的索引和频率,后续计算误差时才能按时间对齐。最稳妥的检查方式是打印series_train.index[-1]和series_test.index[0],确认它们是相邻时间点,中间没有跳空。
4.2 误差指标的正确选择:MAE、RMSE与MAPE的坑
评估预测效果时,我同时算三个指标,因为各有侧重。RMSE 对大误差敏感,适合用来发现极端偏离;MAE 反映平均绝对误差,易于解释;MAPE 不依赖量纲,方便跨序列对比。
from sklearn.metrics import mean_absolute_error, mean_squared_error mae = mean_absolute_error(series_test, pred) rmse = np.sqrt(mean_squared_error(series_test, pred)) mape = np.mean(np.abs((series_test - pred) / series_test)) * 100 print(f'MAE={mae:.2f}, RMSE={rmse:.2f}, MAPE={mape:.2f}%')选模型时我先看 RMSE 在训练期和测试期的差距,如果训练期远小于测试期,大概率是过拟合,此时要降阶数而不是加参数。MAPE 有个先天的坑:测试集里只要有接近 0 的值,百分比会异常放大,这种情况改用对称 MAPE(SMAPE)或直接看 MAE。不要因为某个指标好看就下结论,三个指标放在一起看趋势才靠谱。
4.3 forecast与滚动一步预测:两种落地方式
ARIMA 的预测有两种常见用法。一次性多步预测适合给出未来 n 个点的整体走势;滚动一步预测适合模拟业务里「每天能拿到新观测值」的场景。两者预测结果往往差不少,先看代码:
# 方式一:一次性多步预测 model = ARIMA(series_train, order=order) model_fit = model.fit() pred = model_fit.forecast(steps=len(series_test))# 方式二:滚动一步预测 history = list(series_train.values) pred_rolling = [] for t in range(len(series_test)): model = ARIMA(history, order=order) model_fit = model.fit() yhat = model_fit.forecast().iloc[0] pred_rolling.append(yhat) history.append(series_test.iloc[t])滚动预测每个时间步都要重新估计参数,训练开销大得多,但数据有漂移时准确率通常比一次性外推高。我交付时会给两个版本:业务方如果做月末下月预算,用多步外推;每日收盘后预测次日数值,用滚动。另外注意predict(start, end)和forecast(steps=n)的区别,前者可以回看训练集内的拟合值,真正面向未来预测必须用forecast,用错的话预测结果里会混入已观测值的拟合残差。
5. ARIMA建模避坑记录:五个高频问题与排查思路
5.1 预测曲线变成水平直线,完全看不出跟随性
现象:预测值是一条水平线,后续所有点都落在同一个数值附近,真实波动完全没体现出来。
原因:差分阶数 d 设大了,把趋势和波动全部磨平,模型学到的只剩均值;另一种可能是过差分后 ACF 第 1 阶出现显著负相关,模型被强制往均值回归。解决:减小 d,重新做 ADF 检验。差分后如果 ACF 第 1 阶自相关系数接近 -0.5,基本可以断定是过差分。这时回退到上一阶差分,并检查原序列是否有确定性趋势被过度剥离。
5.2 整体误差不高,但误差集中在某一段时间内
现象:MAE、RMSE 都好看,但把预测和真实值按时间画出来,误差明显堆在某个连续区间里。
原因:序列在该时段出现结构性断点,比如价格跳变、活动冲击,模型在训练期完全没见过这种模式。ARIMA 本身没有外生变量支持,遇到断点只能被动承接。解决:把异常区间标记出来,分段建模;如果周期固定,加季节性参数;如果断点来自已知事件,把事件作为外生回归变量加入模型,虽然 statsmodels 的 ARIMA 不直接支持外生变量,但可以换用 ARIMAX 的SARIMAX接口。
5.3 随机划分训练集导致预测结果虚高
现象:用train_test_split切完数据后,预测误差小得离谱,换自己的数据就崩。
原因:随机划分把未来数据混进了训练集,模型提前看到了测试窗口的信息,属于典型的数据泄露。解决:严格按时间顺序划分,并且要让测试集的起点严格等于训练集终点的下一时刻。我每次写完划分代码都会打印两端索引做断言,防止索引错位。
5.4 训练报错 Singular matrix 或模型不收敛
现象:model.fit()抛出LinAlgError: Singular matrix,或者反复迭代但参数不更新。
原因:序列里有大段重复值或常量片段,协方差矩阵不可逆;也可能是索引有空洞,数据没对齐。这问题在默认参数下很玄学,通常不是算法问题而是数据问题。解决:先series.value_counts()检查重复段,对常量片段做平滑或插值;再把索引重置为连续日期,缺失日期补 NaN 后做填充。把这些处理完,Singular matrix 基本不会再出现。
5.5 auto_arima 搜索耗时极长,跑十几分钟不出结果
现象:auto_arima卡死在搜索阶段,日志一直在跑但迟迟收敛。
原因:max_p、max_q设置过大,同时开了seasonal=True,搜索空间成倍膨胀。解决:用stepwise=True限制为逐步搜索,把max_p、max_q控制在 5 以内;季节性周期 m 先通过周期图或业务常识确定,不盲目开季节搜索。如果数据量超过五千个点,先抽一段做定阶,确定阶数后再全量训练,比直接跑全量快得多。
6. 让模型跟上数据更新:walk-forward验证与自动重拟合
walk-forward 验证的核心思想是:把数据集按时间切成多段,每段依次充当验证集,模型每次都只用验证集之前的数据训练。这本质上模拟了模型上线后的真实运行方式,跟单次划分相比,它给出的是多个时间段误差的分布,而不是一次预测的运气。
n_splits = 5 fold_size = len(series_test) // n_splits mae_scores = [] history_all = list(series_train.values) for i in range(n_splits): start_idx = len(history_all) end_idx = min(start_idx + fold_size, len(series)) val_y = series.iloc[start_idx:end_idx].values model = ARIMA(history_all, order=order) model_fit = model.fit() pred = model_fit.forecast(steps=len(val_y)) mae_scores.append(mean_absolute_error(val_y, pred)) # 关键步骤:本次验证段并入历史,下一轮重拟合 history_all = history_all + list(val_y) print(f'walk-forward MAE: {np.mean(mae_scores):.2f} ± {np.std(mae_scores):.2f}')每次验证结束,历史窗口吞入新数据,下一轮模型是重拟合过的。这个流程特别适合定期重训的生产环境。我上线后一般直接把它改造成每日定时任务:晚上拿当天真实数据更新模型,第二天预测时用的就是最新参数,彻底避开「模型三个月没更新、预测值越飘越远」的问题。
这套方法最早用在一个销量预测报表上。最初模型训练一次就上线,跑了三周误差开始变大;改成每日 walk-forward 重拟合之后,MAE 降了大约三分之一。从那以后我每次交付时间序列项目,都会强制把 walk-forward 验证写进交付清单,而不是只给一份训练完的模型文件。希望帮到你。
本文还有配套的精品资源,点击获取