3.4 评估预测精度
训练集和数据集
使用真实的预测来评估预测精度是很重要的。因此,残差的大小并不能很好地表示真实预测误差的大小。而评估模型预测精度只能通过模型在新数据中的预测效果决定。
在选择模型时,通常我们会将可用数据分成训练数据集和测试数据集两部分,其中训练数据集用于估计预测方法中的任意参数,而测试数据集用于评估预测精度。由于训练数据集并没有用于确定预测模型,因此它能可靠地检验模型对于新数据的预测准确性。
测试数据集的大小取决于总样本集的大小和所希望的预测的距离,通常为总样本集的20%。理想状况下,测试数据集至少和最大预测区间一样长。读者应注意以下几点:
- 一个适合训练数据集的预测模型不一定能完美预测其他数据集。
- 使用足够多参数的模型总能得到完美的拟合。
- 对数据的过度拟合与未能识别数据的系统模式都是不可取的。
一些参考资料会把测试数据集叫做“保留集”,因为这部分数据是为了更好地拟合模型而被保留下来用于评估预测精度的。也有的参考书把测试数据集叫做“样本内数据”,把训练数据集叫做“样本外数据”,本书则更习惯使用测试数据集和训练数据集。
时间序列子集函数
在我们需要提取一个时间序列中的一部分时,第2章中所介绍的window()
函数将非常有用,例如我们在产生训练数据集和测试数据集时,我们就需要使用window()
函数。在这个函数中,我们需要使用时间值来指定所需时间序列部分的开始和结束。下面是代码示例:
window(ausbeer, start=1995)
上述代码表示抓取从1995年开始的全部数据。
另一个非常有用的函数是subset()
函数,它可以允许构造更多类型的子集。这个函数最大的优点是它允许使用索引来选择子集。下面是代码示例:
subset(ausbeer, start=length(ausbeer)-4*5)
上述代码表示从ausbeer
这个数据集中提取过去5年的观测值。除此之外,subset()
函数还允许提取特定时间的所有观测,下面是代码示例:
subset(ausbeer, quarter = 1)
上述代码表示取出所有年份第一季度的数据
最后,head()
函数和tail()
函数主要用于取出前几个或后几个观测。例如,上文中的从ausbeer
这个数据集中取出过去5年的观测也可以使用以下代码实现:
tail(ausbeer, 4*5)
预测误差
预测误差是指实际值与预测值的差,这里的误差并非意味着错误,它指的是观测中不可预知的部分,可以如下表示: \[ e_{T+h} = y_{T+h} - \hat{y}_{T+h|T}, \] 用\(\{y_1,\dots,y_T\}\) 表示训练集,用 \(\{y_{T+1},y_{T+2},\dots\}\)表示测试集。
注意,预测误差与残差的区别有两点:第一,残差是在训练数据集上计算得到的,而预测误差是在测试数据集上计算得到的;第二,残差仅仅是基于一步预测得到的,而预测误差可能是涉及多步预测。
我们可以使用不同的方式去总结预测误差,进而衡量预测的准确性。
尺度效应误差
预测误差与数据的标度相同,因此,仅基于\(e_{t}\)的预测精度测量是依赖标度的,不能用于比较包含不同单位的时间序列。
测量尺度效应(依赖于标度)最常使用的两种方法都是基于绝对误差和平方误差的。 \[\begin{align*} \text{平均绝对误差(Mean absolute error: MAE)} & = \text{mean}(|e_{t}|),\\ \text{均方根误差(Root mean squared error: RMSE)} & = \sqrt{\text{mean}(e_{t}^2)}. \end{align*}\] 当我们将这两种预测方法应用于单个时间序列或者多个具有相同单位的时间序列时,通过对比,我们可以发现,MAE方法更流行,因为这种方法不仅容易被理解,而且便于计算。使MAE最小化的预测方法的预测结果是预测分布的中位数,而使RMSE最小化的预测方法的预测结果是预测分布的平均值。因此,RMSE方法尽管在解释方面有些困难,但使用范围非常广。
百分比误差
百分比误差的计算公式是 \(p_{t} = 100 e_{t}/y_{t}\)。百分误差最大的优点是无单位,因此常常被用来比较不同数据集之间的预测性能。最常用的预测方法是: \[ \text{均方绝对百分比误差(Mean absolute percentage error: MAPE)} = \text{mean}(|p_{t}|). \] 基于百分误差的计算方法有两个缺点:第一,如果在我们感兴趣的时间段内某个\(t\)使得\(y_{t}=0\),则计算结果是无穷或者是无意义的;第二,如果任何\(y_{t}\)值趋近于0,则计算结果有极值。百分误差还有一个常常被忽视的缺点是其假定度量单位是一个有意义的零。2例如,在测量华氏温度或者摄氏温度的温度预测准确性时,百分误差是没有意义的,因为温度具有任意的零点。
这种测量方法还有一个缺点,就是其对负的误差的惩罚比对正的误差要重,因此 Armstrong (1978, p. 348) 提出了对称MAPE法(简称sMAPE法)对这个缺点进行了改进,还曾在M3的预测竞赛中使用了这种方法,其计算公式是: \[ \text{sMAPE} = \text{mean}\left(200|y_{t} - \hat{y}_{t}|/(y_{t}+\hat{y}_{t})\right). \] 然而,如果\(y_{t}\) 值趋近于0,则\(\hat{y}_{t}\)的预测值也可能趋近于0.因此,这种计算方法还是有可能除以一个趋近于0的数,让计算结果不够稳定。除此之外,sMAPE的值也可能是负数,因此,这种衡量方法并不是一个真正意义上能够衡量“绝对百分误差”的方法。
Hyndman & Koehler (2006) 在2006年建议不要使用sMAPE方法,本书之所以讲解这种方法是因为这种方法被广泛使用着,但在本书中,我们不会使用这种方法。
比例误差(scaled errors)
Hyndman & Koehler (2006) 在2006年提出了比例误差的概念,用于替代百分误差去衡量具有不同单位的时间序列的预测准确性。他们的方法思路是通过训练来自简单预测方法的MAE去修正误差。
对于非周期性时间序列,可以使用下面的naïve方法来定义比例误差: \[ q_{j} = \frac{\displaystyle e_{j}} {\displaystyle\frac{1}{T-1}\sum_{t=2}^T |y_{t}-y_{t-1}|}. \] 因为上式中的分子和分母都包含了在原始数据标度上的值,而\(q_{j}\)是一个与数据标度无关的值。如果选用一个比平均naïve预测方法更好的预测方法,那么比例误差会小于1;相对地,如果选用了比平均naïve预测方法更差的预测方法,那么比例误差会大于1。
对于季节性时间序列,可以使用下面的周期性naïve方法来定义比例误差: \[ q_{j} = \frac{\displaystyle e_{j}} {\displaystyle\frac{1}{T-m}\sum_{t=m+1}^T |y_{t}-y_{t-m}|}. \]
平均绝对比例误差 如下表示: \[ \text{MASE} = \text{mean}(|q_{j}|). \]
例子
<- window(ausbeer,start=1992,end=c(2007,4))
beer2 <- meanf(beer2,h=10)
beerfit1 <- rwf(beer2,h=10)
beerfit2 <- snaive(beer2,h=10)
beerfit3 autoplot(window(ausbeer, start=1992)) +
autolayer(beerfit1$mean, series="均值") +
autolayer(beerfit2$mean, series="Naïve") +
autolayer(beerfit3$mean, series="季节性naïve") +
xlab("年份") + ylab("百万升") +
ggtitle("啤酒季度产量预测") +
guides(colour=guide_legend(title="预测")) +
theme(text = element_text(family = "STHeiti")) +
theme(plot.title = element_text(hjust = 0.5))

