Python ARIMA时间序列销量预测:从建模调参到滚动重训实战
发布时间:2026/9/28 12:05:41 锦皓数字建站

简介这份资源是面向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 是自回归项 pI 是差分阶数 dMA 是移动平均项 q。销量序列几乎不可能天生平稳——有趋势、有季节性、有促销脉冲。平稳的意思是均值和方差不随时间漂移而 ARIMA 的数学前提是序列平稳所以 d 的作用就是把非平稳序列差分到平稳。判断 d 最直接的办法是看差分前后序列的均值和方差是否稳定配合 ADF 检验。我一般先画原始序列再画一阶差分肉眼加检验双重确认。很多新手直接设 d1 就往下跑遇到强季节性数据比如每周固定周末高峰就会欠拟合这时候要考虑季节性差分也就是 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_coldate) 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 和 qd 定下来后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, lags30, axaxes[0]) plot_pacf(diff_series, lags30, axaxes[1], methodywm) plt.tight_layout() plt.savefig(acf_pacf.png, dpi120)lags30表示看 30 期内的相关性日销数据一般看 30 天足够。methodywm是偏自相关的估计方法样本量中等时比默认方法稳。看图时重点找第一个冲出置信带图中阴影区之后又回落的阶数。如果 ACF 在 1 阶后迅速落到带内q 可能取 1PACF 在 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 最小的前几行如果最优组合落在搜索边界上比如 p3说明范围开小了要往外扩。这一步是整套流程里最耗时的48 个组合在普通笔记本上大概几十秒到几分钟取决于序列长度。3. 从 CSV 到预测曲线一套能直接抄的建模流程3.1 数据清洗缺失值、异常值和零销量的处理真实销量数据几乎没有干净的。常见问题有三类缺货导致的零值、大促导致的尖峰、录入错误导致的极端值。零值要区分是「真没卖出去」还是「缺货没货卖」后者应该当缺失值处理而不是当真实销量否则模型会学到错误的低谷。异常值我一般用 IQR 或者滚动中位数加 MAD 来识别超过阈值就替换成插值。# 把缺货期的零值标记为缺失 series_clean series.copy() series_clean[series_clean 0] np.nan # 线性插值补缺失 series_clean series_clean.interpolate(methodlinear) # 用滚动中位数识别异常值 window 7 rolling_med series_clean.rolling(window, centerTrue).median() rolling_mad (series_clean - rolling_med).abs().rolling(window, centerTrue).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(methodlinear)做线性插值销量序列短期波动用线性插值够用别用多项式插值容易过冲。MAD 比标准差抗异常值1.4826这个系数让 MAD 在正态分布下等价于标准差。centerTrue保证滚动窗口居中避免引入未来信息。注意如果异常值占比超过 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, orderbest_order) final_res final_model.fit() print(final_res.summary()) # 静态预测测试集 forecast final_res.get_forecast(stepslen(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(fMAE{mae:.2f} RMSE{rmse:.2f} MAPE{mape:.2f}%)get_forecast(stepslen(test))一次性预测整个测试集长度conf_int()给出置信区间。评估指标里 MAPE 最直观但销量有零值时 MAPE 会爆炸这时候用 MAE 或 RMSE。参数说明orderbest_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_dfTrue) print(lb_test) fig, axes plt.subplots(2, 1, figsize(10, 6)) axes[0].plot(resid) axes[0].set_title(残差时序) plot_acf(resid, lags30, axaxes[1]) plt.tight_layout() plt.savefig(residual_check.png, dpi120)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.05d 就取 0。差分不是越多越好每多差一次就多丢一个数据点还会放大噪声。4.5 现象网格搜索跑了几十分钟还没出结果原因搜索范围开太大或者序列太长导致每次拟合都很慢。解决先用 ACF/PACF 把范围缩到 p、q 各 0 到 3d 只试 0 到 2序列超过 1000 个点时先降采样到周频或做滚动窗口。另外ARIMA换成SARIMAX时记得关掉不必要的输出。5. 把 ARIMA 用得更稳季节性拆分与滚动重训的实操技巧单靠基础 ARIMA 处理带周季节性的日销数据往往差一口气。我的做法是先做 STL 分解把趋势、季节、残差拆开对趋势项跑 ARIMA季节项单独用周期均值或 SARIMA 建模最后叠加。这样比直接上 SARIMA 更好调也更容易跟业务解释「哪部分是趋势、哪部分是周末效应」。from statsmodels.tsa.seasonal import STL stl STL(series_clean, period7, robustTrue) stl_res stl.fit() trend stl_res.trend seasonal stl_res.seasonal resid stl_res.resid # 对趋势项建模 trend_model ARIMA(trend, orderbest_order).fit() trend_forecast trend_model.get_forecast(steps30).predicted_mean # 季节项用最近 4 周同星期均值外推 seasonal_forecast np.tile(seasonal[-7:].values, 5)[:30] final_forecast trend_forecast.values seasonal_forecast print(未来30天预测:, final_forecast[:7])period7对应周季节性robustTrue让 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, pathmodel.pkl): model ARIMA(series, orderorder).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 的维护成本比深度学习模型低一个量级特别适合中小团队。我自己踩过最大的坑是早期迷信「一次建模管半年」结果大促一过模型全乱后来老老实实做周级重训才稳住。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。