> For the complete documentation index, see [llms.txt](https://skywateryang.gitbook.io/timeseriesanalysis101/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://skywateryang.gitbook.io/timeseriesanalysis101/8.-mo-xing-ping-gu-he-xing-neng-kao-lv/8.1-mo-xing-ping-gu-de-kao-lv.md).

# 8.1 模型评估的考虑

前面的章节介绍了各种时间序列的算法，其中如何评价模型的好坏也是帮助我们选择模型的一个重要内容。因此本节将重点讨论模型的评估。

#### 8.1.1 如何进行预测

在进行时间序列的预测时要时刻记住，在预测当前时刻的值时不能使用未来的数据。在启示我们在做数据预处理要格外当心，例如指数平滑就会导致信息泄露。你可以做这样一个测试，对一个自回归的时间序列使用线性拟合，以及对自回归的时间序列指数平滑后使用线性拟合，你会发现平滑的程度越大，窗口越大，预测的效果就越好。这是一种非常严重的问题（术语叫做lookahead），却很容易被忽视。

为了避免这种现象，一个黄金标准是对于每个模型都应当在划分好的训练集，验证集和测试集上做回测。回测的做法是利用时间序列中的某一段构建模型，然后在历史数据中进行广泛测试，以尽可能模拟更多的情况可能性。

具体来说，可以采用类似机器学习中的交叉验证方法。假设将数据集分段，以字母ABCD排序后，构建以下的交叉验证组合，也就是训练序列每次向后滑动一段加入训练集中，测试序列每次向后滑动一段。

| 训练序列       | 测试序列 |
| ---------- | ---- |
| \[A]       | \[B] |
| \[A B]     | \[C] |
| \[A B C]   | \[D] |
| \[A B C D] | \[E] |

除此之外，还有一种交叉验证的方式，训练序列和测试序列同时向后滑动一段。

| 训练序列   | 测试序列 |
| ------ | ---- |
| \[A B] | \[C] |
| \[B C] | \[D] |
| \[C D] | \[E] |
| \[D E] | \[F] |

选择何种交叉验证方式取决于几方面因素，如果你认为你的时间序列存在随着时间演变的行为，那第二种会是更好的选择，因为模型拟合的是测试阶段和最相关数据之间的关系。另一个考虑的因素是过拟合，如果你想尽量避免过拟合，第一种方式会是更好的选择。

对于第一类划分方法，sklearn包中的TimeSeriesSplit可以实现这一效果。

```python
import numpy as np
from sklearn.model_selection import TimeSeriesSplit

X = np.array([[1, 2], [3, 4], [1, 2], [3, 4], [1, 2], [3, 4]])
y = np.array([1, 2, 3, 4, 5, 6])
tscv = TimeSeriesSplit()
# 自动划分训练和测试集
for train_index, test_index in tscv.split(X):
    print("TRAIN:", train_index, "TEST:", test_index)
    X_train, X_test = X[train_index], X[test_index]
    y_train, y_test = y[train_index], y[test_index]
```

输出训练和测试序列

```
TRAIN: [0] TEST: [1]
TRAIN: [0 1] TEST: [2]
TRAIN: [0 1 2] TEST: [3]
TRAIN: [0 1 2 3] TEST: [4]
TRAIN: [0 1 2 3 4] TEST: [5]
```

#### 8.1.2 如何知道模型足够好了

模型是否已经足够好了取决于你的总体目标时，最低实现目的的模型质量，以及数据集本身的限制。如果数据本身具有非常高的噪声，你对模型的效果相应也会降低预期。此外在模型评估时要记住该领域的专家知识通常接近于模型的性能上限。

有时你也可以通过一些现象来判断模型是否还有潜在的优化空间：

* 画出模型输出

  模型输出的分布情况应当和你预期的分布接近。例如一个预测股价的场景，从经验上看股价向上或向下的频率是接近的，如果你的模型总是预测股价向上，就有偏差了。
* 画出模型随时间变化的残差

  如果残差随时间变化也存在明显的变化，这暗示着有某些参数未被指定，导致模型没有充分拟合数据。
* 将模型效果和零模型比较

  零模型（null model）可以认为是基准模型，一个最常见的零模型是时间t的值等于时间t-1的值。如果你的模型甚至比这样一个简单的模型差，那就完全没有应用价值，你应该从模型选择，损失函数，预处理等方面来考虑而不是花时间进行超参数的网格探索。
* 研究你的模型如何处理异常值

  在很多行业，离群值就是字面意思的异常情况，是无法预测的，因此一个好的模型应该是忽略这些异常值而不是去拟合它们。如果模型对离群值拟合很好，可能就是过拟合了。
* 进行时间敏感性分析 你需要考虑的是时间序列中的定性相似行为是否会在你的模型中产生相似的结果，也就是我们应当假定模型处理相似的时间模式特征时，会采取相似的做法。例如，如果一个时间序列趋势向上，每天漂移 3 个单位，另一个呈上升趋势，每天漂移 2.9 个单位，你可能希望模型给出的预测也会呈现出类似的规律。

#### 8.1.3 如何估计不确定性

传统统计学方法的一个优势是模型参数可以得到不确定性估计。对于不确定性，我们还可以借助计算模拟方法理解预测模型中的不确定性，即使是非统计学方法也可以用计算模拟方法来分析。

一个经典的计算模拟方法是蒙特卡洛方法，其原理是通过大量随机重复试验，去了解一个系统，进而得到所要计算的值的估计。

假设我们在分析一个AR(1)序列，在已知参数为0.7的情况下模拟生成一个序列，观察模型拟合得到的估计值，重复1000次。

```python
ar = np.array([1,0.7])  
ma = np.array([1]) 
AR_object = ArmaProcess(ar, ma)

# 蒙特卡洛试验重复1000次
estimate = np.zeros(1000)

def MonteCarlo():
    # 生成一个1000
    simulated_data = AR_object.generate_sample(nsample=1000)
    mod = ARMA(simulated_data, order=(1, 0))  
    res = mod.fit()
    return -1*res.params[1]

for i in range(1000):
    estimate[i] = MonteCarlo()

plt.hist(estimate,bins=20,edgecolor='lightblue');
```

通过画出直方图可以帮助我们理解参数的估计值分布情况。

![](/files/-MiFhm_DTjjU-me-fK3l)

下面这个例子帮助我们理解用错误的参数拟合时间序列，参数是如何偏离的。

```python
# 这次是构建一个AR(2)序列，但是用AR(1)去拟合
ar = np.array([1,0.7, -0.2])  
ma = np.array([1]) 
AR_object = ArmaProcess(ar, ma)

# 蒙特卡洛试验重复1000次
estimate = np.zeros(1000)

def MonteCarlo():
    # 生成一个1000
    simulated_data = AR_object.generate_sample(nsample=1000)
    mod = ARMA(simulated_data, order=(1, 0))  
    res = mod.fit()
    return -1*res.params[1]

for i in range(1000):
    estimate[i] = MonteCarlo()

plt.hist(estimate,bins=20,edgecolor='lightblue');
```

![](/files/-MiFhnXilSFh4QBX-xWn)
