使用未观测分量模型模拟时间序列

7

使用statsmodelsUnobservedComponents拟合本地级别模型后,我们正在尝试找到使用结果模拟新时间序列的方法。类似于:

import numpy as np
import statsmodels as sm
from statsmodels.tsa.statespace.structural import UnobservedComponents

np.random.seed(12345)
ar = np.r_[1, 0.9]
ma = np.array([1])
arma_process = sm.tsa.arima_process.ArmaProcess(ar, ma)

X = 100 + arma_process.generate_sample(nsample=100)
y = 1.2 * x + np.random.normal(size=100)
y[70:] += 10

plt.plot(X, label='X')
plt.plot(y, label='y')
plt.axvline(69, linestyle='--', color='k')
plt.legend();

time series example

ss = {}
ss["endog"] = y[:70]
ss["level"] = "llevel"
ss["exog"] = X[:70]

model = UnobservedComponents(**ss)
trained_model = model.fit()

是否可以使用trained_model来模拟给定外生变量X[70:]的新时间序列?就像我们有arma_process.generate_sample(nsample=100)一样,我们想知道是否可以做类似的事情:

trained_model.generate_random_series(nsample=100, exog=X[70:])

这样做的动机是为了计算时间序列与观察到的 y[70:] 一样极端的概率(用于确定响应大于预测值的p值)。 [编辑] 在阅读Josef和cfulton的评论后,我尝试实现以下内容:
mod1 = UnobservedComponents(np.zeros(y_post), 'llevel', exog=X_post)
mod1.simulate(f_model.params, len(X_post))

但是这导致模拟似乎无法跟踪X_post作为exog的predicted_mean预测结果。以下是一个例子:

enter image description here

y_post徘徊在100左右时,模拟值为-400。这种方法总是导致p_value为50%。
因此,当我尝试使用initial_state=0和随机冲击时,结果如下:

enter image description here

现在似乎模拟正在遵循预测的平均值及其95%的置信区间(正如cfulton在下面评论的那样,这实际上是一种错误的方法,因为它替换了训练模型的级别方差)。
我尝试使用这种方法来观察我会得到什么p值。以下是我计算p值的方法:
samples = 1000
r = 0
y_post_sum = y_post.sum()
for _ in range(samples):
    sim = mod1.simulate(f_model.params, len(X_post), initial_state=0, state_shocks=np.random.normal(size=len(X_post)))
    r += sim.sum() >= y_post_sum
print(r / samples)

为了说明,这是由Google开发的Causal Impact模型。由于它已经在R中实现,我们一直在尝试使用statsmodels作为核心来处理时间序列,用Python复制实现。
我们已经有了一个相当酷的WIP 实现, 但我们仍然需要p值来知道我们是否存在影响不仅仅是由于随机性(模拟系列和计算总和超过y_post.sum()的方法在Google的模型中也被实现)。
在我的示例中,我使用了y[70:] += 10。如果我只加了一个而不是十个,Google的p值计算返回0.001y存在影响),而在Python的方法中,它返回0.247(没有影响)。
只有当我将y_post加上+5时,模型才会返回p_value为0.02,并且由于低于0.05,我们认为y_post受到影响。 我正在使用python3,statsmodels版本为0.9.0。在阅读cfulton的评论后,我决定完全调试代码以查看发生了什么。这是我发现的:当我们创建一个类型为UnobservedComponents的对象时,最终启动卡尔曼滤波器的表示形式。默认情况下,它接收参数initial_variance,其设置对象的同一属性为1e6。
当我们运行simulate方法时,使用相同的值创建initial_state_cov is created
initial_state_cov = (
        np.eye(self.k_states, dtype=self.ssm.transition.dtype) *
        self.ssm.initial_variance
    )

最后,同样的值被用来查找 initial_state

initial_state = np.random.multivariate_normal(
    self._initial_state, self._initial_state_cov)

这将导致标准差为1e6的正态分布。 然后我尝试运行以下内容:
mod1 = UnobservedComponents(np.zeros(len(X_post)), level='llevel', exog=X_post, initial_variance=1)
sim = mod1.simulate(f_model.params, len(X_post))
plt.plot(sim, label='simul')
plt.plot(y_post, label='y')
plt.legend();
print(sim.sum() > y_post.sum())

这导致:

enter image description here

我测试了p值,最终发现在y_post变化1个单位时,模型现在能正确地识别添加的信号。

然而,当我使用R的Google包中相同的数据进行测试时,p值仍然不准确。也许需要进一步调整输入以提高其准确性。


