Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1""" 

2Statespace Tools 

3 

4Author: Chad Fulton 

5License: Simplified-BSD 

6""" 

7import numpy as np 

8from scipy.linalg import solve_sylvester 

9import pandas as pd 

10 

11from statsmodels.compat.pandas import Appender 

12from statsmodels.tools.data import _is_using_pandas 

13from scipy.linalg.blas import find_best_blas_type 

14from . import (_initialization, _representation, _kalman_filter, 

15 _kalman_smoother, _simulation_smoother, _tools) 

16 

17 

18compatibility_mode = False 

19has_trmm = True 

20prefix_dtype_map = { 

21 's': np.float32, 'd': np.float64, 'c': np.complex64, 'z': np.complex128 

22} 

23prefix_initialization_map = { 

24 's': _initialization.sInitialization, 

25 'd': _initialization.dInitialization, 

26 'c': _initialization.cInitialization, 

27 'z': _initialization.zInitialization 

28} 

29prefix_statespace_map = { 

30 's': _representation.sStatespace, 'd': _representation.dStatespace, 

31 'c': _representation.cStatespace, 'z': _representation.zStatespace 

32} 

33prefix_kalman_filter_map = { 

34 's': _kalman_filter.sKalmanFilter, 

35 'd': _kalman_filter.dKalmanFilter, 

36 'c': _kalman_filter.cKalmanFilter, 

37 'z': _kalman_filter.zKalmanFilter 

38} 

39prefix_kalman_smoother_map = { 

40 's': _kalman_smoother.sKalmanSmoother, 

41 'd': _kalman_smoother.dKalmanSmoother, 

42 'c': _kalman_smoother.cKalmanSmoother, 

43 'z': _kalman_smoother.zKalmanSmoother 

44} 

45prefix_simulation_smoother_map = { 

46 's': _simulation_smoother.sSimulationSmoother, 

47 'd': _simulation_smoother.dSimulationSmoother, 

48 'c': _simulation_smoother.cSimulationSmoother, 

49 'z': _simulation_smoother.zSimulationSmoother 

50} 

51prefix_pacf_map = { 

52 's': _tools._scompute_coefficients_from_multivariate_pacf, 

53 'd': _tools._dcompute_coefficients_from_multivariate_pacf, 

54 'c': _tools._ccompute_coefficients_from_multivariate_pacf, 

55 'z': _tools._zcompute_coefficients_from_multivariate_pacf 

56} 

57prefix_sv_map = { 

58 's': _tools._sconstrain_sv_less_than_one, 

59 'd': _tools._dconstrain_sv_less_than_one, 

60 'c': _tools._cconstrain_sv_less_than_one, 

61 'z': _tools._zconstrain_sv_less_than_one 

62} 

63prefix_reorder_missing_matrix_map = { 

64 's': _tools.sreorder_missing_matrix, 

65 'd': _tools.dreorder_missing_matrix, 

66 'c': _tools.creorder_missing_matrix, 

67 'z': _tools.zreorder_missing_matrix 

68} 

69prefix_reorder_missing_vector_map = { 

70 's': _tools.sreorder_missing_vector, 

71 'd': _tools.dreorder_missing_vector, 

72 'c': _tools.creorder_missing_vector, 

73 'z': _tools.zreorder_missing_vector 

74} 

75prefix_copy_missing_matrix_map = { 

76 's': _tools.scopy_missing_matrix, 

77 'd': _tools.dcopy_missing_matrix, 

78 'c': _tools.ccopy_missing_matrix, 

79 'z': _tools.zcopy_missing_matrix 

80} 

81prefix_copy_missing_vector_map = { 

82 's': _tools.scopy_missing_vector, 

83 'd': _tools.dcopy_missing_vector, 

84 'c': _tools.ccopy_missing_vector, 

85 'z': _tools.zcopy_missing_vector 

86} 

87prefix_copy_index_matrix_map = { 

88 's': _tools.scopy_index_matrix, 

89 'd': _tools.dcopy_index_matrix, 

90 'c': _tools.ccopy_index_matrix, 

91 'z': _tools.zcopy_index_matrix 

92} 

93prefix_copy_index_vector_map = { 

94 's': _tools.scopy_index_vector, 

95 'd': _tools.dcopy_index_vector, 

96 'c': _tools.ccopy_index_vector, 

97 'z': _tools.zcopy_index_vector 

98} 

99 

100 

101def set_mode(compatibility=None): 

102 if compatibility: 

103 raise NotImplementedError('Compatibility mode is only available in' 

104 ' statsmodels <= 0.9') 

105 

106 

107def companion_matrix(polynomial): 

108 r""" 

109 Create a companion matrix 

110 

111 Parameters 

112 ---------- 

113 polynomial : array_like or list 

114 If an iterable, interpreted as the coefficients of the polynomial from 

115 which to form the companion matrix. Polynomial coefficients are in 

116 order of increasing degree, and may be either scalars (as in an AR(p) 

117 model) or coefficient matrices (as in a VAR(p) model). If an integer, 

118 it is interpreted as the size of a companion matrix of a scalar 

119 polynomial, where the polynomial coefficients are initialized to zeros. 

120 If a matrix polynomial is passed, :math:`C_0` may be set to the scalar 

121 value 1 to indicate an identity matrix (doing so will improve the speed 

122 of the companion matrix creation). 

123 

124 Returns 

125 ------- 

126 companion_matrix : ndarray 

127 

128 Notes 

129 ----- 

130 Given coefficients of a lag polynomial of the form: 

131 

132 .. math:: 

133 

134 c(L) = c_0 + c_1 L + \dots + c_p L^p 

135 

136 returns a matrix of the form 

137 

138 .. math:: 

139 \begin{bmatrix} 

140 \phi_1 & 1 & 0 & \cdots & 0 \\ 

141 \phi_2 & 0 & 1 & & 0 \\ 

142 \vdots & & & \ddots & 0 \\ 

143 & & & & 1 \\ 

144 \phi_n & 0 & 0 & \cdots & 0 \\ 

145 \end{bmatrix} 

146 

147 where some or all of the :math:`\phi_i` may be non-zero (if `polynomial` is 

148 None, then all are equal to zero). 

149 

150 If the coefficients provided are scalars :math:`(c_0, c_1, \dots, c_p)`, 

151 then the companion matrix is an :math:`n \times n` matrix formed with the 

152 elements in the first column defined as 

153 :math:`\phi_i = -\frac{c_i}{c_0}, i \in 1, \dots, p`. 

154 

155 If the coefficients provided are matrices :math:`(C_0, C_1, \dots, C_p)`, 

156 each of shape :math:`(m, m)`, then the companion matrix is an 

157 :math:`nm \times nm` matrix formed with the elements in the first column 

158 defined as :math:`\phi_i = -C_0^{-1} C_i', i \in 1, \dots, p`. 

159 

160 It is important to understand the expected signs of the coefficients. A 

161 typical AR(p) model is written as: 

162 

163 .. math:: 

164 y_t = a_1 y_{t-1} + \dots + a_p y_{t-p} + \varepsilon_t 

165 

166 This can be rewritten as: 

167 

168 .. math:: 

169 (1 - a_1 L - \dots - a_p L^p )y_t = \varepsilon_t \\ 

170 (1 + c_1 L + \dots + c_p L^p )y_t = \varepsilon_t \\ 

171 c(L) y_t = \varepsilon_t 

172 

173 The coefficients from this form are defined to be :math:`c_i = - a_i`, and 

174 it is the :math:`c_i` coefficients that this function expects to be 

175 provided. 

176 """ 

177 identity_matrix = False 

178 if isinstance(polynomial, (int, np.integer)): 

179 # GH 5570, allow numpy integer types, but coerce to python int 

180 n = int(polynomial) 

181 m = 1 

182 polynomial = None 

183 else: 

184 n = len(polynomial) - 1 

185 

186 if n < 1: 

187 raise ValueError("Companion matrix polynomials must include at" 

188 " least two terms.") 

189 

190 if isinstance(polynomial, list) or isinstance(polynomial, tuple): 

191 try: 

192 # Note: cannot use polynomial[0] because of the special 

193 # behavior associated with matrix polynomials and the constant 

194 # 1, see below. 

195 m = len(polynomial[1]) 

196 except TypeError: 

197 m = 1 

198 

199 # Check if we just have a scalar polynomial 

200 if m == 1: 

201 polynomial = np.asanyarray(polynomial) 

202 # Check if 1 was passed as the first argument (indicating an 

203 # identity matrix) 

204 elif polynomial[0] == 1: 

205 polynomial[0] = np.eye(m) 

206 identity_matrix = True 

207 else: 

208 m = 1 

209 polynomial = np.asanyarray(polynomial) 

210 

211 matrix = np.zeros((n * m, n * m), dtype=np.asanyarray(polynomial).dtype) 

212 idx = np.diag_indices((n - 1) * m) 

213 idx = (idx[0], idx[1] + m) 

214 matrix[idx] = 1 

215 if polynomial is not None and n > 0: 

216 if m == 1: 

217 matrix[:, 0] = -polynomial[1:] / polynomial[0] 

218 elif identity_matrix: 

219 for i in range(n): 

220 matrix[i * m:(i + 1) * m, :m] = -polynomial[i+1].T 

