☰
Python ARIMA时间序列销量预测:从建模调参到滚动重训实战
2026/9/28 12:05:39 网站建设 项目流程

简介:这份资源是面向Python数据分析初学者、统计学课程学习者及毕业设计/期末大作业需求者的一套ARIMA时间序列销量预测完整方案,重点解决如何用statsmodels完成数据平稳化、模型定阶、参数估计与模型检验等建模流程。压缩包共13个文件,约254KB,包含4个py脚本、5张png图表、xls与xlsx数据表、md说明及txt依赖清单,脚本负责建模与预测,图表展示时序、差分及自相关偏相关情况,数据表存放原始销量与预测对比结果。资源采用每月上中下旬三次预测策略,将月上旬和中旬实际销量作为先验知识提升准确率,并附有上线效果与测试情况图,便于读者理解从建模到评估的完整链路。目前已有2053人学习下载,适合需要快速搭建可运行预测模型、对照代码与图表复现实验的读者参考。

1. 拿到一份销量数据,为什么我第一反应是跑 ARIMA 而不是上 LSTM

电商运营把一份三年多的日销明细甩过来,问下个月能卖多少。数据是典型的单变量时间序列:一天一个销量值,中间还夹着大促的尖峰和几段缺货的零值。这种场景我一般不会一上来就搭 LSTM,原因很实在——样本量不够喂深度模型,特征工程成本高,而且业务方要的是「下周备多少货」这种能解释、能复现的结论,不是黑匣子。ARIMA 时间序列预测模型恰好卡在这个需求点上:它把序列拆成自回归、差分、移动平均三块,参数含义清楚,残差能检验,预测区间能画出来,用 Python 的 statsmodels 几十行就能跑通。这份「python基于ARIMA时间序列的销量预测模型全部数据.zip」本质上就是一套可复现的落地模板,适合做 python 数据分析与可视化、想入门时间序列预测的从业者,也适合已经会 python 基础语法、但没系统做过销量预测的人照着走一遍。下面我按自己实际做项目的顺序,把选型、建模、调参、避坑全讲清楚。

2. ARIMA 的三个参数到底在拟合什么:从销量序列到 p、d、q 的映射

2.1 平稳性、差分与 d 的确定

ARIMA 全称自回归积分移动平均,三个字母对应三个参数:AR 是自回归项 p,I 是差分阶数 d,MA 是移动平均项 q。销量序列几乎不可能天生平稳——有趋势、有季节性、有促销脉冲。平稳的意思是均值和方差不随时间漂移,而 ARIMA 的数学前提是序列平稳,所以 d 的作用就是把非平稳序列差分到平稳。判断 d 最直接的办法是看差分前后序列的均值和方差是否稳定,配合 ADF 检验。我一般先画原始序列,再画一阶差分,肉眼加检验双重确认。很多新手直接设 d=1 就往下跑,遇到强季节性数据(比如每周固定周末高峰)就会欠拟合,这时候要考虑季节性差分,也就是 SARIMA。

import pandas as pd import numpy as np import matplotlib.pyplot as plt from statsmodels.tsa.stattools import adfuller # 读取销量数据,假设列名为 date 和 sales df = pd.read_csv("sales.csv", parse_dates=["date"], index_col="date") series = df["sales"].asfreq("D") # 补齐日频,缺失日先留 NaN # 原始序列 ADF 检验 def adf_report(s, name): result = adfuller(s.dropna()) print(f"{name} ADF统计量={result[0]:.4f} p值={result[1]:.4f}") for k, v in result[4].items(): print(f" 临界值 {k}: {v:.4f}") adf_report(series, "原始序列") adf_report(series.diff(1), "一阶差分") adf_report(series.diff(1).diff(1), "二阶差分")

这段代码先做 ADF 检验,p 值小于 0.05 通常认为平稳。逻辑上,原始序列 p 值大概率大于 0.05,一阶差分后如果 p 值掉到 0.05 以下,d 就取 1;如果一阶差分还不平稳,再试二阶。参数说明:asfreq("D")把不规则日期对齐成日频,缺的日期补 NaN,这一步很关键,否则差分步长会错乱。dropna()是因为 ADF 不接受缺失值。注意 ADF 检验对样本量敏感,样本少于 50 个点时结论不稳,这时候更依赖肉眼判断差分图。

