MarkovSwitchingDFM#

class cultivars.multivariate.regime_switching.markov_switching.MarkovSwitchingDFM(panel, *, series_names=None)[source]#

Markov-switching dynamic factor model, Kim-Nelson.

One factor, AR(1) with a two-state switching intercept; estimation by maximum likelihood through the continuous-state Kim filter. A fit costs tens of seconds rather than seconds – the filter runs four Kalman updates per period inside the optimizer – which is the known price of the model and is stated rather than hidden. The panel is standardized internally and every reported parameter is on that scale.

Variables:
  • _panel – The validated (nobs, n_series) panel, unstandardized.

  • _series_names – The resolved series labels.

See also

References

Kim, C.-J., & Nelson, C. R. (1998). Business cycle turning points, a new coincident index, and tests of duration dependence based on a dynamic factor model with regime switching. Review of Economics and Statistics, 80(2), 188-201.

Chauvet, M. (1998). An econometric characterization of business cycle dynamics with factor structure and regime switching. International Economic Review, 39(4), 969-996.

Example

Three coincident indicators loading on one factor whose intercept drops in a short-lived low state. The fit recovers the factor, the chronology and the asymmetric durations; about twenty-five seconds on this panel:

>>> import numpy as np
>>> rng = np.random.default_rng(0)
>>> p = np.array([[0.8, 0.2], [0.05, 0.95]])
>>> s, f = np.ones(120, dtype=int), np.zeros(120)
>>> for t in range(1, 120):
...     s[t] = np.searchsorted(np.cumsum(p[s[t - 1]]), rng.random())
...     f[t] = [-1.5, 0.5][s[t]] + 0.6 * f[t - 1] + 0.5 * rng.standard_normal()
>>> x = np.outer(f, [1.0, 0.8, 1.2]) + rng.standard_normal((120, 3)) * [0.5, 0.7, 0.6]
>>> res = MarkovSwitchingDFM(x, series_names=("ip", "emp", "sales")).fit()
>>> res.converged, res.n_params, bool(res.mu[0] < res.mu[1])
(True, 11, True)
>>> bool(np.corrcoef(res.factor, f)[0, 1] > 0.95)
True
>>> bool(((res.recession_probabilities() > 0.5) == (s == 0)).mean() > 0.95)
True
>>> bool(res.expected_durations()[0] < res.expected_durations()[1])
True

Methods#

fit(*[, max_iter])

Maximize the Kim-approximate likelihood from data-driven starts.