@Josef 非常感谢您提供的那些链接!我会尝试一下!我之前看到过simulate方法,但是理解为只适用于模型对象而不是训练好的模型。我也可以从训练好的模型中获取参数来构建一个新模型(有点间接,但我仍然希望它能够奏效)。 - Willian Fuks
嗨@Josef,我按照你的建议进行了操作,看起来它正在工作。仍有一些问题,不知道你能否帮助我。我用更新的信息编辑了我的问题,正如你所看到的,我必须将“initial_state”设置为零,“state_shocks”设置为正常。你知道这为什么是必要的或者它意味着什么吗?我遵循了单元测试,看起来它正在工作,但不太明白为什么。 - Willian Fuks
@cfulton 我正在使用0.9.0版本,在jupyter/datascience-notebook:latest Docker镜像中运行笔记本。我已经尝试重新启动所有内容,但结果仍然与预期相差很大(有时几乎达到-1000)。 - Willian Fuks
嗨@cfulton,我刚在我的问题中添加了一个新的“EDIT2”,以讨论我在模型构建中观察到的初始值。看起来,似乎“initial_variance”接收到一个足够大的默认值,可以显著偏离模拟的“initial_state”。不过我不确定我所做的是否100%正确。 - Willian Fuks
@WillianFuks 感谢您的额外编辑,我已经编辑了我的答案以回应(对于延迟感到抱歉)。 - cfulton
显示剩余4条评论
1个回答

5

@Josef是正确的,你做得很对:

mod1 = UnobservedComponents(np.zeros(y_post), 'llevel', exog=X_post)
mod1.simulate(f_model.params, len(X_post))
simulate方法根据所涉及的模型模拟数据,这就是为什么当你有外生变量时不能直接使用trained_model进行模拟的原因。

但出于某种原因,模拟结果总是低于y_post。

我认为这是可以预料的-运行您的示例并查看估计系数,我们得到:

                       coef    std err          z      P>|z|      [0.025      0.975]
------------------------------------------------------------------------------------
sigma2.irregular     0.9278      0.194      4.794      0.000       0.548       1.307
sigma2.level         0.0021      0.008      0.270      0.787      -0.013       0.018
beta.x1              1.1882      0.058     20.347      0.000       1.074       1.303

水平方差非常小,这意味着根据您指定的模型,在单个周期内上升近10%的水平极不可能发生。

当您使用:

mod1.simulate(f_model.params, len(X_post), initial_state=0, state_shocks=np.random.normal(size=len(X_post))

发生的情况是,水平术语是唯一未观察到的状态,在提供方差等于1的自己的冲击时,您实际上覆盖了模型实际估计的水平方差。我认为在这里将初始状态设置为0并没有太大的影响。(请参见编辑)。
您写道:
“P值计算更接近了,但仍不正确。”
我不确定这是什么意思 - 为什么您希望模型认为这样的跳跃是可能发生的?您期望获得什么p值?
编辑:
感谢您进一步调查(在第二次编辑中)。首先,我认为您应该做的是:
mod1 = UnobservedComponents(np.zeros(y_post), 'llevel', exog=X_post)
initial_state = np.random.multivariate_normal(
    f_model.predicted_state[..., -1], f_model.predicted_state_cov[..., -1])
mod1.simulate(f_model.params, len(X_post), initial_state=initial_state)

现在,解释一下:
在Statsmodels 0.9中,我们还没有对具有漫散初始化的状态进行精确处理(尽管此后已经合并了这种处理方式,这也是我无法重现您的结果的原因之一,直到我使用了0.9代码库来测试您的示例)。这些“最初漫散”的状态没有我们可以求解的长期均值(例如随机游走过程),而在本地水平情况下,状态就是这样一个状态。
“近似”漫散初始化涉及将初始状态均值设为零,将初始状态方差设为一个大数(正如您发现的那样)。
对于模拟,默认情况下,初始状态是从给定的初始状态分布中抽取的。由于该模型是通过近似漫散初始化初始化的,因此这解释了为什么您的过程在某个随机数周围初始化。
您的解决方案是一个不错的补丁,但它并非最优,因为它没有基于估计模型/数据的最后状态来确定模拟期的初始状态。这些值由f_model.predicted_state[..., -1]f_model.predicted_state_cov[..., -1]给出。

嗨,cfulton,感谢您的帮助(也感谢您开发了statsmodels,我一直在研究代码并阅读文档,发现它对Python社区是多么复杂和有价值)。我已经更新了我的问题,并提供了更好的信息。当事实上y_post中存在信号时,我仍然找不到适当的p值。也许在运行模拟时我还是做错了什么。如果您需要更多信息,请告诉我。 - Willian Fuks
太棒了!非常感谢Fulton的帮助,现在它的工作方式与R的模型完全一致!由于您的支持和statsmodels,我们终于可以完全将算法移植到Python中 :)! - Willian Fuks
1
感谢您的跟进!这是一个有趣的用例,我很高兴我们解决了它。 - cfulton

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接