221 else: 

222 inv = np.linalg.inv(polynomial[0]) 

223 for i in range(n): 

224 matrix[i * m:(i + 1) * m, :m] = -np.dot(inv, polynomial[i+1]).T 

225 return matrix 

226 

227 

228def diff(series, k_diff=1, k_seasonal_diff=None, seasonal_periods=1): 

229 r""" 

230 Difference a series simply and/or seasonally along the zero-th axis. 

231 

232 Given a series (denoted :math:`y_t`), performs the differencing operation 

233 

234 .. math:: 

235 

236 \Delta^d \Delta_s^D y_t 

237 

238 where :math:`d =` `diff`, :math:`s =` `seasonal_periods`, 

239 :math:`D =` `seasonal\_diff`, and :math:`\Delta` is the difference 

240 operator. 

241 

242 Parameters 

243 ---------- 

244 series : array_like 

245 The series to be differenced. 

246 diff : int, optional 

247 The number of simple differences to perform. Default is 1. 

248 seasonal_diff : int or None, optional 

249 The number of seasonal differences to perform. Default is no seasonal 

250 differencing. 

251 seasonal_periods : int, optional 

252 The seasonal lag. Default is 1. Unused if there is no seasonal 

253 differencing. 

254 

255 Returns 

256 ------- 

257 differenced : ndarray 

258 The differenced array. 

259 """ 

260 pandas = _is_using_pandas(series, None) 

261 differenced = np.asanyarray(series) if not pandas else series 

262 

263 # Seasonal differencing 

264 if k_seasonal_diff is not None: 

265 while k_seasonal_diff > 0: 

266 if not pandas: 

267 differenced = (differenced[seasonal_periods:] - 

268 differenced[:-seasonal_periods]) 

269 else: 

270 sdiffed = differenced.diff(seasonal_periods) 

271 differenced = sdiffed[seasonal_periods:] 

272 k_seasonal_diff -= 1 

273 

274 # Simple differencing 

275 if not pandas: 

276 differenced = np.diff(differenced, k_diff, axis=0) 

277 else: 

278 while k_diff > 0: 

279 differenced = differenced.diff()[1:] 

280 k_diff -= 1 

281 return differenced 

282 

283 

284def concat(series, axis=0, allow_mix=False): 

285 """ 

286 Concatenate a set of series. 

287 

288 Parameters 

289 ---------- 

290 series : iterable 

291 An iterable of series to be concatenated 

292 axis : int, optional 

293 The axis along which to concatenate. Default is 1 (columns). 

294 allow_mix : bool 

295 Whether or not to allow a mix of pandas and non-pandas objects. Default 

296 is False. If true, the returned object is an ndarray, and additional 

297 pandas metadata (e.g. column names, indices, etc) is lost. 

298 

299 Returns 

300 ------- 

301 concatenated : array or pd.DataFrame 

302 The concatenated array. Will be a DataFrame if series are pandas 

303 objects. 

304 """ 

305 is_pandas = np.r_[[_is_using_pandas(s, None) for s in series]] 

306 

307 if np.all(is_pandas): 

308 if isinstance(series[0], pd.DataFrame): 

309 base_columns = series[0].columns 

310 else: 

311 base_columns = pd.Index([series[0].name]) 

312 for s in series[1:]: 

313 if isinstance(s, pd.DataFrame): 

314 s_columns = s.columns 

315 else: 

316 s_columns = pd.Index([s.name]) 

317 

318 if axis == 0 and not base_columns.equals(s_columns): 

319 raise ValueError('Columns must match to concatenate along' 

320 ' rows.') 

321 elif axis == 1 and not series[0].index.equals(s.index): 

322 raise ValueError('Index must match to concatenate along' 

323 ' columns.') 

324 concatenated = pd.concat(series, axis=axis) 

325 elif np.all(~is_pandas) or allow_mix: 

326 concatenated = np.concatenate(series, axis=axis) 

327 else: 

328 raise ValueError('Attempted to concatenate Pandas objects with' 

329 ' non-Pandas objects with `allow_mix=False`.') 

330 

331 return concatenated 

332 

333 

334def is_invertible(polynomial, threshold=1 - 1e-10): 

335 r""" 

336 Determine if a polynomial is invertible. 

337 

338 Requires all roots of the polynomial lie inside the unit circle. 

339 

340 Parameters 

341 ---------- 

342 polynomial : array_like or tuple, list 

343 Coefficients of a polynomial, in order of increasing degree. 

344 For example, `polynomial=[1, -0.5]` corresponds to the polynomial 

345 :math:`1 - 0.5x` which has root :math:`2`. If it is a matrix 

346 polynomial (in which case the coefficients are coefficient matrices), 

347 a tuple or list of matrices should be passed. 

348 threshold : number 

349 Allowed threshold for `is_invertible` to return True. Default is 1. 

350 

351 Notes 

352 ----- 

353 

354 If the coefficients provided are scalars :math:`(c_0, c_1, \dots, c_n)`, 

355 then the corresponding polynomial is :math:`c_0 + c_1 L + \dots + c_n L^n`. 

356 

357 

358 If the coefficients provided are matrices :math:`(C_0, C_1, \dots, C_n)`, 

359 then the corresponding polynomial is :math:`C_0 + C_1 L + \dots + C_n L^n`. 

360 

361 There are three equivalent methods of determining if the polynomial 

362 represented by the coefficients is invertible: 

363 

364 The first method factorizes the polynomial into: 

365 

366 .. math:: 

367 

368 C(L) & = c_0 + c_1 L + \dots + c_n L^n \\ 

369 & = constant (1 - \lambda_1 L) 

370 (1 - \lambda_2 L) \dots (1 - \lambda_n L) 

371 

372 In order for :math:`C(L)` to be invertible, it must be that each factor 

373 :math:`(1 - \lambda_i L)` is invertible; the condition is then that 

374 :math:`|\lambda_i| < 1`, where :math:`\lambda_i` is a root of the 

375 polynomial. 

376 

377 The second method factorizes the polynomial into: 

378 

379 .. math:: 

380 

381 C(L) & = c_0 + c_1 L + \dots + c_n L^n \\ 

382 & = constant (L - \zeta_1) (L - \zeta_2) \dots (L - \zeta_3) 

383 

384 The condition is now :math:`|\zeta_i| > 1`, where :math:`\zeta_i` is a root 

385 of the polynomial with reversed coefficients and 

386 :math:`\lambda_i = \frac{1}{\zeta_i}`. 

387 

388 Finally, a companion matrix can be formed using the coefficients of the 

389 polynomial. Then the eigenvalues of that matrix give the roots of the 

390 polynomial. This last method is the one actually used. 

391 

392 See Also 

393 -------- 

394 companion_matrix 

395 """ 

396 # First method: 

397 # np.all(np.abs(np.roots(np.r_[1, params])) < 1) 

398 # Second method: 

399 # np.all(np.abs(np.roots(np.r_[1, params][::-1])) > 1) 

400 # Final method: 

401 eigvals = np.linalg.eigvals(companion_matrix(polynomial)) 

402 return np.all(np.abs(eigvals) < threshold) 

403 

404 

405def solve_discrete_lyapunov(a, q, complex_step=False): 

406 r""" 

407 Solves the discrete Lyapunov equation using a bilinear transformation. 

408 

409 Notes 

410 ----- 

411 This is a modification of the version in Scipy (see 

412 https://github.com/scipy/scipy/blob/master/scipy/linalg/_solvers.py) 

413 which allows passing through the complex numbers in the matrix a 

414 (usually the transition matrix) in order to allow complex step 

415 differentiation. 

416 """ 

417 eye = np.eye(a.shape[0], dtype=a.dtype) 

418 if not complex_step: 

419 aH = a.conj().transpose() 

420 aHI_inv = np.linalg.inv(aH + eye) 

421 b = np.dot(aH - eye, aHI_inv) 

422 c = 2*np.dot(np.dot(np.linalg.inv(a + eye), q), aHI_inv) 

423 return solve_sylvester(b.conj().transpose(), b, -c) 

424 else: 

425 aH = a.transpose() 

426 aHI_inv = np.linalg.inv(aH + eye) 

427 b = np.dot(aH - eye, aHI_inv) 

428 c = 2*np.dot(np.dot(np.linalg.inv(a + eye), q), aHI_inv) 

429 return solve_sylvester(b.transpose(), b, -c) 

430 

431 

432def constrain_stationary_univariate(unconstrained): 

433 """ 

434 Transform unconstrained parameters used by the optimizer to constrained 

435 parameters used in likelihood evaluation 

436 

437 Parameters 

438 ---------- 

439 unconstrained : ndarray 

440 Unconstrained parameters used by the optimizer, to be transformed to 

441 stationary coefficients of, e.g., an autoregressive or moving average 

442 component. 

443 

444 Returns 

445 ------- 

446 constrained : ndarray 

447 Constrained parameters of, e.g., an autoregressive or moving average 

448 component, to be transformed to arbitrary parameters used by the 

449 optimizer. 

450 

451 References 

452 ---------- 

453 .. [*] Monahan, John F. 1984. 

454 "A Note on Enforcing Stationarity in 

455 Autoregressive-moving Average Models." 

456 Biometrika 71 (2) (August 1): 403-404. 

457 """ 

