使用 Python 从零开始预测波士顿每月武装抢劫数量,其中 statsmodels
库版本为 0.13.5,博客首发于 MonthlyArmedRobberiesInBoston | Cloudy1225’s Blog
时间序列预测是一个过程,获得良好预测的唯一方法是实践这个过程。我们将了解如何使用 Python 预测波士顿每月武装抢劫的数量。此项目将提供一个可以用于处理自己时间序列预测问题的框架。完成本项目,可以知道:
我们将从头到尾完成一个时间序列预测项目,从下载数据集、定义问题到训练最终模型、做预测。本项目并不详尽,但展示了如何通过系统地处理时间序列预测问题来快速获得不错的结果。项目步骤如下:
这将为解决时间序列预测问题提供一个模板,你可以在自己的数据集上使用该模板。
我们的问题是预测美国波士顿每月武装抢劫的数量。该数据集提供了 1966 年 1 月至 1975 年 10 月将近10年波士顿每月武装抢劫的数量,总共有 118 个观测值。该数据集由 McCleary 和 Hay 在 1980 年贡献出来。下面是数据集前几行的样本:
Month | Robberies |
---|---|
1966-01 | 41 |
1966-02 | 39 |
1966-03 | 50 |
1966-04 | 40 |
1966-05 | 43 |
下载数据集的 CSV 文件,命名为 robberies.csv
,并放于当前工作目录下。
我们必须开发一个测试工具来研究数据并评估候选模型。有两个步骤:
数据集不是最新的,这意味着我们无法轻松收集更新的数据来验证模型。因此,我们将假装现在是 1974 年 10 月,并从分析和模型选择中排除最后一年的数据。最后一年的数据将用于验证最终模型。下面的代码将数据集加载为 pandas.Series
,并被分为两部分:一个用于模型开发(dataset.csv),另一个用于验证(validation.csv)。
In:
# 加载原始数据集,并将其分割为训练集与验证集
from pandas import read_csv
series = read_csv('robberies.csv', header=0, index_col=0, parse_dates=True).squeeze('columns')
split_point = len(series) - 12
dataset, validation = series[0:split_point], series[split_point:]
print('Dataset %d, Validation %d' % (len(dataset), len(validation)))
dataset.to_csv('dataset.csv', header=False)
validation.to_csv('validation.csv', header=False)
Out:
Dataset 106, Validation 12
运行该代码将创建两个文件并打印每个文件中的观测值数量:
dataset 106, Validation 12
两个文件的具体内容为:
验证集只占原始数据集的 10% 。注意,保存的数据集没有标题行,因此在以后处理这些文件时,我们也不需要标题行。
模型评估将仅对上一节 dataset.csv
中的数据执行。模型评估涉及两个元素:
数据集中的观测值是抢劫的数量。我们将使用 均方根误差(RMSE)来评估预测性能。RMSE 与原始数据量纲相同,并且它可以放大较大误差。为了使不同方法之间的性能可以直接比较,在计算 RMSE 之前,不应对数据进行任何变换。
可以使用 scikit-learn 库中的函数 mean_squared_error()
来计算 RMSE。这个函数可以计算期望值序列(测试集)和预测值序列之间的均方误差,然后取平方根即可。例如:
from sklearn.metrics import mean_squared_error
from math import sqrt
...
test = ...
predictions = ...
mse = mean_squared_error(test, predictions)
rmse = sqrt(mse)
print('RMSE: %.3f' % rmse)
候选模型将使用前移验证(walk-forward validation)进行评估。这是因为问题的定义使得我们需要一个类型为滚动预测的模型,需要根据所有之前可用数据来进行下一步预测。本次前移验证细节如下:
鉴于数据量较小,我们将在每次预测之前根据所有可用数据重新训练模型。可以使用 NumPy 和 Python 来编写测试工具的代码,首先,直接将数据集拆分为训练集和测试集。注意应始终将加载的数据转换为 float32
类型,以防一些是字符串或整数类型。
# 数据准备
X = series.values
X = X.astype('float32')
train_size = int(len(X) * 0.50)
train, test = X[0:train_size], X[train_size:]
接下来,我们可以迭代测试数据集中的时间步。训练集存储在 Python 的 list 中,因为我们需要在每次迭代时很容易地添加一个新的观测值,而使用 NumPy 数组进行连接感觉有点过了。习惯上,模型做出的预测称为 yhat,因为结果或观测值被称为 y,而 yhat(即 y ^ \hat{y} y^)是预测 y 变量的数学符号。每次迭代,预测值和观测值都会打印,以便在模型出现问题时进行合理性检验(sanity check)。
# walk-forward validation
history = [x for x in train]
predictions = list()
for i in range(len(test)):
# 预测值
yhat = ...
predictions.append(yhat)
# 观测值
obs = test[i]
history.append(obs)
print('>Predicted=%.3f, Expected=%.3f' % (yhat, obs))
陷入数据分析和建模泥潭之前的第一步是建立性能基线(baseline)。这既为测试工具评估模型提供一个模板,又提供了一个可以比较所有更复杂预测模型的性能度量。时间序列预测的基线预测称为 naive forecast 或 persistence。它其实就是令预测值等于前一时间步的观测值。我们可以将其直接插入上一节中定义的测试工具中。完整代码如下:
In:
# 评估 persistence model
from pandas import read_csv
from sklearn.metrics import mean_squared_error
from math import sqrt
# 加载数据
series = read_csv('dataset.csv', header=None, index_col=0, parse_dates=True).squeeze('columns')
# 准备数据
X = series.values
X = X.astype('float32')
train_size = int(len(X) * 0.50)
train, test = X[0:train_size], X[train_size:]
# walk-forward validation
history = [x for x in train]
predictions = list()
for i in range(len(test)):
# 预测值
yhat = history[-1] # 前一步值作为下一步的预测值
predictions.append(yhat)
# 实际观测值
obs = test[i]
history.append(obs)
print('>Predicted=%.3f, Expected=%.3f' % (yhat, obs))
# 计算性能
rmse = sqrt(mean_squared_error(test, predictions))
print('RMSE: %.3f' % rmse)
Out:
>Predicted=98.000, Expected=125.000 >Predicted=125.000, Expected=155.000 ... >Predicted=355.000, Expected=460.000 >Predicted=460.000, Expected=364.000 >Predicted=364.000, Expected=487.000 RMSE: 51.844
运行测试工具会打印测试集每次迭代的预测值和实际观测值。该例子以打印模型的 RMSE 结束。在本例中,可以看到基线模型的 RMSE 达到51.844。这意味着平均而言,对于每次预测,模型错估了大约 51 次抢劫。
...
>Predicted=241.000, Expected=287.000
>Predicted=287.000, Expected=355.000
>Predicted=355.000, Expected=460.000
>Predicted=460.000, Expected=364.000
>Predicted=364.000, Expected=487.000
RMSE: 51.844
现在我们有了一个基准预测方法与基准性能,可以深入挖掘数据了。
我们可以使用概括性统计量(summary statistics)和数据图来快速了解有关预测问题结构的更多信息。本节中,我们将从四个角度观察数据:
在文本编辑器中打开 robberies.csv
文件并查看数据,快速扫一下有没有明显的缺失值。这样便可能在将 NaN
或 ?
强制转换为浮点数之前发现它。概括性统计量提供了数据集的总体视图,可以我们帮助快速了解正在使用的数据。下面的代码计算并打印时间序列的 summary statistics:
In:
# summary statistics of time series
from pandas import read_csv
series = read_csv('dataset.csv', header=None, index_col=0, parse_dates=True).squeeze('columns')
print(series.describe())
Out:
count 106.000000 mean 173.103774 std 112.231133 min 29.000000 25% 74.750000 50% 144.500000 75% 271.750000 max 487.000000 Name: 1, dtype: float64
运行上面代码将打印几个概括性统计量,我们可以从中观察到:
如果数据取值的较大差异是由随机波动(如非系统性波动)引起的,那么高准确预测将变得困难。
时间序列的折线图可以提供对问题的大量见解。下面的代码创建并显示了数据集的折线图:
In:
# 时间序列的折线图
from pandas import read_csv
from matplotlib import pyplot
series = read_csv('dataset.csv', header=None, index_col=0, parse_dates=True).squeeze('columns')
series.plot()
pyplot.show()
Out:
运行代码并观察折线图,注意时间序列中任何明显的时间结构。我们可以从折线图中观察到:
这些简单的观察结果表明,对趋势进行建模并将其从时间序列中移除可能会很有帮助。或者,我们可以用差分使序列平稳以进行建模。如果以后几年的波动有增长趋势,我们甚至可能需要两个层次的差分。
观察观测值的密度图可以进一步了解数据的结构。下面的代码创建了没有任何时间结构的观测值的直方图和密度图(没有任何时间结构指我们忽略时间序列数据的先后关系,仅看其取值):
In:
# 时间序列数据的密度图
from pandas import read_csv
from matplotlib import pyplot
series = read_csv('dataset.csv', header=None, index_col=0, parse_dates=True).squeeze('columns')
pyplot.figure(1)
pyplot.subplot(211)
series.hist()
pyplot.subplot(212)
series.plot(kind='kde')
pyplot.show()
Out:
这说明在建模之前探索数据的一些幂变换可能会有不错的效果。
我们可以按年份对每月数据进行分组,并了解每年观测值的分布以及其可能如何变化。我们确实希望看到一些趋势(均值或中位数的增大),但看看分布的其余部分可能如何变化也很有趣。下面的代码按年份对观测值进行分组,并为每年的观测值创建一个箱线图。1974 年只包含 10 个月,与其他年份的 12 个月的观测值相比可能没有用。因此,仅绘制了 1966 年至 1973 年之间的数据。
In:
# 时间序列的箱线图
from pandas import read_csv
from pandas import DataFrame
from pandas import Grouper
from matplotlib import pyplot
series = read_csv('dataset.csv', header=None, index_col=0, parse_dates=True).squeeze('columns')
groups = series['1966':'1973'].groupby(Grouper(freq='A'))
years = DataFrame()
for name, group in groups:
years[name.year] = group.values
years.boxplot()
pyplot.show()
Out:
运行程序将创建8个箱线图,依次代表每年的数据,我们可以观察到:
这表明,逐年波动可能不是系统性的,也难以建模,同时也暗示了如果确实完全不同,那么从建模中裁剪前两年的数据可能会有一些作用。这种年度数据视图是一个有趣的途径,可以通过查看每年的概括性统计量及其变化来进一步研究。接下来,我们开始建立时间序列预测模型。
在本节中,我们将开发 ARIMA(Autoregressive Integrated Moving Average)模型来解决问题。主要有四步:
非季节性 ARIMA(p, d, q) 需要三个参数,传统上是手动配置的。对时间序列数据的分析需要假设其是平稳的。但该时间序列几乎可以肯定是非平稳的。我们可以先通过对序列进行差分使其平稳,然后使用统计检验来确认结果确实是平稳的来达到假设条件。下面的代码创建该时间系列的平稳版本并将其保存到文件 stationary.csv
。
In:
# 时间序列平稳性的统计检验
from pandas import read_csv
from pandas import Series
from statsmodels.tsa.stattools import adfuller
# 创建差分时间序列
def difference(dataset):
diff = list()
for i in range(1, len(dataset)):
value = dataset[i] - dataset[i-1]
diff.append(value)
return Series(diff)
series = read_csv('dataset.csv', header=None, index_col=0, parse_dates=True).squeeze('columns')
X = series.values
# 差分数据
stationary = difference(X)
stationary.index = series.index[1:]
# 检验是否平稳
result = adfuller(stationary)
print('ADF Statistic: %f' % result[0])
print('p-value: %f' % result[1])
print('Critical Values:')
for key, value in result[4].items():
print('\t%s: %.3f' % (key, value))
# 保存
stationary.to_csv('stationary.csv', header=False)
Out:
ADF Statistic: -3.980946 p-value: 0.001514 Critical Values: 1%: -3.503 5%: -2.893 10%: -2.584
运行代码输出 ADF检验 的结果—— “1-lag” 差分后的时间序列是否平稳。可以发现检验统计量的值 -3.980946 小于 -3.503,这表明我们可以在显著性水平为 0.01 下拒绝原假设(序列是非平稳的,序列有单位根),即 “1-lag” 差分后的时间序列是平稳的,没有时间相关的结构。
以上可知,ARIMA模型中的参数 d 应至少为 1。下一步是分别为自回归AR和移动平均MA选择参数。我们可以通过观察自相关函数ACF和偏自相关函数PACF的图来启发性地选择。下面的代码为时间序列创建 ACF 和 PACF 图。
In:
# 时间序列的 ACF & PCAF 图
from pandas import read_csv
from statsmodels.graphics.tsaplots import plot_acf
from statsmodels.graphics.tsaplots import plot_pacf
from matplotlib import pyplot
series = read_csv('dataset.csv', header=None, index_col=0, parse_dates=True).squeeze('columns')
pyplot.figure()
pyplot.subplot(211)
plot_acf(series, lags=50, ax=pyplot.gca())
pyplot.subplot(212)
plot_pacf(series, lags=50, ax=pyplot.gca(), method='ywm')
pyplot.tight_layout() # 调整子图间距,防止标题重叠
pyplot.show()
Out:
运行代码并查看绘图,深入了解如何为 ARIMA 模型设置 p 和 q 参数。图中可以看出:
对于 p 和 q 来说,11 和 2 应该是不错的初始值。
上面的分析表明,ARIMA(11, 1, 2) 可能是一个很好的起点。但实验表明,ARIMA 的这种配置不会收敛并导致底层库出错,类似的大 AR 值也是如此。一些实验表明,当同时定义了非零 AR 和 MA 阶,该模型似乎并不稳定。该模型可以简化为 ARIMA(0, 1, 2)。下面的示例展示了此 ARIMA 模型在测试工具上的性能。
In:
# 评估手动配置的 ARIMA 模型
from pandas import read_csv
from sklearn.metrics import mean_squared_error
from statsmodels.tsa.arima.model import ARIMA
from math import sqrt
from pandas import Series
# 加载数据
series = read_csv('dataset.csv', header=None, index_col=0, parse_dates=True).squeeze('columns')
# 准备数据
X = series.values
X = X.astype('float32')
train_size = int(len(X) * 0.50)
train, test = X[0:train_size], X[train_size:]
# walk-forward validation
history = [x for x in train]
predictions = list()
for i in range(len(test)):
# 预测值
model = ARIMA(history, order=(0,1,2))
model_fit = model.fit()
yhat = model_fit.forecast()[0]
predictions.append(yhat)
# 实际观测值
obs = test[i]
history.append(obs)
print('>Predicted=%.3f, Expected=%.3f' % (yhat, obs))
# 计算性能
rmse = sqrt(mean_squared_error(test, predictions))
print('RMSE: %.3f' % rmse)
Out:
>Predicted=99.923, Expected=125.000 >Predicted=116.442, Expected=155.000 ... >Predicted=329.367, Expected=460.000 >Predicted=409.007, Expected=364.000 >Predicted=335.283, Expected=487.000 RMSE: 51.115
运行代码,RMSE 为 51.115,低于上面的基线模型。这是一个良好的开端,但我们可以通过配置更好的 ARIMA 模型来改进结果。
许多 ARIMA 配置在此数据集上不稳定,但可能还有其他超参数可以改进模型性能。在本节中,我们将搜索 p、d、q 的值以查找不会导致错误的组合,并找到导致最佳性能的组合。我们将使用网格搜索来探索整数值子集中的所有组合。具体来说,我们将搜索以下参数的所有组合:
共需运行 13 × 4 × 13 = 676 13 \times 4 \times 13 = 676 13×4×13=676 次。下面列出了测试工具的网格搜索版本的完整工作代码:
In:
# 时间序列 ARMIA 参数的网格搜索
import warnings
from pandas import read_csv
from sklearn.metrics import mean_squared_error
from statsmodels.tsa.arima.model import ARIMA
from math import sqrt
# 根据给定的order(p,d,q)来评估ARIMA模型,返回 RMSE
def evaluate_arima_model(X, arima_order):
# 准备训练集
X = X.astype('float32')
train_size = int(len(X) * 0.50)
train, test = X[0:train_size], X[train_size:]
history = [x for x in train]
# 进行预测
predictions = list()
for t in range(len(test)):
model = ARIMA(history, order=(arima_order))
model_fit = model.fit()
yhat = model_fit.forecast()[0]
predictions.append(yhat)
history.append(test[t])
# 计算 RMSE
rmse = sqrt(mean_squared_error(test, predictions))
return rmse
# 评估 ARIMA 模型的 (p,d,q) 组合
def evaluate_models(dataset, p_values, d_values, q_values):
dataset = dataset.astype('float32')
best_score, best_cfg = float('inf'), None
for p in p_values:
for d in d_values:
for q in q_values:
order = (p, d, q)
try:
rmse = evaluate_arima_model(dataset, order)
if rmse < best_score:
best_score, best_cfg = rmse, order
print('ARIMA%s RMSE=%.3f' % (order, rmse))
except:
continue
print('Best ARIMA%s RMSE=%.3f' % (best_cfg, best_score))
# 加载数据集
series = read_csv('dataset.csv', header=None, index_col=0, parse_dates=True).squeeze('columns')
# 评估参数
p_values = range(0, 13)
d_values = range(0, 4)
q_values = range(0, 13)
warnings.filterwarnings('ignore')
evaluate_models(series.values, p_values, d_values, q_values)
Out:
ARIMA(0, 0, 0) RMSE=154.962 ARIMA(0, 0, 1) RMSE=99.354 ARIMA(0, 0, 2) RMSE=92.071 ARIMA(0, 0, 3) RMSE=72.271 ARIMA(0, 0, 4) RMSE=72.136 ARIMA(0, 1, 0) RMSE=51.844 ARIMA(0, 1, 1) RMSE=50.717 ARIMA(0, 1, 2) RMSE=51.115 ARIMA(0, 1, 3) RMSE=52.058 ...
运行该程序将遍历所有组合,并报告收敛组合的结果,而不会出错。结果表明,发现的最佳配置为ARIMA(5, 3, 8)。
模型的最终检查是查看残余预测误差。理想情况下,残差的分布应该是均值为零的高斯分布。我们可以通过绘制残差的直方图和密度图来检查这一点。下面的示例计算测试集预测的残差并绘制密度图。
In:
# 绘制 ARIMA 模型的残差密度图
import warnings
warnings.filterwarnings('ignore')
from pandas import read_csv
from pandas import DataFrame
from statsmodels.tsa.arima.model import ARIMA
from matplotlib import pyplot
# 加载数据
series = read_csv('dataset.csv', header=None, index_col=0, parse_dates=True).squeeze('columns')
# 准备数据
X = series.values
X = X.astype('float32')
train_size = int(len(X) * 0.50)
train, test = X[0:train_size], X[train_size:]
# walk-forward validation
history = [x for x in train]
predictions = list()
for i in range(len(test)):
# 预测
model = ARIMA(history, order=(5,3,8))
model_fit = model.fit()
yhat = model_fit.forecast()[0]
predictions.append(yhat)
# 观测值
obs = test[i]
history.append(obs)
# errors
residuals = [test[i] - predictions[i] for i in range(len(test))]
residuals = DataFrame(residuals)
pyplot.figure()
pyplot.subplot(211)
residuals.hist(ax = pyplot.gca())
pyplot.subplot(212)
residuals.plot(kind='kde', ax=pyplot.gca())
pyplot.show()
Out:
运行代码将绘制两张图,它们表现为具有较长右尾的高斯分布。这可能是预测有偏差的迹象,在这种情况下,在建模之前对原始数据进行基于幂的转换可能是有用的。
我们还可以检查残差的时间序列看看有没有任何类型的自相关。如果有,则表明模型有更多机会对数据中的时间结构进行建模。下面的代码重新计算残差并创建 ACF 和 PACF 图以检查是否有任何显著的自相关:
In:
# 预测残差的 ACF 和 PACF 图
import warnings
warnings.filterwarnings('ignore')
from pandas import read_csv
from pandas import DataFrame
from statsmodels.tsa.arima.model import ARIMA
from statsmodels.graphics.tsaplots import plot_acf
from statsmodels.graphics.tsaplots import plot_pacf
from matplotlib import pyplot
# 加载数据
series = read_csv('dataset.csv', header=None, index_col=0, parse_dates=True).squeeze('columns')
# 准备数据
X = series.values
X = X.astype('float32')
train_size = int(len(X) * 0.50)
train, test = X[0:train_size], X[train_size:]
# walk-forward validation
history = [x for x in train]
predictions = list()
for i in range(len(test)):
# 预测
model = ARIMA(history, order=(5,3,8))
model_fit = model.fit()
yhat = model_fit.forecast()[0]
predictions.append(yhat)
# 观测值
obs = test[i]
history.append(obs)
# errors
residuals = [test[i] - predictions[i] for i in range(len(test))]
residuals = DataFrame(residuals)
pyplot.figure()
pyplot.subplot(211)
plot_acf(residuals, lags=25, ax=pyplot.gca())
pyplot.subplot(212)
plot_pacf(residuals, lags=25, ax=pyplot.gca())
pyplot.tight_layout()
pyplot.show()
Out:
Box-Cox 变换是一种能够评估一套幂变换的方法,包括但不限于对数、平方根和倒数变换。下面的代码进行数据的对数转换,并生成一些绘图来查看对时间序列的影响。
In:
# 绘制 Box-Cox 变换后的数据集
from pandas import read_csv
from scipy.stats import boxcox
from matplotlib import pyplot
from statsmodels.graphics.gofplots import qqplot
series = read_csv('dataset.csv', header=None, index_col=0, parse_dates=True).squeeze('columns')
X = series.values
transformed, lam = boxcox(X)
print('Lambda: %f' % lam)
pyplot.figure(1)
# 折线图
pyplot.subplot(311)
pyplot.plot(transformed)
# 直方图
pyplot.subplot(312)
pyplot.hist(transformed)
# Q-Q 图
pyplot.subplot(313)
qqplot(transformed, line='r', ax=pyplot.gca())
pyplot.tight_layout()
pyplot.show()
Out:
Lambda: 0.260060
运行程序将创建三张图:转换后的时间序列的折线图、显示转换值分布的直方图,以及显示值分布与理想高斯分布相比情况的 Q-Q 图。从图中可以观察到:
毫无疑问,Box-Cox 变换对时间序列做了一些很有用的事情。在继续使用转换后的数据测试 ARIMA 模型之前,我们必须有一种逆转换的方法,以便将使用在转换后的数据上训练的模型所做的预测转换回原始尺度。程序中使用的 boxcox()
函数通过优化代价函数来寻找理想的 λ \lambda λ 值。
Box-Cox 变换公式如下:
y = { log ( x ) , λ = 0 x λ − 1 λ , λ ≠ 0 y = \begin{cases} \log(x) & ,\lambda=0 \\ \frac{x^{\lambda}-1}{\lambda} & ,\lambda \neq 0 \end{cases} y={log(x)λxλ−1,λ=0,λ=0
故逆变换公式:
x = { e y , λ = 0 e log ( λ y + 1 ) λ , λ ≠ 0 x = \begin{cases} e^{y} & ,\lambda=0 \\ e^{\frac{\log{(\lambda y + 1)}}{\lambda}} & ,\lambda \neq 0 \end{cases} x={eyeλlog(λy+1),λ=0,λ=0
逆 Box-Cox 变换函数的Python实现如下:
# 逆 Box-Cox 变换
from math import log
from math import exp
def boxcox_inverse(value, lam):
if lam == 0:
return exp(value)
return exp(log(lam*value + 1) / lam)
我们将使用 Box-Cox 变换后的数据重新评估 ARIMA(5, 3, 8)。这包括在拟合 ARIMA 模型之前首先转换先前历史数据,然后在存储预测值之前逆转换以供以后与预期值进行比较。boxcox() 函数可能会失败:在实践中,它似乎是由返回的 λ \lambda λ 值小于 -5 发出的信号。按照惯例, λ \lambda λ 值的计算值介于 -5 和 5 之间。
首先会检查 λ \lambda λ 是否小于 -5,如果小于 -5,则假定 λ \lambda λ 值为 1,并使用原始历史记录来拟合模型,因为 λ \lambda λ 值 1 相当于无变换。下面列出了完整的代码:
In:
# 使用 Box-Cox 转换后的时间序列评估 ARIMA 模型
# 预测残差的 ACF 和 PACF 图
import warnings
warnings.filterwarnings('ignore')
from pandas import read_csv
from sklearn.metrics import mean_squared_error
from statsmodels.tsa.arima.model import ARIMA
from math import sqrt
from math import log
from math import exp
from scipy.stats import boxcox
# 逆 Box-Cox 变换
def boxcox_inverse(value, lam):
if lam == 0:
return exp(value)
return exp(log(lam*value + 1) / lam)
# 加载数据
series = read_csv('dataset.csv', header=None, index_col=0, parse_dates=True).squeeze('columns')
# 准备数据
X = series.values
X = X.astype('float32')
train_size = int(len(X) * 0.50)
train, test = X[0:train_size], X[train_size:]
# walk-forward validation
history = [x for x in train]
predictions = list()
for i in range(len(test)):
# Box-Cox 变换
transformed, lam = boxcox(history)
if lam < -5:
transformed, lam = history, 1
# 预测
model = ARIMA(transformed, order=(5, 3, 8))
model_fit = model.fit()
yhat = model_fit.forecast()[0]
# 逆转换预测值
yhat = boxcox_inverse(yhat, lam)
predictions.append(yhat)
# 观测值
obs = test[i]
history.append(obs)
print('>Predicted=%.3f, Expected=%.3f' % (yhat, obs))
# 计算 RMSE
rmse = sqrt(mean_squared_error(test, predictions))
print('RMSE: %.3f' % rmse)
Out:
>Predicted=92.319, Expected=125.000 >Predicted=126.906, Expected=155.000 ... >Predicted=312.469, Expected=460.000 >Predicted=419.800, Expected=364.000 >Predicted=372.240, Expected=487.000 RMSE: 54.285
然而尴尬的是,最终 RMSE 为 54.285,并没有变换之前的好。我们还是选择未变换数据 ARIMA(5, 3, 8) 作为最终模型。
在开发和选择最终模型后,在开发模型并选择最终模型后,必须对其进行验证和最终确定。验证是一个可选部分,但它提供了最后的检查,以确保我们没有欺骗自己。本节有以下几步:
最终确定模型(Finalize Model)涉及在完整数据集上拟合 ARIMA 模型,在本例中,在完整数据集的转换版本上拟合。拟合后,可以将模型保存到文件中供以后使用。由于也对数据进行了 Box-Cox 变换,因此我们需要知道所选的 lambda,以便模型所做的任何预测都可以转换回原始的未变换比例。下面的示例在 Box-Cox 变换后数据集上拟合 ARIMA(0,1,2) 模型,并将整个拟合对象和 lambda 值保存到文件中。
In:
from pandas import read_csv
from statsmodels.tsa.arima.model import ARIMA
# 加载数据
series = read_csv('dataset.csv', header=None, index_col=0, parse_dates=True).squeeze('columns')
# 准备数据
X = series.values
X = X.astype('float32')
# 拟合模型
model = ARIMA(X, order=(5, 3, 8))
model_fit = model.fit()
# 保存模型
model_fit.save('model.pkl')
运行程序会创建一个本地文件:
ARIMA.fit()
返回的 ARIMAResult
对象。这包括拟合模型时返回的系数和所有其他内部数据。我们真正需要的是模型中的 AR 和 MA 系数、差分的 d 参数、滞后观测值和模型残差。
一般是加载模型并进行单步预测,这相对简单,包括恢复保存的模型并调用 forecast()
函数。下面的代码将加载模型,对下一个时间步进行预测,然后打印预测值。
In:
# 加载模型,进行预测
from statsmodels.tsa.arima.model import ARIMAResults
model_fit = ARIMAResults.load('model.pkl')
yhat = model_fit.forecast()[0]
print('Predicted: %.3f' % yhat)
Out:
Predicted: 486.600
运行程序,预测值为 486.600,如果我们查看 validation.csv
内容,可以看到下一个时间段的第一行上的值为 452,模型不是很准确。
在“测试工具”节中,我们将原始数据集的最后 12 个月保存在一个单独的文件中,以验证最终模型。我们现在可以加载这个 validation.csv
文件,并使用它来看看我们的模型在未知数据上的表现情况,.我们可以通过两种方式实现:
与前面节中的模型评估一样,我们将以滚动预测的方式进行预测,将新的观测值作为历史记录来更新模型。
In:
# 在验证集上评估最终模型
from pandas import read_csv
from matplotlib import pyplot
from statsmodels.tsa.arima.model import ARIMA
from statsmodels.tsa.arima.model import ARIMAResults
from sklearn.metrics import mean_squared_error
from math import sqrt
# 加载并准备数据
dataset = read_csv('dataset.csv', header=None, index_col=0, parse_dates=True).squeeze('columns')
X = dataset.values.astype('float32')
history = [x for x in X]
validation = read_csv('validation.csv', header=None, index_col=0, parse_dates=True).squeeze('columns')
y = validation.values.astype('float32')
# 加载模型
model_fit = ARIMAResults.load('model.pkl')
# 进行第一次预测
predictions = list()
yhat = model_fit.forecast()[0]
predictions.append(yhat)
history.append(y[0])
print('>Predicted=%.3f, Expected=%.3f' % (yhat, y[0]))
# 滚动预测
for i in range(1, len(y)):
# 预测值
model = ARIMA(history, order=(5, 3, 8))
model_fit = model.fit()
yhat = model_fit.forecast()[0]
predictions.append(yhat)
# 观测值
obs = y[i]
history.append(obs)
print('>Predicted=%.3f, Expected=%.3f' % (yhat, obs))
# 计算 RMSE
rmse = sqrt(mean_squared_error(y, predictions))
print('RMSE: %.3f' % rmse)
pyplot.plot(y)
pyplot.plot(predictions, color='red')
pyplot.show()
Out:
>Predicted=486.600, Expected=452.000 >Predicted=457.122, Expected=391.000 >Predicted=414.261, Expected=500.000 >Predicted=465.520, Expected=451.000 >Predicted=487.513, Expected=375.000 >Predicted=428.620, Expected=372.000 >Predicted=364.956, Expected=302.000 >Predicted=355.342, Expected=316.000 >Predicted=355.303, Expected=398.000 >Predicted=320.682, Expected=394.000 >Predicted=431.841, Expected=431.000 >Predicted=450.471, Expected=431.000 RMSE: 59.221
运行程序将打印验证集中每个时间步的预测值和实际值。最终验证集的 RMSE 约为 59。
我们还画了预测值与实际值相比的预测图。预测的结果具有“惯性”,可见尽管这个时间序列具有明显的趋势,但想要准确预测仍然相当困难。
项目并不详尽,可以做更多的事情来改进结果,比如:
我们可以学习到:
Introduction — statsmodels
Monthly Armed Robberies in Boston | Kaggle
Time Series Forecast Case Study with Python: Monthly Armed Robberies in Boston (machinelearningmastery.com)