Repository: statsmodels/statsmodels

Issue:
BUG: MarkovRegression failed to converge (underflow error)
Hi!
I am trying to fit MarkovRegression model to (financial) data and for some of the assets it fails to converge. 
My code:
`m = sm.tsa.MarkovRegression(series, k_regimes=2, switching_variance=True).fit(disp=True)`

 Output:
```
Warning: Desired error not necessarily achieved due to precision loss.
         Current function value: nan
         Iterations: 0
         Function evaluations: 1
         Gradient evaluations: 1
C:\Program Files\Anaconda\lib\site-packages\statsmodels\tsa\regime_switching\markov_switching.py:391: RuntimeWarning: invalid value encountered in true_divide
  predicted_joint_probabilities[..., t+1])
C:\Program Files\Anaconda\lib\site-packages\numpy\linalg\linalg.py:1876: RuntimeWarning: invalid value encountered in greater
  large = s > cutoff
C:\Program Files\Anaconda\lib\site-packages\numpy\core\fromnumeric.py:83: RuntimeWarning: invalid value encountered in reduce
  return ufunc.reduce(obj, axis, dtype, out, **passkwargs)
C:\Program Files\Anaconda\lib\site-packages\statsmodels\tools\numdiff.py:96: RuntimeWarning: invalid value encountered in maximum
  h = EPS**(1. / s) * np.maximum(np.abs(x), 0.1)
C:\Program Files\Anaconda\lib\site-packages\scipy\special\_logsumexp.py:118: RuntimeWarning: invalid value encountered in log
  out = np.log(s)
C:\Program Files\Anaconda\lib\site-packages\statsmodels\tsa\regime_switching\markov_regression.py:190: RuntimeWarning: invalid value encountered in true_divide
  np.exp(-0.5 * resid**2 / variance) / np.sqrt(2 * np.pi * variance))
C:\Program Files\Anaconda\lib\site-packages\statsmodels\tsa\regime_switching\markov_switching.py:1053: RuntimeWarning: invalid value encountered in log
  return np.log(results[5])
C:\Program Files\Anaconda\lib\site-packages\statsmodels\base\model.py:496: ConvergenceWarning: Maximum Likelihood optimization failed to converge. Check mle_retvals
  "Check mle_retvals", ConvergenceWarning)
```

This is the case for around 70 time series out of 850 in my dataset. I tried other methods, e.g. 'powell', but with no success. It looks like `predicted_joint_probabilities[..., t+1]``gives `nan` or `inf` values. There are no outliers in the data. 


Discussion:
The loglikelihood function for these models can be relatively flat with a lot of local maxima.  I don't know if I can recommend something that will guarantee success for all series. You might want to play around with more or fewer iterations of the EM algorithm (in your `fit` call, maybe try `em_iter=0` and `em_iter=20`).

You might also want to try the randomly drawn starting parameters option, by setting `search_reps=50` (although this may slow down your estimation process).
Also, if you can post a replicable example where it fails, we could maybe be of more help.
Hi @ChadFulton  and thanks for your reply. I will give two examples, the first one as I said is that model does not converge. The second one is that the model is not stable, i.e. I can fit it on a time series (though, I get couple of warnings and the results are strange) if I exclude the last observation, but it fails to converge on the full time series. 

Example 1.
```
series = [-2.28833689e-04, -4.83175915e-05, -4.93236435e-04, -2.92251109e-04,
       -4.03295179e-04, -2.01009784e-04,  2.85109215e-04,  1.85710827e-04,
       -2.04503930e-04, -9.06050944e-05,  1.06177818e-03, -4.97251569e-04,
        7.16669183e-04,  1.81161945e-04, -6.87139289e-04, -4.85979721e-05,
       -1.68387973e-05, -4.43257154e-04,  5.37457383e-04, -8.17822909e-04,
        6.06814040e-04, -1.05481968e-05, -1.15698730e-04, -8.57638924e-04,
        8.98143953e-05,  1.70216877e-04, -2.84874742e-04, -2.81999910e-05,
       -3.43787510e-04, -5.74808548e-04, -6.47844682e-05,  1.08667397e-04,
       -1.28790034e-04,  7.07615608e-05,  1.17633886e-04,  8.98163443e-07,
        1.03860559e-05, -2.50298430e-06, -9.09673045e-07, -1.94645227e-05,
       -5.21373877e-05, -1.38431813e-04,  1.11092157e-04,  3.09541647e-05,
       -1.57756488e-04,  4.93176285e-05, -4.87488913e-05,  2.08097182e-04,
       -7.08581132e-05, -8.12182224e-06,  8.03090891e-05, -1.02981093e-05,
        2.07550066e-05,  3.75383116e-05, -3.96307514e-05, -1.12007982e-05,
       -4.00958429e-05,  2.01206738e-05, -2.95802891e-05,  1.26422639e-04,
       -3.95617289e-05,  1.94523738e-04,  4.63964270e-05,  2.07712951e-05,
        2.92417981e-04,  2.15111259e-04,  1.23762736e-04,  7.78305555e-05,
       -3.44022033e-04, -2.84249114e-05, -1.08351559e-04, -1.76944408e-04,
        7.99332875e-05,  5.09785758e-04,  3.24506365e-04, -2.47943721e-04,
        2.22346033e-04, -6.66164024e-05,  2.00911206e-04,  4.58750048e-04,
       -1.55390954e-04,  3.67913831e-04,  1.08274314e-04, -1.27400351e-04,
        1.50654063e-04,  2.69976025e-04,  4.51532804e-05, -2.21579419e-04,
        1.54792373e-04,  1.98397630e-04,  3.96943388e-04,  6.18663277e-04,
        8.86151537e-04,  6.16949774e-04,  5.44538857e-03,  7.33282114e-05,
        3.93393013e-03,  6.66556165e-04,  6.18815111e-03, -6.40775088e-03,
       -1.98864768e-03, -3.42828364e-05, -7.92023127e-04, -1.64656177e-03,
       -3.31112273e-03, -2.63499766e-03, -2.55118821e-04, -3.63858500e-04,
       -6.58065806e-04, -3.38492405e-04, -1.10458161e-03, -2.08228620e-07,
       -7.21562092e-05,  7.92946105e-05,  1.25472212e-05, -1.27190212e-04,
        3.94913196e-05, -1.87353437e-05, -6.57929565e-06,  1.61597869e-05,
        1.90031845e-05,  8.76129739e-03, -8.34566734e-03, -5.82053353e-05,
        1.28770725e-04,  2.38160836e-04, -1.84590083e-05,  3.10903734e-05,
        1.89366264e-05,  7.61985181e-05,  3.20208326e-04, -6.90388272e-05,
        2.09928647e-05, -3.14720707e-05,  1.32614437e-04,  1.10385710e-04,
       -4.91256074e-05, -1.16967974e-04,  4.88977021e-05,  6.19231048e-05,
        2.95994863e-04,  1.91592526e-03, -2.22511393e-04, -8.04871984e-04,
       -6.66886069e-04, -3.80754506e-04, -4.06613941e-05, -4.66164600e-04,
       -1.48077847e-04,  1.31787219e-04,  5.12392307e-05, -2.61346302e-05,
       -1.46071993e-04, -3.17839957e-04, -2.52143621e-04, -1.73768102e-04,
       -6.88107229e-05, -2.14130430e-05,  5.25348170e-07, -9.80972737e-05,
       -6.90120785e-05, -1.28208365e-04,  2.09816018e-05,  1.02199123e-05,
        2.08633684e-05,  1.05609818e-05,  1.35835269e-05,  1.50357742e-04,
        5.72303827e-05,  1.27501839e-04,  1.20706157e-04,  8.27340411e-06,
        3.77430540e-05,  8.10426523e-05,  9.99617511e-05,  1.21103273e-04,
        1.10255996e-04,  6.89558159e-05,  3.22442426e-05,  8.49416109e-05,
        4.05396296e-04,  1.69699170e-04,  1.10892701e-04,  7.21186150e-05,
        1.24978570e-04, -2.91922347e-04,  5.50447651e-05, -1.68467417e-04,
       -7.06084859e-05, -3.73250482e-04, -1.49829854e-04, -7.23752254e-05,
       -9.80670951e-05, -3.00179050e-05,  4.15704679e-05,  2.87503307e-05,
       -5.09988933e-05, -9.55791344e-06, -2.74369249e-05, -2.23330122e-05,
        9.74727035e-06, -8.58146574e-06,  2.97704340e-05, -1.12990428e-05,
       -5.90507361e-05,  9.50772649e-07, -7.35502474e-07,  5.66683332e-07,
       -9.81320792e-06, -6.60845729e-07, -3.19848073e-05, -1.10779191e-04]