2.2 用 ACF 和 PACF 定 p 和 q

d 定下来后,p 和 q 靠自相关函数 ACF 和偏自相关函数 PACF 的截尾、拖尾特征来定。经验规则是:ACF 拖尾、PACF 在 p 阶后截尾,说明是 AR(p);ACF 在 q 阶后截尾、PACF 拖尾,说明是 MA(q);两者都拖尾就是 ARMA。实际销量数据往往两个都拖尾,这时候不能死磕图形,要结合 AIC/BIC 网格搜索。我一般先用图形圈定一个大致范围,比如 p 在 0 到 3、q 在 0 到 3,然后遍历组合看信息准则。

from statsmodels.graphics.tsaplots import plot_acf, plot_pacf diff_series = series.diff(1).dropna() fig, axes = plt.subplots(2, 1, figsize=(10, 6)) plot_acf(diff_series, lags=30, ax=axes[0]) plot_pacf(diff_series, lags=30, ax=axes[1], method="ywm") plt.tight_layout() plt.savefig("acf_pacf.png", dpi=120)

lags=30表示看 30 期内的相关性,日销数据一般看 30 天足够。method="ywm"是偏自相关的估计方法,样本量中等时比默认方法稳。看图时重点找第一个冲出置信带(图中阴影区)之后又回落的阶数。如果 ACF 在 1 阶后迅速落到带内,q 可能取 1;PACF 在 2 阶后截尾,p 可能取 2。但图形只是初筛,真正定参还得靠下面的网格搜索。

2.3 网格搜索 + AIC/BIC 选最优阶数

AIC 和 BIC 都是「拟合优度减复杂度惩罚」的指标,值越小越好。BIC 对参数个数惩罚更重,样本大时倾向更简洁的模型。我一般两个都看,如果结论一致就直接用,不一致优先 BIC,因为销量预测更怕过拟合。

import warnings from statsmodels.tsa.arima.model import ARIMA warnings.filterwarnings("ignore") best_aic, best_order = np.inf, None results_table = [] for p in range(0, 4): for d in range(0, 3): for q in range(0, 4): try: model = ARIMA(series, order=(p, d, q)) res = model.fit() results_table.append((p, d, q, res.aic, res.bic)) if res.aic < best_aic: best_aic, best_order = res.aic, (p, d, q) except Exception as e: continue results_df = pd.DataFrame(results_table, columns=["p", "d", "q", "AIC", "BIC"]) print(results_df.sort_values("AIC").head(10)) print("最优阶数:", best_order)

这段遍历 p 从 0 到 3、d 从 0 到 2、q 从 0 到 3 共 48 种组合,每种拟合后记录 AIC 和 BIC。warnings.filterwarnings("ignore")是因为部分组合会报收敛警告,不影响筛选。try/except兜住不收敛的组合。参数说明:order=(p,d,q)就是 ARIMA 的核心配置。跑完看 AIC 最小的前几行,如果最优组合落在搜索边界上(比如 p=3),说明范围开小了,要往外扩。这一步是整套流程里最耗时的,48 个组合在普通笔记本上大概几十秒到几分钟,取决于序列长度。

3. 从 CSV 到预测曲线:一套能直接抄的建模流程

3.1 数据清洗:缺失值、异常值和零销量的处理

真实销量数据几乎没有干净的。常见问题有三类:缺货导致的零值、大促导致的尖峰、录入错误导致的极端值。零值要区分是「真没卖出去」还是「缺货没货卖」,后者应该当缺失值处理而不是当真实销量,否则模型会学到错误的低谷。异常值我一般用 IQR 或者滚动中位数加 MAD 来识别,超过阈值就替换成插值。