图 3.8: 利用2007年年底相关数据预测澳大利亚啤酒季度产量。
图 3.8 显示的是使用2007年年底相关数据预测澳大利亚啤酒季度产量的三种方法。还显示了2008至2010期间的实际产量值。可以看出,我们计算出了这一时期的预测准确度。
<- window(ausbeer, start=2008)
beer3 accuracy(beerfit1, beer3)
accuracy(beerfit2, beer3)
accuracy(beerfit3, beer3)
RMSE | MAE | MAPE | MASE | |
---|---|---|---|---|
平均值法 | 38.45 | 34.83 | 8.28 | 2.44 |
Naïve方法 | 62.69 | 57.40 | 14.18 | 4.01 |
季节性naïve方法 | 14.31 | 13.40 | 3.17 | 0.94 |
由图可知,对于这些数据而言,周期性naïve方法是最好的;但之后我们就会发现,这种方法仍然能够被改进。有时,当我们需要去选择其中最佳的预测方法时,发现不同的精确度测量方法会产生不同的结果.但在这个例子中,所有的结果显示季节性naïve方法是三种方法中最好的。
下面举一个非季节性数据的例子,考虑谷歌的股票价格。下图是截止2013年12月6日之前的观测数据,并用三种方法对之后40天股市收盘价进行预测的结果。
<- meanf(goog200, h=40)
googfc1 <- rwf(goog200, h=40)
googfc2 <- rwf(goog200, drift=TRUE, h=40)
googfc3 autoplot(subset(goog, end = 240)) +
autolayer(googfc1, PI=FALSE, series="均值") +
autolayer(googfc2, PI=FALSE, series="Naïve") +
autolayer(googfc3, PI=FALSE, series="漂移") +
xlab("天") + ylab("收盘价(美元)") +
ggtitle("谷歌公司每日股价(截止至2013年12月6日)") +
guides(colour=guide_legend(title="预测"))+
theme(text = element_text(family = "STHeiti"))+
theme(plot.title = element_text(hjust = 0.5))