```
```
m = sm.tsa.MarkovRegression(series, k_regimes=2, switching_variance=True)
m_res = m.fit(disp=True)
m_res.summary()
```
```
Warning: Desired error not necessarily achieved due to precision loss.
         Current function value: nan
         Iterations: 0
         Function evaluations: 1
         Gradient evaluations: 1
C:\Program Files\Anaconda\lib\site-packages\statsmodels\tsa\regime_switching\markov_switching.py:391: RuntimeWarning: invalid value encountered in true_divide
  predicted_joint_probabilities[..., t+1])
C:\Program Files\Anaconda\lib\site-packages\numpy\linalg\linalg.py:1876: RuntimeWarning: invalid value encountered in greater
  large = s > cutoff
C:\Program Files\Anaconda\lib\site-packages\numpy\core\fromnumeric.py:83: RuntimeWarning: invalid value encountered in reduce
  return ufunc.reduce(obj, axis, dtype, out, **passkwargs)
C:\Program Files\Anaconda\lib\site-packages\statsmodels\tools\numdiff.py:96: RuntimeWarning: invalid value encountered in maximum
  h = EPS**(1. / s) * np.maximum(np.abs(x), 0.1)
C:\Program Files\Anaconda\lib\site-packages\scipy\special\_logsumexp.py:118: RuntimeWarning: invalid value encountered in log
  out = np.log(s)
C:\Program Files\Anaconda\lib\site-packages\statsmodels\tsa\regime_switching\markov_regression.py:190: RuntimeWarning: invalid value encountered in true_divide
  np.exp(-0.5 * resid**2 / variance) / np.sqrt(2 * np.pi * variance))
C:\Program Files\Anaconda\lib\site-packages\statsmodels\tsa\regime_switching\markov_switching.py:1053: RuntimeWarning: invalid value encountered in log
  return np.log(results[5])
C:\Program Files\Anaconda\lib\site-packages\statsmodels\base\model.py:496: ConvergenceWarning: Maximum Likelihood optimization failed to converge. Check mle_retvals
  "Check mle_retvals", ConvergenceWarning)
C:\Program Files\Anaconda\lib\site-packages\scipy\stats\_distn_infrastructure.py:879: RuntimeWarning: invalid value encountered in greater
  return (self.a < x) & (x < self.b)
C:\Program Files\Anaconda\lib\site-packages\scipy\stats\_distn_infrastructure.py:879: RuntimeWarning: invalid value encountered in less
  return (self.a < x) & (x < self.b)
C:\Program Files\Anaconda\lib\site-packages\scipy\stats\_distn_infrastructure.py:1818: RuntimeWarning: invalid value encountered in less_equal
  cond2 = cond0 & (x <= self.a)
```

Example 2.
```
series = [ 0.01943226, -0.01827994,  0.04916938,  0.00449823, -0.03663548,
        0.06181673, -0.00986665, -0.06042372, -0.01167192,  0.01819027,
        0.01843091,  0.05275182,  0.0254082 , -0.01758291, -0.00049648,
       -0.01472029, -0.00837104, -0.03093994, -0.03946722,  0.00413527,
        0.00951247,  0.02407921, -0.0032922 , -0.03760829,  0.04497337,
        0.00899329,  0.01127307,  0.03509824,  0.0481716 ,  0.01763814,
       -0.03647593, -0.01476061, -0.01816494,  0.01155411,  0.00905093,
       -0.01646968, -0.03898337,  0.0239889 , -0.02726607,  0.0130577 ,
       -0.00239653,  0.01885253,  0.02577028, -0.01322179,  0.05352735,
       -0.00364629, -0.00202996, -0.01587582, -0.020863  ,  0.01321897,
        0.04587664, -0.05832084,  0.0339881 ,  0.04756151,  0.0161308 ,
        0.0174749 ,  0.04666765,  0.00694068,  0.04182448, -0.01051155,
        0.00720019, -0.00436486, -0.00967831,  0.00888586,  0.00179477,
        0.00858298, -0.01407356,  0.04665307, -0.00325535, -0.01555412,
       -0.02539219,  0.00760659,  0.00285179, -0.01280679,  0.00783214,
        0.05742366,  0.05599797,  0.02068453, -0.03557952, -0.01916985,
        0.04884488,  0.05153808, -0.08652624,  0.01948242, -0.06029372,
        0.00199015, -0.08561824,  0.06443061,  0.02564269, -0.01686629,
        0.0099289 , -0.02940573, -0.02003841, -0.18276886, -0.05887577,
       -0.03190597, -0.02358457,  0.06088818,  0.01860154,  0.04981721,
        0.05859497, -0.05181903,  0.05700829, -0.03564319, -0.00788613,
       -0.00752196, -0.01960004,  0.0909113 , -0.04713009, -0.00719579,
        0.09202208,  0.00556483, -0.05963546, -0.04137063,  0.00842413,
       -0.06218641,  0.0280072 , -0.02406726,  0.02966198,  0.00233849,
        0.00189074,  0.02783063,  0.01539913,  0.00323196, -0.01967641,
       -0.00513658, -0.03423835, -0.02892088, -0.05306196,  0.05554565,
       -0.02769257, -0.0052395 ,  0.00314764,  0.08070956,  0.00761153,
       -0.02310144, -0.06121651,  0.03249197, -0.00526019,  0.0190141 ,
       -0.00265647,  0.00849418,  0.04026139,  0.04810309,  0.05168079,
       -0.02027668,  0.03161191,  0.04586055,  0.00616104, -0.03153142,
        0.01287839, -0.02386042,  0.02289202, -0.01204607,  0.02254442,
        0.02746986, -0.07072074,  0.00333032,  0.01119351, -0.0008726 ,
        0.00538012,  0.01242282, -0.00580497,  0.01435689,  0.02149159,
        0.01620545,  0.04432912, -0.0061059 , -0.10547993,  0.03721238,
       -0.01173251,  0.04590641,  0.00634984, -0.01757477, -0.02813118,
       -0.03456161, -0.02651727,  0.03355129, -0.0013064 , -0.05741823,
       -0.00854879, -0.03184707,  0.0390702 , -0.01880449, -0.0067859 ,
       -0.0590652 , -0.01505461,  0.00476122, -0.01725296,  0.01305418,
        0.08667038,  0.01843112, -0.00021765, -0.0272848 , -0.00759367,
       -0.02466797,  0.00360978,  0.05593358,  0.0223789 , -0.00541636,
        0.02587898, -0.02289043, -0.010653  ,  0.03028159, -0.01197355,
       -0.06158124, -0.01058272,  0.03227813, -0.01389285,  0.0044258 ,
        0.01983702, -0.01047232,  0.03127857]
