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""" 

2Markov switching regression models 

3 

4Author: Chad Fulton 

5License: BSD-3 

6""" 

7 

8 

9import numpy as np 

10import statsmodels.base.wrapper as wrap 

11 

12from statsmodels.tsa.regime_switching import markov_switching 

13 

14 

15class MarkovRegression(markov_switching.MarkovSwitching): 

16 r""" 

17 First-order k-regime Markov switching regression model 

18 

19 Parameters 

20 ---------- 

21 endog : array_like 

22 The endogenous variable. 

23 k_regimes : int 

24 The number of regimes. 

25 trend : {'nc', 'c', 't', 'ct'} 

26 Whether or not to include a trend. To include an intercept, time trend, 

27 or both, set `trend='c'`, `trend='t'`, or `trend='ct'`. For no trend, 

28 set `trend='nc'`. Default is an intercept. 

29 exog : array_like, optional 

30 Array of exogenous regressors, shaped nobs x k. 

31 order : int, optional 

32 The order of the model describes the dependence of the likelihood on 

33 previous regimes. This depends on the model in question and should be 

34 set appropriately by subclasses. 

35 exog_tvtp : array_like, optional 

36 Array of exogenous or lagged variables to use in calculating 

37 time-varying transition probabilities (TVTP). TVTP is only used if this 

38 variable is provided. If an intercept is desired, a column of ones must 

39 be explicitly included in this array. 

40 switching_trend : bool or iterable, optional 

41 If a boolean, sets whether or not all trend coefficients are 

42 switching across regimes. If an iterable, should be of length equal 

43 to the number of trend variables, where each element is 

44 a boolean describing whether the corresponding coefficient is 

45 switching. Default is True. 

46 switching_exog : bool or iterable, optional 

47 If a boolean, sets whether or not all regression coefficients are 

48 switching across regimes. If an iterable, should be of length equal 

49 to the number of exogenous variables, where each element is 

50 a boolean describing whether the corresponding coefficient is 

51 switching. Default is True. 

52 switching_variance : bool, optional 

53 Whether or not there is regime-specific heteroskedasticity, i.e. 

54 whether or not the error term has a switching variance. Default is 

55 False. 

56 

57 Notes 

58 ----- 

59 This model is new and API stability is not guaranteed, although changes 

60 will be made in a backwards compatible way if possible. 

61 

62 The model can be written as: 

63 

64 .. math:: 

65 

66 y_t = a_{S_t} + x_t' \beta_{S_t} + \varepsilon_t \\ 

67 \varepsilon_t \sim N(0, \sigma_{S_t}^2) 

68 

69 i.e. the model is a dynamic linear regression where the coefficients and 

70 the variance of the error term may be switching across regimes. 

71 

72 The `trend` is accommodated by prepending columns to the `exog` array. Thus 

73 if `trend='c'`, the passed `exog` array should not already have a column of 

74 ones. 

75 

76 References 

77 ---------- 

78 Kim, Chang-Jin, and Charles R. Nelson. 1999. 

79 "State-Space Models with Regime Switching: 

80 Classical and Gibbs-Sampling Approaches with Applications". 

81 MIT Press Books. The MIT Press. 

82 """ 

83 

84 def __init__(self, endog, k_regimes, trend='c', exog=None, order=0, 

85 exog_tvtp=None, switching_trend=True, switching_exog=True, 

86 switching_variance=False, dates=None, freq=None, 

87 missing='none'): 

88 

89 # Properties 

90 self.trend = trend 

91 self.switching_trend = switching_trend 

92 self.switching_exog = switching_exog 

93 self.switching_variance = switching_variance 

94 

95 # Exogenous data 

96 self.k_exog, exog = markov_switching.prepare_exog(exog) 

97 

98 # Trend 

99 nobs = len(endog) 

100 self.k_trend = 0 

101 self._k_exog = self.k_exog 

102 trend_exog = None 

103 if trend == 'c': 

104 trend_exog = np.ones((nobs, 1)) 

105 self.k_trend = 1 

106 elif trend == 't': 

107 trend_exog = (np.arange(nobs) + 1)[:, np.newaxis] 

108 self.k_trend = 1 

109 elif trend == 'ct': 

