Repository: statsmodels/statsmodels

Issue:
BUG: State space: incorrect ergodic mean if state intercept is non-zero
Currently the computation of the ergodic distribution for stationary processes (i.e. `mod.initialize_stationary()`) always sets the mean to a vector of zeros, whereas it should be the steady-state value `(I - T)^{-1} c` where T is the transition matrix and c is the state intercept.

Hasn't affected any of the built-in models, which don't use a state intercept, but would affect custom models with one.


Discussion:

Resolving patch:
diff --git a/statsmodels/tsa/statespace/kalman_filter.py b/statsmodels/tsa/statespace/kalman_filter.py
--- a/statsmodels/tsa/statespace/kalman_filter.py
+++ b/statsmodels/tsa/statespace/kalman_filter.py
@@ -948,8 +948,22 @@ def simulate(self, nsimulations, measurement_shocks=None,
                 raise ValueError('Invalid shape of provided initial state'
                                  ' vector. Required (%d, 1)' % self.k_states)
         elif self.initialization == 'known':
-            initial_state = self._initial_state
-        elif self.initialization in ['approximate_diffuse', 'stationary']:
+            initial_state = np.random.multivariate_normal(
+                self._initial_state, self._initial_state_cov)
+        elif self.initialization == 'stationary':
+            from scipy.linalg import solve_discrete_lyapunov
+            # (I - T)^{-1} c = x => (I - T) x = c
+            initial_state_mean = np.linalg.solve(
+                np.eye(self.k_states) - self['transition', :, :, 0],
+                self['state_intercept', :, 0])
+            R = self['selection', :, :, 0]
+            Q = self['state_cov', :, :, 0]
+            selected_state_cov = R.dot(Q).dot(R.T)
+            initial_state_cov = solve_discrete_lyapunov(
+                self['transition', :, :, 0], selected_state_cov)
+            initial_state = np.random.multivariate_normal(
+                initial_state_mean, initial_state_cov)
+        elif self.initialization == 'approximate_diffuse':
             initial_state = np.zeros(self.k_states)
         else:
             initial_state = np.zeros(self.k_states)
diff --git a/statsmodels/tsa/statespace/simulation_smoother.py b/statsmodels/tsa/statespace/simulation_smoother.py
--- a/statsmodels/tsa/statespace/simulation_smoother.py
+++ b/statsmodels/tsa/statespace/simulation_smoother.py
@@ -167,7 +167,7 @@ def _simulate(self, nsimulations, measurement_shocks, state_shocks,
                 self._statespaces[prefix],
                 filter_method, inversion_method, stability_method,
                 conserve_memory, filter_timing, tolerance, loglikelihood_burn,
-                smoother_output, simulation_output, nsimulations, True
+                smoother_output, simulation_output, nsimulations
             )
         simulator = self._simulators[prefix]
 
@@ -176,13 +176,14 @@ def _simulate(self, nsimulations, measurement_shocks, state_shocks,
             np.r_[measurement_shocks.ravel(), state_shocks.ravel()],
             dtype=self.dtype
         ).squeeze())
-        simulator.set_disturbance_variates(disturbance_variates)
+        simulator.set_disturbance_variates(disturbance_variates,
+                                           pretransformed=True)
 
         # Set the intial state vector