```
Converged:
```
m = sm.tsa.MarkovRegression(series[:-1], k_regimes=2, switching_variance=True)
m_res = m.fit(disp=True)
m_res.summary()
```
Did not converge:
```
m = sm.tsa.MarkovRegression(series, k_regimes=2, switching_variance=True)
m_res = m.fit(disp=True)
m_res.summary()
```
I'm guessing this is an overflow error as was discussed in #4125.  I think to properly handle this case we will need to rewrite in log scale.

Resolving patch:
diff --git a/statsmodels/tsa/regime_switching/markov_autoregression.py b/statsmodels/tsa/regime_switching/markov_autoregression.py
--- a/statsmodels/tsa/regime_switching/markov_autoregression.py
+++ b/statsmodels/tsa/regime_switching/markov_autoregression.py
@@ -219,9 +219,9 @@ def predict_conditional(self, params):
     def _resid(self, params):
         return self.endog - self.predict_conditional(params)
 
-    def _conditional_likelihoods(self, params):
+    def _conditional_loglikelihoods(self, params):
         """
-        Compute likelihoods conditional on the current period's regime and
+        Compute loglikelihoods conditional on the current period's regime and
         the last `self.order` regimes.
         """
         # Get the residuals
@@ -232,10 +232,10 @@ def _conditional_likelihoods(self, params):
         if self.switching_variance:
             variance = np.reshape(variance, (self.k_regimes, 1, 1))
 
-        conditional_likelihoods = (
-            np.exp(-0.5 * resid**2 / variance) / np.sqrt(2 * np.pi * variance))
+        conditional_loglikelihoods = (
+            -0.5 * resid**2 / variance - 0.5 * np.log(2 * np.pi * variance))
 
-        return conditional_likelihoods
+        return conditional_loglikelihoods
 
     @property
     def _res_classes(self):
diff --git a/statsmodels/tsa/regime_switching/markov_regression.py b/statsmodels/tsa/regime_switching/markov_regression.py
--- a/statsmodels/tsa/regime_switching/markov_regression.py
+++ b/statsmodels/tsa/regime_switching/markov_regression.py
@@ -173,9 +173,9 @@ def _resid(self, params):
                             self.k_regimes, axis=1)
         return self.endog - predict
 
-    def _conditional_likelihoods(self, params):
+    def _conditional_loglikelihoods(self, params):
         """
-        Compute likelihoods conditional on the current period's regime
+        Compute loglikelihoods conditional on the current period's regime
         """
 
         # Get residuals
@@ -186,10 +186,10 @@ def _conditional_likelihoods(self, params):
         if self.switching_variance:
             variance = np.reshape(variance, (self.k_regimes, 1, 1))
 
-        conditional_likelihoods = (
-            np.exp(-0.5 * resid**2 / variance) / np.sqrt(2 * np.pi * variance))
+        conditional_loglikelihoods = (
+            -0.5 * resid**2 / variance - 0.5 * np.log(2 * np.pi * variance))
 
-        return conditional_likelihoods
+        return conditional_loglikelihoods
 
     @property
     def _res_classes(self):
diff --git a/statsmodels/tsa/regime_switching/markov_switching.py b/statsmodels/tsa/regime_switching/markov_switching.py
--- a/statsmodels/tsa/regime_switching/markov_switching.py
+++ b/statsmodels/tsa/regime_switching/markov_switching.py
@@ -27,18 +27,19 @@
 from statsmodels.tsa.statespace.tools import find_best_blas_type, prepare_exog
 
 from statsmodels.tsa.regime_switching._hamilton_filter import (
-    shamilton_filter, dhamilton_filter, chamilton_filter, zhamilton_filter)
+    shamilton_filter_log, dhamilton_filter_log, chamilton_filter_log,
+    zhamilton_filter_log)
 from statsmodels.tsa.regime_switching._kim_smoother import (
-    skim_smoother, dkim_smoother, ckim_smoother, zkim_smoother)
+    skim_smoother_log, dkim_smoother_log, ckim_smoother_log, zkim_smoother_log)
 
-prefix_hamilton_filter_map = {
-    's': shamilton_filter, 'd': dhamilton_filter,
-    'c': chamilton_filter, 'z': zhamilton_filter
+prefix_hamilton_filter_log_map = {
+    's': shamilton_filter_log, 'd': dhamilton_filter_log,
+    'c': chamilton_filter_log, 'z': zhamilton_filter_log
 }
 
-prefix_kim_smoother_map = {
-    's': skim_smoother, 'd': dkim_smoother,
-    'c': ckim_smoother, 'z': zkim_smoother
+prefix_kim_smoother_log_map = {
+    's': skim_smoother_log, 'd': dkim_smoother_log,
+    'c': ckim_smoother_log, 'z': zkim_smoother_log
 }
 
 
@@ -102,10 +103,10 @@ def _partials_logistic(x):
     return partials
 
 
-def py_hamilton_filter(initial_probabilities, regime_transition,
-                       conditional_likelihoods):
+def cy_hamilton_filter_log(initial_probabilities, regime_transition,
+                           conditional_loglikelihoods):
     """
-    Hamilton filter using pure Python
+    Hamilton filter in log space using Cython inner loop.
 
     Parameters
     ----------
@@ -122,8 +123,8 @@ def py_hamilton_filter(initial_probabilities, regime_transition,
         stochastic.  The first order entries and initial_probabilities are
         used to produce the initial joint distribution of dimension order +
         1 at time t=0.
-    conditional_likelihoods : array
-        Array of likelihoods conditional on the last `order+1` regimes,
+    conditional_loglikelihoods : array
+        Array of loglikelihoods conditional on the last `order+1` regimes,
         shaped (k_regimes,)*(order + 1) + (nobs,).
 
     Returns
@@ -136,160 +137,33 @@ def py_hamilton_filter(initial_probabilities, regime_transition,
         the joint probability of the current and previous `order` periods
         being in each combination of regimes conditional on time t-1
         information. Shaped (k_regimes,) * (order + 1) + (nobs,).
-    joint_likelihoods : array
-        Array of likelihoods condition on time t information, shaped (nobs,).
+    joint_loglikelihoods : array
+        Array of loglikelihoods condition on time t information,
+        shaped (nobs,).
     filtered_joint_probabilities : array
         Array containing Pr[S_t=s_t, ..., S_{t-order}=s_{t-order} | Y_{t}] -
         the joint probability of the current and previous `order` periods
         being in each combination of regimes conditional on time t
         information. Shaped (k_regimes,) * (order + 1) + (nobs,).
-
     """
 
     # Dimensions
     k_regimes = len(initial_probabilities)
-    nobs = conditional_likelihoods.shape[-1]
-    order = conditional_likelihoods.ndim - 2
-    dtype = conditional_likelihoods.dtype
+    nobs = conditional_loglikelihoods.shape[-1]
+    order = conditional_loglikelihoods.ndim - 2
+    dtype = conditional_loglikelihoods.dtype
 
     # Check for compatible shapes.
     incompatible_shapes = (
         regime_transition.shape[-1] not in (1, nobs + order)
         or regime_transition.shape[:2] != (k_regimes, k_regimes)
-        or conditional_likelihoods.shape[0] != k_regimes)
+        or conditional_loglikelihoods.shape[0] != k_regimes)
     if incompatible_shapes:
         raise ValueError('Arguments do not have compatible shapes')
 
-    # Storage
-    # Pr[S_t = s_t | Y_t]
-    filtered_marginal_probabilities = (
-        np.zeros((k_regimes, nobs), dtype=dtype))
-    # Pr[S_t = s_t, ... S_{t-r} = s_{t-r} | Y_{t-1}]
-    predicted_joint_probabilities = np.zeros(
-        (k_regimes,) * (order + 1) + (nobs,), dtype=dtype)
-    # f(y_t | Y_{t-1})
-    joint_likelihoods = np.zeros((nobs,), dtype)
-    # Pr[S_t = s_t, ... S_{t-r} = s_{t-r} | Y_t]
-    filtered_joint_probabilities = np.zeros(
-        (k_regimes,) * (order + 1) + (nobs + 1,), dtype=dtype)
-
-    # Initial probabilities
-    filtered_marginal_probabilities[:, 0] = initial_probabilities
-    tmp = np.copy(initial_probabilities)
-    shape = (k_regimes, k_regimes)
-    for i in range(order):
-        tmp = np.reshape(regime_transition[..., i], shape + (1,) * i) * tmp
-    filtered_joint_probabilities[..., 0] = tmp
-
-    # Check that regime_transition is oriented correctly.
-    if not np.allclose(np.sum(regime_transition, axis=0), 1):
-        raise ValueError('regime_transition does not contain '
-                         'left stochastic matrices.')
-
-    # Reshape regime_transition so we can use broadcasting
-    shape = (k_regimes, k_regimes)
-    shape += (1,) * (order-1)
-    shape += (regime_transition.shape[-1],)
-    regime_transition = np.reshape(regime_transition, shape)
-
-    # Get appropriate subset of transition matrix
-    if regime_transition.shape[-1] > 1:
-        regime_transition = regime_transition[..., order:]
-
-    # Hamilton filter iterations
-    transition_t = 0
-    for t in range(nobs):
-        if regime_transition.shape[-1] > 1:
-            transition_t = t
-
-        # S_t, S_{t-1}, ..., S_{t-r} | t-1, stored at zero-indexed location t
-        if order > 0:
-            predicted_joint_probabilities[..., t] = (
-                # S_t | S_{t-1}
-                regime_transition[..., transition_t] *
-                # S_{t-1}, S_{t-2}, ..., S_{t-r} | t-1
-                filtered_joint_probabilities[..., t].sum(axis=-1))
-        else:
-            predicted_joint_probabilities[..., t] = (
-                np.dot(regime_transition[..., transition_t],
-                       filtered_joint_probabilities[..., t]))
-
-        # f(y_t, S_t, ..., S_{t-r} | t-1)
-        tmp = (conditional_likelihoods[..., t] *
-               predicted_joint_probabilities[..., t])
-        # f(y_t | t-1)
-        joint_likelihoods[t] = np.sum(tmp)
-
-        # S_t, S_{t-1}, ..., S_{t-r} | t, stored at index t+1
-        filtered_joint_probabilities[..., t+1] = (
-            tmp / joint_likelihoods[t])
-
-    # S_t | t
-    filtered_marginal_probabilities = filtered_joint_probabilities[..., 1:]
-    for i in range(1, filtered_marginal_probabilities.ndim - 1):
-        filtered_marginal_probabilities = np.sum(
-            filtered_marginal_probabilities, axis=-2)
-
-    return (filtered_marginal_probabilities, predicted_joint_probabilities,
-            joint_likelihoods, filtered_joint_probabilities[..., 1:])
-
-
-def cy_hamilton_filter(initial_probabilities, regime_transition,
-                       conditional_likelihoods):
-    """
-    Hamilton filter using Cython inner loop
-
-    Parameters
-    ----------
-    initial_probabilities : array
-        Array of initial probabilities, shaped (k_regimes,) giving the
-        distribution of the regime process at time t = -order where order
-        is a nonnegative integer.
-    regime_transition : array
-        Matrix of regime transition probabilities, shaped either
-        (k_regimes, k_regimes, 1) or if there are time-varying transition
-        probabilities (k_regimes, k_regimes, nobs + order).  Entry [i, j,
-        t] contains the probability of moving from j at time t-1 to i at
-        time t, so each matrix regime_transition[:, :, t] should be left
-        stochastic.  The first order entries and initial_probabilities are
-        used to produce the initial joint distribution of dimension order +
-        1 at time t=0.
-    conditional_likelihoods : array
-        Array of likelihoods conditional on the last `order+1` regimes,
-        shaped (k_regimes,)*(order + 1) + (nobs,).
-
-    Returns
-    -------
-    filtered_marginal_probabilities : array
-        Array containing Pr[S_t=s_t | Y_t] - the probability of being in each
-        regime conditional on time t information. Shaped (k_regimes, nobs).
-    predicted_joint_probabilities : array
-        Array containing Pr[S_t=s_t, ..., S_{t-order}=s_{t-order} | Y_{t-1}] -
-        the joint probability of the current and previous `order` periods
-        being in each combination of regimes conditional on time t-1
-        information. Shaped (k_regimes,) * (order + 1) + (nobs,).
-    joint_likelihoods : array
-        Array of likelihoods condition on time t information, shaped (nobs,).
-    filtered_joint_probabilities : array
-        Array containing Pr[S_t=s_t, ..., S_{t-order}=s_{t-order} | Y_{t}] -
-        the joint probability of the current and previous `order` periods
-        being in each combination of regimes conditional on time t
-        information. Shaped (k_regimes,) * (order + 1) + (nobs,).
-    """
-
-    # Dimensions
-    k_regimes = len(initial_probabilities)
-    nobs = conditional_likelihoods.shape[-1]
-    order = conditional_likelihoods.ndim - 2
-    dtype = conditional_likelihoods.dtype
-
-    # Check for compatible shapes.
-    incompatible_shapes = (
-        regime_transition.shape[-1] not in (1, nobs + order)
-        or regime_transition.shape[:2] != (k_regimes, k_regimes)
-        or conditional_likelihoods.shape[0] != k_regimes)
-    if incompatible_shapes:
-        raise ValueError('Arguments do not have compatible shapes')
+    # Convert to log space
+    initial_probabilities = np.log(initial_probabilities)
+    regime_transition = np.log(np.maximum(regime_transition, 1e-20))
 
     # Storage
     # Pr[S_t = s_t | Y_t]
@@ -299,8 +173,8 @@ def cy_hamilton_filter(initial_probabilities, regime_transition,
     # Has k_regimes^(order+1) elements
     predicted_joint_probabilities = np.zeros(
         (k_regimes,) * (order + 1) + (nobs,), dtype=dtype)
-    # f(y_t | Y_{t-1})
-    joint_likelihoods = np.zeros((nobs,), dtype)
+    # log(f(y_t | Y_{t-1}))
+    joint_loglikelihoods = np.zeros((nobs,), dtype)
     # Pr[S_t = s_t, ... S_{t-r+1} = s_{t-r+1} | Y_t]
     # Has k_regimes^order elements
     filtered_joint_probabilities = np.zeros(
@@ -315,7 +189,7 @@ def cy_hamilton_filter(initial_probabilities, regime_transition,
         if regime_transition.shape[-1] > 1:
             transition_t = i
         tmp = np.reshape(regime_transition[..., transition_t],
-                         shape + (1,) * i) * tmp
+                         shape + (1,) * i) + tmp
     filtered_joint_probabilities[..., 0] = tmp
 
     # Get appropriate subset of transition matrix
@@ -324,15 +198,23 @@ def cy_hamilton_filter(initial_probabilities, regime_transition,
 
     # Run Cython filter iterations
     prefix, dtype, _ = find_best_blas_type((
-        regime_transition, conditional_likelihoods, joint_likelihoods,
+        regime_transition, conditional_loglikelihoods, joint_loglikelihoods,
         predicted_joint_probabilities, filtered_joint_probabilities))
-    func = prefix_hamilton_filter_map[prefix]
+    func = prefix_hamilton_filter_log_map[prefix]
     func(nobs, k_regimes, order, regime_transition,
-         conditional_likelihoods.reshape(k_regimes**(order+1), nobs),
-         joint_likelihoods,
+         conditional_loglikelihoods.reshape(k_regimes**(order+1), nobs),
+         joint_loglikelihoods,
          predicted_joint_probabilities.reshape(k_regimes**(order+1), nobs),
          filtered_joint_probabilities.reshape(k_regimes**(order+1), nobs+1))
 
+    # Save log versions for smoother
+    predicted_joint_probabilities_log = predicted_joint_probabilities
+    filtered_joint_probabilities_log = filtered_joint_probabilities
+
+    # Convert out of log scale
+    predicted_joint_probabilities = np.exp(predicted_joint_probabilities)
+    filtered_joint_probabilities = np.exp(filtered_joint_probabilities)
+
     # S_t | t
     filtered_marginal_probabilities = filtered_joint_probabilities[..., 1:]
     for i in range(1, filtered_marginal_probabilities.ndim - 1):
@@ -340,13 +222,15 @@ def cy_hamilton_filter(initial_probabilities, regime_transition,
             filtered_marginal_probabilities, axis=-2)
 
     return (filtered_marginal_probabilities, predicted_joint_probabilities,
-            joint_likelihoods, filtered_joint_probabilities[..., 1:])
+            joint_loglikelihoods, filtered_joint_probabilities[..., 1:],
+            predicted_joint_probabilities_log,
+            filtered_joint_probabilities_log[..., 1:])
 
 
-def py_kim_smoother(regime_transition, predicted_joint_probabilities,
-                    filtered_joint_probabilities):
+def cy_kim_smoother_log(regime_transition, predicted_joint_probabilities,
+                        filtered_joint_probabilities):
     """
-    Kim smoother using pure Python
+    Kim smoother in log space using Cython inner loop.
 
     Parameters
     ----------
@@ -386,106 +270,27 @@ def py_kim_smoother(regime_transition, predicted_joint_probabilities,
     # Storage
     smoothed_joint_probabilities = np.zeros(
         (k_regimes,) * (order + 1) + (nobs,), dtype=dtype)
-    smoothed_marginal_probabilities = np.zeros((k_regimes, nobs), dtype=dtype)
-
-    # S_T, S_{T-1}, ..., S_{T-r} | T
-    smoothed_joint_probabilities[..., -1] = (
-        filtered_joint_probabilities[..., -1])
-
-    # Reshape transition so we can use broadcasting
-    shape = (k_regimes, k_regimes)
-    shape += (1,) * (order)
-    shape += (regime_transition.shape[-1],)
-    regime_transition = np.reshape(regime_transition, shape)
 
     # Get appropriate subset of transition matrix
     if regime_transition.shape[-1] == nobs + order:
         regime_transition = regime_transition[..., order:]
 
-    # Kim smoother iterations
-    transition_t = 0
-    for t in range(nobs - 2, -1, -1):
-        if regime_transition.shape[-1] > 1:
-            transition_t = t + 1
-
-        # S_{t+1}, S_t, ..., S_{t-r+1} | t
-        # x = predicted_joint_probabilities[..., t]
-        x = (filtered_joint_probabilities[..., t] *
-             regime_transition[..., transition_t])
-        # S_{t+1}, S_t, ..., S_{t-r+2} | T / S_{t+1}, S_t, ..., S_{t-r+2} | t
-        y = (smoothed_joint_probabilities[..., t+1] /
-             predicted_joint_probabilities[..., t+1])
-        # S_t, S_{t-1}, ..., S_{t-r+1} | T
-        smoothed_joint_probabilities[..., t] = (x * y[..., None]).sum(axis=0)
-
-    # Get smoothed marginal probabilities S_t | T by integrating out
-    # S_{t-k+1}, S_{t-k+2}, ..., S_{t-1}
-    smoothed_marginal_probabilities = smoothed_joint_probabilities
-    for i in range(1, smoothed_marginal_probabilities.ndim - 1):
-        smoothed_marginal_probabilities = np.sum(
-            smoothed_marginal_probabilities, axis=-2)
-
-    return smoothed_joint_probabilities, smoothed_marginal_probabilities
-
-
-def cy_kim_smoother(regime_transition, predicted_joint_probabilities,
-                    filtered_joint_probabilities):
-    """
-    Kim smoother using Cython inner loop
-
-    Parameters
-    ----------
-    regime_transition : array
-        Matrix of regime transition probabilities, shaped either
-        (k_regimes, k_regimes, 1) or if there are time-varying transition
-        probabilities (k_regimes, k_regimes, nobs).
-    predicted_joint_probabilities : array
-        Array containing Pr[S_t=s_t, ..., S_{t-order}=s_{t-order} | Y_{t-1}] -
-        the joint probability of the current and previous `order` periods
-        being in each combination of regimes conditional on time t-1
-        information. Shaped (k_regimes,) * (order + 1) + (nobs,).
-    filtered_joint_probabilities : array
-        Array containing Pr[S_t=s_t, ..., S_{t-order}=s_{t-order} | Y_{t}] -
-        the joint probability of the current and previous `order` periods
-        being in each combination of regimes conditional on time t
-        information. Shaped (k_regimes,) * (order + 1) + (nobs,).
-
-    Returns
-    -------
-    smoothed_joint_probabilities : array
-        Array containing Pr[S_t=s_t, ..., S_{t-order}=s_{t-order} | Y_T] -
-        the joint probability of the current and previous `order` periods
-        being in each combination of regimes conditional on all information.
-        Shaped (k_regimes,) * (order + 1) + (nobs,).
-    smoothed_marginal_probabilities : array
-        Array containing Pr[S_t=s_t | Y_T] - the probability of being in each
-        regime conditional on all information. Shaped (k_regimes, nobs).
-    """
-
-    # Dimensions
-    k_regimes = filtered_joint_probabilities.shape[0]
-    nobs = filtered_joint_probabilities.shape[-1]
-    order = filtered_joint_probabilities.ndim - 2
-    dtype = filtered_joint_probabilities.dtype
-
-    # Storage
-    smoothed_joint_probabilities = np.zeros(
-        (k_regimes,) * (order + 1) + (nobs,), dtype=dtype)
-
-    # Get appropriate subset of transition matrix
-    if regime_transition.shape[-1] == nobs + order:
-        regime_transition = regime_transition[..., order:]
+    # Convert to log space
+    regime_transition = np.log(np.maximum(regime_transition, 1e-20))
 
     # Run Cython smoother iterations
     prefix, dtype, _ = find_best_blas_type((
         regime_transition, predicted_joint_probabilities,
         filtered_joint_probabilities))
-    func = prefix_kim_smoother_map[prefix]
+    func = prefix_kim_smoother_log_map[prefix]
     func(nobs, k_regimes, order, regime_transition,
          predicted_joint_probabilities.reshape(k_regimes**(order+1), nobs),
          filtered_joint_probabilities.reshape(k_regimes**(order+1), nobs),
          smoothed_joint_probabilities.reshape(k_regimes**(order+1), nobs))
 
+    # Convert back from log space
+    smoothed_joint_probabilities = np.exp(smoothed_joint_probabilities)
+
     # Get smoothed marginal probabilities S_t | T by integrating out
     # S_{t-k+1}, S_{t-k+2}, ..., S_{t-1}
     smoothed_marginal_probabilities = smoothed_joint_probabilities
@@ -787,6 +592,10 @@ def initial_probabilities(self, params, regime_transition=None):
         else:
             raise RuntimeError('Invalid initialization method selected.')
 
+        # Slightly bound probabilities away from zero (for filters in log
+        # space)
+        probabilities = np.maximum(probabilities, 1e-20)
+
         return probabilities
 
     def _regime_transition_matrix_tvtp(self, params, exog_tvtp=None):
@@ -944,7 +753,7 @@ def predict_conditional(self, params):
         """
         raise NotImplementedError
 
-    def _conditional_likelihoods(self, params):
+    def _conditional_loglikelihoods(self, params):
         """
         Compute likelihoods conditional on the current period's regime (and
         the last self.order periods' regimes if self.order > 0).
@@ -962,13 +771,14 @@ def _filter(self, params, regime_transition=None):
             params, regime_transition)
 
         # Compute the conditional likelihoods
-        conditional_likelihoods = self._conditional_likelihoods(params)
+        conditional_loglikelihoods = self._conditional_loglikelihoods(params)
 
         # Apply the filter
         return ((regime_transition, initial_probabilities,
-                 conditional_likelihoods) +
-                cy_hamilton_filter(initial_probabilities, regime_transition,
-                                   conditional_likelihoods))
+                 conditional_loglikelihoods) +
+                cy_hamilton_filter_log(
+                    initial_probabilities, regime_transition,
+                    conditional_loglikelihoods))
 
     def filter(self, params, transformed=True, cov_type=None, cov_kwds=None,
                return_raw=False, results_class=None,
@@ -1014,9 +824,12 @@ def filter(self, params, transformed=True, cov_type=None, cov_kwds=None,
 
         # Get the result
         names = ['regime_transition', 'initial_probabilities',
-                 'conditional_likelihoods', 'filtered_marginal_probabilities',
-                 'predicted_joint_probabilities', 'joint_likelihoods',
-                 'filtered_joint_probabilities']
+                 'conditional_loglikelihoods',
+                 'filtered_marginal_probabilities',
+                 'predicted_joint_probabilities', 'joint_loglikelihoods',
+                 'filtered_joint_probabilities',
+                 'predicted_joint_probabilities_log',
+                 'filtered_joint_probabilities_log']
         result = HamiltonFilterResults(
             self, Bunch(**dict(zip(names, self._filter(params)))))
 
@@ -1025,17 +838,16 @@ def filter(self, params, transformed=True, cov_type=None, cov_kwds=None,
                                   cov_kwds, results_class,
                                   results_wrapper_class)
 
-    def _smooth(self, params, filtered_marginal_probabilities,
-                predicted_joint_probabilities,
-                filtered_joint_probabilities, regime_transition=None):
+    def _smooth(self, params, predicted_joint_probabilities_log,
+                filtered_joint_probabilities_log, regime_transition=None):
         # Get the regime transition matrix
         if regime_transition is None:
             regime_transition = self.regime_transition_matrix(params)
 
         # Apply the smoother
-        return cy_kim_smoother(regime_transition,
-                               predicted_joint_probabilities,
-                               filtered_joint_probabilities)
+        return cy_kim_smoother_log(regime_transition,
+                                   predicted_joint_probabilities_log,
+                                   filtered_joint_probabilities_log)
 
     @property
     def _res_classes(self):
@@ -1103,16 +915,20 @@ def smooth(self, params, transformed=True, cov_type=None, cov_kwds=None,
         self.data.param_names = self.param_names
 
         # Hamilton filter
+        # TODO add option to filter to return logged values so that we don't
+        # need to re-log them for smoother
         names = ['regime_transition', 'initial_probabilities',
-                 'conditional_likelihoods', 'filtered_marginal_probabilities',
-                 'predicted_joint_probabilities', 'joint_likelihoods',
-                 'filtered_joint_probabilities']
+                 'conditional_loglikelihoods',
+                 'filtered_marginal_probabilities',
+                 'predicted_joint_probabilities', 'joint_loglikelihoods',
+                 'filtered_joint_probabilities',
+                 'predicted_joint_probabilities_log',
+                 'filtered_joint_probabilities_log']
         result = Bunch(**dict(zip(names, self._filter(params))))
 
         # Kim smoother
-        out = self._smooth(params, result.filtered_marginal_probabilities,
-                           result.predicted_joint_probabilities,
-                           result.filtered_joint_probabilities)
+        out = self._smooth(params, result.predicted_joint_probabilities_log,
+                           result.filtered_joint_probabilities_log)
         result['smoothed_joint_probabilities'] = out[0]
         result['smoothed_marginal_probabilities'] = out[1]
         result = KimSmootherResults(self, result)
@@ -1141,7 +957,7 @@ def loglikeobs(self, params, transformed=True):
 
         results = self._filter(params)
 
-        return np.log(results[5])
+        return results[5]
 
     def loglike(self, params, transformed=True):
         """
@@ -1723,15 +1539,15 @@ class HamiltonFilterResults(object):
         Initialization method for regime probabilities.
     initial_probabilities : array
         Initial regime probabilities
-    conditional_likelihoods : array
-        The likelihood values at each time period, conditional on regime.
+    conditional_loglikelihoods : array
+        The loglikelihood values at each time period, conditional on regime.
     predicted_joint_probabilities : array
         Predicted joint probabilities at each time period.
     filtered_marginal_probabilities : array
         Filtered marginal probabilities at each time period.
     filtered_joint_probabilities : array
         Filtered joint probabilities at each time period.
-    joint_likelihoods : array
+    joint_loglikelihoods : array
         The likelihood values at each time period.
     llf_obs : array
         The loglikelihood values at each time period.
@@ -1745,16 +1561,16 @@ def __init__(self, model, result):
         self.k_regimes = model.k_regimes
 
         attributes = ['regime_transition', 'initial_probabilities',
-                      'conditional_likelihoods',
+                      'conditional_loglikelihoods',
                       'predicted_joint_probabilities',
                       'filtered_marginal_probabilities',
                       'filtered_joint_probabilities',
-                      'joint_likelihoods']
+                      'joint_loglikelihoods']
         for name in attributes:
             setattr(self, name, getattr(result, name))
 
         self.initialization = model._initialization
-        self.llf_obs = np.log(self.joint_likelihoods)
+        self.llf_obs = self.joint_loglikelihoods
         self.llf = np.sum(self.llf_obs)
 
         # Subset transition if necessary (e.g. for Markov autoregression)
@@ -1779,7 +1595,23 @@ def expected_durations(self):
         """
         (array) Expected duration of a regime, possibly time-varying.
         """
-        return 1. / (1 - np.diagonal(self.regime_transition).squeeze())
+        # It is possible that we will have a degenerate system, so that there
+        # is no possibility of transitioning to a different state. In that
+        # case, we do want the expected duration of one state to be np.inf,
+        # and the expected duration of the other states to be np.nan
+        diag = np.diagonal(self.regime_transition)
+        expected_durations = np.zeros_like(diag)
+        degenerate = np.any(diag == 1, axis=1)
+
+        # For non-degenerate states, use the usual computation
+        expected_durations[~degenerate] = 1 / (1 - diag[~degenerate])
+
+        # For degenerate states, everything is np.nan, except for the one
+        # state that is np.inf.
+        expected_durations[degenerate] = np.nan
+        expected_durations[diag == 1] = np.inf
+
+        return expected_durations.squeeze()
 
 
 class KimSmootherResults(HamiltonFilterResults):
@@ -1893,12 +1725,12 @@ def __init__(self, model, params, results, cov_type='opg', cov_kwds=None,
 
         # Copy over arrays
         attributes = ['regime_transition', 'initial_probabilities',
-                      'conditional_likelihoods',
+                      'conditional_loglikelihoods',
                       'predicted_marginal_probabilities',
                       'predicted_joint_probabilities',
                       'filtered_marginal_probabilities',
                       'filtered_joint_probabilities',
-                      'joint_likelihoods', 'expected_durations']
+                      'joint_loglikelihoods', 'expected_durations']
         for name in attributes:
             setattr(self, name, getattr(self.filter_results, name))
 
@@ -2082,6 +1914,10 @@ def resid(self):
         """
         return self.model.endog - self.fittedvalues
 
+    @property
+    def joint_likelihoods(self):
+        return np.exp(self.joint_loglikelihoods)
+
     def predict(self, start=None, end=None, probabilities=None,
                 conditional=False):
         """


Test patch:
diff --git a/statsmodels/tsa/regime_switching/tests/test_markov_autoregression.py b/statsmodels/tsa/regime_switching/tests/test_markov_autoregression.py
--- a/statsmodels/tsa/regime_switching/tests/test_markov_autoregression.py
+++ b/statsmodels/tsa/regime_switching/tests/test_markov_autoregression.py
@@ -168,7 +168,7 @@ def test_predict():
     assert_allclose(mod_resid[1, 1, :], resids[1, 1, :])
 
 
-def test_conditional_likelihoods():
+def test_conditional_loglikelihoods():
     # AR(1) without mean, k_regimes=2, non-switching variance
     endog = np.ones(10)
     mod = markov_autoregression.MarkovAutoregression(
@@ -180,7 +180,8 @@ def test_conditional_likelihoods():
     resid = mod._resid(params)
     conditional_likelihoods = (
         np.exp(-0.5 * resid**2 / 2) / np.sqrt(2 * np.pi * 2))
-    assert_equal(mod._conditional_likelihoods(params), conditional_likelihoods)
+    assert_allclose(mod._conditional_loglikelihoods(params),
+                    np.log(conditional_likelihoods))
 
     # AR(1) without mean, k_regimes=3, switching variance
     endog = np.ones(10)
@@ -190,27 +191,27 @@ def test_conditional_likelihoods():
     assert_equal(mod.endog, np.ones(9))
 
     params = np.r_[[0.3]*6, 2., 3., 4., 1.5, 3., 4.5, 0.1, 0.5, 0.8]
-    mod_conditional_likelihoods = mod._conditional_likelihoods(params)
+    mod_conditional_loglikelihoods = mod._conditional_loglikelihoods(params)
     conditional_likelihoods = mod._resid(params)
 
     # S_t = 0
     conditional_likelihoods[0, :, :] = (
         np.exp(-0.5 * conditional_likelihoods[0, :, :]**2 / 1.5) /
         np.sqrt(2 * np.pi * 1.5))
-    assert_allclose(mod_conditional_likelihoods[0, :, :],
-                    conditional_likelihoods[0, :, :])
+    assert_allclose(mod_conditional_loglikelihoods[0, :, :],
+                    np.log(conditional_likelihoods[0, :, :]))
     # S_t = 1
     conditional_likelihoods[1, :, :] = (
         np.exp(-0.5 * conditional_likelihoods[1, :, :]**2 / 3.) /
         np.sqrt(2 * np.pi * 3.))
-    assert_allclose(mod_conditional_likelihoods[1, :, :],
-                    conditional_likelihoods[1, :, :])
+    assert_allclose(mod_conditional_loglikelihoods[1, :, :],
+                    np.log(conditional_likelihoods[1, :, :]))
     # S_t = 2
     conditional_likelihoods[2, :, :] = (
         np.exp(-0.5 * conditional_likelihoods[2, :, :]**2 / 4.5) /
         np.sqrt(2 * np.pi * 4.5))
-    assert_allclose(mod_conditional_likelihoods[2, :, :],
-                    conditional_likelihoods[2, :, :])
+    assert_allclose(mod_conditional_loglikelihoods[2, :, :],
+                    np.log(conditional_likelihoods[2, :, :]))
 
 
 class MarkovAutoregression(object):
diff --git a/statsmodels/tsa/regime_switching/tests/test_markov_regression.py b/statsmodels/tsa/regime_switching/tests/test_markov_regression.py
--- a/statsmodels/tsa/regime_switching/tests/test_markov_regression.py
+++ b/statsmodels/tsa/regime_switching/tests/test_markov_regression.py
@@ -964,13 +964,10 @@ def test_hamilton_filter_order_zero(self):
         expected_marginals[:, :2] = [[1/3], [1/3], [1/3]]
         expected_marginals[:, 2:] = [[0], [1], [0]]
 
-        py_results = markov_switching.py_hamilton_filter(
-            initial_probabilities, regime_transition, conditional_likelihoods)
-        assert_allclose(py_results[0], expected_marginals)
-
-        cy_results = markov_switching.cy_hamilton_filter(
-            initial_probabilities, regime_transition, conditional_likelihoods)
-        assert_allclose(cy_results[0], expected_marginals)
+        cy_results = markov_switching.cy_hamilton_filter_log(
+            initial_probabilities, regime_transition,
+            np.log(conditional_likelihoods + 1e-20))
+        assert_allclose(cy_results[0], expected_marginals, atol=1e-15)
 
     def test_hamilton_filter_order_zero_with_tvtp(self):
         k_regimes = 3
@@ -1015,13 +1012,10 @@ def test_hamilton_filter_order_zero_with_tvtp(self):
         expected_marginals[:, 4:6] = [[0], [1/2], [1/2]]
         expected_marginals[:, 6:8] = [[0], [0], [1]]
 
-        py_results = markov_switching.py_hamilton_filter(
-            initial_probabilities, regime_transition, conditional_likelihoods)
-        assert_allclose(py_results[0], expected_marginals)
-
-        cy_results = markov_switching.cy_hamilton_filter(
-            initial_probabilities, regime_transition, conditional_likelihoods)
-        assert_allclose(cy_results[0], expected_marginals)
+        cy_results = markov_switching.cy_hamilton_filter_log(
+            initial_probabilities, regime_transition,
+            np.log(conditional_likelihoods + 1e-20))
+        assert_allclose(cy_results[0], expected_marginals, atol=1e-15)
 
     def test_hamilton_filter_shape_checks(self):
         k_regimes = 3
@@ -1030,53 +1024,12 @@ def test_hamilton_filter_shape_checks(self):
 
         initial_probabilities = np.ones(k_regimes) / k_regimes
         regime_transition = np.ones((k_regimes, k_regimes, nobs)) / k_regimes
-        conditional_likelihoods = np.ones(order * (k_regimes,) + (nobs,))
-
-        for func in [markov_switching.py_hamilton_filter,
-                     markov_switching.cy_hamilton_filter]:
-            with assert_raises(ValueError):
-                func(initial_probabilities,
-                     regime_transition,
-                     conditional_likelihoods)
-
-    def test_py_hamilton_filter(self):
-        mod = self.model
-        params = self.true['params']
-
-        regime_transition = mod.regime_transition_matrix(params)
-        initial_probabilities = mod.initial_probabilities(
-            params, regime_transition)
-        conditional_likelihoods = mod._conditional_likelihoods(params)
-
-        actual = markov_switching.py_hamilton_filter(
-            initial_probabilities, regime_transition, conditional_likelihoods)
-        desired = markov_switching.cy_hamilton_filter(
-            initial_probabilities, regime_transition, conditional_likelihoods)
-
-        for i in range(3):
-            assert_allclose(actual[i], desired[i])
+        conditional_loglikelihoods = np.ones(order * (k_regimes,) + (nobs,))
 
-    def test_py_kim_smoother(self):
-        mod = self.model
-        params = self.true['params']
-
-        regime_transition = mod.regime_transition_matrix(params)
-        initial_probabilities = mod.initial_probabilities(
-            params, regime_transition)
-        conditional_likelihoods = mod._conditional_likelihoods(params)
-
-        # Hamilton filter
-        filter_output = markov_switching.cy_hamilton_filter(
-            initial_probabilities, regime_transition, conditional_likelihoods)
-
-        # Kim smoother
-        actual = markov_switching.py_kim_smoother(
-            regime_transition, filter_output[1], filter_output[3])
-        desired = markov_switching.cy_kim_smoother(
-            regime_transition, filter_output[1], filter_output[3])
-
-        for i in range(2):
-            assert_allclose(actual[i], desired[i])
+        with assert_raises(ValueError):
+            markov_switching.cy_hamilton_filter_log(
+                initial_probabilities, regime_transition,
+                conditional_loglikelihoods)
 
 
 class TestFedFundsConstL1(MarkovRegression):
@@ -1233,3 +1186,91 @@ def setup_class(cls):
         super(TestMumpspcNoconstL1Variance, cls).setup_class(
             true, mumpspc[1:], k_regimes=2, trend='nc', exog=mumpspc[:-1],
             switching_variance=True, atol=1e-4)
+
+
+gh5380_series = [
+    -2.28833689e-04, -4.83175915e-05, -4.93236435e-04, -2.92251109e-04,
+    -4.03295179e-04, -2.01009784e-04, 2.85109215e-04, 1.85710827e-04,
+    -2.04503930e-04, -9.06050944e-05, 1.06177818e-03, -4.97251569e-04,
+    7.16669183e-04, 1.81161945e-04, -6.87139289e-04, -4.85979721e-05,
+    -1.68387973e-05, -4.43257154e-04, 5.37457383e-04, -8.17822909e-04,
+    6.06814040e-04, -1.05481968e-05, -1.15698730e-04, -8.57638924e-04,
+    8.98143953e-05, 1.70216877e-04, -2.84874742e-04, -2.81999910e-05,
+    -3.43787510e-04, -5.74808548e-04, -6.47844682e-05, 1.08667397e-04,
+    -1.28790034e-04, 7.07615608e-05, 1.17633886e-04, 8.98163443e-07,
+    1.03860559e-05, -2.50298430e-06, -9.09673045e-07, -1.94645227e-05,
+    -5.21373877e-05, -1.38431813e-04, 1.11092157e-04, 3.09541647e-05,
+    -1.57756488e-04, 4.93176285e-05, -4.87488913e-05, 2.08097182e-04,
+    -7.08581132e-05, -8.12182224e-06, 8.03090891e-05, -1.02981093e-05,
+    2.07550066e-05, 3.75383116e-05, -3.96307514e-05, -1.12007982e-05,
+    -4.00958429e-05, 2.01206738e-05, -2.95802891e-05, 1.26422639e-04,
+    -3.95617289e-05, 1.94523738e-04, 4.63964270e-05, 2.07712951e-05,
+    2.92417981e-04, 2.15111259e-04, 1.23762736e-04, 7.78305555e-05,
+    -3.44022033e-04, -2.84249114e-05, -1.08351559e-04, -1.76944408e-04,
+    7.99332875e-05, 5.09785758e-04, 3.24506365e-04, -2.47943721e-04,
+    2.22346033e-04, -6.66164024e-05, 2.00911206e-04, 4.58750048e-04,
+    -1.55390954e-04, 3.67913831e-04, 1.08274314e-04, -1.27400351e-04,
+    1.50654063e-04, 2.69976025e-04, 4.51532804e-05, -2.21579419e-04,
+    1.54792373e-04, 1.98397630e-04, 3.96943388e-04, 6.18663277e-04,
+    8.86151537e-04, 6.16949774e-04, 5.44538857e-03, 7.33282114e-05,
+    3.93393013e-03, 6.66556165e-04, 6.18815111e-03, -6.40775088e-03,
+    -1.98864768e-03, -3.42828364e-05, -7.92023127e-04, -1.64656177e-03,
+    -3.31112273e-03, -2.63499766e-03, -2.55118821e-04, -3.63858500e-04,
+    -6.58065806e-04, -3.38492405e-04, -1.10458161e-03, -2.08228620e-07,
+    -7.21562092e-05, 7.92946105e-05, 1.25472212e-05, -1.27190212e-04,
+    3.94913196e-05, -1.87353437e-05, -6.57929565e-06, 1.61597869e-05,
+    1.90031845e-05, 8.76129739e-03, -8.34566734e-03, -5.82053353e-05,
+    1.28770725e-04, 2.38160836e-04, -1.84590083e-05, 3.10903734e-05,
+    1.89366264e-05, 7.61985181e-05, 3.20208326e-04, -6.90388272e-05,
+    2.09928647e-05, -3.14720707e-05, 1.32614437e-04, 1.10385710e-04,
+    -4.91256074e-05, -1.16967974e-04, 4.88977021e-05, 6.19231048e-05,
+    2.95994863e-04, 1.91592526e-03, -2.22511393e-04, -8.04871984e-04,
+    -6.66886069e-04, -3.80754506e-04, -4.06613941e-05, -4.66164600e-04,
+    -1.48077847e-04, 1.31787219e-04, 5.12392307e-05, -2.61346302e-05,
+    -1.46071993e-04, -3.17839957e-04, -2.52143621e-04, -1.73768102e-04,
+    -6.88107229e-05, -2.14130430e-05, 5.25348170e-07, -9.80972737e-05,
+    -6.90120785e-05, -1.28208365e-04, 2.09816018e-05, 1.02199123e-05,
+    2.08633684e-05, 1.05609818e-05, 1.35835269e-05, 1.50357742e-04,
+    5.72303827e-05, 1.27501839e-04, 1.20706157e-04, 8.27340411e-06,
+    3.77430540e-05, 8.10426523e-05, 9.99617511e-05, 1.21103273e-04,
+    1.10255996e-04, 6.89558159e-05, 3.22442426e-05, 8.49416109e-05,
+    4.05396296e-04, 1.69699170e-04, 1.10892701e-04, 7.21186150e-05,
+    1.24978570e-04, -2.91922347e-04, 5.50447651e-05, -1.68467417e-04,
+    -7.06084859e-05, -3.73250482e-04, -1.49829854e-04, -7.23752254e-05,
+    -9.80670951e-05, -3.00179050e-05, 4.15704679e-05, 2.87503307e-05,
+    -5.09988933e-05, -9.55791344e-06, -2.74369249e-05, -2.23330122e-05,
+    9.74727035e-06, -8.58146574e-06, 2.97704340e-05, -1.12990428e-05,
+    -5.90507361e-05, 9.50772649e-07, -7.35502474e-07, 5.66683332e-07,
+    -9.81320792e-06, -6.60845729e-07, -3.19848073e-05, -1.10779191e-04]
+
+
+@pytest.mark.smoke
+def test_avoid_underflow():
+    # See GH5380 for example and dataset demonstrating underflow. This test is
+    # the "Example 1" given there.
+
+    # The underlying problem was an underflow error in computing the smoothed
+    # states, which resulted in NaN values in e.g. smoothed_joint_probabilities
+    # This causes NaNs in the EM algorithm (e.g. _em_regime_transition), and
+    # so the _fit_em method would return NaN parameters, as would the fit
+    # method.
+
+    # This test runs the smoother at the last set of good parameter values
+    # (i.e. these parameters were generated by _fit_em and did not have any
+    # NaN values in them, but the smoothed_joint_probabilities computed at
+    # those parameters have NaNs in them, so that the next set of parameters
+    # would have NaNs). The test then checks that the underflow problem has
+    # been resolved, i.e. that there are no NaNs in the predicted, filtered,
+    # and smoothed joint probabilities.
+    m = markov_regression.MarkovRegression(
+        gh5380_series, k_regimes=2, switching_variance=True)
+    # To get these params, the following command was used:
+    # params = m._fit_em(maxiter=1).params
+    params = np.array([
+        6.97337611e-01, 6.26116329e-01, -6.41266551e-06, 3.81141202e-06,
+        4.72462327e-08, 4.45291473e-06])
+    res = m.smooth(params)
+
+    assert not np.any(np.isnan(res.predicted_joint_probabilities))
+    assert not np.any(np.isnan(res.filtered_joint_probabilities))
+    assert not np.any(np.isnan(res.smoothed_joint_probabilities))