458 

459 n = unconstrained.shape[0] 

460 y = np.zeros((n, n), dtype=unconstrained.dtype) 

461 r = unconstrained/((1 + unconstrained**2)**0.5) 

462 for k in range(n): 

463 for i in range(k): 

464 y[k, i] = y[k - 1, i] + r[k] * y[k - 1, k - i - 1] 

465 y[k, k] = r[k] 

466 return -y[n - 1, :] 

467 

468 

469def unconstrain_stationary_univariate(constrained): 

470 """ 

471 Transform constrained parameters used in likelihood evaluation 

472 to unconstrained parameters used by the optimizer 

473 

474 Parameters 

475 ---------- 

476 constrained : ndarray 

477 Constrained parameters of, e.g., an autoregressive or moving average 

478 component, to be transformed to arbitrary parameters used by the 

479 optimizer. 

480 

481 Returns 

482 ------- 

483 unconstrained : ndarray 

484 Unconstrained parameters used by the optimizer, to be transformed to 

485 stationary coefficients of, e.g., an autoregressive or moving average 

486 component. 

487 

488 References 

489 ---------- 

490 .. [*] Monahan, John F. 1984. 

491 "A Note on Enforcing Stationarity in 

492 Autoregressive-moving Average Models." 

493 Biometrika 71 (2) (August 1): 403-404. 

494 """ 

495 n = constrained.shape[0] 

496 y = np.zeros((n, n), dtype=constrained.dtype) 

497 y[n-1:] = -constrained 

498 for k in range(n-1, 0, -1): 

499 for i in range(k): 

500 y[k-1, i] = (y[k, i] - y[k, k]*y[k, k-i-1]) / (1 - y[k, k]**2) 

501 r = y.diagonal() 

502 x = r / ((1 - r**2)**0.5) 

503 return x 

504 

505 

506def _constrain_sv_less_than_one_python(unconstrained, order=None, 

507 k_endog=None): 

508 """ 

509 Transform arbitrary matrices to matrices with singular values less than 

510 one. 

511 

512 Parameters 

513 ---------- 

514 unconstrained : list 

515 Arbitrary matrices. Should be a list of length `order`, where each 

516 element is an array sized `k_endog` x `k_endog`. 

517 order : int, optional 

518 The order of the autoregression. 

519 k_endog : int, optional 

520 The dimension of the data vector. 

521 

522 Returns 

523 ------- 

524 constrained : list 

525 Partial autocorrelation matrices. Should be a list of length 

526 `order`, where each element is an array sized `k_endog` x `k_endog`. 

527 

528 Notes 

529 ----- 

530 Corresponds to Lemma 2.2 in Ansley and Kohn (1986). See 

531 `constrain_stationary_multivariate` for more details. 

532 

533 There is a Cython implementation of this function that can be much faster, 

534 but which requires SciPy 0.14.0 or greater. See 

535 `constrain_stationary_multivariate` for details. 

536 """ 

537 

538 from scipy import linalg 

539 

540 constrained = [] # P_s, s = 1, ..., p 

541 if order is None: 

542 order = len(unconstrained) 

543 if k_endog is None: 

544 k_endog = unconstrained[0].shape[0] 

545 

546 eye = np.eye(k_endog) 

547 for i in range(order): 

548 A = unconstrained[i] 

549 B, lower = linalg.cho_factor(eye + np.dot(A, A.T), lower=True) 

550 constrained.append(linalg.solve_triangular(B, A, lower=lower)) 

551 return constrained 

552 

553 

554def _compute_coefficients_from_multivariate_pacf_python( 

555 partial_autocorrelations, error_variance, transform_variance=False, 

556 order=None, k_endog=None): 

557 """ 

558 Transform matrices with singular values less than one to matrices 

559 corresponding to a stationary (or invertible) process. 

560 

561 Parameters 

562 ---------- 

563 partial_autocorrelations : list 

564 Partial autocorrelation matrices. Should be a list of length `order`, 

565 where each element is an array sized `k_endog` x `k_endog`. 

566 error_variance : ndarray 

567 The variance / covariance matrix of the error term. Should be sized 

568 `k_endog` x `k_endog`. This is used as input in the algorithm even if 

569 is not transformed by it (when `transform_variance` is False). The 

570 error term variance is required input when transformation is used 

571 either to force an autoregressive component to be stationary or to 

572 force a moving average component to be invertible. 

573 transform_variance : bool, optional 

574 Whether or not to transform the error variance term. This option is 

575 not typically used, and the default is False. 

576 order : int, optional 

577 The order of the autoregression. 

578 k_endog : int, optional 

579 The dimension of the data vector. 

580 

581 Returns 

582 ------- 

583 coefficient_matrices : list 

584 Transformed coefficient matrices leading to a stationary VAR 

585 representation. 

586 

587 Notes 

588 ----- 

589 Corresponds to Lemma 2.1 in Ansley and Kohn (1986). See 

590 `constrain_stationary_multivariate` for more details. 

591 

592 There is a Cython implementation of this function that can be much faster, 

593 but which requires SciPy 0.14.0 or greater. See 

594 `constrain_stationary_multivariate` for details. 

595 """ 

596 from scipy import linalg 

597 

598 if order is None: 

599 order = len(partial_autocorrelations) 

600 if k_endog is None: 

601 k_endog = partial_autocorrelations[0].shape[0] 

602 

603 # If we want to keep the provided variance but with the constrained 

604 # coefficient matrices, we need to make a copy here, and then after the 

605 # main loop we will transform the coefficients to match the passed variance 

606 if not transform_variance: 

607 initial_variance = error_variance 

608 # Need to make the input variance large enough that the recursions 

609 # do not lead to zero-matrices due to roundoff error, which would case 

610 # exceptions from the Cholesky decompositions. 

611 # Note that this will still not always ensure positive definiteness, 

612 # and for k_endog, order large enough an exception may still be raised 

613 error_variance = np.eye(k_endog) * (order + k_endog)**10 

614 

615 forward_variances = [error_variance] # \Sigma_s 

616 backward_variances = [error_variance] # \Sigma_s^*, s = 0, ..., p 

617 autocovariances = [error_variance] # \Gamma_s 

618 # \phi_{s,k}, s = 1, ..., p 

619 # k = 1, ..., s+1 

620 forwards = [] 

621 # \phi_{s,k}^* 

622 backwards = [] 

623 

624 error_variance_factor = linalg.cholesky(error_variance, lower=True) 

625 

626 forward_factors = [error_variance_factor] 

627 backward_factors = [error_variance_factor] 

628 

629 # We fill in the entries as follows: 

630 # [1,1] 

631 # [2,2], [2,1] 

632 # [3,3], [3,1], [3,2] 

633 # ... 

634 # [p,p], [p,1], ..., [p,p-1] 

635 # the last row, correctly ordered, is then used as the coefficients 

636 for s in range(order): # s = 0, ..., p-1 

637 prev_forwards = forwards 

638 prev_backwards = backwards 

639 forwards = [] 

640 backwards = [] 

641 

642 # Create the "last" (k = s+1) matrix 

643 # Note: this is for k = s+1. However, below we then have to fill 

644 # in for k = 1, ..., s in order. 

645 # P L*^{-1} = x 

646 # x L* = P 

647 # L*' x' = P' 

648 forwards.append( 

649 linalg.solve_triangular( 

650 backward_factors[s], partial_autocorrelations[s].T, 

651 lower=True, trans='T')) 

652 forwards[0] = np.dot(forward_factors[s], forwards[0].T) 

653 

654 # P' L^{-1} = x 

655 # x L = P' 

656 # L' x' = P 

657 backwards.append( 

658 linalg.solve_triangular( 

659 forward_factors[s], partial_autocorrelations[s], 

660 lower=True, trans='T')) 

661 backwards[0] = np.dot(backward_factors[s], backwards[0].T) 

662 

663 # Update the variance 

664 # Note: if s >= 1, this will be further updated in the for loop 

665 # below 

666 # Also, this calculation will be re-used in the forward variance 

667 tmp = np.dot(forwards[0], backward_variances[s]) 

668 autocovariances.append(tmp.copy().T) 

669 

670 # Create the remaining k = 1, ..., s matrices, 

671 # only has an effect if s >= 1 

672 for k in range(s): 

673 forwards.insert(k, prev_forwards[k] - np.dot( 

674 forwards[-1], prev_backwards[s-(k+1)])) 

675 

676 backwards.insert(k, prev_backwards[k] - np.dot( 

677 backwards[-1], prev_forwards[s-(k+1)])) 

678 

679 autocovariances[s+1] += np.dot(autocovariances[k+1], 

680 prev_forwards[s-(k+1)].T) 

681 

682 # Create forward and backwards variances 

683 forward_variances.append( 

684 forward_variances[s] - np.dot(tmp, forwards[s].T) 

685 ) 

686 backward_variances.append( 

687 backward_variances[s] - 

688 np.dot( 

689 np.dot(backwards[s], forward_variances[s]), 

690 backwards[s].T 

691 ) 

692 ) 

693 

694 # Cholesky factors 

695 forward_factors.append( 

696 linalg.cholesky(forward_variances[s+1], lower=True) 

697 ) 

698 backward_factors.append( 

699 linalg.cholesky(backward_variances[s+1], lower=True) 

700 ) 

