Python ARMA MLE(从文献中实现算法)

时间:2017-06-01 07:32:35

标签: python algorithm optimization kalman-filter mle

概述

我试图通过卡尔曼滤波器使用最大似然估计(MLE)来实现自回归移动平均(ARMA)参数优化。我知道我可以使用Python中的statsmodels包来适应ARMA模型,但我想编写自己的ARMA可能性实现和后续优化作为未来C / C ++实现的原型。此外,当我查看statsmodels文档时,我发现statsmodels卡尔曼滤波器对数似然实现了与我在文献中发现的略有不同的表达。

算法

为了计算ARMA对数可能性,我遵循Pearlman的1980年论文:

Pearlman,J。G.“一种高阶自回归 - 移动平均过程的精确可能性算法。” Biometrika 67.1(1980):232-233。)。 Available from JSTOR.

为了计算初始P矩阵,我遵循

中的算法

Gardner,G.,Andrew C. Harvey和Garry DA Phillips。 “算法AS 154:通过卡尔曼滤波对自回归 - 移动平均模型进行精确最大似然估计的算法。”皇家统计学会期刊。 C系列(应用统计学)29.3(1980):311-322。 Available from JSTOR.

对于初始参数值,我目前正在使用statsmodels ARMA模型用于计算ARMA参数的初始猜测的内部方法。将来我打算转到我自己的实现,但是在调试我的MLE时我正在使用_fit_starts_params。

为了优化MLE,我只是在Scipy中使用L-BFGS求解器。

代码

import numpy as np
import statsmodels.api as sm
import statsmodels.tsa.arima_model
import scipy.optimize

class ARMA(object):
    def __init__(self, endo, nar, nma):
        np.random.seed(0)
        # endogenous variables
        self.endo = endo
        # Number of AR terms
        self.nar = nar
        # Number of MA terms
        self.nma = nma
        # "Dimension" of the ARMA fit
        self.dim = max(nar, nma+1)
        # Current ARMA parameters
        self.params = np.zeros(self.nar+self.nma, dtype='float')

    def __g(self, ma_params):
        '''
        Build MA parameter vector
        '''
        g = np.zeros(self.dim, dtype='float')
        g[0] = 1.0
        if self.nma > 0:
            g[1:self.nma+1] = ma_params
        return g

    def __F(self, ar_params):
        '''
        Build AR parameter matrix
        '''
        F = np.zeros((self.dim, self.dim), dtype='float')
        F[:self.nar, 0] =  ar_params
        for i in xrange(1, self.dim):
            F[i-1, i] = 1.0
        return F

    def __initial_P(self, R, T):
        '''
        Solve for initial P matrix

        Solves P = TPT' + RR'
        '''
        v = np.zeros(self.dim*self.dim, dtype='float')
        for i in xrange(self.dim):
            for j in xrange(self.dim):
                v[i+j*self.dim] = R[i]*R[j]
        R = np.array([R])
        S = np.identity(self.dim**2, dtype='float')-np.kron(T, T)
        V = np.outer(R, R).ravel('F')
        Pmat = np.linalg.solve(S,V).reshape(self.dim, self.dim, order='F')
        return Pmat

    def __likelihood(self, params):
        '''
        Compute log likehood for a parameter vector

        Implements the Pearlman 1980 algorithm
        '''
        # these checks are pilfered from statsmodels
        if self.nar > 0 and not np.all(np.abs(np.roots(np.r_[1, -params[:self.nar]]))<1):
            print 'AR coefficients are not stationary'
        if self.nma > 0 and not np.all(np.abs(np.roots(np.r_[1, -params[-self.nma:]]))<1):
           print 'MA coefficients are not stationary'
        ar_params = params[:self.nar]
        ma_params = params[-self.nma:]
        g = self.__g(ma_params)
        F = self.__F(ar_params)
        w = self.endo
        P = self.__initial_P(g, F)
        n = len(w)
        z = np.zeros(self.dim, dtype='float')
        R = np.zeros(n, dtype='float')
        a = np.zeros(n, dtype='float')
        K = np.dot(F, P[:, 0])
        L = K.copy()
        R[0] = P[0, 0]
        for i in xrange(1, n):
            a[i-1] = w[i-1] - z[0]
            z = np.dot(F, z) + K*(a[i-1]/R[i-1])
            Kupdate = -(L[0]/R[i-1])*np.dot(F, L)
            Rupdate = -L[0]*L[0]/R[i-1]
            P -= np.outer(L, L)/R[i-1]
            L = np.dot(F, L) - (L[0]/R[i-1])*K
            K += Kupdate
            R[i] = R[i-1] + Rupdate
            if np.abs(R[i] - 1.0) < 1e-9:
                R[i:] = 1.0
                break
        for j in xrange(i, n):
            a[j] = w[j] - z[0]
            z = np.dot(F, z) + K*(a[i-1]/R[i-1])
        likelihood = 0.0
        for i in xrange(n):
            likelihood += np.log(R[i])
        likelihood *= -0.5
        ssum = 0.0
        for i in xrange(n):
            ssum += a[i]*a[i]/R[i]
        likelihood += -0.5*n*np.log(ssum)
        return likelihood

    def fit(self):
        '''
        Fit the ARMA model by minimizing the loglikehood

        Uses scipy.optimize.minimize
        '''
        sm_arma = statsmodels.tsa.arima_model.ARMA(endog=self.endo, order=(self.nar, self.nma, 0))
        params = statsmodels.tsa.arima_model.ARMA._fit_start_params_hr(sm_arma, order=(self.nar, self.nma, 0))
        opt = scipy.optimize.minimize(fun=self.__likelihood, x0=params, method='L-BFGS-B')
        print opt

