简介:针对Python时间序列预测的完整可运行代码包,适合数据分析与机器学习学习者,也适用于金融、气象、销售等需要趋势预测的场景。包内以181个可执行脚本为主,配合32个示例数据文件和1份说明文档,覆盖数据读取、清洗、缺失值处理、差分平稳化、模型构建、训练、评估与可视化等环节,压缩包共214个文件,整体仅237KB,便携易用。通过太阳黑子、气温、每日出生人数等真实样例数据,读者可动手验证经典统计模型、季节性模型与机器学习方法的差异,学习如何选取评价指标并优化预测结果。已有7257人学习该资源,既能作为课程配套练习,也能帮助入门者建立完整的时间序列建模思路。
1. 这份时间序列预测 Python 代码包:三个数据集带你走完完整建模流程
如果你正在学时间序列预测,大概率卡在同一个位置:理论看懂了 ARIMA 的公式,但真拿到一份 CSV 数据,不知道第一步该做什么。这份代码包恰好解决这个问题——它内置了三个经典数据集(每日最低气温、太阳黑子数、每日女性出生人数),覆盖了无季节性、强季节性、纯随机三种典型形态,配套代码从数据读取一路写到模型评估。换句话说,它不是一个泛泛的教程,而是一套能直接跑通的完整建模流程。适合刚入门 Python 时间序列分析的读者,也适合想快速对照标准实现方式的老手。我会在下面把每个文件怎么用、参数怎么调、坑在哪里,逐一拆开讲透。
2. 三个数据集的差异决定模型方向:先搞清楚你在预测什么
2.1 从文件名读懂数据形态:气温、太阳黑子与出生人数
压缩包里三个 CSV 文件不是随便放的,它们分别对应时间序列预测里的三种典型场景。先看daily-minimum-temperatures.csv,这是澳大利亚墨尔本 1981 到 1990 年的每日最低气温,天然带明显的年度季节性——冬天冷夏天热,周期固定,适合用 SARIMA 或者 Prophet 这类能捕捉季节性的模型。再看sunspots.csv,记录的是每月太阳黑子数量,这个数据的特点是周期大约 11 年,但周期长度和振幅都会波动,不是严格的固定季节性,更适合用来练习 ARIMA 或者状态空间模型这类能处理非平稳序列的方法。最后是daily-total-female-births.csv,美国每日女性出生人数,这个序列没有明显的趋势和季节性,波动接近随机,用它来理解"预测的极限"特别合适——有些序列本身可预测性就很低。
import pandas as pd # 分别读取三个数据集,注意日期列的解析 temps = pd.read_csv('daily-minimum-temperatures.csv', parse_dates=['Date'], index_col='Date') sunspots = pd.read_csv('sunspots.csv', parse_dates=['Month'], index_col='Month') births = pd.read_csv('daily-total-female-births.csv', parse_dates=['Date'], index_col='Date') print(temps.head()) print(temps.index)这里parse_dates参数会把日期字符串转成DatetimeIndex,这是后面所有时间序列操作的前提。如果没有正确解析日期索引,pandas 的resample、shift、rolling这些方法都没法用。我一般会在读取后立刻打印index确认类型,这一步能省掉后面排查索引问题的半天时间。三个数据集的索引频率也不同——气温和出生人数是日频,太阳黑子是月频,这个频率信息在建模时要传给模型,后面我会细说。
2.2 先用可视化建立直觉:趋势、季节性与噪声
在跑任何模型之前,先把数据画出来看。这一步不是走形式,而是通过肉眼判断序列里有没有趋势、季节性,以及波动幅度是否随时间变化。matplotlib 是基础工具,但直接画长序列的折线图容易糊成一团,我习惯同时画原始序列和滚动均值,滚动均值能平滑短期波动,突出长期趋势。
import matplotlib.pyplot as plt fig, axes = plt.subplots(3, 1, figsize=(12, 10)) # 气温数据:能看到明显的年季节性 temps['1981':'1982'].plot(ax=axes[0], title='Daily Min Temperatures') axes[0].set_ylabel('Celsius') # 太阳黑子:周期长且不规则 sunspots.plot(ax=axes[1], title='Monthly Sunspots') axes[1].set_ylabel('Count') # 出生人数:无明显趋势,接近随机波动 births.plot(ax=axes[2], title='Daily Female Births') axes[2].set_ylabel('Count') plt.tight_layout() plt.show()画完图之后要做三件事:第一,判断趋势——长期来看序列是上升、下降还是水平;第二,判断季节性——是否在固定时间间隔内重复出现相似形态;第三,判断方差是否稳定——波动幅度是否随时间变化。这三个判断直接决定后面选哪个模型。气温数据有明显的年度季节性,太阳黑子有长周期但不够规律,出生人数则几乎没有可预测的结构。很多新手跳过这一步直接套 ARIMA,结果模型拟合了一堆噪声,预测出来全是直线,问题就出在没有先用可视化确认数据特征。
2.3 数据预处理里的两个关键动作:缺失值填充与差分
真实项目里的数据很少是干净的,这份代码里的数据虽然相对规整,但预处理步骤不能省。最常遇到的问题是缺失值,处理方式取决于缺失的比例和位置。如果缺失值不多,前向填充ffill或者线性插值interpolate都够用;如果缺失较多,可能要先用resample统一时间频率再插值。另一个关键操作是差分——ARIMA 模型要求输入序列平稳,而差分是消除趋势最直接的手段。
# 检查缺失值 print(temps.isnull().sum()) # 线性插值填充缺失值 temps_filled = temps.interpolate(method='linear', limit_direction='both') # 一阶差分消除趋势,观察是否变得更平稳 temps_diff = temps_filled.diff().dropna() fig, axes = plt.subplots(2, 1, figsize=(12, 8)) temps_filled.plot(ax=axes[0], title='Original') temps_diff.plot(ax=axes[1], title='First Differenced') plt.show()差分操作的意义在于把非平稳序列转换成平稳序列。以气温数据为例,原始序列呈现明显的年度周期性,均值在不同季节差别很大,这不符合平稳性要求。做了一阶差分之后,每个值变成"当天与前一天的温度差",周期性被大幅削弱。差分阶数不是越多越好——过度差分会让序列丢失太多信息,噪声比例反而上升。判断差分效果的标准做法是观察差分后的序列是否在零附近随机波动,也可以用 ADF 检验来量化判断,这个我在下一章展开。
3. 平稳性是 ARIMA 的入场券:ADF 检验与定阶实战
3.1 ADF 检验:别用眼睛判断平稳性
上一章提到差分后"看起来"平稳了,但肉眼判断在论文和技术评审时站不住脚,需要用统计检验来确认。ADF(Augmented Dickey-Fuller)检验是应用最广的平稳性检验方法,它的原假设是序列存在单位根,即非平稳。当 p 值小于显著性水平(通常取 0.05)时,拒绝原假设,认为序列平稳。
from statsmodels.tsa.stattools import adfuller def test_stationarity(series, name): result = adfuller(series.dropna()) print(f'{name} ADF stat: {result[0]:.4f}') print(f'{name} p-value: {result[1]:.4f}') if result[1] < 0.05: print(f'{name}: 平稳,拒绝单位根假设') else: print(f'{name}: 非平稳,需要进一步差分') # 分别检验原始序列和一阶差分序列 test_stationarity(temps['1981'], 'temps original') test_stationarity(temps['1981'].diff().dropna(), 'temps diff1')注意我在这里用了temps['1981']而不是全量数据。时间序列的平稳性是随时间变化的,用 10 年数据测试出来的结果可能和最近一年不同。实际操作中我会多测几个窗口——比如最近一年、最近三年和全量,综合判断。如果全量数据平稳但最近一年不平稳,说明序列的结构近期发生了变化,这时候直接建模风险很大,可能需要用更近的数据训练。ADF 检验输出的第一个值是检验统计量,第二个是 p 值,还有几个辅助参数这里没打印——滞后阶数、样本量等——一般不需要自己调整,statsmodels 默认的autolag='AIC'会自动选最优滞后阶数,这个设计比较合理,多数情况直接用就行。
3.2 ACF 与 PACF 定阶:理解自相关图的信息
确认序列平稳之后,下一步是确定 ARIMA 模型的阶数——p(自回归阶数)、d(差分阶数)、q(移动平均阶数)。d 已经在差分时确定了,p 和 q 看 ACF(自相关函数)和 PACF(偏自相关函数)图。这个环节是新手最容易懵的地方,很多教程画了两张图然后说"看图可知",但没解释怎么看。
from statsmodels.graphics.tsaplots import plot_acf, plot_pacf # 以气温数据一阶差分序列为例 temps_diff = temps.diff().dropna() fig, axes = plt.subplots(2, 1, figsize=(12, 8)) plot_acf(temps_diff, ax=axes[0], lags=40) plot_pacf(temps_diff, ax=axes[1], lags=40) plt.show()判定逻辑其实不复杂:看 ACF 图,如果自相关在某个滞后阶数之后突然截断(掉到置信区间内),这个截断的阶数就是 q 的候选值;看 PACF 图,如果偏自相关在某个滞后阶数之后截断,这个阶数就是 p 的候选值。更常见的情况是两者都缓慢衰减(拖尾),这时候 p 和 q 都需要取较小的值,再结合 AIC/BIC 去搜索。气温数据做一阶差分后仍有年度季节性残余,ACF 在滞后 365 附近会出现明显峰值,这说明仅靠非季节性 ARIMA 不够,需要引入 SARIMA。这是后面第四章的内容。
3.3 网格搜索定阶:让 AIC 帮你做选择
手动看图定阶,尤其是 p 和 q 范围较大时,效率太低且容易漏掉更好的组合。我习惯写一个小网格搜索,遍历候选的 p、d、q 组合,用 AIC(赤池信息准则)选最优。AIC 的哲学是在拟合优度和模型复杂度之间取平衡——AIC 越低越好,但差值小于 2 的模型之间没有显著差异,这时选参数更少的那个。
import itertools import warnings from statsmodels.tsa.arima.model import ARIMA warnings.filterwarnings('ignore') best_aic = float('inf') best_order = None # 用全量气温数据做差分后搜索,d 固定为 1 for p in range(0, 3): for q in range(0, 3): order = (p, 1, q) try: model = ARIMA(temps, order=order) result = model.fit() if result.aic < best_aic: best_aic = result.aic best_order = order except Exception: continue print(f'best order: {best_order}, AIC: {best_aic}')这段代码里有个细节值得注意:搜索范围只取了 0 到 2,因为气温数据本身结构并不复杂,更高阶的参数容易过拟合。如果你处理的数据周期性更强,可以适当扩大范围,但搜索空间会指数增长——3 个参数各取 0 到 5 就有 216 种组合,每种组合都要拟合一次模型,数据量大时要等很久。实际工程中我会分两步:先用小步长粗搜定位一个大致区域,再在最优值附近细搜,这样能省一半时间。另外,ARIMA拟合时会输出大量日志,加上warnings.filterwarnings('ignore')可以屏蔽收敛警告,但注意——如果结果里出现大量ConvergenceError,说明数据有问题或者模型阶数不合适,不能一股脑忽略掉。
4. 季节性数据用 SARIMA 与 Prophet:两个方案的选型对比
4.1 SARIMA 四个季节性参数的组合逻辑
气温数据有明显的年度季节性,直接用 ARIMA 拟合效果不会好,因为误差项里还残留着季节周期信息。SARIMA 在 ARIMA 的基础上增加了四个季节性参数:P(季节性自回归阶数)、D(季节性差分阶数)、Q(季节性移动平均阶数)和 s(季节周期长度)。对日频气温数据,s=365;对月度数据,s=12。
from statsmodels.tsa.statespace.sarimax import SARIMAX # 用 SARIMA(1,1,1)(1,1,1,12) 拟合月度太阳黑子数据 # 注意:太阳黑子数据的周期约 11 年,但如果用月度数据,先用 s=12 尝试 model = SARIMAX(sunspots, order=(1, 1, 1), seasonal_order=(1, 1, 1, 12), enforce_stationarity=False, enforce_invertibility=False) result = model.fit(disp=False) print(result.summary())seasonal_order的最后一个参数 s 是最需要谨慎的。气温数据是日频,周期 365 天,直接把 s 设为 365 会带来两个问题:一是模型参数骤增,季节性部分每个周期要估计多个系数,样本量不够时拟合极不稳定;二是计算量成倍增加。实际操作中我一般先把数据降采样到月频或者周频,把 s 降到 12 或 52,模型训练速度快很多,预测效果也不差。太阳黑子数据的周期实际接近 11 年(约 132 个月),用 s=12 只能捕捉到年度周期,长周期部分要靠非季节性参数和差分去处理,这是一个很常见的取舍——模型的周期假设和真实周期不完全一致时,先保证主要周期被捕捉,剩余部分用自回归项吸收。
4.2 Prophet 默认配置与参数调优习惯
Prophet 在处理带强季节性的序列时非常高效,因为它把预测问题拆成了趋势项、季节项和节假日项的组合,用类似广义加性模型的框架拟合。facebook 开源的这个库对新手友好,默认参数在很多场景下表现就不错。但"默认好用"不等于"不需要调参",实际项目中我最常调三个参数:changepoint_prior_scale控制趋势拐点的灵活度,默认 0.05,数据趋势变化剧烈时调到 0.1 以上,但也会增加过拟合风险;seasonality_prior_scale控制季节性的强度,默认 10,如果季节性在预测中表现过于强烈、把噪声也当成周期了,可以降到 3 左右;seasonality_mode设为'multiplicative'适用于季节性振幅随时间增大的序列,比如太阳黑子这种周期波峰波谷差异明显的。
from prophet import Prophet import pandas as pd # Prophet 要求两列:ds(日期)、y(数值) sunspots_prophet = sunspots.reset_index() sunspots_prophet.columns = ['ds', 'y'] model = Prophet( changepoint_prior_scale=0.08, seasonality_prior_scale=5.0, seasonality_mode='multiplicative' ) model.fit(sunspots_prophet) # 预测未来 60 个月 future = model.make_future_dataframe(periods=60, freq='MS') forecast = model.predict(future) fig = model.plot(forecast) plt.show()Prophet 内部会自动检测年度季节性,但对太阳黑子这种 11 年周期,默认检测不出来,所以我会手动添加周期性成分:model.add_seasonality(name='solar_cycle', period=132, fourier_order=6)。fourier_order控制拟合该周期的复杂度,默认对年度季节性取 10,但如果设定的周期很长,需要把它调低,防止过拟合。另一个常见坑是日期频率——make_future_dataframe需要指定freq参数,月频数据要用'MS'(月初)而不是'M'(月末),否则预测结果的日期对齐会出问题。
4.3 两种方案的适用边界
SARIMA 和 Prophet 不是互斥关系,更像是不同场景的不同工具。SARIMA 的优势在于对单个序列的拟合精度高,尤其是数据量适中、周期稳定的时候;缺点是参数搜索麻烦、对缺失值敏感、预测分布计算慢。Prophet 的优势是调参门槛低、对缺失值和异常值容忍度高、预测结果自带置信区间,适合快速出基线结果;缺点是可解释性相对弱,而且对复杂非线性关系(比如多序列交互)支持有限。我的经验是:先跑一版 Prophet 拿到基线,建立对数据可预测性的基本认知,再用 SARIMA 精调——如果 Prophet 的基线误差已经很低,SARIMA 的优化空间有限,不值得投入太多时间;如果基线误差明显偏高,说明序列结构有 SARIMA 能捕捉而 Prophet 忽略的部分,值得深挖。
5. 时间序列预测的常见问题与避坑指南
5.1 数据泄漏:用未来数据训练模型
现象:模型在训练集上误差极低,验证集上一塌糊涂,预测结果严重滞后——比如预测的下一天温度基本等于真实值前一天的温度。
原因:最常见的是在预处理阶段用了全量数据的统计量。比如用全量数据的均值填充缺失值,或者用全量数据做标准化,这些操作把未来的信息泄露进了训练集。另一个高频错误是网格搜索定阶时用了全量数据计算 ACF/PACF,这相当于提前知道了整个序列的结构特征。
解决:所有预处理步骤都要在训练集内部完成。先切分数据,再做填充、差分、标准化。填充缺失值用训练集部分的插值结果;差分操作按时间顺序进行,不用窗口外数据参与计算;定阶搜索也只用训练集。我习惯把数据切分写成如下模式,强制自己在每步操作前先确认训练集边界:
train = temps[:'1988'] test = temps['1989':'1990'] # 所有预处理、模型拟合都基于 train,test 只出现在最终评估5.2 差分还原:预测值是差分结果,不是原始量纲
现象:用 ARIMA 做预测,得到的预测值画在图上和原始数据对不上,数值差了一整截,看起来模型完全无效。
原因:训练时做了差分,模型学习的是"差值的变化规律",预测出来的也是差值。如果直接把预测的差值当成原始值输出,量纲和实际数据完全对不上。
解决:预测之后必须做差分还原。如果只做了diff()(一阶差分),还原方式是预测值 = 上一个实际观测值 + 差分预测值;如果做了多阶差分,还原要逐层累加。statsmodels 的ARIMAResults对象在调用predict时如果传入typ='levels'参数,会自动做这个还原,但要注意它依赖训练数据的最后一个观测值,预测时间越长,误差累计越明显。
# 正确用法:用 typ='levels' 得到原始量纲的预测值 forecast = result.predict(start=len(train), end=len(train)+10, typ='levels')5.3 时序交叉验证顺序错误:随机打乱训练集
现象:用train_test_split默认参数切分时间序列,或者更糟——在交叉验证时用KFold打乱数据顺序,模型训练集里混入了比验证集更晚的数据。
原因:时间序列数据本质上是按时间排序的,未来不能影响过去。很多从机器学习转过来的同学习惯性地用随机划分,因为常规分类任务里这样做是对的,但时间序列里它会制造虚假的高性能——模型实际是"偷看了"未来的答案。
解决:时间序列必须用法时序交叉验证,最常用的是TimeSeriesSplit,它保证每次训练集都在验证集之前。另一个替代方案是滚动的单步验证——每次用截至 t 的数据训练,预测 t+1,然后不断向右推进窗口。后者计算量大但更接近实际业务中"每天重新预测明天"的场景。
from sklearn.model_selection import TimeSeriesSplit tscv = TimeSeriesSplit(n_splits=5) for train_idx, val_idx in tscv.split(temps): train_fold = temps.iloc[train_idx] val_fold = temps.iloc[val_idx] # 在 train_fold 上拟合模型,在 val_fold 上验证5.4 日频数据的季节周期设置过大,模型训练卡死
现象:气温数据日频,直接设SARIMAX的seasonal_order=(1,1,1,365),代码跑了几十分钟没结果,甚至内存溢出。
原因:季节周期 365 意味着季节差分和季节 AR/MA 项要在最近 365 个观测上计算,状态空间模型的滤波过程涉及高维矩阵运算,计算复杂度和 s 近似线性增长,s=365 的成本是 s=12 的几十倍。
解决:先把数据降采样到周频或月频再建模。气温数据降采样到月频后,季节周期变成 12,模型的训练时间从几十分钟降到几秒,预测精度下降通常在一个可接受范围内。如果业务必须日频预测,考虑用 Prophet——它的季节项用傅里叶级数拟合,计算成本对周期长度的敏感度远低于 SARIMA。
# 降采样到月频,再拟合 SARIMA temps_monthly = temps.resample('MS').mean() model = SARIMAX(temps_monthly, order=(1, 1, 1), seasonal_order=(1, 1, 1, 12))这个坑太常见了,我几乎每次处理日频数据都要提醒自己一遍:不要为了理论上的"更完整"而承担不必要的计算成本,先降采样跑通流程,再评估是否需要回到日频精调。
6. 滚动验证与残差分析:给模型的可靠性做一次体检
模型训练完成、误差指标也算了,但"预测效果好"和"模型可靠"是两回事。可靠性的验证靠两个手段:滚动验证和残差分析。滚动验证模拟的是真实预测场景——每次只用截至当前时刻的数据做训练,预测未来一个或多个时间点,然后窗口向前移动,重复这个过程。相比一次性的训练/测试划分,这样验证出的误差更能代表模型上线后的真实水平。
import numpy as np from statsmodels.tsa.arima.model import ARIMA def rolling_forecast(series, train_len, horizon, order=(1, 1, 1)): predictions = [] history = series.iloc[:train_len].copy() for i in range(len(series) - train_len - horizon + 1): model = ARIMA(history, order=order) result = model.fit() pred = result.forecast(horizon) predictions.append(pred) # 窗口向前移动,把真实观测加入历史 history = pd.concat([history, series.iloc[[train_len + i]]]) return np.array(predictions)滚动验证输出的是一组预测值,把它们和真实值对比,计算 RMSE 和 MAE。但这里有个关键点:RMSE 低不代表模型没问题,还要看残差是否是白噪声——残差的自相关越小越好。如果残差里还有明显的自相关模式,说明模型遗漏了某种结构,可能是季节项阶数不对,也可能是需要加入外生变量。画残差 ACF 图是最直接的检验方式。
# 画残差 ACF 图 residuals = result.resid plot_acf(residuals, lags=40) plt.show()如果残差 ACF 在滞后 1 或滞后 2 处有明显峰值,通常是模型欠拟合,检查一下是否少加了 MA 项;如果多个滞后处都有显著相关,考虑原始序列是否存在未捕捉到的周期。
这几步全部走完,模型的可靠性才算在"能交代的水平"。从那以后我做任何时间序列任务,都会强制走一遍这个流程——先看数据形态,再定模型类型,然后网格搜索定阶,最后滚动验证加残差分析,缺一步心里都不踏实。这套流程在这个包里都配好了对应代码,照着跑一遍,比自己从零写要省掉大量排查成本。希望帮到你。
本文还有配套的精品资源,点击获取