701 

702 # If we do not want to use the transformed variance, we need to 

703 # adjust the constrained matrices, as presented in Lemma 2.3, see above 

704 variance = forward_variances[-1] 

705 if not transform_variance: 

706 # Here, we need to construct T such that: 

707 # variance = T * initial_variance * T' 

708 # To do that, consider the Cholesky of variance (L) and 

709 # input_variance (M) to get: 

710 # L L' = T M M' T' = (TM) (TM)' 

711 # => L = T M 

712 # => L M^{-1} = T 

713 initial_variance_factor = np.linalg.cholesky(initial_variance) 

714 transformed_variance_factor = np.linalg.cholesky(variance) 

715 transform = np.dot(initial_variance_factor, 

716 np.linalg.inv(transformed_variance_factor)) 

717 inv_transform = np.linalg.inv(transform) 

718 

719 for i in range(order): 

720 forwards[i] = ( 

721 np.dot(np.dot(transform, forwards[i]), inv_transform) 

722 ) 

723 

724 return forwards, variance 

725 

726 

727def constrain_stationary_multivariate_python(unconstrained, error_variance, 

728 transform_variance=False, 

729 prefix=None): 

730 r""" 

731 Transform unconstrained parameters used by the optimizer to constrained 

732 parameters used in likelihood evaluation for a vector autoregression. 

733 

734 Parameters 

735 ---------- 

736 unconstrained : array or list 

737 Arbitrary matrices to be transformed to stationary coefficient matrices 

738 of the VAR. If a list, should be a list of length `order`, where each 

739 element is an array sized `k_endog` x `k_endog`. If an array, should be 

740 the matrices horizontally concatenated and sized 

741 `k_endog` x `k_endog * order`. 

742 error_variance : ndarray 

743 The variance / covariance matrix of the error term. Should be sized 

744 `k_endog` x `k_endog`. This is used as input in the algorithm even if 

745 is not transformed by it (when `transform_variance` is False). The 

746 error term variance is required input when transformation is used 

747 either to force an autoregressive component to be stationary or to 

748 force a moving average component to be invertible. 

749 transform_variance : bool, optional 

750 Whether or not to transform the error variance term. This option is 

751 not typically used, and the default is False. 

752 prefix : {'s','d','c','z'}, optional 

753 The appropriate BLAS prefix to use for the passed datatypes. Only 

754 use if absolutely sure that the prefix is correct or an error will 

755 result. 

756 

757 Returns 

758 ------- 

759 constrained : array or list 

760 Transformed coefficient matrices leading to a stationary VAR 

761 representation. Will match the type of the passed `unconstrained` 

762 variable (so if a list was passed, a list will be returned). 

763 

764 Notes 

765 ----- 

766 In the notation of [1]_, the arguments `(variance, unconstrained)` are 

767 written as :math:`(\Sigma, A_1, \dots, A_p)`, where :math:`p` is the order 

768 of the vector autoregression, and is here determined by the length of 

769 the `unconstrained` argument. 

770 

771 There are two steps in the constraining algorithm. 

772 

773 First, :math:`(A_1, \dots, A_p)` are transformed into 

774 :math:`(P_1, \dots, P_p)` via Lemma 2.2 of [1]_. 

775 

776 Second, :math:`(\Sigma, P_1, \dots, P_p)` are transformed into 

777 :math:`(\Sigma, \phi_1, \dots, \phi_p)` via Lemmas 2.1 and 2.3 of [1]_. 

778 

779 If `transform_variance=True`, then only Lemma 2.1 is applied in the second 

780 step. 

781 

782 While this function can be used even in the univariate case, it is much 

783 slower, so in that case `constrain_stationary_univariate` is preferred. 

784 

785 References 

786 ---------- 

787 .. [1] Ansley, Craig F., and Robert Kohn. 1986. 

788 "A Note on Reparameterizing a Vector Autoregressive Moving Average Model 

789 to Enforce Stationarity." 

790 Journal of Statistical Computation and Simulation 24 (2): 99-106. 

791 .. [*] Ansley, Craig F, and Paul Newbold. 1979. 

792 "Multivariate Partial Autocorrelations." 

793 In Proceedings of the Business and Economic Statistics Section, 349-53. 

794 American Statistical Association 

795 """ 

796 

797 use_list = type(unconstrained) == list 

798 if not use_list: 

799 k_endog, order = unconstrained.shape 

800 order //= k_endog 

801 

802 unconstrained = [ 

803 unconstrained[:k_endog, i*k_endog:(i+1)*k_endog] 

804 for i in range(order) 

805 ] 

806 

807 order = len(unconstrained) 

808 k_endog = unconstrained[0].shape[0] 

809 

810 # Step 1: convert from arbitrary matrices to those with singular values 

811 # less than one. 

812 sv_constrained = _constrain_sv_less_than_one_python( 

813 unconstrained, order, k_endog) 

814 

815 # Step 2: convert matrices from our "partial autocorrelation matrix" space 

816 # (matrices with singular values less than one) to the space of stationary 

817 # coefficient matrices 

818 constrained, var = _compute_coefficients_from_multivariate_pacf_python( 

819 sv_constrained, error_variance, transform_variance, order, k_endog) 

820 

821 if not use_list: 

822 constrained = np.concatenate(constrained, axis=1).reshape( 

823 k_endog, k_endog * order) 

824 

825 return constrained, var 

826 

827 

828@Appender(constrain_stationary_multivariate_python.__doc__) 

829def constrain_stationary_multivariate(unconstrained, variance, 

830 transform_variance=False, 

831 prefix=None): 

832 

833 use_list = type(unconstrained) == list 

834 if use_list: 

835 unconstrained = np.concatenate(unconstrained, axis=1) 

836 

837 k_endog, order = unconstrained.shape 

838 order //= k_endog 

839 

840 if order < 1: 

841 raise ValueError('Must have order at least 1') 

842 if k_endog < 1: 

843 raise ValueError('Must have at least 1 endogenous variable') 

844 

845 if prefix is None: 

846 prefix, dtype, _ = find_best_blas_type( 

847 [unconstrained, variance]) 

848 dtype = prefix_dtype_map[prefix] 

849 

850 unconstrained = np.asfortranarray(unconstrained, dtype=dtype) 

851 variance = np.asfortranarray(variance, dtype=dtype) 

852 

853 # Step 1: convert from arbitrary matrices to those with singular values 

854 # less than one. 

855 # sv_constrained = _constrain_sv_less_than_one(unconstrained, order, 

856 # k_endog, prefix) 

857 sv_constrained = prefix_sv_map[prefix](unconstrained, order, k_endog) 

858 

859 # Step 2: convert matrices from our "partial autocorrelation matrix" 

860 # space (matrices with singular values less than one) to the space of 

861 # stationary coefficient matrices 

862 constrained, variance = prefix_pacf_map[prefix]( 

863 sv_constrained, variance, transform_variance, order, k_endog) 

864 

865 constrained = np.array(constrained, dtype=dtype) 

866 variance = np.array(variance, dtype=dtype) 

867 

868 if use_list: 

869 constrained = [ 

870 constrained[:k_endog, i*k_endog:(i+1)*k_endog] 

871 for i in range(order) 

872 ] 

873 

874 return constrained, variance 

875 

876 

877def _unconstrain_sv_less_than_one(constrained, order=None, k_endog=None): 

878 """ 

879 Transform matrices with singular values less than one to arbitrary 

880 matrices. 

881 

882 Parameters 

883 ---------- 

884 constrained : list 

885 The partial autocorrelation matrices. Should be a list of length 

886 `order`, where each element is an array sized `k_endog` x `k_endog`. 

887 order : int, optional 

888 The order of the autoregression. 

889 k_endog : int, optional 

890 The dimension of the data vector. 

891 

892 Returns 

893 ------- 

894 unconstrained : list 

895 Unconstrained matrices. A list of length `order`, where each element is 

896 an array sized `k_endog` x `k_endog`. 

897 

898 Notes 

899 ----- 

900 Corresponds to the inverse of Lemma 2.2 in Ansley and Kohn (1986). See 

901 `unconstrain_stationary_multivariate` for more details. 

902 """ 

903 from scipy import linalg 

904 

905 unconstrained = [] # A_s, s = 1, ..., p 

906 if order is None: 

907 order = len(constrained) 

908 if k_endog is None: 

909 k_endog = constrained[0].shape[0] 

910 

911 eye = np.eye(k_endog) 

912 for i in range(order): 

913 P = constrained[i] 

914 # B^{-1} B^{-1}' = I - P P' 

915 B_inv, lower = linalg.cho_factor(eye - np.dot(P, P.T), lower=True) 

916 # A = BP 

917 # B^{-1} A = P 

918 unconstrained.append(linalg.solve_triangular(B_inv, P, lower=lower)) 

919 return unconstrained 

920 

921 

922def _compute_multivariate_sample_acovf(endog, maxlag): 