110 trend_exog = np.c_[np.ones((nobs, 1)), 

111 (np.arange(nobs) + 1)[:, np.newaxis]] 

112 self.k_trend = 2 

113 if trend_exog is not None: 

114 exog = trend_exog if exog is None else np.c_[trend_exog, exog] 

115 self._k_exog += self.k_trend 

116 

117 # Initialize the base model 

118 super(MarkovRegression, self).__init__( 

119 endog, k_regimes, order=order, exog_tvtp=exog_tvtp, exog=exog, 

120 dates=dates, freq=freq, missing=missing) 

121 

122 # Switching options 

123 if self.switching_trend is True or self.switching_trend is False: 

124 self.switching_trend = [self.switching_trend] * self.k_trend 

125 elif not len(self.switching_trend) == self.k_trend: 

126 raise ValueError('Invalid iterable passed to `switching_trend`.') 

127 if self.switching_exog is True or self.switching_exog is False: 

128 self.switching_exog = [self.switching_exog] * self.k_exog 

129 elif not len(self.switching_exog) == self.k_exog: 

130 raise ValueError('Invalid iterable passed to `switching_exog`.') 

131 

132 self.switching_coeffs = ( 

133 np.r_[self.switching_trend, 

134 self.switching_exog].astype(bool).tolist()) 

135 

136 # Parameters 

137 self.parameters['exog'] = self.switching_coeffs 

138 self.parameters['variance'] = [1] if self.switching_variance else [0] 

139 

140 def predict_conditional(self, params): 

141 """ 

142 In-sample prediction, conditional on the current regime 

143 

144 Parameters 

145 ---------- 

146 params : array_like 

147 Array of parameters at which to perform prediction. 

148 

149 Returns 

150 ------- 

151 predict : array_like 

152 Array of predictions conditional on current, and possibly past, 

153 regimes 

154 """ 

155 params = np.array(params, ndmin=1) 

156 

157 # Since in the base model the values are the same across columns, we 

158 # only compute a single column, and then expand it below. 

159 predict = np.zeros((self.k_regimes, self.nobs), dtype=params.dtype) 

160 

161 for i in range(self.k_regimes): 

162 # Predict 

163 if self._k_exog > 0: 

164 coeffs = params[self.parameters[i, 'exog']] 

165 predict[i] = np.dot(self.exog, coeffs) 

166 

167 return predict[:, None, :] 

168 

169 def _resid(self, params): 

170 predict = np.repeat(self.predict_conditional(params), 

171 self.k_regimes, axis=1) 

172 return self.endog - predict 

173 

174 def _conditional_loglikelihoods(self, params): 

175 """ 

176 Compute loglikelihoods conditional on the current period's regime 

177 """ 

178 

179 # Get residuals 

180 resid = self._resid(params) 

181 

182 # Compute the conditional likelihoods 

183 variance = params[self.parameters['variance']].squeeze() 

184 if self.switching_variance: 

185 variance = np.reshape(variance, (self.k_regimes, 1, 1)) 

186 

187 conditional_loglikelihoods = ( 

188 -0.5 * resid**2 / variance - 0.5 * np.log(2 * np.pi * variance)) 

189 

190 return conditional_loglikelihoods 

191 

192 @property 

193 def _res_classes(self): 

194 return {'fit': (MarkovRegressionResults, 

195 MarkovRegressionResultsWrapper)} 

196 

197 def _em_iteration(self, params0): 

198 """ 

199 EM iteration 

200 

201 Notes 

202 ----- 

203 This uses the inherited _em_iteration method for computing the 

204 non-TVTP transition probabilities and then performs the EM step for 

205 regression coefficients and variances. 

206 """ 

207 # Inherited parameters 

208 result, params1 = super(MarkovRegression, self)._em_iteration(params0) 

209 

210 tmp = np.sqrt(result.smoothed_marginal_probabilities) 

211 

212 # Regression coefficients 

213 coeffs = None 

214 if self._k_exog > 0: 

215 coeffs = self._em_exog(result, self.endog, self.exog, 

216 self.parameters.switching['exog'], tmp) 

217 for i in range(self.k_regimes): 

218 params1[self.parameters[i, 'exog']] = coeffs[i] 

219 