# 把缺货期的零值标记为缺失 series_clean = series.copy() series_clean[series_clean == 0] = np.nan # 线性插值补缺失 series_clean = series_clean.interpolate(method="linear") # 用滚动中位数识别异常值 window = 7 rolling_med = series_clean.rolling(window, center=True).median() rolling_mad = (series_clean - rolling_med).abs().rolling(window, center=True).median() threshold = 3 * 1.4826 * rolling_mad # 1.4826 是 MAD 到标准差的换算系数 outlier_mask = (series_clean - rolling_med).abs() > threshold series_clean[outlier_mask] = rolling_med[outlier_mask] print("修正异常值数量:", outlier_mask.sum())

逻辑是先处理零值再处理异常值,顺序不能反。interpolate(method="linear")做线性插值,销量序列短期波动用线性插值够用,别用多项式插值,容易过冲。MAD 比标准差抗异常值,1.4826这个系数让 MAD 在正态分布下等价于标准差。center=True保证滚动窗口居中,避免引入未来信息。注意:如果异常值占比超过 5%,说明数据质量本身有问题,要先回去查采集环节,别硬修。

3.2 训练集/测试集切分与滚动预测

时间序列不能随机切分,必须按时间顺序切。我一般留最后 20% 到 30% 做测试集,或者留最后 30 天。更严谨的做法是滚动预测(walk-forward),每次用历史数据训练、预测下一步、再把真实值纳入训练集,模拟真实上线场景。

train_size = int(len(series_clean) * 0.8) train, test = series_clean[:train_size], series_clean[train_size:] # 用前面选出的最优阶数建模 final_model = ARIMA(train, order=best_order) final_res = final_model.fit() print(final_res.summary()) # 静态预测测试集 forecast = final_res.get_forecast(steps=len(test)) pred_mean = forecast.predicted_mean pred_ci = forecast.conf_int() # 评估 from sklearn.metrics import mean_absolute_error, mean_squared_error mae = mean_absolute_error(test, pred_mean) rmse = np.sqrt(mean_squared_error(test, pred_mean)) mape = np.mean(np.abs((test - pred_mean) / test)) * 100 print(f"MAE={mae:.2f} RMSE={rmse:.2f} MAPE={mape:.2f}%")

get_forecast(steps=len(test))一次性预测整个测试集长度,conf_int()给出置信区间。评估指标里 MAPE 最直观,但销量有零值时 MAPE 会爆炸,这时候用 MAE 或 RMSE。参数说明:order=best_order就是上一章网格搜索的结果。如果测试集 MAPE 超过 20%,先别急着换模型,回去检查数据清洗和阶数选择,八成是这两步出了问题。

3.3 残差检验:模型到底有没有把信息榨干

残差应该是白噪声——均值接近零、方差恒定、无自相关。如果残差还有结构,说明模型没拟合充分。我一般看残差时序图、残差 ACF 和 Ljung-Box 检验。

from statsmodels.stats.diagnostic import acorr_ljungbox resid = final_res.resid lb_test = acorr_ljungbox(resid, lags=[10, 20], return_df=True) print(lb_test) fig, axes = plt.subplots(2, 1, figsize=(10, 6)) axes[0].plot(resid) axes[0].set_title("残差时序") plot_acf(resid, lags=30, ax=axes[1]) plt.tight_layout() plt.savefig("residual_check.png", dpi=120)

Ljung-Box 的 p 值大于 0.05 说明残差无显著自相关,模型合格。lags=[10,20]分别检验 10 阶和 20 阶。如果 p 值小于 0.05,说明还有信息没提取,要么升阶,要么考虑季节性项。残差图里如果看到明显的周期性波动,基本可以确定需要 SARIMA。这一步是很多人跳过的,但它是判断模型能不能上线的关键依据。

4. 销量预测里最容易翻车的五个地方

4.1 现象:预测曲线整体平移,比真实值高一大截

原因:训练集里包含了大促尖峰,模型把尖峰当成常态学到了。解决:对大促日期做标记,建模前把大促值替换成同期正常水平,或者用干预分析(intervention analysis)单独建模。我一般会在数据里加一列is_promo,预测时把大促效应单独叠加回去。