923 r""" 

924 Computer multivariate sample autocovariances 

925 

926 Parameters 

927 ---------- 

928 endog : array_like 

929 Sample data on which to compute sample autocovariances. Shaped 

930 `nobs` x `k_endog`. 

931 

932 Returns 

933 ------- 

934 sample_autocovariances : list 

935 A list of the first `maxlag` sample autocovariance matrices. Each 

936 matrix is shaped `k_endog` x `k_endog`. 

937 

938 Notes 

939 ----- 

940 This function computes the forward sample autocovariances: 

941 

942 .. math:: 

943 

944 \hat \Gamma(s) = \frac{1}{n} \sum_{t=1}^{n-s} 

945 (Z_t - \bar Z) (Z_{t+s} - \bar Z)' 

946 

947 See page 353 of Wei (1990). This function is primarily implemented for 

948 checking the partial autocorrelation functions below, and so is quite slow. 

949 

950 References 

951 ---------- 

952 .. [*] Wei, William. 1990. 

953 Time Series Analysis : Univariate and Multivariate Methods. 

954 Boston: Pearson. 

955 """ 

956 # Get the (demeaned) data as an array 

957 endog = np.array(endog) 

958 if endog.ndim == 1: 

959 endog = endog[:, np.newaxis] 

960 endog -= np.mean(endog, axis=0) 

961 

962 # Dimensions 

963 nobs, k_endog = endog.shape 

964 

965 sample_autocovariances = [] 

966 for s in range(maxlag + 1): 

967 sample_autocovariances.append(np.zeros((k_endog, k_endog))) 

968 for t in range(nobs - s): 

969 sample_autocovariances[s] += np.outer(endog[t], endog[t+s]) 

970 sample_autocovariances[s] /= nobs 

971 

972 return sample_autocovariances 

973 

974 

975def _compute_multivariate_acovf_from_coefficients( 

976 coefficients, error_variance, maxlag=None, 

977 forward_autocovariances=False): 

978 r""" 

979 Compute multivariate autocovariances from vector autoregression coefficient 

980 matrices 

981 

982 Parameters 

983 ---------- 

984 coefficients : array or list 

985 The coefficients matrices. If a list, should be a list of length 

986 `order`, where each element is an array sized `k_endog` x `k_endog`. If 

987 an array, should be the coefficient matrices horizontally concatenated 

988 and sized `k_endog` x `k_endog * order`. 

989 error_variance : ndarray 

990 The variance / covariance matrix of the error term. Should be sized 

991 `k_endog` x `k_endog`. 

992 maxlag : int, optional 

993 The maximum autocovariance to compute. Default is `order`-1. Can be 

994 zero, in which case it returns the variance. 

995 forward_autocovariances : bool, optional 

996 Whether or not to compute forward autocovariances 

997 :math:`E(y_t y_{t+j}')`. Default is False, so that backward 

998 autocovariances :math:`E(y_t y_{t-j}')` are returned. 

999 

1000 Returns 

1001 ------- 

1002 autocovariances : list 

1003 A list of the first `maxlag` autocovariance matrices. Each matrix is 

1004 shaped `k_endog` x `k_endog`. 

1005 

1006 Notes 

1007 ----- 

1008 Computes 

1009 

1010 .. math:: 

1011 

1012 \Gamma(j) = E(y_t y_{t-j}') 

1013 

1014 for j = 1, ..., `maxlag`, unless `forward_autocovariances` is specified, 

1015 in which case it computes: 

1016 

1017 .. math:: 

1018 

1019 E(y_t y_{t+j}') = \Gamma(j)' 

1020 

1021 Coefficients are assumed to be provided from the VAR model: 

1022 

1023 .. math:: 

1024 y_t = A_1 y_{t-1} + \dots + A_p y_{t-p} + \varepsilon_t 

1025 

1026 Autocovariances are calculated by solving the associated discrete Lyapunov 

1027 equation of the state space representation of the VAR process. 

1028 """ 

1029 from scipy import linalg 

1030 

1031 # Convert coefficients to a list of matrices, for use in 

1032 # `companion_matrix`; get dimensions 

1033 if type(coefficients) == list: 

1034 order = len(coefficients) 

1035 k_endog = coefficients[0].shape[0] 

1036 else: 

1037 k_endog, order = coefficients.shape 

1038 order //= k_endog 

1039 

1040 coefficients = [ 

1041 coefficients[:k_endog, i*k_endog:(i+1)*k_endog] 

1042 for i in range(order) 

1043 ] 

1044 

1045 if maxlag is None: 

1046 maxlag = order-1 

1047 

1048 # Start with VAR(p): w_{t+1} = phi_1 w_t + ... + phi_p w_{t-p+1} + u_{t+1} 

1049 # Then stack the VAR(p) into a VAR(1) in companion matrix form: 

1050 # z_{t+1} = F z_t + v_t 

1051 companion = companion_matrix( 

1052 [1] + [-np.squeeze(coefficients[i]) for i in range(order)] 

1053 ).T 

1054 

1055 # Compute the error variance matrix for the stacked form: E v_t v_t' 

1056 selected_variance = np.zeros(companion.shape) 

1057 selected_variance[:k_endog, :k_endog] = error_variance 

1058 

1059 # Compute the unconditional variance of z_t: E z_t z_t' 

1060 stacked_cov = linalg.solve_discrete_lyapunov(companion, selected_variance) 

1061 

1062 # The first (block) row of the variance of z_t gives the first p-1 

1063 # autocovariances of w_t: \Gamma_i = E w_t w_t+i with \Gamma_0 = Var(w_t) 

1064 # Note: these are okay, checked against ArmaProcess 

1065 autocovariances = [ 

1066 stacked_cov[:k_endog, i*k_endog:(i+1)*k_endog] 

1067 for i in range(min(order, maxlag+1)) 

1068 ] 

1069 

1070 for i in range(maxlag - (order-1)): 

1071 stacked_cov = np.dot(companion, stacked_cov) 

1072 autocovariances += [ 

1073 stacked_cov[:k_endog, -k_endog:] 

1074 ] 

1075 

1076 if forward_autocovariances: 

1077 for i in range(len(autocovariances)): 

1078 autocovariances[i] = autocovariances[i].T 

1079 

1080 return autocovariances 

1081 

1082 

1083def _compute_multivariate_sample_pacf(endog, maxlag): 

1084 """ 

1085 Computer multivariate sample partial autocorrelations 

1086 

1087 Parameters 

1088 ---------- 

1089 endog : array_like 

1090 Sample data on which to compute sample autocovariances. Shaped 

1091 `nobs` x `k_endog`. 

1092 maxlag : int 

1093 Maximum lag for which to calculate sample partial autocorrelations. 

1094 

1095 Returns 

1096 ------- 

1097 sample_pacf : list 

1098 A list of the first `maxlag` sample partial autocorrelation matrices. 

1099 Each matrix is shaped `k_endog` x `k_endog`. 

1100 """ 

1101 sample_autocovariances = _compute_multivariate_sample_acovf(endog, maxlag) 

1102 

1103 return _compute_multivariate_pacf_from_autocovariances( 

1104 sample_autocovariances) 

1105 

1106 

1107def _compute_multivariate_pacf_from_autocovariances(autocovariances, 

1108 order=None, k_endog=None): 

1109 """ 

1110 Compute multivariate partial autocorrelations from autocovariances. 

1111 

1112 Parameters 

1113 ---------- 

1114 autocovariances : list 

1115 Autocorrelations matrices. Should be a list of length `order` + 1, 

1116 where each element is an array sized `k_endog` x `k_endog`. 

1117 order : int, optional 

1118 The order of the autoregression. 

1119 k_endog : int, optional 

1120 The dimension of the data vector. 

1121 

1122 Returns 

1123 ------- 

1124 pacf : list 

1125 List of first `order` multivariate partial autocorrelations. 

1126 

1127 Notes 

1128 ----- 

1129 Note that this computes multivariate partial autocorrelations. 

1130 

1131 Corresponds to the inverse of Lemma 2.1 in Ansley and Kohn (1986). See 

1132 `unconstrain_stationary_multivariate` for more details. 

1133 

1134 Notes 

1135 ----- 

1136 Computes sample partial autocorrelations if sample autocovariances are 

1137 given. 

1138 """ 

1139 from scipy import linalg 

1140 

1141 if order is None: 

1142 order = len(autocovariances)-1 

1143 if k_endog is None: 

1144 k_endog = autocovariances[0].shape[0] 

1145 

1146 # Now apply the Ansley and Kohn (1986) algorithm, except that instead of 

1147 # calculating phi_{s+1, s+1} = L_s P_{s+1} {L_s^*}^{-1} (which requires 

1148 # the partial autocorrelation P_{s+1} which is what we're trying to 

1149 # calculate here), we calculate it as in Ansley and Newbold (1979), using 

1150 # the autocovariances \Gamma_s and the forwards and backwards residual 

1151 # variances \Sigma_s, \Sigma_s^*: 

1152 # phi_{s+1, s+1} = [ \Gamma_{s+1}' - \phi_{s,1} \Gamma_s' - ... - 

1153 # \phi_{s,s} \Gamma_1' ] {\Sigma_s^*}^{-1} 

1154 

1155 # Forward and backward variances 

1156 forward_variances = [] # \Sigma_s 

1157 backward_variances = [] # \Sigma_s^*, s = 0, ..., p 

1158 # \phi_{s,k}, s = 1, ..., p 

1159 # k = 1, ..., s+1 

1160 forwards = [] 

1161 # \phi_{s,k}^* 