-        initial_state_variates = np.atleast_1d(np.array(
+        initial_state = np.atleast_1d(np.array(
             initial_state, dtype=self.dtype
         ).squeeze())
-        simulator.set_initial_state_variates(initial_state_variates)
+        simulator.set_initial_state(initial_state)
 
         # Perform simulation smoothing
         # Note: simulation_output=-1 corresponds to whatever was setup when
diff --git a/statsmodels/tsa/statespace/tools.py b/statsmodels/tsa/statespace/tools.py
--- a/statsmodels/tsa/statespace/tools.py
+++ b/statsmodels/tsa/statespace/tools.py
@@ -513,7 +513,7 @@ def solve_discrete_lyapunov(a, q, complex_step=False):
     (usually the transition matrix) in order to allow complex step
     differentiation.
     """
-    eye = np.eye(a.shape[0])
+    eye = np.eye(a.shape[0], dtype=a.dtype)
     if not complex_step:
         aH = a.conj().transpose()
         aHI_inv = np.linalg.inv(aH + eye)


Test patch:
diff --git a/statsmodels/tsa/statespace/tests/test_kalman.py b/statsmodels/tsa/statespace/tests/test_kalman.py
--- a/statsmodels/tsa/statespace/tests/test_kalman.py
+++ b/statsmodels/tsa/statespace/tests/test_kalman.py
@@ -35,6 +35,8 @@ def find_best_blas_type(arrays):
         return (prefix, dtype, None)
 
 
+from scipy.linalg import solve_discrete_lyapunov
+from statsmodels.tsa.statespace.mlemodel import MLEModel
 from statsmodels.tsa.statespace.sarimax import SARIMAX
 from statsmodels.tsa.statespace import _statespace as ss
 from .results import results_kalman_filter
@@ -704,3 +706,80 @@ def test_filtered_state(self):
             self.result['state'][5][-1],
             self.true_states.iloc[end-1, 3], 4
         )
+
+
+def check_stationary_initialization_1dim(dtype=float):
+    endog = np.zeros(10, dtype=dtype)
+
+    # 1-dimensional example
+    mod = MLEModel(endog, k_states=1, k_posdef=1)
+    mod.ssm.initialize_stationary()
+    intercept = np.array([2.3], dtype=dtype)
+    phi = np.diag([0.9]).astype(dtype)
+    sigma2 = np.diag([1.3]).astype(dtype)
+
+    mod['state_intercept'] = intercept
+    mod['transition'] = phi
+    mod['selection'] = np.eye(1).astype(dtype)
+    mod['state_cov'] = sigma2
+
+    mod.ssm._initialize_filter()
+    mod.ssm._initialize_state()
+
+    _statespace = mod.ssm._statespace
+    initial_state = np.array(_statespace.initial_state)
+    initial_state_cov = np.array(_statespace.initial_state_cov)
+    # precision reductions only required for float complex case
+
+    # mean = intercept + phi * mean
+    # intercept = (1 - phi) * mean
+    # mean = intercept / (1 - phi)
+    assert_allclose(initial_state, intercept / (1 - phi[0, 0]))
+    desired = np.linalg.inv(np.eye(1) - phi).dot(intercept)
+    assert_allclose(initial_state, desired)
+    # var = phi**2 var + sigma2
+    # var = sigma2 / (1 - phi**2)
+    assert_allclose(initial_state_cov, sigma2 / (1 - phi**2))
+    assert_allclose(initial_state_cov, solve_discrete_lyapunov(phi, sigma2))
+
+
+def check_stationary_initialization_2dim(dtype=float):
+    endog = np.zeros(10, dtype=dtype)
+    # 2-dimensional example
+    mod = MLEModel(endog, k_states=2, k_posdef=2)
+    mod.ssm.initialize_stationary()
+    intercept = np.array([2.3, -10.2], dtype=dtype)
+    phi = np.array([[0.8, 0.1],
+                    [-0.2, 0.7]], dtype=dtype)
+    sigma2 = np.array([[1.4, -0.2],
+                       [-0.2, 4.5]], dtype=dtype)
+
+    mod['state_intercept'] = intercept
+    mod['transition'] = phi
+    mod['selection'] = np.eye(2).astype(dtype)
+    mod['state_cov'] = sigma2
+
+    mod.ssm._initialize_filter()
+    mod.ssm._initialize_state()
+
+    _statespace = mod.ssm._statespace
+    initial_state = np.array(_statespace.initial_state)
+    initial_state_cov = np.array(_statespace.initial_state_cov)
+
+    desired = np.linalg.solve(np.eye(2).astype(dtype) - phi, intercept)
+    assert_allclose(initial_state, desired)
+    desired = solve_discrete_lyapunov(phi, sigma2)
+    # precision reductions only required for single precision float / complex
+    assert_allclose(initial_state_cov, desired, atol=1e-5)
+
+
+def test_stationary_initialization():
+    check_stationary_initialization_1dim(np.float32)
+    check_stationary_initialization_1dim(np.float64)
+    check_stationary_initialization_1dim(np.complex64)
+    check_stationary_initialization_1dim(np.complex128)
+
+    check_stationary_initialization_2dim(np.float32)
+    check_stationary_initialization_2dim(np.float64)
+    check_stationary_initialization_2dim(np.complex64)
+    check_stationary_initialization_2dim(np.complex128)
diff --git a/statsmodels/tsa/statespace/tests/test_representation.py b/statsmodels/tsa/statespace/tests/test_representation.py
--- a/statsmodels/tsa/statespace/tests/test_representation.py
+++ b/statsmodels/tsa/statespace/tests/test_representation.py
@@ -1010,7 +1010,7 @@ def test_simulate():
     actual = mod.simulate(
         nsimulations, measurement_shocks=measurement_shocks,
         state_shocks=state_shocks)[0].squeeze()
-    desired = np.r_[0, np.cumsum(state_shocks)[:-1] + np.arange(1,10)]
+    desired = np.r_[0, np.cumsum(state_shocks)[:-1] + np.arange(1, 10)]
 
     assert_allclose(actual, desired)
 
@@ -1027,24 +1027,25 @@ def test_simulate():
     # ARMA(1,1): phi = [0.1], theta = [0.5], sigma^2 = 2
     phi = np.r_[0.1]
     theta = np.r_[0.5]
-    mod = sarimax.SARIMAX([0], order=(1,0,1))
+    mod = sarimax.SARIMAX([0], order=(1, 0, 1))
     mod.update(np.r_[phi, theta, sigma2])
 
     actual = mod.ssm.simulate(
         nsimulations, measurement_shocks=measurement_shocks,
-        state_shocks=state_shocks)[0].squeeze()
+        state_shocks=state_shocks,
+        initial_state=np.zeros(mod.k_states))[0].squeeze()
     desired = lfilter([1, theta], [1, -phi], np.r_[0, state_shocks[:-1]])
 
     assert_allclose(actual, desired)
 
     # SARIMAX(1,0,1)x(1,0,1,4), this time using the results object call
-    mod = sarimax.SARIMAX([0.1, 0.5, -0.2], order=(1,0,1),
-                          seasonal_order=(1,0,1,4))
+    mod = sarimax.SARIMAX([0.1, 0.5, -0.2], order=(1, 0, 1),
+                          seasonal_order=(1, 0, 1, 4))
     res = mod.filter([0.1, 0.5, 0.2, -0.3, 1])
 
     actual = res.simulate(
         nsimulations, measurement_shocks=measurement_shocks,
-        state_shocks=state_shocks)
+        state_shocks=state_shocks, initial_state=np.zeros(mod.k_states))
     desired = lfilter(
         res.polynomial_reduced_ma, res.polynomial_reduced_ar,
         np.r_[0, state_shocks[:-1]])
diff --git a/statsmodels/tsa/statespace/tests/test_simulate.py b/statsmodels/tsa/statespace/tests/test_simulate.py
--- a/statsmodels/tsa/statespace/tests/test_simulate.py
+++ b/statsmodels/tsa/statespace/tests/test_simulate.py
@@ -31,19 +31,22 @@ def test_arma_lfilter():
 
     # AR(1)
     mod = sarimax.SARIMAX([0], order=(1, 0, 0))
-    actual = mod.simulate([0.5, 1.], nobs + 1, state_shocks=np.r_[eps, 0])
+    actual = mod.simulate([0.5, 1.], nobs + 1, state_shocks=np.r_[eps, 0],
+                          initial_state=np.zeros(mod.k_states))
     desired = lfilter([1], [1, -0.5], eps)
     assert_allclose(actual[1:], desired)
 
     # MA(1)
     mod = sarimax.SARIMAX([0], order=(0, 0, 1))
-    actual = mod.simulate([0.5, 1.], nobs + 1, state_shocks=np.r_[eps, 0])
+    actual = mod.simulate([0.5, 1.], nobs + 1, state_shocks=np.r_[eps, 0],
+                          initial_state=np.zeros(mod.k_states))
     desired = lfilter([1, 0.5], [1], eps)
     assert_allclose(actual[1:], desired)
 
     # ARMA(1, 1)
     mod = sarimax.SARIMAX([0], order=(1, 0, 1))
-    actual = mod.simulate([0.5, 0.2, 1.], nobs + 1, state_shocks=np.r_[eps, 0])
+    actual = mod.simulate([0.5, 0.2, 1.], nobs + 1, state_shocks=np.r_[eps, 0],
+                          initial_state=np.zeros(mod.k_states))
     desired = lfilter([1, 0.2], [1, -0.5], eps)
     assert_allclose(actual[1:], desired)
 
@@ -60,7 +63,8 @@ def test_arma_direct():
 
     # AR(1)
     mod = sarimax.SARIMAX([0], order=(1, 0, 0))
-    actual = mod.simulate([0.5, 1.], nobs + 1, state_shocks=np.r_[eps, 0])
+    actual = mod.simulate([0.5, 1.], nobs + 1, state_shocks=np.r_[eps, 0],
+                          initial_state=np.zeros(mod.k_states))
     desired = np.zeros(nobs)
     for i in range(nobs):
         if i == 0:
@@ -71,7 +75,8 @@ def test_arma_direct():
 
     # MA(1)
     mod = sarimax.SARIMAX([0], order=(0, 0, 1))
-    actual = mod.simulate([0.5, 1.], nobs + 1, state_shocks=np.r_[eps, 0])
+    actual = mod.simulate([0.5, 1.], nobs + 1, state_shocks=np.r_[eps, 0],
+                          initial_state=np.zeros(mod.k_states))
     desired = np.zeros(nobs)
     for i in range(nobs):
         if i == 0:
@@ -82,7 +87,8 @@ def test_arma_direct():
 
     # ARMA(1, 1)
     mod = sarimax.SARIMAX([0], order=(1, 0, 1))
-    actual = mod.simulate([0.5, 0.2, 1.], nobs + 1, state_shocks=np.r_[eps, 0])
+    actual = mod.simulate([0.5, 0.2, 1.], nobs + 1, state_shocks=np.r_[eps, 0],
+                          initial_state=np.zeros(mod.k_states))
     desired = np.zeros(nobs)
     for i in range(nobs):
         if i == 0:
@@ -94,7 +100,8 @@ def test_arma_direct():
     # ARMA(1, 1) + intercept
     mod = sarimax.SARIMAX([0], order=(1, 0, 1), trend='c')
     actual = mod.simulate([1.3, 0.5, 0.2, 1.], nobs + 1,
-                          state_shocks=np.r_[eps, 0])
+                          state_shocks=np.r_[eps, 0],
+                          initial_state=np.zeros(mod.k_states))
     desired = np.zeros(nobs)
     for i in range(nobs):
         trend = 1.3
@@ -110,7 +117,8 @@ def test_arma_direct():
     # give it 101 observations up front
     mod = sarimax.SARIMAX(np.zeros(nobs + 1), order=(1, 0, 1), trend='ct')
     actual = mod.simulate([1.3, 0.2, 0.5, 0.2, 1.], nobs + 1,
-                          state_shocks=np.r_[eps, 0])
+                          state_shocks=np.r_[eps, 0],
+                          initial_state=np.zeros(mod.k_states))
     desired = np.zeros(nobs)
     for i in range(nobs):
         trend = 1.3 + 0.2 * (i + 1)
@@ -129,7 +137,8 @@ def test_arma_direct():
     mod = sarimax.SARIMAX(np.zeros(nobs + 1), exog=np.r_[0, exog],
                           order=(1, 0, 1), trend='ct')
     actual = mod.simulate([1.3, 0.2, -0.5, 0.5, 0.2, 1.], nobs + 1,
-                          state_shocks=np.r_[eps, 0])
+                          state_shocks=np.r_[eps, 0],
+                          initial_state=np.zeros(mod.k_states))
     desired = np.zeros(nobs)
     for i in range(nobs):
         trend = 1.3 + 0.2 * (i + 1)
@@ -160,21 +169,26 @@ def test_structural():
     # AR(1)
     mod1 = structural.UnobservedComponents([0], autoregressive=1)
     mod2 = sarimax.SARIMAX([0], order=(1, 0, 0))
-    actual = mod1.simulate([1, 0.5], nobs, state_shocks=eps)
-    desired = mod2.simulate([0.5, 1], nobs, state_shocks=eps)
+    actual = mod1.simulate([1, 0.5], nobs, state_shocks=eps,
+                           initial_state=np.zeros(mod1.k_states))
+    desired = mod2.simulate([0.5, 1], nobs, state_shocks=eps,
+                            initial_state=np.zeros(mod2.k_states))
     assert_allclose(actual, desired)
 
     # ARX(1)
     mod1 = structural.UnobservedComponents(np.zeros(nobs), exog=exog,
                                            autoregressive=1)
     mod2 = sarimax.SARIMAX(np.zeros(nobs), exog=exog, order=(1, 0, 0))
-    actual = mod1.simulate([1, 0.5, 0.2], nobs, state_shocks=eps)
-    desired = mod2.simulate([0.2, 0.5, 1], nobs, state_shocks=eps)
+    actual = mod1.simulate([1, 0.5, 0.2], nobs, state_shocks=eps,
+                           initial_state=np.zeros(mod2.k_states))
+    desired = mod2.simulate([0.2, 0.5, 1], nobs, state_shocks=eps,
+                            initial_state=np.zeros(mod2.k_states))
     assert_allclose(actual, desired)
 
     # Irregular
     mod = structural.UnobservedComponents([0], 'irregular')
-    actual = mod.simulate([1.], nobs, measurement_shocks=eps)
+    actual = mod.simulate([1.], nobs, measurement_shocks=eps,
+                          initial_state=np.zeros(mod.k_states))
     assert_allclose(actual, eps)
 
     # Fixed intercept
@@ -196,13 +210,15 @@ def test_structural():
     # Local level
     mod = structural.UnobservedComponents([0], 'local level')
     actual = mod.simulate([1., 1.], nobs, measurement_shocks=eps,
-                          state_shocks=eps2)
+                          state_shocks=eps2,
+                          initial_state=np.zeros(mod.k_states))
     assert_allclose(actual, eps + eps3)
 
     # Random walk
     mod = structural.UnobservedComponents([0], 'random walk')
     actual = mod.simulate([1.], nobs, measurement_shocks=eps,
-                          state_shocks=eps2)
+                          state_shocks=eps2,
+                          initial_state=np.zeros(mod.k_states))
     assert_allclose(actual, eps + eps3)
 
     # Fixed slope
@@ -335,15 +351,19 @@ def test_varmax():
     # VAR(2) - single series
     mod1 = varmax.VARMAX([[0]], order=(2, 0), trend='nc')
     mod2 = sarimax.SARIMAX([0], order=(2, 0, 0))
-    actual = mod1.simulate([0.5, 0.2, 1], nobs, state_shocks=eps)
-    desired = mod2.simulate([0.5, 0.2, 1], nobs, state_shocks=eps)
+    actual = mod1.simulate([0.5, 0.2, 1], nobs, state_shocks=eps,
+                           initial_state=np.zeros(mod1.k_states))
+    desired = mod2.simulate([0.5, 0.2, 1], nobs, state_shocks=eps,
+                            initial_state=np.zeros(mod2.k_states))
     assert_allclose(actual, desired)
 
     # VMA(2) - single series
     mod1 = varmax.VARMAX([[0]], order=(0, 2), trend='nc')
     mod2 = sarimax.SARIMAX([0], order=(0, 0, 2))
-    actual = mod1.simulate([0.5, 0.2, 1], nobs, state_shocks=eps)
-    desired = mod2.simulate([0.5, 0.2, 1], nobs, state_shocks=eps)
+    actual = mod1.simulate([0.5, 0.2, 1], nobs, state_shocks=eps,
+                           initial_state=np.zeros(mod1.k_states))
+    desired = mod2.simulate([0.5, 0.2, 1], nobs, state_shocks=eps,
+                            initial_state=np.zeros(mod2.k_states))
     assert_allclose(actual, desired)
 
     # VARMA(2, 2) - single series
@@ -351,17 +371,21 @@ def test_varmax():
         warnings.simplefilter("ignore")
         mod1 = varmax.VARMAX([[0]], order=(2, 2), trend='nc')
     mod2 = sarimax.SARIMAX([0], order=(2, 0, 2))
-    actual = mod1.simulate([0.5, 0.2, 0.1, -0.2, 1], nobs, state_shocks=eps)
-    desired = mod2.simulate([0.5, 0.2, 0.1, -0.2, 1], nobs, state_shocks=eps)
+    actual = mod1.simulate([0.5, 0.2, 0.1, -0.2, 1], nobs, state_shocks=eps,
+                           initial_state=np.zeros(mod1.k_states))
+    desired = mod2.simulate([0.5, 0.2, 0.1, -0.2, 1], nobs, state_shocks=eps,
+                            initial_state=np.zeros(mod2.k_states))
     assert_allclose(actual, desired)
 
     # VARMA(2, 2) + trend - single series
     mod1 = varmax.VARMAX([[0]], order=(2, 2), trend='c')
     mod2 = sarimax.SARIMAX([0], order=(2, 0, 2), trend='c')
     actual = mod1.simulate([10, 0.5, 0.2, 0.1, -0.2, 1], nobs,
-                           state_shocks=eps)
+                           state_shocks=eps,
+                           initial_state=np.zeros(mod1.k_states))
     desired = mod2.simulate([10, 0.5, 0.2, 0.1, -0.2, 1], nobs,
-                            state_shocks=eps)
+                            state_shocks=eps,
+                            initial_state=np.zeros(mod2.k_states))
     assert_allclose(actual, desired)
 
     # VAR(1)
@@ -370,7 +394,8 @@ def test_varmax():
 
     mod = varmax.VARMAX([[0, 0]], order=(1, 0), trend='nc')
     actual = mod.simulate(np.r_[transition.ravel(), 1., 0, 1.], nobs,
-                          state_shocks=np.c_[eps1, eps1])
+                          state_shocks=np.c_[eps1, eps1],
+                          initial_state=np.zeros(mod.k_states))
     assert_allclose(actual, 0)
 
     actual = mod.simulate(np.r_[transition.ravel(), 1., 0, 1.], nobs,
@@ -387,7 +412,8 @@ def test_varmax():
                         measurement_error=True)
     actual = mod.simulate(np.r_[transition.ravel(), 1., 0, 1., 1., 1.], nobs,
                           measurement_shocks=np.c_[eps, eps],
-                          state_shocks=np.c_[eps1, eps1])
+                          state_shocks=np.c_[eps1, eps1],
+                          initial_state=np.zeros(mod.k_states))
     assert_allclose(actual, np.c_[eps, eps])
 
     # VARX(1)
@@ -436,8 +462,10 @@ def test_dynamic_factor():
     mod2 = sarimax.SARIMAX([0], order=(2, 0, 0))
     actual = mod1.simulate([-0.9, 0.8, 1., 1., 0.5, 0.2], nobs,
                            measurement_shocks=np.c_[eps1, eps1],
-                           state_shocks=eps)
-    desired = mod2.simulate([0.5, 0.2, 1], nobs, state_shocks=eps)
+                           state_shocks=eps,
+                           initial_state=np.zeros(mod1.k_states))
+    desired = mod2.simulate([0.5, 0.2, 1], nobs, state_shocks=eps,
+                            initial_state=np.zeros(mod2.k_states))
     assert_allclose(actual[:, 0], -0.9 * desired)
     assert_allclose(actual[:, 1], 0.8 * desired)
 
@@ -447,8 +475,10 @@ def test_dynamic_factor():
     mod2 = sarimax.SARIMAX([0], order=(2, 0, 0))
     actual = mod1.simulate([-0.9, 0.8, 5, -2, 1., 1., 0.5, 0.2], nobs,
                            measurement_shocks=np.c_[eps1, eps1],
-                           state_shocks=eps)
-    desired = mod2.simulate([0.5, 0.2, 1], nobs, state_shocks=eps)
+                           state_shocks=eps,
+                           initial_state=np.zeros(mod1.k_states))
+    desired = mod2.simulate([0.5, 0.2, 1], nobs, state_shocks=eps,
+                            initial_state=np.zeros(mod2.k_states))
     assert_allclose(actual[:, 0], -0.9 * desired + 5 * exog[:, 0])
     assert_allclose(actual[:, 1], 0.8 * desired - 2 * exog[:, 0])