Coverage for /home/martinb/.local/share/virtualenvs/camcops/lib/python3.6/site-packages/statsmodels/tsa/regime_switching/markov_regression.py : 14%

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
4Author: Chad Fulton
5License: BSD-3
6"""
9import numpy as np
10import statsmodels.base.wrapper as wrap
12from statsmodels.tsa.regime_switching import markov_switching
15class MarkovRegression(markov_switching.MarkovSwitching):
16 r"""
17 First-order k-regime Markov switching regression model
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.
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.
62 The model can be written as:
64 .. math::
66 y_t = a_{S_t} + x_t' \beta_{S_t} + \varepsilon_t \\
67 \varepsilon_t \sim N(0, \sigma_{S_t}^2)
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.
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.
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 """
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'):
89 # Properties
90 self.trend = trend
91 self.switching_trend = switching_trend
92 self.switching_exog = switching_exog
93 self.switching_variance = switching_variance
95 # Exogenous data
96 self.k_exog, exog = markov_switching.prepare_exog(exog)
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
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)
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`.')
132 self.switching_coeffs = (
133 np.r_[self.switching_trend,
134 self.switching_exog].astype(bool).tolist())
136 # Parameters
137 self.parameters['exog'] = self.switching_coeffs
138 self.parameters['variance'] = [1] if self.switching_variance else [0]
140 def predict_conditional(self, params):
141 """
142 In-sample prediction, conditional on the current regime
144 Parameters
145 ----------
146 params : array_like
147 Array of parameters at which to perform prediction.
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)
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)
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)
167 return predict[:, None, :]
169 def _resid(self, params):
170 predict = np.repeat(self.predict_conditional(params),
171 self.k_regimes, axis=1)
172 return self.endog - predict
174 def _conditional_loglikelihoods(self, params):
175 """
176 Compute loglikelihoods conditional on the current period's regime
177 """
179 # Get residuals
180 resid = self._resid(params)
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))
187 conditional_loglikelihoods = (
188 -0.5 * resid**2 / variance - 0.5 * np.log(2 * np.pi * variance))
190 return conditional_loglikelihoods
192 @property
193 def _res_classes(self):
194 return {'fit': (MarkovRegressionResults,
195 MarkovRegressionResultsWrapper)}
197 def _em_iteration(self, params0):
198 """
199 EM iteration
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)
210 tmp = np.sqrt(result.smoothed_marginal_probabilities)
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]
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
225 return result, params1
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))
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)
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))
253 return coeffs
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]
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
287 @property
288 def start_params(self):
289 """
290 (array) Starting parameters for maximum likelihood estimation.
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)
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))
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)
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
325 return params
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)
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
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'
353 return param_names.tolist()
355 def transform_params(self, unconstrained):
356 """
357 Transform unconstrained parameters used by the optimizer to constrained
358 parameters used in likelihood evaluation
360 Parameters
361 ----------
362 unconstrained : array_like
363 Array of unconstrained parameters used by the optimizer, to be
364 transformed.
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)
376 # Nothing to do for regression coefficients
377 constrained[self.parameters['exog']] = (
378 unconstrained[self.parameters['exog']])
380 # Force variances to be positive
381 constrained[self.parameters['variance']] = (
382 unconstrained[self.parameters['variance']]**2)
384 return constrained
386 def untransform_params(self, constrained):
387 """
388 Transform constrained parameters used in likelihood evaluation
389 to unconstrained parameters used by the optimizer
391 Parameters
392 ----------
393 constrained : array_like
394 Array of constrained parameters used in likelihood evaluation, to
395 be transformed.
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)
406 # Nothing to do for regression coefficients
407 unconstrained[self.parameters['exog']] = (
408 constrained[self.parameters['exog']])
410 # Force variances to be positive
411 unconstrained[self.parameters['variance']] = (
412 constrained[self.parameters['variance']]**0.5)
414 return unconstrained
417class MarkovRegressionResults(markov_switching.MarkovSwitchingResults):
418 r"""
419 Class to hold results from fitting a Markov switching regression model
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'.
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
449class MarkovRegressionResultsWrapper(
450 markov_switching.MarkovSwitchingResultsWrapper):
451 pass
452wrap.populate_wrapper(MarkovRegressionResultsWrapper, # noqa:E305
453 MarkovRegressionResults)