1162 backwards = [] 

1163 

1164 forward_factors = [] # L_s 

1165 backward_factors = [] # L_s^*, s = 0, ..., p 

1166 

1167 # Ultimately we want to construct the partial autocorrelation matrices 

1168 # Note that this is "1-indexed" in the sense that it stores P_1, ... P_p 

1169 # rather than starting with P_0. 

1170 partial_autocorrelations = [] 

1171 

1172 # We fill in the entries of phi_{s,k} as follows: 

1173 # [1,1] 

1174 # [2,2], [2,1] 

1175 # [3,3], [3,1], [3,2] 

1176 # ... 

1177 # [p,p], [p,1], ..., [p,p-1] 

1178 # the last row, correctly ordered, should be the same as the coefficient 

1179 # matrices provided in the argument `constrained` 

1180 for s in range(order): # s = 0, ..., p-1 

1181 prev_forwards = list(forwards) 

1182 prev_backwards = list(backwards) 

1183 forwards = [] 

1184 backwards = [] 

1185 

1186 # Create forward and backwards variances Sigma_s, Sigma*_s 

1187 forward_variance = autocovariances[0].copy() 

1188 backward_variance = autocovariances[0].T.copy() 

1189 

1190 for k in range(s): 

1191 forward_variance -= np.dot(prev_forwards[k], 

1192 autocovariances[k+1]) 

1193 backward_variance -= np.dot(prev_backwards[k], 

1194 autocovariances[k+1].T) 

1195 

1196 forward_variances.append(forward_variance) 

1197 backward_variances.append(backward_variance) 

1198 

1199 # Cholesky factors 

1200 forward_factors.append( 

1201 linalg.cholesky(forward_variances[s], lower=True) 

1202 ) 

1203 backward_factors.append( 

1204 linalg.cholesky(backward_variances[s], lower=True) 

1205 ) 

1206 

1207 # Create the intermediate sum term 

1208 if s == 0: 

1209 # phi_11 = \Gamma_1' \Gamma_0^{-1} 

1210 # phi_11 \Gamma_0 = \Gamma_1' 

1211 # \Gamma_0 phi_11' = \Gamma_1 

1212 forwards.append(linalg.cho_solve( 

1213 (forward_factors[0], True), autocovariances[1]).T) 

1214 # backwards.append(forwards[-1]) 

1215 # phi_11_star = \Gamma_1 \Gamma_0^{-1} 

1216 # phi_11_star \Gamma_0 = \Gamma_1 

1217 # \Gamma_0 phi_11_star' = \Gamma_1' 

1218 backwards.append(linalg.cho_solve( 

1219 (backward_factors[0], True), autocovariances[1].T).T) 

1220 else: 

1221 # G := \Gamma_{s+1}' - 

1222 # \phi_{s,1} \Gamma_s' - .. - \phi_{s,s} \Gamma_1' 

1223 tmp_sum = autocovariances[s+1].T.copy() 

1224 

1225 for k in range(s): 

1226 tmp_sum -= np.dot(prev_forwards[k], autocovariances[s-k].T) 

1227 

1228 # Create the "last" (k = s+1) matrix 

1229 # Note: this is for k = s+1. However, below we then have to 

1230 # fill in for k = 1, ..., s in order. 

1231 # phi = G Sigma*^{-1} 

1232 # phi Sigma* = G 

1233 # Sigma*' phi' = G' 

1234 # Sigma* phi' = G' 

1235 # (because Sigma* is symmetric) 

1236 forwards.append(linalg.cho_solve( 

1237 (backward_factors[s], True), tmp_sum.T).T) 

1238 

1239 # phi = G' Sigma^{-1} 

1240 # phi Sigma = G' 

1241 # Sigma' phi' = G 

1242 # Sigma phi' = G 

1243 # (because Sigma is symmetric) 

1244 backwards.append(linalg.cho_solve( 

1245 (forward_factors[s], True), tmp_sum).T) 

1246 

1247 # Create the remaining k = 1, ..., s matrices, 

1248 # only has an effect if s >= 1 

1249 for k in range(s): 

1250 forwards.insert(k, prev_forwards[k] - np.dot( 

1251 forwards[-1], prev_backwards[s-(k+1)])) 

1252 backwards.insert(k, prev_backwards[k] - np.dot( 

1253 backwards[-1], prev_forwards[s-(k+1)])) 

1254 

1255 # Partial autocorrelation matrix: P_{s+1} 

1256 # P = L^{-1} phi L* 

1257 # L P = (phi L*) 

1258 partial_autocorrelations.append(linalg.solve_triangular( 

1259 forward_factors[s], np.dot(forwards[s], backward_factors[s]), 

1260 lower=True)) 

1261 

1262 return partial_autocorrelations 

1263 

1264 

1265def _compute_multivariate_pacf_from_coefficients(constrained, error_variance, 

1266 order=None, k_endog=None): 

1267 r""" 

1268 Transform matrices corresponding to a stationary (or invertible) process 

1269 to matrices with singular values less than one. 

1270 

1271 Parameters 

1272 ---------- 

1273 constrained : array or list 

1274 The coefficients matrices. If a list, should be a list of length 

1275 `order`, where each element is an array sized `k_endog` x `k_endog`. If 

1276 an array, should be the coefficient matrices horizontally concatenated 

1277 and sized `k_endog` x `k_endog * order`. 

1278 error_variance : ndarray 

1279 The variance / covariance matrix of the error term. Should be sized 

1280 `k_endog` x `k_endog`. 

1281 order : int, optional 

1282 The order of the autoregression. 

1283 k_endog : int, optional 

1284 The dimension of the data vector. 

1285 

1286 Returns 

1287 ------- 

1288 pacf : list 

1289 List of first `order` multivariate partial autocorrelations. 

1290 

1291 Notes 

1292 ----- 

1293 Note that this computes multivariate partial autocorrelations. 

1294 

1295 Corresponds to the inverse of Lemma 2.1 in Ansley and Kohn (1986). See 

1296 `unconstrain_stationary_multivariate` for more details. 

1297 

1298 Notes 

1299 ----- 

1300 

1301 Coefficients are assumed to be provided from the VAR model: 

1302 

1303 .. math:: 

1304 y_t = A_1 y_{t-1} + \dots + A_p y_{t-p} + \varepsilon_t 

1305 """ 

1306 

1307 if type(constrained) == list: 

1308 order = len(constrained) 

1309 k_endog = constrained[0].shape[0] 

1310 else: 

1311 k_endog, order = constrained.shape 

1312 order //= k_endog 

1313 

1314 # Get autocovariances for the process; these are defined to be 

1315 # E z_t z_{t-j}' 

1316 # However, we want E z_t z_{t+j}' = (E z_t z_{t-j}')' 

1317 _acovf = _compute_multivariate_acovf_from_coefficients 

1318 

1319 autocovariances = [ 

1320 autocovariance.T for autocovariance in 

1321 _acovf(constrained, error_variance, maxlag=order)] 

1322 

1323 return _compute_multivariate_pacf_from_autocovariances(autocovariances) 

1324 

1325 

1326def unconstrain_stationary_multivariate(constrained, error_variance): 

1327 """ 

1328 Transform constrained parameters used in likelihood evaluation 

1329 to unconstrained parameters used by the optimizer 

1330 

1331 Parameters 

1332 ---------- 

1333 constrained : array or list 

1334 Constrained parameters of, e.g., an autoregressive or moving average 

1335 component, to be transformed to arbitrary parameters used by the 

1336 optimizer. If a list, should be a list of length `order`, where each 

1337 element is an array sized `k_endog` x `k_endog`. If an array, should be 

1338 the coefficient matrices horizontally concatenated and sized 

1339 `k_endog` x `k_endog * order`. 

1340 error_variance : ndarray 

1341 The variance / covariance matrix of the error term. Should be sized 

1342 `k_endog` x `k_endog`. This is used as input in the algorithm even if 

1343 is not transformed by it (when `transform_variance` is False). 

1344 

1345 Returns 

1346 ------- 

1347 unconstrained : ndarray 

1348 Unconstrained parameters used by the optimizer, to be transformed to 

1349 stationary coefficients of, e.g., an autoregressive or moving average 

1350 component. Will match the type of the passed `constrained` 

1351 variable (so if a list was passed, a list will be returned). 

1352 

1353 Notes 

1354 ----- 

1355 Uses the list representation internally, even if an array is passed. 

1356 

1357 References 

1358 ---------- 

1359 .. [*] Ansley, Craig F., and Robert Kohn. 1986. 

1360 "A Note on Reparameterizing a Vector Autoregressive Moving Average Model 

1361 to Enforce Stationarity." 

1362 Journal of Statistical Computation and Simulation 24 (2): 99-106. 

1363 """ 

1364 use_list = type(constrained) == list 

1365 if not use_list: 

1366 k_endog, order = constrained.shape 

1367 order //= k_endog 

1368 

1369 constrained = [ 

1370 constrained[:k_endog, i*k_endog:(i+1)*k_endog] 

1371 for i in range(order) 

1372 ] 

1373 else: 

1374 order = len(constrained) 

1375 k_endog = constrained[0].shape[0] 

1376 

1377 # Step 1: convert matrices from the space of stationary 

1378 # coefficient matrices to our "partial autocorrelation matrix" space 