# Test the code on statsmodels sunspots data
nar = 2
nma = 1

endo = sm.datasets.sunspots.load_pandas().data['SUNACTIVITY'].tolist()
arma = ARMA(endo=endo, nar=nar, nma=nma)
arma.fit()

问题

我发现上面的例子没有收敛。在ARMA._likelihood的第三次调用中,代码抛出以下警告:

RuntimeWarning: invalid value encountered in log
  likelihood += np.log(R[i])

这是因为ARMA._initial_P解决了P [0] [0]&lt; 0.0。此时,AR参数的当前估计变得不稳定。然后,所有后续迭代都会警告AR和MA参数是非平稳的。

问题

  1. 此实施是否正确?我已经检查过初始P矩阵满足它应该满足的等式。对于可能性计算,我看到了我对Pearlman论文的几种行为:

    • R倾向于一个。对于具有p AR参数的纯AR过程,它以p步骤实现此限制。基本上,_likelihood函数中的break语句在Pearlman算法步骤的p次迭代后生效。
    • L倾向于零向量。
    • K倾向于F.g.我在计算可能性时通过查看abs(K - F.g)来检查这一点。
    • 在对数值为负值的警告之后,不再遵守上述限制。
  2. 我也尝试过实现ARMA参数的转换以防止溢出/下溢,如

    中所建议的那样

    琼斯,理查德H.“ARMA模型最大可能拟合时间序列缺失观测值。” Technometrics 22.3(1980):389-395。 Available from JSTOR.

    这种转变似乎对我观察到的错误没有影响。

    1. 如果实现正确,那么我该如何处理负R值?当scipy.optimize返回对应于顶部对角线元素为负的P矩阵的参数向量时,似乎会出现这个问题。优化程序是否应限制为防止负R值?我也尝试使用复数对数来表示负值,并将所有numpy dtype参数更改为“complex”。例如:

      def complex_log(val):
          '''
          Complex logarithm for negative values
      
          Returns log(val) + I*pi
          '''
          if val < 0.0:
              return complex(np.log(np.abs(val)), np.pi)
          return np.log(val) 
      
    2. 然而,scipy.optimize无法处理复数值函数,所以这个假设修复到目前为止还没有起作用。有关预防或处理这些行为的任何建议吗?

      感谢您阅读此内容。非常感谢任何帮助。

0 个答案:

没有答案