在本教程中,您将了解如何使用Python预测法国香槟的月销售额。
完成本教程将为您提供一个框架,用于完成您自己的时间序列预测问题的步骤和工具。
完成本教程后,您将了解:
在本教程中,我们将完成从端到端的时间序列预测项目,从下载数据集和定义问题到培训最终模型和进行预测。
该项目并非详尽无遗,但展示了如何通过系统地处理时间序列预测问题来快速获得良好结果。
我们将通过的这个项目的步骤如下。
本教程假定已安装且正在运行的SciPy环境和依赖项,包括:
如果您需要在工作站上安装Python和SciPy环境的帮助,请考虑为您管理大部分内容的Anaconda发行版。
此脚本将帮助您检查这些库的已安装版本。
# scipy
import scipy
print('scipy: %s' % scipy.__version__)
# numpy
import numpy
print('numpy: %s' % numpy.__version__)
# matplotlib
import matplotlib
print('matplotlib: %s' % matplotlib.__version__)
# pandas
import pandas
print('pandas: %s' % pandas.__version__)
# scikit-learn
import sklearn
print('sklearn: %s' % sklearn.__version__)
# statsmodels
import statsmodels
print('statsmodels: %s' % statsmodels.__version__)
用于编写本教程的工作站上的结果如下:
scipy: 0.18.1
numpy: 1.11.2
matplotlib: 1.5.3
pandas: 0.19.1
sklearn: 0.18.1
statsmodels: 0.6.1
问题是要预测Perrin Freres标签(以法国某个地区命名)的每月香槟销售数量。
该数据集提供了1964年1月至1972年9月的香槟月销售数量,或者不到10年的数据。
这些数值是数百万销售额,有105个观察值。
该数据集归功于Makridakis和Wheelwright,1989。
您可以了解有关此数据集的更多信息,并直接从DataMarket下载。
将数据集下载为CSV文件,并将其放在当前工作目录中,文件名为“ champagne.csv ”。
我们必须开发一个测试工具来研究数据并评估候选模型。
这涉及两个步骤:
数据集不是最新的。这意味着我们无法轻松收集更新的数据来验证模型。
因此,我们将假装它是1971年9月,并保留最后一年的分析和模型选择数据。
最后一年的数据将用于验证最终模型。
下面的代码将数据集作为Pandas系列加载并分成两部分,一部分用于模型开发(dataset.csv),另一部分用于验证(validation.csv)。
from pandas import Series
series = Series.from_csv('champagne.csv', header=0)
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')
validation.to_csv('validation.csv')
运行该示例将创建两个文件并打印每个文件中的观察数。
Dataset 93, Validation 12
这些文件的具体内容是:
验证数据集约占原始数据集的11%。
请注意,保存的数据集没有标题行,因此我们稍后在处理这些文件时无需满足此要求。
模型评估仅对上一节中准备的dataset.csv中的数据执行。
模型评估涉及两个要素:
3.2.1绩效衡量
观察结果是数百万单位的香槟销量。
我们将使用均方根误差(RMSE)来评估预测的性能。这将更加重视严重错误的预测,并且与原始数据具有相同的单位。
在计算和报告RMSE之前,必须颠倒对数据的任何变换,以使不同方法之间的性能直接相当。
我们可以使用scikit-learn库mean_squared_error()中的辅助函数来计算RMSE,该函数计算预期值列表(测试集)和预测列表之间的均方误差。然后我们可以取这个值的平方根来给我们一个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)
3.2.2测试策略
候选模型将使用前向验证进行评估。
这是因为问题定义需要滚动预测类型模型。这是在给定所有可用数据的情况下需要一步预测的地方。
前瞻性验证将如下工作:
鉴于数据量很小,我们将允许在每次预测之前根据所有可用数据重新训练模型。
我们可以使用简单的NumPy和Python代码编写测试工具的代码。
首先,我们可以直接将数据集拆分为训练集和测试集。我们总是将加载的数据集转换为float32,以防加载的数据仍然有一些String或Integer数据类型。
# prepare data
X = series.values
X = X.astype('float32')
train_size = int(len(X) * 0.50)
train, test = X[0:train_size], X[train_size:]
接下来,我们可以迭代测试数据集中的时间步长。训练数据集存储在Python列表中,因为我们需要在每次迭代时附加新的观察点并且NumPy数组连接感觉有点多余。
由模型做出的预测被称为yhat for convention,因为结果或观察被称为y和yhat,是用于预测y变量的数学符号。
如果模型存在问题,则在每次观察时打印预测和观察以进行健全性检查预测。
# walk-forward validation
history = [x for x in train]
predictions = list()
for i in range(len(test)):
# predict
yhat = ...
predictions.append(yhat)
# observation
obs = test[i]
history.append(obs)
print('>Predicted=%.3f, Expected=%3.f' % (yhat, obs))
在陷入数据分析和建模之前的第一步是建立性能基准。
这将提供用于使用所提出的测试工具评估模型的模板和用于比较所有更精细的预测模型的性能测量。
时间序列预测的基线预测称为简单预测或持久性。
这是前一时间步骤的观察被用作下一时间步骤的观察预测的地方。
我们可以将其直接插入上一节中定义的测试工具中。
完整的代码清单如下。
from pandas import Series
from sklearn.metrics import mean_squared_error
from math import sqrt
# load data
series = Series.from_csv('dataset.csv')
# prepare data
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)):
# predict
yhat = history[-1]
predictions.append(yhat)
# observation
obs = test[i]
history.append(obs)
print('>Predicted=%.3f, Expected=%3.f' % (yhat, obs))
# report performance
mse = mean_squared_error(test, predictions)
rmse = sqrt(mse)
print('RMSE: %.3f' % rmse)
运行测试工具会打印测试数据集的每次迭代的预测和观察。
该示例以打印模型的RMSE结束。
在这种情况下,我们可以看到持久性模型实现了3186.501的RMSE。这意味着平均而言,每个预测的销售额约为31.66亿美元。
...
>Predicted=4676.000, Expected=5010
>Predicted=5010.000, Expected=4874
>Predicted=4874.000, Expected=4633
>Predicted=4633.000, Expected=1659
>Predicted=1659.000, Expected=5951
RMSE: 3186.501
我们现在有一个基线预测方法和性能; 现在我们可以开始挖掘我们的数据了。
我们可以使用汇总统计数据和数据图来快速了解有关预测问题结构的更多信息。
在本节中,我们将从五个角度来看待数据:
摘要统计信息可快速查看观察值的限制。它有助于快速了解我们正在使用的内容。
以下示例计算并打印时间序列的摘要统计信息。
from pandas import Series
series = Series.from_csv('dataset.csv')
print(series.describe())
运行该示例提供了许多要查看的摘要统计信息。
这些统计数据的一些观察包括:
count 93.000000
mean 4641.118280
std 2486.403841
min 1573.000000
25% 3036.000000
50% 4016.000000
75% 5048.000000
max 13916.000000
时间序列的线图可以提供对问题的大量观察信息。
下面的示例创建并显示数据集的线图。
from pandas import Series
from matplotlib import pyplot
series = Series.from_csv('dataset.csv')
series.plot()
pyplot.show()
运行示例并查看绘图。注意系列中任何明显的时间结构。
该图的一些观察结果包括:
香槟销售线图
明确建模季节性组件并将其删除可能会有好处。您还可以探索使用一个或两个级别的差分,以使系列静止。
季节性成分的增加趋势或增长可能表明使用对数或其他功率变换。
我们可以通过逐年观察数据集的线图来确认季节性是一个年度周期的假设。
下面的示例将7个完整年份的数据作为单独的组,并为每个数据创建一个线图。线图垂直对齐,以帮助发现任何逐年模式。
from pandas import Series
from pandas import DataFrame
from pandas import TimeGrouper
from matplotlib import pyplot
series = Series.from_csv('dataset.csv')
groups = series['1964':'1970'].groupby(TimeGrouper('A'))
years = DataFrame()
pyplot.figure()
i = 1
n_groups = len(groups)
for name, group in groups:
pyplot.subplot((n_groups*100) + 10 + i)
i += 1
pyplot.plot(group)
pyplot.show()
运行该示例将创建7个线图的堆栈。
我们可以清楚地看到每年八月下降,并从每年八月到十二月上升。虽然处于不同的水平,但这种模式每年看起来都是一样的。
这将有助于以后任何明确的基于季节的建模。
每年季节性线图
如果将所有季节线图添加到一个图中以帮助对比每年的数据,可能会更容易。
查看观测密度图可以进一步了解数据结构。
下面的示例创建了没有任何时间结构的观测的直方图和密度图。
from pandas import Series
from matplotlib import pyplot
series = Series.from_csv('dataset.csv')
pyplot.figure(1)
pyplot.subplot(211)
series.hist()
pyplot.subplot(212)
series.plot(kind='kde')
pyplot.show()
运行示例并查看绘图。
这些情节的一些观察包括:
这为在建模之前探索数据的一些功率变换提供了更多支持。
我们可以按年度对月度数据进行分组,并了解每年观察的传播情况以及这种情况可能会如何变化。
我们确实希望看到一些趋势(增加均值或中位数),但看看分布的其他部分可能会如何变化可能会很有趣。
下面的示例按年份对观测值进行分组,并为每年的观测值创建一个框和须状图。去年(1971年)仅包含9个月,与其他年份的12个月观察结果可能无差别。因此,仅绘制了1964年至1970年之间的数据。
from pandas import Series
from pandas import DataFrame
from pandas import TimeGrouper
from matplotlib import pyplot
series = Series.from_csv('dataset.csv')
groups = series['1964':'1970'].groupby(TimeGrouper('A'))
years = DataFrame()
for name, group in groups:
years[name.year] = group.values
years.boxplot()
pyplot.show()
运行该示例并排创建7个框和胡须图,每7年选择一个数据。
审查这些图的一些观察结果包括:
香槟销售盒和晶须图
这些观察结果表明,这些年可能出现一些增长趋势,而异常值可能是季节性周期的一部分。
这种年度数据视图是一个有趣的途径,可以通过查看逐年的摘要统计数据和每年的摘要统计数据的变化来进一步追求。
在本节中,我们将针对该问题开发自回归集成移动平均线(ARIMA)模型。
我们将通过手动和自动配置ARIMA模型来进行建模。接下来是调查所选模型的残差的第三步。
因此,本节分为3个步骤:
ARIMA(p,d,q)模型需要三个参数,并且传统上是手动配置的。
对时间序列数据的分析假设我们正在使用固定的时间序列。
时间序列几乎肯定是非平稳的。我们可以通过首先对系列进行差分并使用统计检验确认结果是静止的来使其静止。
该系列的季节性似乎是逐年的。季节性数据可以通过从前一周期中的同一时间减去观察值来区分,在这种情况下是前一年的同一月份。这确实意味着我们将失去第一年的观察,因为没有前一年的差异。
下面的示例创建该系列的延长版本并将其保存到文件stationary.csv。
from pandas import Series
from pandas import read_csv
from statsmodels.tsa.stattools import adfuller
from matplotlib import pyplot
# 根据给出的步长interval,创建一个差分序列,间隔12个数字相减
def difference(dataset, interval=1):
diff = list()
for i in range(interval, len(dataset)):
value = dataset[i] - dataset[i - interval]
diff.append(value)
return Series(diff)
series = Series.from_csv('dataset.csv')
"""
1964-01-01 2815
1964-02-01 2672
1964-03-01 2755
1964-04-01 2721
1964-05-01 2946
"""
X = series.values
r = read_csv('dataset.csv')
"""
1964-01-01 2815
0 1964-02-01 2672
1 1964-03-01 2755
2 1964-04-01 2721
3 1964-05-01 2946
4 1964-06-01 3036
"""
r = r.values
X = X.astype('float32')
# difference data
months_in_year = 12
stationary = difference(X, months_in_year)
stationary.index = series.index[months_in_year:]
# check if stationary
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))
# save
stationary.to_csv('stationary.csv')
# plot
stationary.plot()
pyplot.show()
运行该示例输出差异系列是否静止的统计显着性检验的结果。具体来说,增强Dickey-Fuller测试。
结果表明,检验统计值-7.134898小于临界值1%-3.515。这表明我们可以拒绝具有小于1%的显着性水平的零假设(即结果是统计学侥幸的低概率)。
拒绝原假设意味着该过程没有单位根,反过来,时间序列是静止的或没有时间依赖的结构。
ADF Statistic: -7.134898
p-value: 0.000000
Critical Values:
5%: -2.898
1%: -3.515
10%: -2.586
作为参考,可以通过添加前一年的同一月份的观察来反转季节性差异操作。在通过适合季节性差异数据的模型进行预测的情况下需要这样做。下面列出了反转季节性差异操作的功能的完整性。
# invert differenced value
def inverse_difference(history, yhat, interval=1):
return yhat + history[-interval]
还创建了差异数据集的图。
该图未显示任何明显的季节性或趋势,表明季节性差异的数据集是建模的良好起点。
我们将使用此数据集作为ARIMA模型的输入。它还表明不需要进一步的差分,并且d参数可以设置为0。
季节性差异香槟销售线图
下一步是分别为自回归(AR)和移动平均(MA)参数p和q选择滞后值。
我们可以通过查看自相关函数(ACF)和部分自相关函数(PACF)图来做到这一点。
注意,我们现在使用季节性差异的stationary.csv作为我们的数据集。
下面的示例为该系列创建了ACF和PACF图。
from pandas import Series
from statsmodels.graphics.tsaplots import plot_acf
from statsmodels.graphics.tsaplots import plot_pacf
from matplotlib import pyplot
series = Series.from_csv('stationary.csv')
pyplot.figure()
# 当前子图为两行一列的第一行
pyplot.subplot(211)
# 画出输入序列的ACF图
plot_acf(series, ax=pyplot.gca())
# 当前子图为两行一列的第二行
pyplot.subplot(212)
# 画出输入序列的PACF图
plot_pacf(series, ax=pyplot.gca())
pyplot.show()
运行该示例并查看绘图,以获得有关如何为ARIMA模型设置p和q变量的见解。
以下是该图的一些观察结果。
p和q值的良好起点也是1。
PACF图还表明差异数据中仍存在一些季节性。
我们可能会考虑更好的季节性模型,例如直接建模并明确地将其从模型中移除而不是季节性差异。
ACF和PACF季节性差异香槟销售情节
这种快速分析表明固定数据的ARIMA(1,0,1)可能是一个很好的起点。
在拟合每个ARIMA模型之前,历史观察将在季节上有所不同。对于所有预测,差异将被反转,以使它们与原始销售计数单位中的预期观察直接相当。
实验表明,ARIMA的这种配置不会收敛并导致底层库出错。进一步的实验表明,在静止数据中增加一级差分使模型更稳定。该模型可以扩展到ARIMA(1,1,1)。
我们还将禁用从模型中自动添加趋势常量,方法是将' trend '参数设置为' nc ',以便在调用fit()时不设置常量。通过实验,我发现这可以在某些问题上产生更好的预测性能。
下面的示例演示了此ARIMA模型在测试工具上的性能。
from pandas import Series
from sklearn.metrics import mean_squared_error
from statsmodels.tsa.arima_model import ARIMA
from math import sqrt
# 根据给出的步长interval,创建一个差分序列,间隔12个数字相减
def difference(dataset, interval=1):
diff = list()
for i in range(interval, len(dataset)):
value = dataset[i] - dataset[i - interval]
diff.append(value)
return diff
# 还原差分值
def inverse_difference(history, yhat, interval=1):
return yhat + history[-interval]
# load data
series = Series.from_csv('dataset.csv')
# prepare data
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)):
# difference data
months_in_year = 12
# 获得差分剔除季节性后的新序列
diff = difference(history, months_in_year)
# 用新序列训练模型
model = ARIMA(diff, order=(1,1,1))
# 禁用从模型中自动添加趋势常量
model_fit = model.fit(trend='nc', disp=0)
# 获取预测结果
yhat = model_fit.forecast()[0]
# 将预测后的结果反差分
yhat = inverse_difference(history, yhat, months_in_year)
# 保存预测值
predictions.append(yhat)
# 获取真实值
obs = test[i]
history.append(obs)
# 对比预测结果
print('>Predicted=%.3f, Expected=%3.f' % (yhat, obs))
# 获取误差分析报告
mse = mean_squared_error(test, predictions)
rmse = sqrt(mse)
print('RMSE: %.3f' % rmse)
注意,您可能会看到来自底层线性代数库的警告消息; 现在可以忽略这一点。
运行此示例会导致RMSE为956.942,这明显优于3186.501的持久性RMSE。
...
>Predicted=3157.018, Expected=5010
>Predicted=4615.082, Expected=4874
>Predicted=4624.998, Expected=4633
>Predicted=2044.097, Expected=1659
>Predicted=5404.428, Expected=5951
RMSE: 956.942
这是一个很好的开始,但我们可以通过更好的配置ARIMA模型获得改进的结果。
ACF和PACF图显示ARIMA(1,0,1)可能是我们能做的最好的。
为了确认这一分析,我们可以对一组ARIMA超参数进行网格搜索,并检查没有模型可以获得更好的样本外RMSE性能。
在本节中,我们将搜索组合的p,d和q的值(跳过那些未收敛的组合),并找到在测试集上获得最佳性能的组合。我们将使用网格搜索来探索整数值子集中的所有组合。
具体来说,我们将搜索以下参数的所有组合:
这是(7 * 3 * 7)或147次潜在的测试工具运行,需要一些时间才能执行。
评估具有12或13滞后的MA模型可能是有趣的,因为通过回顾ACF和PACF图可能会感兴趣。实验表明这些模型可能不稳定,导致基础数学库中的错误。
下面列出了测试工具的网格搜索版本的完整工作示例。
import warnings
from pandas import Series
from statsmodels.tsa.arima_model import ARIMA
from sklearn.metrics import mean_squared_error
from math import sqrt
import numpy
# 根据给出的步长interval,创建一个差分序列,间隔12个数字相减
def difference(dataset, interval=1):
diff = list()
for i in range(interval, len(dataset)):
value = dataset[i] - dataset[i - interval]
diff.append(value)
return numpy.array(diff)
# 还原差分值
def inverse_difference(history, yhat, interval=1):
return yhat + history[-interval]
# 评估order(p,d,q)的ARIMA模型并返回RMSE
def evaluate_arima_model(X, arima_order):
# prepare training dataset
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]
# make predictions
predictions = list()
for t in range(len(test)):
# difference data
months_in_year = 12
diff = difference(history, months_in_year)
model = ARIMA(diff, order=arima_order)
model_fit = model.fit(trend='nc', disp=0)
yhat = model_fit.forecast()[0]
yhat = inverse_difference(history, yhat, months_in_year)
predictions.append(yhat)
history.append(test[t])
# calculate out of sample error
mse = mean_squared_error(test, predictions)
rmse = sqrt(mse)
return rmse
# 评估ARIMA模型的p,d和q值的组合,获取最小的RMSE数值
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:
mse = evaluate_arima_model(dataset, order)
if mse < best_score:
best_score, best_cfg = mse, order
print('ARIMA%s RMSE=%.3f' % (order,mse))
except:
continue
print('Best ARIMA%s RMSE=%.3f' % (best_cfg, best_score))
# load dataset
series = Series.from_csv('dataset.csv')
# evaluate parameters
p_values = range(0, 7)
d_values = range(0, 3)
q_values = range(0, 7)
warnings.filterwarnings("ignore")
evaluate_models(series.values, p_values, d_values, q_values)
运行该示例将遍历所有组合,并在无错误收敛的情况下报告结果。该示例需要2分多钟才能在现代硬件上运行。
结果显示,发现的最佳配置是ARIMA(0,0,1),RMSE为939.464,略低于上一节中手动配置的ARIMA。这种差异可能是也可能不具有统计学意义。
...
ARIMA(5, 1, 2) RMSE=1003.200
ARIMA(5, 2, 1) RMSE=1053.728
ARIMA(6, 0, 0) RMSE=996.466
ARIMA(6, 1, 0) RMSE=1018.211
ARIMA(6, 1, 1) RMSE=1023.762
Best ARIMA(0, 0, 1) RMSE=939.464
我们将继续选择此ARIMA(0,0,1)模型。
对模型进行良好的最终检查是检查残差预测误差。
理想情况下,残差的分布应该是具有零均值的高斯分布。
我们可以使用汇总统计和图来检查这一点,以调查ARIMA(0,0,1)模型中的残差。以下示例计算并总结了残差预测误差。
from pandas import Series
from pandas import DataFrame
from statsmodels.tsa.arima_model import ARIMA
from matplotlib import pyplot
# 根据给出的步长interval,创建一个差分序列,间隔12个数字相减
def difference(dataset, interval=1):
diff = list()
for i in range(interval, len(dataset)):
value = dataset[i] - dataset[i - interval]
diff.append(value)
return diff
# 还原差分值
def inverse_difference(history, yhat, interval=1):
return yhat + history[-interval]
# load data
series = Series.from_csv('dataset.csv')
# prepare data
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)):
# difference data
months_in_year = 12
# 获得差分剔除季节性后的新序列
diff = difference(history, months_in_year)
# predict
# 用新序列训练模型
model = ARIMA(diff, order=(0,0,1))
# 禁用从模型中自动添加趋势常量
model_fit = model.fit(trend='nc', disp=0)
# 获取预测结果
yhat = model_fit.forecast()[0]
# 将预测后的结果反差分
yhat = inverse_difference(history, yhat, months_in_year)
# 保存预测值
predictions.append(yhat)
# observation
obs = test[i]
history.append(obs)
# errors
# 所有的 (测试值-预测值),获得残差值,
# 对残差值进行分布观测,看是否符合高斯分布,残差均值mean与0的距离,准备下一步的修正
residuals = [test[i]-predictions[i] for i in range(len(test))]
residuals = DataFrame(residuals)
print(residuals.describe())
# plot
pyplot.figure()
pyplot.subplot(211)
residuals.hist(ax=pyplot.gca())
pyplot.subplot(212)
residuals.plot(kind='kde', ax=pyplot.gca())
pyplot.show()
首先运行该示例描述了残差的分布。
我们可以看到分布有一个正确的偏移,并且平均值在165.904728处非零。
这可能是预测偏向的一个信号。
count 47.000000
mean 165.904728
std 934.696199
min -2164.247449
25% -289.651596
50% 191.759548
75% 732.992187
max 2367.304748
还绘制了残差的分布。
这些图表显示了具有崎岖左尾的高斯分布,进一步证明了可能值得探索的功率变换。
剩余预测误差密度图
我们可以通过将每个预测的平均残差165.904728添加到偏差校正预测来使用此信息。
以下示例执行此偏差关联。
from pandas import Series
from pandas import DataFrame
from statsmodels.tsa.arima_model import ARIMA
from matplotlib import pyplot
from sklearn.metrics import mean_squared_error
from math import sqrt
# 根据给出的步长interval,创建一个差分序列,间隔12个数字相减
def difference(dataset, interval=1):
diff = list()
for i in range(interval, len(dataset)):
value = dataset[i] - dataset[i - interval]
diff.append(value)
return diff
# 还原差分值
def inverse_difference(history, yhat, interval=1):
return yhat + history[-interval]
# load data
series = Series.from_csv('dataset.csv')
# prepare data
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()
bias = 165.904728
for i in range(len(test)):
# difference data
months_in_year = 12
diff = difference(history, months_in_year)
# predict
model = ARIMA(diff, order=(0,0,1))
model_fit = model.fit(trend='nc', disp=0)
yhat = model_fit.forecast()[0]
# 预测后的数据加上残差均值进行修正
yhat = bias + inverse_difference(history, yhat, months_in_year)
predictions.append(yhat)
# observation
obs = test[i]
history.append(obs)
# report performance
mse = mean_squared_error(test, predictions)
rmse = sqrt(mse)
print('RMSE: %.3f' % rmse)
# errors
residuals = [test[i]-predictions[i] for i in range(len(test))]
residuals = DataFrame(residuals)
print(residuals.describe())
# plot
pyplot.figure()
pyplot.subplot(211)
residuals.hist(ax=pyplot.gca())
pyplot.subplot(212)
residuals.plot(kind='kde', ax=pyplot.gca())
pyplot.show()
预测的表现从939.464略微改善至924.699,这可能是也可能不是很重要。
预测残差的总结表明,平均值确实被移动到非常接近零的值。
RMSE: 924.699
count 4.700000e+01
mean 4.965016e-07
std 9.346962e+02
min -2.330152e+03
25% -4.555563e+02
50% 2.585482e+01
75% 5.670875e+02
max 2.201400e+03
最后,剩余误差的密度图确实显示向零的小偏移。
这种偏差校正是否值得,值得商榷,但我们现在将使用它。
偏差校正残差预测误差密度图
检查任何类型的自相关的残差的时间序列也是一个好主意。如果存在,则表明该模型有更多机会对数据中的时间结构进行建模。
以下示例重新计算残差,并创建ACF和PACF图以检查是否存在任何显着的自相关。
from pandas import Series
from pandas import DataFrame
from statsmodels.tsa.arima_model import ARIMA
from matplotlib import pyplot
from statsmodels.graphics.tsaplots import plot_acf
from statsmodels.graphics.tsaplots import plot_pacf
# create a differenced series
def difference(dataset, interval=1):
diff = list()
for i in range(interval, len(dataset)):
value = dataset[i] - dataset[i - interval]
diff.append(value)
return diff
# invert differenced value
def inverse_difference(history, yhat, interval=1):
return yhat + history[-interval]
# load data
series = Series.from_csv('dataset.csv')
# prepare data
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)):
# difference data
months_in_year = 12
diff = difference(history, months_in_year)
# predict
model = ARIMA(diff, order=(0,0,1))
model_fit = model.fit(trend='nc', disp=0)
yhat = model_fit.forecast()[0]
yhat = inverse_difference(history, yhat, months_in_year)
predictions.append(yhat)
# observation
obs = test[i]
history.append(obs)
# errors
residuals = [test[i]-predictions[i] for i in range(len(test))]
residuals = DataFrame(residuals)
print(residuals.describe())
# plot
pyplot.figure()
pyplot.subplot(211)
plot_acf(residuals, ax=pyplot.gca())
pyplot.subplot(212)
plot_pacf(residuals, ax=pyplot.gca())
pyplot.show()
结果表明,该模型捕获了时间序列中存在的少量自相关。
剩余预测误差ACF和PACF图
在开发模型并选择最终模型后,必须对其进行验证和最终确定。
验证是流程的可选部分,但提供“最后检查”以确保我们没有被欺骗或误导自己。
本节包括以下步骤:
最终确定模型涉及在整个数据集上拟合ARIMA模型,在这种情况下,在整个数据集的变换版本上。
一旦适合,模型可以保存到文件中供以后使用。
下面的示例在数据集上训练ARIMA(0,0,1)模型,并将整个拟合对象和偏差保存到文件中。
当前稳定版本的statsmodels库(v0.6.1)中存在一个错误,当您尝试从文件加载已保存的ARIMA模型时会导致错误。报告的错误是:
TypeError: __new__() takes at least 3 arguments (1 given)
当我测试它时,这个bug似乎也出现在statsmodels的0.8版本候选版本1中。有关更多详细信息,请参阅Zae Myung Kim的讨论并修复此GitHub问题。
我们可以使用一个猴子补丁解决这个问题,该补丁在保存之前将一个__getnewargs __()实例函数添加到ARIMA类。
下面的示例将拟合模型保存到处于正确状态的文件,以便以后可以成功加载。
from pandas import Series
from statsmodels.tsa.arima_model import ARIMA
from scipy.stats import boxcox
import numpy
# monkey patch around bug in ARIMA class
def __getnewargs__(self):
return ((self.endog),(self.k_lags, self.k_diff, self.k_ma))
ARIMA.__getnewargs__ = __getnewargs__
# 计算差分序列
def difference(dataset, interval=1):
diff = list()
for i in range(interval, len(dataset)):
value = dataset[i] - dataset[i - interval]
diff.append(value)
return diff
# load data
series = Series.from_csv('dataset.csv')
# prepare data
X = series.values
X = X.astype('float32')
# 数据差分
months_in_year = 12
diff = difference(X, months_in_year)
# 训练模型
model = ARIMA(diff, order=(0,0,1))
model_fit = model.fit(trend='nc', disp=0)
# 偏差常数,可以从样本内平均残差计算得出
bias = 165.904728
# save model
model_fit.save('model.pkl')
# 将残差作为一个参数存储起来
numpy.save('model_bias.npy', [bias])
运行该示例将创建两个本地文件:
一个自然的例子可能是加载模型并进行单一预测。
这是相对简单的,包括恢复保存的模型和偏差并调用forecast()方法。要反转季节差异,还必须加载历史数据。
下面的示例加载模型,对下一个时间步进行预测,并打印预测。
from pandas import Series
from statsmodels.tsa.arima_model import ARIMAResults
import numpy
# invert differenced value
def inverse_difference(history, yhat, interval=1):
return yhat + history[-interval]
series = Series.from_csv('dataset.csv')
months_in_year = 12
# 加载之前保存的模型参数文件
model_fit = ARIMAResults.load('model.pkl')
# 加载误差值
bias = numpy.load('model_bias.npy')
yhat = float(model_fit.forecast()[0])
# 预测结果加上误差值作为新的预测结果
yhat = bias + inverse_difference(series.values, yhat, months_in_year)
print('Predicted: %.3f' % yhat)
运行该示例打印约6794的预测。
Predicted: 6794.773
如果我们查看validation.csv,我们可以看到下一个时间段第一行的值是6981。
预测是在正确的球场。
我们可以加载模型并以假装操作方式使用它。
在测试工具部分中,我们将原始数据集的最后12个月保存在单独的文件中以验证最终模型。
我们现在可以加载这个validation.csv文件并使用它来查看我们的模型在“看不见的”数据上的真实情况。
我们可以通过两种方式进行:
与前几节中的模型评估一样,我们将以滚动预测的方式进行预测。这意味着我们将在验证数据集中逐步超过提前期,并将观察结果作为历史记录的更新。
from pandas import Series
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
import numpy
# create a differenced series
def difference(dataset, interval=1):
diff = list()
for i in range(interval, len(dataset)):
value = dataset[i] - dataset[i - interval]
diff.append(value)
return diff
# invert differenced value
def inverse_difference(history, yhat, interval=1):
return yhat + history[-interval]
# load and prepare datasets
dataset = Series.from_csv('dataset.csv')
X = dataset.values.astype('float32')
history = [x for x in X]
months_in_year = 12
validation = Series.from_csv('validation.csv')
y = validation.values.astype('float32')
# load model
model_fit = ARIMAResults.load('model.pkl')
bias = numpy.load('model_bias.npy')
# make first prediction
predictions = list()
yhat = float(model_fit.forecast()[0])
yhat = bias + inverse_difference(history, yhat, months_in_year)
predictions.append(yhat)
history.append(y[0])
print('>Predicted=%.3f, Expected=%3.f' % (yhat, y[0]))
# rolling forecasts
for i in range(1, len(y)):
# 差分
months_in_year = 12
diff = difference(history, months_in_year)
# predict
# 配置模型参数
model = ARIMA(diff, order=(0, 0, 1))
# 训练模型
model_fit = model.fit(trend='nc', disp=0)
yhat = model_fit.forecast()[0]
# 获得预测值,保存和修正预测值
yhat = bias + inverse_difference(history, yhat, months_in_year)
predictions.append(yhat)
# observation
obs = y[i]
history.append(obs)
print('>Predicted=%.3f, Expected=%3.f' % (yhat, obs))
# report performance
mse = mean_squared_error(y, predictions)
rmse = sqrt(mse)
print('RMSE: %.3f' % rmse)
pyplot.plot(y)
pyplot.plot(predictions, color='red')
pyplot.show()
运行该示例将打印验证数据集中时间步长的每个预测值和预期值。
验证期的最终RMSE预计为361.110万销售额。
这比预计每月销售额超过9.24亿美元的预期要好得多。
>Predicted=6794.773, Expected=6981
>Predicted=10101.763, Expected=9851
>Predicted=13219.067, Expected=12670
>Predicted=3996.535, Expected=4348
>Predicted=3465.934, Expected=3564
>Predicted=4522.683, Expected=4577
>Predicted=4901.336, Expected=4788
>Predicted=5190.094, Expected=4618
>Predicted=4930.190, Expected=5312
>Predicted=4944.785, Expected=4298
>Predicted=1699.409, Expected=1413
>Predicted=6085.324, Expected=5877
RMSE: 361.110
还提供了与验证数据集相比的预测图。
在这个规模上,12个月的预测销售数据看起来很棒。
香槟销售验证数据集预测