4.2 现象:MAPE 很低但业务方说预测没用

原因:MAPE 在低销量时段权重被放大,模型为了压低整体 MAPE 牺牲了高销量时段的精度。解决:分时段评估,或者改用加权 MAPE、RMSE。业务上更关心的是高峰期的备货准确度,评估指标要跟业务目标对齐,别只盯一个数。

4.3 现象:模型在测试集上表现好,上线后迅速劣化

原因:数据分布漂移,比如换了促销策略、上了新品、疫情改变了消费习惯。解决:建立滚动重训机制,每周或每月用最新数据重新拟合,同时监控残差均值是否偏移。我一般会设一个残差监控阈值,连续三天残差同号就触发重训。

4.4 现象:差分后序列方差反而变大

原因:对已经平稳的序列做了过度差分,引入了额外噪声。解决:回到 ADF 检验,如果原始序列 p 值已经小于 0.05,d 就取 0。差分不是越多越好,每多差一次就多丢一个数据点,还会放大噪声。

4.5 现象:网格搜索跑了几十分钟还没出结果

原因:搜索范围开太大,或者序列太长导致每次拟合都很慢。解决:先用 ACF/PACF 把范围缩到 p、q 各 0 到 3,d 只试 0 到 2;序列超过 1000 个点时先降采样到周频或做滚动窗口。另外ARIMA换成SARIMAX时记得关掉不必要的输出。

5. 把 ARIMA 用得更稳:季节性拆分与滚动重训的实操技巧

单靠基础 ARIMA 处理带周季节性的日销数据,往往差一口气。我的做法是先做 STL 分解,把趋势、季节、残差拆开,对趋势项跑 ARIMA,季节项单独用周期均值或 SARIMA 建模,最后叠加。这样比直接上 SARIMA 更好调,也更容易跟业务解释「哪部分是趋势、哪部分是周末效应」。

from statsmodels.tsa.seasonal import STL stl = STL(series_clean, period=7, robust=True) stl_res = stl.fit() trend = stl_res.trend seasonal = stl_res.seasonal resid = stl_res.resid # 对趋势项建模 trend_model = ARIMA(trend, order=best_order).fit() trend_forecast = trend_model.get_forecast(steps=30).predicted_mean # 季节项用最近 4 周同星期均值外推 seasonal_forecast = np.tile(seasonal[-7:].values, 5)[:30] final_forecast = trend_forecast.values + seasonal_forecast print("未来30天预测:", final_forecast[:7])

period=7对应周季节性,robust=True让 STL 对异常值更稳。趋势项用 ARIMA,季节项用最近一周的模式重复外推,简单但实用。参数说明:np.tile(seasonal[-7:].values, 5)[:30]把最近 7 天的季节因子重复 5 次取前 30 天,适合季节性稳定的场景。如果季节模式在变,改用 SARIMA 的 seasonal_order 更合适。

滚动重训我一般写成定时任务,每周一凌晨用最新数据重跑一遍网格搜索,把新阶数和旧阶数对比,如果连续两周阶数变化超过 1,就人工介入看看是不是业务变了。重训脚本里我会保留每次的 AIC、BIC 和 MAPE,存成一张监控表,时间长了能看出模型衰减的规律。

import joblib from datetime import datetime def retrain_and_save(series, order, path="model.pkl"): model = ARIMA(series, order=order).fit() joblib.dump({"model": model, "order": order, "trained_at": datetime.now().isoformat()}, path) return model.aic # 每周调用一次 new_aic = retrain_and_save(series_clean, best_order) print("重训完成,AIC:", new_aic)

joblib.dump把模型和训练时间一起存下来,方便追溯。参数说明:order用当次网格搜索的最优值,不要写死。这套流程跑顺之后,ARIMA 的维护成本比深度学习模型低一个量级,特别适合中小团队。我自己踩过最大的坑是早期迷信「一次建模管半年」,结果大促一过模型全乱,后来老老实实做周级重训才稳住。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询