220 # Variances 

221 params1[self.parameters['variance']] = self._em_variance( 

222 result, self.endog, self.exog, coeffs, tmp) 

223 # params1[self.parameters['variance']] = 0.33282116 

224 

225 return result, params1 

226 

227 def _em_exog(self, result, endog, exog, switching, tmp=None): 

228 """ 

229 EM step for regression coefficients 

230 """ 

231 k_exog = exog.shape[1] 

232 coeffs = np.zeros((self.k_regimes, k_exog)) 

233 

234 # First, estimate non-switching coefficients 

235 if not np.all(switching): 

236 nonswitching_exog = exog[:, ~switching] 

237 nonswitching_coeffs = ( 

238 np.dot(np.linalg.pinv(nonswitching_exog), endog)) 

239 coeffs[:, ~switching] = nonswitching_coeffs 

240 endog = endog - np.dot(nonswitching_exog, nonswitching_coeffs) 

241 

242 # Next, get switching coefficients 

243 if np.any(switching): 

244 switching_exog = exog[:, switching] 

245 if tmp is None: 

246 tmp = np.sqrt(result.smoothed_marginal_probabilities) 

247 for i in range(self.k_regimes): 

248 tmp_endog = tmp[i] * endog 

249 tmp_exog = tmp[i][:, np.newaxis] * switching_exog 

250 coeffs[i, switching] = ( 

251 np.dot(np.linalg.pinv(tmp_exog), tmp_endog)) 

252 

253 return coeffs 

254 

255 def _em_variance(self, result, endog, exog, betas, tmp=None): 

256 """ 

257 EM step for variances 

258 """ 

259 k_exog = 0 if exog is None else exog.shape[1] 

260 

261 if self.switching_variance: 

262 variance = np.zeros(self.k_regimes) 

263 for i in range(self.k_regimes): 

264 if k_exog > 0: 

265 resid = endog - np.dot(exog, betas[i]) 

266 else: 

267 resid = endog 

268 variance[i] = ( 

269 np.sum(resid**2 * 

270 result.smoothed_marginal_probabilities[i]) / 

271 np.sum(result.smoothed_marginal_probabilities[i])) 

272 else: 

273 variance = 0 

274 if tmp is None: 

275 tmp = np.sqrt(result.smoothed_marginal_probabilities) 

276 for i in range(self.k_regimes): 

277 tmp_endog = tmp[i] * endog 

278 if k_exog > 0: 

279 tmp_exog = tmp[i][:, np.newaxis] * exog 

280 resid = tmp_endog - np.dot(tmp_exog, betas[i]) 

281 else: 

282 resid = tmp_endog 

283 variance += np.sum(resid**2) 

284 variance /= self.nobs 

285 return variance 

286 

287 @property 

288 def start_params(self): 

289 """ 

290 (array) Starting parameters for maximum likelihood estimation. 

291 

292 Notes 

293 ----- 

294 These are not very sophisticated and / or good. We set equal transition 

295 probabilities and interpolate regression coefficients between zero and 

296 the OLS estimates, where the interpolation is based on the regime 

297 number. We rely heavily on the EM algorithm to quickly find much better 

298 starting parameters, which are then used by the typical scoring 

299 approach. 

300 """ 

301 # Inherited parameters 

302 params = markov_switching.MarkovSwitching.start_params.fget(self) 

303 

304 # Regression coefficients 

305 if self._k_exog > 0: 

306 beta = np.dot(np.linalg.pinv(self.exog), self.endog) 

307 variance = np.var(self.endog - np.dot(self.exog, beta)) 

308 

309 if np.any(self.switching_coeffs): 

310 for i in range(self.k_regimes): 

311 params[self.parameters[i, 'exog']] = ( 

312 beta * (i / self.k_regimes)) 

313 else: 

314 params[self.parameters['exog']] = beta 

315 else: 

316 variance = np.var(self.endog) 

317 

318 # Variances 

319 if self.switching_variance: 

320 params[self.parameters['variance']] = ( 

321 np.linspace(variance / 10., variance, num=self.k_regimes)) 

322 else: 

323 params[self.parameters['variance']] = variance 

324 

325 return params 

326 

327 @property 

328 def param_names(self): 