1379 # (matrices with singular values less than one) 

1380 partial_autocorrelations = _compute_multivariate_pacf_from_coefficients( 

1381 constrained, error_variance, order, k_endog) 

1382 

1383 # Step 2: convert from arbitrary matrices to those with singular values 

1384 # less than one. 

1385 unconstrained = _unconstrain_sv_less_than_one( 

1386 partial_autocorrelations, order, k_endog) 

1387 

1388 if not use_list: 

1389 unconstrained = np.concatenate(unconstrained, axis=1) 

1390 

1391 return unconstrained, error_variance 

1392 

1393 

1394def validate_matrix_shape(name, shape, nrows, ncols, nobs): 

1395 """ 

1396 Validate the shape of a possibly time-varying matrix, or raise an exception 

1397 

1398 Parameters 

1399 ---------- 

1400 name : str 

1401 The name of the matrix being validated (used in exception messages) 

1402 shape : array_like 

1403 The shape of the matrix to be validated. May be of size 2 or (if 

1404 the matrix is time-varying) 3. 

1405 nrows : int 

1406 The expected number of rows. 

1407 ncols : int 

1408 The expected number of columns. 

1409 nobs : int 

1410 The number of observations (used to validate the last dimension of a 

1411 time-varying matrix) 

1412 

1413 Raises 

1414 ------ 

1415 ValueError 

1416 If the matrix is not of the desired shape. 

1417 """ 

1418 ndim = len(shape) 

1419 

1420 # Enforce dimension 

1421 if ndim not in [2, 3]: 

1422 raise ValueError('Invalid value for %s matrix. Requires a' 

1423 ' 2- or 3-dimensional array, got %d dimensions' % 

1424 (name, ndim)) 

1425 # Enforce the shape of the matrix 

1426 if not shape[0] == nrows: 

1427 raise ValueError('Invalid dimensions for %s matrix: requires %d' 

1428 ' rows, got %d' % (name, nrows, shape[0])) 

1429 if not shape[1] == ncols: 

1430 raise ValueError('Invalid dimensions for %s matrix: requires %d' 

1431 ' columns, got %d' % (name, ncols, shape[1])) 

1432 

1433 # If we do not yet know `nobs`, do not allow time-varying arrays 

1434 if nobs is None and not (ndim == 2 or shape[-1] == 1): 

1435 raise ValueError('Invalid dimensions for %s matrix: time-varying' 

1436 ' matrices cannot be given unless `nobs` is specified' 

1437 ' (implicitly when a dataset is bound or else set' 

1438 ' explicity)' % name) 

1439 

1440 # Enforce time-varying array size 

1441 if ndim == 3 and nobs is not None and not shape[-1] in [1, nobs]: 

1442 raise ValueError('Invalid dimensions for time-varying %s' 

1443 ' matrix. Requires shape (*,*,%d), got %s' % 

1444 (name, nobs, str(shape))) 

1445 

1446 

1447def validate_vector_shape(name, shape, nrows, nobs): 

1448 """ 

1449 Validate the shape of a possibly time-varying vector, or raise an exception 

1450 

1451 Parameters 

1452 ---------- 

1453 name : str 

1454 The name of the vector being validated (used in exception messages) 

1455 shape : array_like 

1456 The shape of the vector to be validated. May be of size 1 or (if 

1457 the vector is time-varying) 2. 

1458 nrows : int 

1459 The expected number of rows (elements of the vector). 

1460 nobs : int 

1461 The number of observations (used to validate the last dimension of a 

1462 time-varying vector) 

1463 

1464 Raises 

1465 ------ 

1466 ValueError 

1467 If the vector is not of the desired shape. 

1468 """ 

1469 ndim = len(shape) 

1470 # Enforce dimension 

1471 if ndim not in [1, 2]: 

1472 raise ValueError('Invalid value for %s vector. Requires a' 

1473 ' 1- or 2-dimensional array, got %d dimensions' % 

1474 (name, ndim)) 

1475 # Enforce the shape of the vector 

1476 if not shape[0] == nrows: 

1477 raise ValueError('Invalid dimensions for %s vector: requires %d' 

1478 ' rows, got %d' % (name, nrows, shape[0])) 

1479 

1480 # If we do not yet know `nobs`, do not allow time-varying arrays 

1481 if nobs is None and not (ndim == 1 or shape[-1] == 1): 

1482 raise ValueError('Invalid dimensions for %s vector: time-varying' 

1483 ' vectors cannot be given unless `nobs` is specified' 

1484 ' (implicitly when a dataset is bound or else set' 

1485 ' explicity)' % name) 

1486 

1487 # Enforce time-varying array size 

1488 if ndim == 2 and not shape[1] in [1, nobs]: 

1489 raise ValueError('Invalid dimensions for time-varying %s' 

1490 ' vector. Requires shape (*,%d), got %s' % 

1491 (name, nobs, str(shape))) 

1492 

1493 

1494def reorder_missing_matrix(matrix, missing, reorder_rows=False, 

1495 reorder_cols=False, is_diagonal=False, 

1496 inplace=False, prefix=None): 

1497 """ 

1498 Reorder the rows or columns of a time-varying matrix where all non-missing 

1499 values are in the upper left corner of the matrix. 

1500 

1501 Parameters 

1502 ---------- 

1503 matrix : array_like 

1504 The matrix to be reordered. Must have shape (n, m, nobs). 

1505 missing : array_like of bool 

1506 The vector of missing indices. Must have shape (k, nobs) where `k = n` 

1507 if `reorder_rows is True` and `k = m` if `reorder_cols is True`. 

1508 reorder_rows : bool, optional 

1509 Whether or not the rows of the matrix should be re-ordered. Default 

1510 is False. 

1511 reorder_cols : bool, optional 

1512 Whether or not the columns of the matrix should be re-ordered. Default 

1513 is False. 

1514 is_diagonal : bool, optional 

1515 Whether or not the matrix is diagonal. If this is True, must also have 

1516 `n = m`. Default is False. 

1517 inplace : bool, optional 

1518 Whether or not to reorder the matrix in-place. 

1519 prefix : {'s', 'd', 'c', 'z'}, optional 

1520 The Fortran prefix of the vector. Default is to automatically detect 

1521 the dtype. This parameter should only be used with caution. 

1522 

1523 Returns 

1524 ------- 

1525 reordered_matrix : array_like 

1526 The reordered matrix. 

1527 """ 

1528 if prefix is None: 

1529 prefix = find_best_blas_type((matrix,))[0] 

1530 reorder = prefix_reorder_missing_matrix_map[prefix] 

1531 

1532 if not inplace: 

1533 matrix = np.copy(matrix, order='F') 

1534 

1535 reorder(matrix, np.asfortranarray(missing), reorder_rows, reorder_cols, 

1536 is_diagonal) 

1537 

1538 return matrix 

1539 

1540 

1541def reorder_missing_vector(vector, missing, inplace=False, prefix=None): 

1542 """ 

1543 Reorder the elements of a time-varying vector where all non-missing 

1544 values are in the first elements of the vector. 

1545 

1546 Parameters 

1547 ---------- 

1548 vector : array_like 

1549 The vector to be reordered. Must have shape (n, nobs). 

1550 missing : array_like of bool 

1551 The vector of missing indices. Must have shape (n, nobs). 

1552 inplace : bool, optional 

1553 Whether or not to reorder the matrix in-place. Default is False. 

1554 prefix : {'s', 'd', 'c', 'z'}, optional 

1555 The Fortran prefix of the vector. Default is to automatically detect 

1556 the dtype. This parameter should only be used with caution. 

1557 

1558 Returns 

1559 ------- 

1560 reordered_vector : array_like 

1561 The reordered vector. 

1562 """ 

1563 if prefix is None: 

1564 prefix = find_best_blas_type((vector,))[0] 

1565 reorder = prefix_reorder_missing_vector_map[prefix] 

1566 

1567 if not inplace: 

1568 vector = np.copy(vector, order='F') 

1569 

1570 reorder(vector, np.asfortranarray(missing)) 

1571 

1572 return vector 

1573 

1574 

1575def copy_missing_matrix(A, B, missing, missing_rows=False, missing_cols=False, 

1576 is_diagonal=False, inplace=False, prefix=None): 

1577 """ 

1578 Copy the rows or columns of a time-varying matrix where all non-missing 

1579 values are in the upper left corner of the matrix. 

1580 

1581 Parameters 

1582 ---------- 

1583 A : array_like 

1584 The matrix from which to copy. Must have shape (n, m, nobs) or 

1585 (n, m, 1). 

1586 B : array_like 

1587 The matrix to copy to. Must have shape (n, m, nobs). 

1588 missing : array_like of bool 

1589 The vector of missing indices. Must have shape (k, nobs) where `k = n` 

1590 if `reorder_rows is True` and `k = m` if `reorder_cols is True`. 

1591 missing_rows : bool, optional 

1592 Whether or not the rows of the matrix are a missing dimension. Default 

1593 is False. 

1594 missing_cols : bool, optional 

1595 Whether or not the columns of the matrix are a missing dimension. 

1596 Default is False. 

1597 is_diagonal : bool, optional 

1598 Whether or not the matrix is diagonal. If this is True, must also have 

1599 `n = m`. Default is False. 

1600 inplace : bool, optional 

1601 Whether or not to copy to B in-place. Default is False. 

1602 prefix : {'s', 'd', 'c', 'z'}, optional 

1603 The Fortran prefix of the vector. Default is to automatically detect 

1604 the dtype. This parameter should only be used with caution. 

1605 

1606 Returns 

1607 ------- 

1608 copied_matrix : array_like 

1609 The matrix B with the non-missing submatrix of A copied onto it. 

1610 """ 