图 3.9: 从2013年12月7日开始对于谷歌股价的预测。
<- window(goog, start=201, end=240)
googtest accuracy(googfc1, googtest)
accuracy(googfc2, googtest)
accuracy(googfc3, googtest)
RMSE | MAE | MAPE | MASE | |
---|---|---|---|---|
均值法 | 114.21 | 113.27 | 20.32 | 30.28 |
Naïve方法 | 28.43 | 24.59 | 4.36 | 6.57 |
漂移法 | 14.08 | 11.67 | 2.07 | 3.12 |
在此例中,最好的方法是趋势法。(不管采用哪种精度度量)
时间序列交叉验证
更加复杂的训练数据集和测试数据集叫做时间序列交叉验证。在这个过程中,有一系列的测试数据集,每个测试数据集都由一个单独的观测值组成。相对应的训练测试集仅包含组成测试数据集的观测值之前发生的观测值。因此,没有未来的观测值能够被用来构建预测。由于在一个较小的训练数据集中得到可靠的预测是不可能的,所以最早的观测值不视为测试数据集。
下面的图演示了一系列训练数据集和测试数据集,其中蓝色的观测点组成训练数据集,红色的观测点组成测试数据集。
预测精度是通过测试数据集中观测值的平均值得到的,这个过程有时被称为“评估滚动原点预测”,因为预测所依据的“原点”是及时向前推进的。
在时间序列预测中,一步预测可能不如多步预测相关性那么高,在这种情况下,基于滚动预测原点的交叉验证过程可以通过使用多步误差进行改进。假设我们对于能够尝试良好四步预测的模型感兴趣,那么相应的图如下所示:
时间序列的交叉验证可以使用tsCV()
函数来实现,在下面的代码示例中,我们比较了通过时间序列的交叉验证过程得到的RMSE和通过残差得到的RMSE.
<- tsCV(goog200, rwf, drift=TRUE, h=1)
e sqrt(mean(e^2, na.rm=TRUE))
#> [1] 6.233
sqrt(mean(residuals(rwf(goog200, drift=TRUE))^2, na.rm=TRUE))
#> [1] 6.169
正如所预期的那样,通过残差计算得到的RMSE较小,说明相应的“预测”是基于拟合到整个数据集的模型的拟合值,并非真正的预测值。 选择最佳预测模型的一个好方法是选择用时间序列交叉过程计算得出RMSE最小的模型。
管道(Pipe)操作
上述的R代码有些难看,所以这里我们介绍一些把R函数串在一起的其他方法。在上述代码中,我们在函数中的函数嵌套函数,所以你必须从内到外的读取代码,这使得我们很难理解正在计算的到底是什么。因此我们可以使用管道操作符%>%
进行替代,代码如下:
%>% tsCV(forecastfunction=rwf, drift=TRUE, h=1) -> e
goog200 ^2 %>% mean(na.rm=TRUE) %>% sqrt()
e#> [1] 6.233
%>% rwf(drift=TRUE) %>% residuals() -> res
goog200 ^2 %>% mean(na.rm=TRUE) %>% sqrt()
res#> [1] 6.169
每个管道的左边作为第一个参数传递给右边的函数。这与我们读英语从左到右读的习惯相一致。使用管道的一个重要前提是必须指定所有其他参数,同时这也有助于提高可读性。当使用管道时,使用右箭头赋值->
而不是使用左箭头赋值是很自然的。例如,上述代码的第三行可以被读作把goog200
这个参数,以drift=TRUE
这个条件传递给rwf()
函数,计算得到的残差结果,存储为res。
在本书的其余部分,我们将使用管道操作符让代码变得易于阅读。为了保持整体一致,即使函数没有参数,我们也将始终使用括号来区分函数与其他对象。正如上面我们所看到的,我们对sqrt()
函数和residuals()
函数的表示。
示例:使用tsCV()
函数
绘制在图 3.4中的goog200
数据包括了在纳斯达克交易所从2013年2月25日开始连续200个交易日的谷歌公司每日股市收盘价。
下面的代码将使用tsCV()
函数来评估用naïve方法进行1步到8步预测的预测性能,使用MSE来衡量预测误差。根据我们的预期,这个图表明预测误差会随着预测区间的增大而增大。
<- tsCV(goog200, forecastfunction=naive, h=8)
e # Compute the MSE values and remove missing values
<- colMeans(e^2, na.rm = T)
mse # Plot the MSE values against the forecast horizon
data.frame(h = 1:8, MSE = mse) %>%
ggplot(aes(x = h, y = MSE)) + geom_point()
参考文献
也就是说,百分比在定比型数据上是有意义的,但在定距型数据上是不成立的。因为只有定比型数据具有有意义的零。↩︎