329 """ 

330 (list of str) List of human readable parameter names (for parameters 

331 actually included in the model). 

332 """ 

333 # Inherited parameters 

334 param_names = np.array( 

335 markov_switching.MarkovSwitching.param_names.fget(self), 

336 dtype=object) 

337 

338 # Regression coefficients 

339 if np.any(self.switching_coeffs): 

340 for i in range(self.k_regimes): 

341 param_names[self.parameters[i, 'exog']] = [ 

342 '%s[%d]' % (exog_name, i) for exog_name in self.exog_names] 

343 else: 

344 param_names[self.parameters['exog']] = self.exog_names 

345 

346 # Variances 

347 if self.switching_variance: 

348 for i in range(self.k_regimes): 

349 param_names[self.parameters[i, 'variance']] = 'sigma2[%d]' % i 

350 else: 

351 param_names[self.parameters['variance']] = 'sigma2' 

352 

353 return param_names.tolist() 

354 

355 def transform_params(self, unconstrained): 

356 """ 

357 Transform unconstrained parameters used by the optimizer to constrained 

358 parameters used in likelihood evaluation 

359 

360 Parameters 

361 ---------- 

362 unconstrained : array_like 

363 Array of unconstrained parameters used by the optimizer, to be 

364 transformed. 

365 

366 Returns 

367 ------- 

368 constrained : array_like 

369 Array of constrained parameters which may be used in likelihood 

370 evaluation. 

371 """ 

372 # Inherited parameters 

373 constrained = super(MarkovRegression, self).transform_params( 

374 unconstrained) 

375 

376 # Nothing to do for regression coefficients 

377 constrained[self.parameters['exog']] = ( 

378 unconstrained[self.parameters['exog']]) 

379 

380 # Force variances to be positive 

381 constrained[self.parameters['variance']] = ( 

382 unconstrained[self.parameters['variance']]**2) 

383 

384 return constrained 

385 

386 def untransform_params(self, constrained): 

387 """ 

388 Transform constrained parameters used in likelihood evaluation 

389 to unconstrained parameters used by the optimizer 

390 

391 Parameters 

392 ---------- 

393 constrained : array_like 

394 Array of constrained parameters used in likelihood evaluation, to 

395 be transformed. 

396 

397 Returns 

398 ------- 

399 unconstrained : array_like 

400 Array of unconstrained parameters used by the optimizer. 

401 """ 

402 # Inherited parameters 

403 unconstrained = super(MarkovRegression, self).untransform_params( 

404 constrained) 

405 

406 # Nothing to do for regression coefficients 

407 unconstrained[self.parameters['exog']] = ( 

408 constrained[self.parameters['exog']]) 

409 

410 # Force variances to be positive 

411 unconstrained[self.parameters['variance']] = ( 

412 constrained[self.parameters['variance']]**0.5) 

413 

414 return unconstrained 

415 

416 

417class MarkovRegressionResults(markov_switching.MarkovSwitchingResults): 

418 r""" 

419 Class to hold results from fitting a Markov switching regression model 

420 

421 Parameters 

422 ---------- 

423 model : MarkovRegression instance 

424 The fitted model instance 

425 params : ndarray 

426 Fitted parameters 

427 filter_results : HamiltonFilterResults or KimSmootherResults instance 

428 The underlying filter and, optionally, smoother output 

429 cov_type : str 

430 The type of covariance matrix estimator to use. Can be one of 'approx', 

431 'opg', 'robust', or 'none'. 

432 

433 Attributes 

434 ---------- 

435 model : Model instance 

436 A reference to the model that was fit. 

437 filter_results : HamiltonFilterResults or KimSmootherResults instance 

438 The underlying filter and, optionally, smoother output 

439 nobs : float 

440 The number of observations used to fit the model. 

441 params : ndarray 

442 The parameters of the model. 

443 scale : float 

444 This is currently set to 1.0 and not used by the model or its results. 

445 """ 

446 pass 

447 

448 

449class MarkovRegressionResultsWrapper( 

450 markov_switching.MarkovSwitchingResultsWrapper): 

451 pass 

452wrap.populate_wrapper(MarkovRegressionResultsWrapper, # noqa:E305 

453 MarkovRegressionResults)