1611 if prefix is None: 

1612 prefix = find_best_blas_type((A, B))[0] 

1613 copy = prefix_copy_missing_matrix_map[prefix] 

1614 

1615 if not inplace: 

1616 B = np.copy(B, order='F') 

1617 

1618 # We may have been given an F-contiguous memoryview; in that case, we do 

1619 # not want to alter it or convert it to a numpy array 

1620 try: 

1621 if not A.is_f_contig(): 

1622 raise ValueError() 

1623 except (AttributeError, ValueError): 

1624 A = np.asfortranarray(A) 

1625 

1626 copy(A, B, np.asfortranarray(missing), missing_rows, missing_cols, 

1627 is_diagonal) 

1628 

1629 return B 

1630 

1631 

1632def copy_missing_vector(a, b, missing, inplace=False, prefix=None): 

1633 """ 

1634 Reorder the elements of a time-varying vector where all non-missing 

1635 values are in the first elements of the vector. 

1636 

1637 Parameters 

1638 ---------- 

1639 a : array_like 

1640 The vector from which to copy. Must have shape (n, nobs) or (n, 1). 

1641 b : array_like 

1642 The vector to copy to. Must have shape (n, nobs). 

1643 missing : array_like of bool 

1644 The vector of missing indices. Must have shape (n, nobs). 

1645 inplace : bool, optional 

1646 Whether or not to copy to b in-place. Default is False. 

1647 prefix : {'s', 'd', 'c', 'z'}, optional 

1648 The Fortran prefix of the vector. Default is to automatically detect 

1649 the dtype. This parameter should only be used with caution. 

1650 

1651 Returns 

1652 ------- 

1653 copied_vector : array_like 

1654 The vector b with the non-missing subvector of b copied onto it. 

1655 """ 

1656 if prefix is None: 

1657 prefix = find_best_blas_type((a, b))[0] 

1658 copy = prefix_copy_missing_vector_map[prefix] 

1659 

1660 if not inplace: 

1661 b = np.copy(b, order='F') 

1662 

1663 # We may have been given an F-contiguous memoryview; in that case, we do 

1664 # not want to alter it or convert it to a numpy array 

1665 try: 

1666 if not a.is_f_contig(): 

1667 raise ValueError() 

1668 except (AttributeError, ValueError): 

1669 a = np.asfortranarray(a) 

1670 

1671 copy(a, b, np.asfortranarray(missing)) 

1672 

1673 return b 

1674 

1675 

1676def copy_index_matrix(A, B, index, index_rows=False, index_cols=False, 

1677 is_diagonal=False, inplace=False, prefix=None): 

1678 """ 

1679 Copy the rows or columns of a time-varying matrix where all non-index 

1680 values are in the upper left corner of the matrix. 

1681 

1682 Parameters 

1683 ---------- 

1684 A : array_like 

1685 The matrix from which to copy. Must have shape (n, m, nobs) or 

1686 (n, m, 1). 

1687 B : array_like 

1688 The matrix to copy to. Must have shape (n, m, nobs). 

1689 index : array_like of bool 

1690 The vector of index indices. Must have shape (k, nobs) where `k = n` 

1691 if `reorder_rows is True` and `k = m` if `reorder_cols is True`. 

1692 index_rows : bool, optional 

1693 Whether or not the rows of the matrix are a index dimension. Default 

1694 is False. 

1695 index_cols : bool, optional 

1696 Whether or not the columns of the matrix are a index dimension. 

1697 Default is False. 

1698 is_diagonal : bool, optional 

1699 Whether or not the matrix is diagonal. If this is True, must also have 

1700 `n = m`. Default is False. 

1701 inplace : bool, optional 

1702 Whether or not to copy to B in-place. Default is False. 

1703 prefix : {'s', 'd', 'c', 'z'}, optional 

1704 The Fortran prefix of the vector. Default is to automatically detect 

1705 the dtype. This parameter should only be used with caution. 

1706 

1707 Returns 

1708 ------- 

1709 copied_matrix : array_like 

1710 The matrix B with the non-index submatrix of A copied onto it. 

1711 """ 

1712 if prefix is None: 

1713 prefix = find_best_blas_type((A, B))[0] 

1714 copy = prefix_copy_index_matrix_map[prefix] 

1715 

1716 if not inplace: 

1717 B = np.copy(B, order='F') 

1718 

1719 # We may have been given an F-contiguous memoryview; in that case, we do 

1720 # not want to alter it or convert it to a numpy array 

1721 try: 

1722 if not A.is_f_contig(): 

1723 raise ValueError() 

1724 except (AttributeError, ValueError): 

1725 A = np.asfortranarray(A) 

1726 

1727 copy(A, B, np.asfortranarray(index), index_rows, index_cols, 

1728 is_diagonal) 

1729 

1730 return B 

1731 

1732 

1733def copy_index_vector(a, b, index, inplace=False, prefix=None): 

1734 """ 

1735 Reorder the elements of a time-varying vector where all non-index 

1736 values are in the first elements of the vector. 

1737 

1738 Parameters 

1739 ---------- 

1740 a : array_like 

1741 The vector from which to copy. Must have shape (n, nobs) or (n, 1). 

1742 b : array_like 

1743 The vector to copy to. Must have shape (n, nobs). 

1744 index : array_like of bool 

1745 The vector of index indices. Must have shape (n, nobs). 

1746 inplace : bool, optional 

1747 Whether or not to copy to b in-place. Default is False. 

1748 prefix : {'s', 'd', 'c', 'z'}, optional 

1749 The Fortran prefix of the vector. Default is to automatically detect 

1750 the dtype. This parameter should only be used with caution. 

1751 

1752 Returns 

1753 ------- 

1754 copied_vector : array_like 

1755 The vector b with the non-index subvector of b copied onto it. 

1756 """ 

1757 if prefix is None: 

1758 prefix = find_best_blas_type((a, b))[0] 

1759 copy = prefix_copy_index_vector_map[prefix] 

1760 

1761 if not inplace: 

1762 b = np.copy(b, order='F') 

1763 

1764 # We may have been given an F-contiguous memoryview; in that case, we do 

1765 # not want to alter it or convert it to a numpy array 

1766 try: 

1767 if not a.is_f_contig(): 

1768 raise ValueError() 

1769 except (AttributeError, ValueError): 

1770 a = np.asfortranarray(a) 

1771 

1772 copy(a, b, np.asfortranarray(index)) 

1773 

1774 return b 

1775 

1776 

1777def prepare_exog(exog): 

1778 k_exog = 0 

1779 if exog is not None: 

1780 exog_is_using_pandas = _is_using_pandas(exog, None) 

1781 if not exog_is_using_pandas: 

1782 exog = np.asarray(exog) 

1783 

1784 # Make sure we have 2-dimensional array 

1785 if exog.ndim == 1: 

1786 if not exog_is_using_pandas: 

1787 exog = exog[:, None] 

1788 else: 

1789 exog = pd.DataFrame(exog) 

1790 

1791 k_exog = exog.shape[1] 

1792 return (k_exog, exog) 

1793 

1794 

1795def prepare_trend_spec(trend): 

1796 # Trend 

1797 if trend is None or trend == 'n': 

1798 polynomial_trend = np.ones(0) 

1799 elif trend == 'c': 

1800 polynomial_trend = np.r_[1] 

1801 elif trend == 't': 

1802 polynomial_trend = np.r_[0, 1] 

1803 elif trend == 'ct': 

1804 polynomial_trend = np.r_[1, 1] 

1805 elif trend == 'ctt': 

1806 # TODO deprecate ctt? 

1807 polynomial_trend = np.r_[1, 1, 1] 

1808 else: 

1809 trend = np.array(trend) 

1810 if trend.ndim > 0: 

1811 polynomial_trend = (trend > 0).astype(int) 

1812 else: 

1813 raise ValueError('Invalid trend method.') 

1814 

1815 # Note: k_trend is not the degree of the trend polynomial, because e.g. 

1816 # k_trend = 1 corresponds to the degree zero polynomial (with only a 

1817 # constant term). 

1818 k_trend = int(np.sum(polynomial_trend)) 

1819 

1820 return polynomial_trend, k_trend 

1821 

1822 

1823def prepare_trend_data(polynomial_trend, k_trend, nobs, offset=1): 

1824 # Cache the arrays for calculating the intercept from the trend 

1825 # components 

1826 time_trend = np.arange(offset, nobs + offset) 

1827 trend_data = np.zeros((nobs, k_trend)) 

1828 i = 0 

1829 for k in polynomial_trend.nonzero()[0]: 

1830 if k == 0: 

1831 trend_data[:, i] = np.ones(nobs,) 

1832 else: 

1833 trend_data[:, i] = time_trend**k 

1834 i += 1 

1835 

1836 return trend_data