Repository: statsmodels/statsmodels

Issue:
ENH: Recursive least squares (via statespace)
This comes up enough and would be easy enough that it may make sense to add a new recursive least squares (RLS) model / class. This issue is just to poll for whether or not it should be included and what features people would like that I haven't thought of.

Technically, this class would simply be a wrapper around the `SARIMAX` class which can compute RLS estimates already (optionally with SARIMA errors). So if we do create a new RLS class, it would be more for "marketing" purposes and for RLS-specific output than for functionality.



Discussion:

Resolving patch:
diff --git a/statsmodels/api.py b/statsmodels/api.py
--- a/statsmodels/api.py
+++ b/statsmodels/api.py
@@ -4,6 +4,7 @@
 from .tools.tools import add_constant, categorical
 from . import regression
 from .regression.linear_model import OLS, GLS, WLS, GLSAR
+from .regression.recursive_ls import RecursiveLS
 from .regression.quantile_regression import QuantReg
 from .regression.mixed_linear_model import MixedLM
 from .genmod import api as genmod
diff --git a/statsmodels/regression/recursive_ls.py b/statsmodels/regression/recursive_ls.py
new file mode 100644
--- /dev/null
+++ b/statsmodels/regression/recursive_ls.py
@@ -0,0 +1,697 @@
+"""
+Recursive least squares model
+
+Author: Chad Fulton
+License: Simplified-BSD
+"""
+from __future__ import division, absolute_import, print_function
+
+from warnings import warn
+from statsmodels.compat.collections import OrderedDict
+
+import numpy as np
+import pandas as pd
+from statsmodels.regression.linear_model import OLS
+from statsmodels.tools.data import _is_using_pandas
+from statsmodels.tsa.statespace.mlemodel import (
+    MLEModel, MLEResults, MLEResultsWrapper)
+from statsmodels.tools.tools import Bunch
+from statsmodels.tools.decorators import cache_readonly, resettable_cache
+import statsmodels.base.wrapper as wrap
+
+# Columns are alpha = 0.1, 0.05, 0.025, 0.01, 0.005
+_cusum_squares_scalars = np.array([
+    [1.0729830,   1.2238734,  1.3581015,  1.5174271,  1.6276236],
+    [-0.6698868, -0.6700069, -0.6701218, -0.6702672, -0.6703724],
+    [-0.5816458, -0.7351697, -0.8858694, -1.0847745, -1.2365861]
+])
+
+
+class RecursiveLS(MLEModel):
+    r"""
+    Recursive least squares
+
+    Parameters
+    ----------
+    endog : array_like
+        The observed time-series process :math:`y`
+    exog : array_like
+        Array of exogenous regressors, shaped nobs x k.
+
+    Notes
+    -----
+    Recursive least squares (RLS) corresponds to expanding window ordinary
+    least squares (OLS).
+
+    This model applies the Kalman filter to compute recursive estimates of the
+    coefficients and recursive residuals.
+
+    References
+    ----------
+    .. [1] Durbin, James, and Siem Jan Koopman. 2012.
+       Time Series Analysis by State Space Methods: Second Edition.
+       Oxford University Press.
+
+    """
+    def __init__(self, endog, exog, **kwargs):
+        # Standardize data
+        if not _is_using_pandas(endog, None):
+            endog = np.asanyarray(endog)
+
+        exog_is_using_pandas = _is_using_pandas(exog, None)
+        if not exog_is_using_pandas:
+            exog = np.asarray(exog)
+
+        # Make sure we have 2-dimensional array
+        if exog.ndim == 1:
+            if not exog_is_using_pandas:
+                exog = exog[:, None]
+            else:
+                exog = pd.DataFrame(exog)
+
+        self.k_exog = exog.shape[1]
+
+        # Handle coefficient initialization
+        # By default, do not calculate likelihood while it is controlled by
+        # diffuse initial conditions.
+        kwargs.setdefault('loglikelihood_burn', self.k_exog)
+        kwargs.setdefault('initialization', 'approximate_diffuse')
+        kwargs.setdefault('initial_variance', 1e9)
+
+        # Initialize the state space representation
+        super(RecursiveLS, self).__init__(
+            endog, k_states=self.k_exog, exog=exog, **kwargs
+        )
+
+        # Setup the state space representation
+        self['design'] = self.exog[:, :, None].T
+        self['transition'] = np.eye(self.k_states)
+
+        # Notice that the filter output does not depend on the measurement
+        # variance, so we set it here to 1
+        self['obs_cov', 0, 0] = 1.
+
+    @classmethod
+    def from_formula(cls, formula, data, subset=None):
+        """
+        Not implemented for state space models
+        """
+        return super(MLEModel, cls).from_formula(formula, data, subset)
+
+    def fit(self):
+        """
+        Fits the model by application of the Kalman filter
+
+        Returns
+        -------
+        RecursiveLSResults
+        """
+        # Get the smoother results with an arbitrary measurement variance
+        smoother_results = self.smooth(return_ssm=True)
+        # Compute the MLE of sigma2 (see Harvey, 1989 equation 4.2.5)
+        resid = smoother_results.standardized_forecasts_error[0]
+        sigma2 = (np.inner(resid, resid) /
+                  (self.nobs - self.loglikelihood_burn))
+
+        # Now construct a results class, where the params are the final
+        # estimates of the regression coefficients
+        self['obs_cov', 0, 0] = sigma2
+        return self.smooth()
+
+    def filter(self, return_ssm=False, **kwargs):
+        # Get the state space output
+        result = super(RecursiveLS, self).filter([], transformed=True,
+                                                 cov_type='none',
+                                                 return_ssm=True, **kwargs)
+
+        # Wrap in a results object
+        if not return_ssm:
+            params = result.filtered_state[:, -1]
+            cov_kwds = {
+                'custom_cov_type': 'nonrobust',
+                'custom_cov_params': result.filtered_state_cov[:, :, -1],
+                'custom_description': ('Parameters and covariance matrix'
+                                       ' estimates are RLS estimates'
+                                       ' conditional on the entire sample.')
+            }
+            result = RecursiveLSResultsWrapper(
+                RecursiveLSResults(self, params, result, cov_type='custom',
+                                   cov_kwds=cov_kwds)
+            )
+
+        return result
+
+    def smooth(self, return_ssm=False, **kwargs):
+        # Get the state space output
+        result = super(RecursiveLS, self).smooth([], transformed=True,
+                                                 cov_type='none',
+                                                 return_ssm=True, **kwargs)
+
+        # Wrap in a results object
+        if not return_ssm:
+            params = result.filtered_state[:, -1]
+            cov_kwds = {
+                'custom_cov_type': 'nonrobust',
+                'custom_cov_params': result.filtered_state_cov[:, :, -1],
+                'custom_description': ('Parameters and covariance matrix'
+                                       ' estimates are RLS estimates'
+                                       ' conditional on the entire sample.')
+            }
+            result = RecursiveLSResultsWrapper(
+                RecursiveLSResults(self, params, result, cov_type='custom',
+                                   cov_kwds=cov_kwds)
+            )
+
+        return result
+
+    @property
+    def param_names(self):
+        return self.exog_names
+
+    @property
+    def start_params(self):
+        # Only parameter is the measurment disturbance standard deviation
+        return np.zeros(0)
+
+    def update(self, params, **kwargs):
+        """
+        Update the parameters of the model
+
+        Updates the representation matrices to fill in the new parameter
+        values.
+
+        Parameters
+        ----------
+        params : array_like
+            Array of new parameters.
+        transformed : boolean, optional
+            Whether or not `params` is already transformed. If set to False,
+            `transform_params` is called. Default is True..
+
+        Returns
+        -------
+        params : array_like
+            Array of parameters.
+        """
+        pass
+
+
+class RecursiveLSResults(MLEResults):
+    """
+    Class to hold results from fitting a recursive least squares model.
+
+    Parameters
+    ----------
+    model : RecursiveLS instance
+        The fitted model instance
+
+    Attributes
+    ----------
+    specification : dictionary
+        Dictionary including all attributes from the recursive least squares
+        model instance.
+
+    See Also
+    --------
+    statsmodels.tsa.statespace.kalman_filter.FilterResults
+    statsmodels.tsa.statespace.mlemodel.MLEResults
+    """
+
+    def __init__(self, model, params, filter_results, cov_type='opg',
+                 **kwargs):
+        super(RecursiveLSResults, self).__init__(
+            model, params, filter_results, cov_type, **kwargs)
+
+        self.df_resid = np.inf  # attribute required for wald tests
+
+        # Save _init_kwds
+        self._init_kwds = self.model._get_init_kwds()
+
+        # Save the model specification
+        self.specification = Bunch(**{
+            'k_exog': self.model.k_exog})
+
+    @property
+    def recursive_coefficients(self):
+        """
+        Estimates of regression coefficients, recursively estimated
+
+        Returns
+        -------
+        out: Bunch
+            Has the following attributes:
+
+            - `filtered`: a time series array with the filtered estimate of
+                          the component
+            - `filtered_cov`: a time series array with the filtered estimate of
+                          the variance/covariance of the component
+            - `smoothed`: a time series array with the smoothed estimate of
+                          the component
+            - `smoothed_cov`: a time series array with the smoothed estimate of
+                          the variance/covariance of the component
+            - `offset`: an integer giving the offset in the state vector where
+                        this component begins
+        """
+        out = None
+        spec = self.specification
+        start = offset = 0
+        end = offset + spec.k_exog
+        out = Bunch(
+            filtered=self.filtered_state[start:end],
+            filtered_cov=self.filtered_state_cov[start:end, start:end],
+            smoothed=None, smoothed_cov=None,
+            offset=offset
+        )
+        if self.smoothed_state is not None:
+            out.smoothed = self.smoothed_state[start:end]
+        if self.smoothed_state_cov is not None:
+            out.smoothed_cov = (
+                self.smoothed_state_cov[start:end, start:end])
+        return out
+
+    @cache_readonly
+    def resid_recursive(self):
+        """
+        Recursive residuals
+
+        Returns
+        -------
+        resid_recursive : array_like
+            An array of length `nobs` holding the recursive
+            residuals.
+
+        Notes
+        -----
+        The first `k_exog` residuals are typically unreliable due to
+        initialization.
+
+        """
+        # See Harvey (1989) section 5.4; he defines the standardized
+        # innovations in 5.4.1, but they have non-unit variance, whereas
+        # the standardized forecast errors assume unit variance. To convert
+        # to Harvey's definition, we need to multiply by the standard
+        # deviation.
+        return (self.filter_results.standardized_forecasts_error.squeeze() *
+                self.filter_results.obs_cov[0, 0]**0.5)
+
+    @cache_readonly
+    def cusum(self):
+        r"""
+        Cumulative sum of standardized recursive residuals statistics
+
+        Returns
+        -------
+        cusum : array_like
+            An array of length `nobs - k_exog` holding the
+            CUSUM statistics.
+
+
+        Notes
+        -----
+        The CUSUM statistic takes the form:
+
+        .. math::
+
+            W_t = \frac{1}{\hat \sigma} \sum_{j=k+1}^t w_j
+
+        where :math:`w_j` is the recursive residual at time :math:`j` and
+        :math:`\hat \sigma` is the estimate of the standard deviation
+        from the full sample.
+
+        Excludes the first `k_exog` datapoints.
+
+        Due to differences in the way :math:`\hat \sigma` is calculated, the
+        output of this function differs slightly from the output in the
+        R package strucchange and the Stata contributed .ado file cusum6. The
+        calculation in this package is consistent with the description of
+        Brown et al. (1975)
+
+        References
+        ----------
+        .. [1] Brown, R. L., J. Durbin, and J. M. Evans. 1975.
+           "Techniques for Testing the Constancy of
+           Regression Relationships over Time."
+           Journal of the Royal Statistical Society.
+           Series B (Methodological) 37 (2): 149-92.
+
+        """
+        llb = self.loglikelihood_burn
+        return (np.cumsum(self.resid_recursive[self.loglikelihood_burn:]) /
+                np.std(self.resid_recursive[llb:], ddof=1))
+
+    @cache_readonly
+    def cusum_squares(self):
+        r"""
+        Cumulative sum of squares of standardized recursive residuals
+        statistics
+
+        Returns
+        -------
+        cusum_squares : array_like
+            An array of length `nobs - k_exog` holding the
+            CUSUM of squares statistics.
+
+        Notes
+        -----
+        The CUSUM of squares statistic takes the form:
+
+        .. math::
+
+            s_t = \left ( \sum_{j=k+1}^t w_j^2 \right ) \Bigg /
+                  \left ( \sum_{j=k+1}^T w_j^2 \right )
+
+        where :math:`w_j` is the recursive residual at time :math:`j`.
+
+        Excludes the first `k_exog` datapoints.
+
+        References
+        ----------
+        .. [1] Brown, R. L., J. Durbin, and J. M. Evans. 1975.
+           "Techniques for Testing the Constancy of
+           Regression Relationships over Time."
+           Journal of the Royal Statistical Society.
+           Series B (Methodological) 37 (2): 149-92.
+
+        """
+        numer = np.cumsum(self.resid_recursive[self.loglikelihood_burn:]**2)
+        denom = numer[-1]
+        return numer / denom
+
+    def plot_recursive_coefficient(self, variables=0, alpha=0.05,
+                                   legend_loc='upper left', fig=None,
+                                   figsize=None):
+        r"""
+        Plot the recursively estimated coefficients on a given variable
+
+        Parameters
+        ----------
+        variables : int or str or iterable of int or string, optional
+            Integer index or string name of the variable whose coefficient will
+            be plotted. Can also be an iterable of integers or strings. Default
+            is the first variable.
+        alpha : float, optional
+            The confidence intervals for the coefficient are (1 - alpha) %
+        legend_loc : string, optional
+            The location of the legend in the plot. Default is upper left.
+        fig : Matplotlib Figure instance, optional
+            If given, subplots are created in this figure instead of in a new
+            figure. Note that the grid will be created in the provided
+            figure using `fig.add_subplot()`.
+        figsize : tuple, optional
+            If a figure is created, this argument allows specifying a size.
+            The tuple is (width, height).
+
+        Notes
+        -----
+        All plots contain (1 - `alpha`) %  confidence intervals.
+        """
+        # Get variables
+        if isinstance(variables, (int, str)):
+            variables = [variables]
+        k_variables = len(variables)
+
+        # If a string was given for `variable`, try to get it from exog names
+        exog_names = self.model.exog_names
+        for i in range(k_variables):
+            variable = variables[i]
+            if isinstance(variable, str):
+                variables[i] = exog_names.index(variable)
+
+        # Create the plot
+        from scipy.stats import norm
+        from statsmodels.graphics.utils import _import_mpl, create_mpl_fig
+        plt = _import_mpl()
+        fig = create_mpl_fig(fig, figsize)
+
+        for i in range(k_variables):
+            variable = variables[i]
+            ax = fig.add_subplot(k_variables, 1, i + 1)
+
+            # Get dates, if applicable
+            if hasattr(self.data, 'dates') and self.data.dates is not None:
+                dates = self.data.dates._mpl_repr()
+            else:
+                dates = np.arange(self.nobs)
+            llb = self.loglikelihood_burn
+
+            # Plot the coefficient
+            coef = self.recursive_coefficients
+            ax.plot(dates[llb:], coef.filtered[variable, llb:],
+                    label='Recursive estimates: %s' % exog_names[variable])
+
+            # Legend
+            handles, labels = ax.get_legend_handles_labels()
+
+            # Get the critical value for confidence intervals
+            if alpha is not None:
+                critical_value = norm.ppf(1 - alpha / 2.)
+
+                # Plot confidence intervals
+                std_errors = np.sqrt(coef.filtered_cov[variable, variable, :])
+                ci_lower = (
+                    coef.filtered[variable] - critical_value * std_errors)
+                ci_upper = (
+                    coef.filtered[variable] + critical_value * std_errors)
+                ci_poly = ax.fill_between(
+                    dates[llb:], ci_lower[llb:], ci_upper[llb:], alpha=0.2
+                )
+                ci_label = ('$%.3g \\%%$ confidence interval'
+                            % ((1 - alpha)*100))
+
+                # Only add CI to legend for the first plot
+                if i == 0:
+                    # Proxy artist for fill_between legend entry
+                    # See http://matplotlib.org/1.3.1/users/legend_guide.html
+                    p = plt.Rectangle((0, 0), 1, 1,
+                                      fc=ci_poly.get_facecolor()[0])
+
+                    handles.append(p)
+                    labels.append(ci_label)
+
+            ax.legend(handles, labels, loc=legend_loc)
+
+            # Remove xticks for all but the last plot
+            if i < k_variables - 1:
+                ax.xaxis.set_ticklabels([])
+
+        fig.tight_layout()
+
+        return fig
+
+    def _cusum_significance_bounds(self, alpha, ddof=0, points=None):
+        """
+        Parameters
+        ----------
+        alpha : float, optional
+            The significance bound is alpha %.
+        ddof : int, optional
+            The number of periods additional to `k_exog` to exclude in
+            constructing the bounds. Default is zero. This is usually used
+            only for testing purposes.
+        points : iterable, optional
+            The points at which to evaluate the significance bounds. Default is
+            two points, beginning and end of the sample.
+
+        Notes
+        -----
+        Comparing against the cusum6 package for Stata, this does not produce
+        exactly the same confidence bands (which are produced in cusum6 by
+        lw, uw) because they burn the first k_exog + 1 periods instead of the
+        first k_exog. If this change is performed
+        (so that `tmp = (self.nobs - llb - 1)**0.5`), then the output here
+        matches cusum6.
+
+        The cusum6 behavior does not seem to be consistent with
+        Brown et al. (1975); it is likely they did that because they needed
+        three initial observations to get the initial OLS estimates, whereas
+        we do not need to do that.
+        """
+        # Get the constant associated with the significance level
+        if alpha == 0.01:
+            scalar = 1.143
+        elif alpha == 0.05:
+            scalar = 0.948
+        elif alpha == 0.10:
+            scalar = 0.950
+        else:
+            raise ValueError('Invalid significance level.')
+
+        # Get the points for the significance bound lines
+        llb = self.loglikelihood_burn
+        tmp = (self.nobs - llb - ddof)**0.5
+        upper_line = lambda x: scalar * tmp + 2 * scalar * (x - llb) / tmp
+
+        if points is None:
+            points = np.array([llb, self.nobs])
+        return -upper_line(points), upper_line(points)
+
+    def plot_cusum(self, alpha=0.05, legend_loc='upper left',
+                   fig=None, figsize=None):
+        r"""
+        Plot the CUSUM statistic and significance bounds.
+
+        Parameters
+        ----------
+        alpha : float, optional
+            The plotted significance bounds are alpha %.
+        legend_loc : string, optional
+            The location of the legend in the plot. Default is upper left.
+        fig : Matplotlib Figure instance, optional
+            If given, subplots are created in this figure instead of in a new
+            figure. Note that the grid will be created in the provided
+            figure using `fig.add_subplot()`.
+        figsize : tuple, optional
+            If a figure is created, this argument allows specifying a size.
+            The tuple is (width, height).
+
+        Notes
+        -----
+        Evidence of parameter instability may be found if the CUSUM statistic
+        moves out of the significance bounds.
+
+        References
+        ----------
+        .. [1] Brown, R. L., J. Durbin, and J. M. Evans. 1975.
+           "Techniques for Testing the Constancy of
+           Regression Relationships over Time."
+           Journal of the Royal Statistical Society.
+           Series B (Methodological) 37 (2): 149-92.
+
+        """
+        # Create the plot
+        from statsmodels.graphics.utils import _import_mpl, create_mpl_fig
+        plt = _import_mpl()
+        fig = create_mpl_fig(fig, figsize)
+        ax = fig.add_subplot(1, 1, 1)
+
+        # Get dates, if applicable
+        if hasattr(self.data, 'dates') and self.data.dates is not None:
+            dates = self.data.dates._mpl_repr()
+        else:
+            dates = np.arange(self.nobs)
+        llb = self.loglikelihood_burn
+
+        # Plot cusum series and reference line
+        ax.plot(dates[llb:], self.cusum, label='CUSUM')
+        ax.hlines(0, dates[llb], dates[-1], color='k', alpha=0.3)
+
+        # Plot significance bounds
+        lower_line, upper_line = self._cusum_significance_bounds(alpha)
+        ax.plot([dates[llb], dates[-1]], upper_line, 'k--',
+                label='%d%% significance' % (alpha * 100))
+        ax.plot([dates[llb], dates[-1]], lower_line, 'k--')
+
+        ax.legend(loc=legend_loc)
+
+        return fig
+
+    def _cusum_squares_significance_bounds(self, alpha, points=None):
+        """
+        Notes
+        -----
+        Comparing against the cusum6 package for Stata, this does not produce
+        exactly the same confidence bands (which are produced in cusum6 by
+        lww, uww) because they use a different method for computing the
+        critical value; in particular, they use tabled values from
+        Table C, pp. 364-365 of "The Econometric Analysis of Time Series"
+        Harvey, (1990), and use the value given to 99 observations for any
+        larger number of observations. In contrast, we use the approximating
+        critical values suggested in Edgerton and Wells (1994) which allows
+        computing relatively good approximations for any number of
+        observations.
+        """
+        # Get the approximate critical value associated with the significance
+        # level
+        llb = self.loglikelihood_burn
+        n = 0.5 * (self.nobs - llb) - 1
+        try:
+            ix = [0.1, 0.05, 0.025, 0.01, 0.005].index(alpha / 2)
+        except ValueError:
+            raise ValueError('Invalid significance level.')
+        scalars = _cusum_squares_scalars[:, ix]
+        crit = scalars[0] / n**0.5 + scalars[1] / n + scalars[2] / n**1.5
+
+        # Get the points for the significance bound lines
+        if points is None:
+            points = np.array([llb, self.nobs])
+        line = (points - llb) / (self.nobs - llb)
+
+        return line - crit, line + crit
+
+    def plot_cusum_squares(self, alpha=0.05, legend_loc='upper left',
+                           fig=None, figsize=None):
+        r"""
+        Plot the CUSUM of squares statistic and significance bounds.
+
+        Parameters
+        ----------
+        alpha : float, optional
+            The plotted significance bounds are alpha %.
+        legend_loc : string, optional
+            The location of the legend in the plot. Default is upper left.
+        fig : Matplotlib Figure instance, optional
+            If given, subplots are created in this figure instead of in a new
+            figure. Note that the grid will be created in the provided
+            figure using `fig.add_subplot()`.
+        figsize : tuple, optional
+            If a figure is created, this argument allows specifying a size.
+            The tuple is (width, height).
+
+        Notes
+        -----
+        Evidence of parameter instability may be found if the CUSUM of squares
+        statistic moves out of the significance bounds.
+
+        Critical values used in creating the significance bounds are computed
+        using the approximate formula of [2]_.
+
+        References
+        ----------
+        .. [1] Brown, R. L., J. Durbin, and J. M. Evans. 1975.
+           "Techniques for Testing the Constancy of
+           Regression Relationships over Time."
+           Journal of the Royal Statistical Society.
+           Series B (Methodological) 37 (2): 149-92.
+        .. [2] Edgerton, David, and Curt Wells. 1994.
+           "Critical Values for the Cusumsq Statistic
+           in Medium and Large Sized Samples."
+           Oxford Bulletin of Economics and Statistics 56 (3): 355-65.
+
+        """
+        # Create the plot
+        from statsmodels.graphics.utils import _import_mpl, create_mpl_fig
+        plt = _import_mpl()
+        fig = create_mpl_fig(fig, figsize)
+        ax = fig.add_subplot(1, 1, 1)
+
+        # Get dates, if applicable
+        if hasattr(self.data, 'dates') and self.data.dates is not None:
+            dates = self.data.dates._mpl_repr()
+        else:
+            dates = np.arange(self.nobs)
+        llb = self.loglikelihood_burn
+
+        # Plot cusum series and reference line
+        ax.plot(dates[llb:], self.cusum_squares, label='CUSUM of squares')
+        ref_line = (np.arange(llb, self.nobs) - llb) / (self.nobs - llb)
+        ax.plot(dates[llb:], ref_line, 'k', alpha=0.3)
+
+        # Plot significance bounds
+        lower_line, upper_line = self._cusum_squares_significance_bounds(alpha)
+        ax.plot([dates[llb], dates[-1]], upper_line, 'k--',
+                label='%d%% significance' % (alpha * 100))
+        ax.plot([dates[llb], dates[-1]], lower_line, 'k--')
+
+        ax.legend(loc=legend_loc)
+
+        return fig
+
+
+class RecursiveLSResultsWrapper(MLEResultsWrapper):
+    _attrs = {}
+    _wrap_attrs = wrap.union_dicts(MLEResultsWrapper._wrap_attrs,
+                                   _attrs)
+    _methods = {}
+    _wrap_methods = wrap.union_dicts(MLEResultsWrapper._wrap_methods,
+                                     _methods)
+wrap.populate_wrapper(RecursiveLSResultsWrapper, RecursiveLSResults)
diff --git a/statsmodels/tsa/statespace/mlemodel.py b/statsmodels/tsa/statespace/mlemodel.py
--- a/statsmodels/tsa/statespace/mlemodel.py
+++ b/statsmodels/tsa/statespace/mlemodel.py
@@ -1659,6 +1659,11 @@ def _get_robustcov_results(self, cov_type='opg', **kwargs):
             res._rank = 0
             res.cov_kwds['description'] = (
                 'No parameters estimated.')
+        elif cov_type == 'custom':
+            res.cov_type = kwargs['custom_cov_type']
+            res.cov_params_default = kwargs['custom_cov_params']
+            res.cov_kwds['description'] = kwargs['custom_description']
+            res._rank = np.linalg.matrix_rank(res.cov_params_default)
         elif cov_type == 'none':
             res.cov_params_default = np.zeros((k_params, k_params)) * np.nan
             res._rank = np.nan


Test patch:
diff --git a/statsmodels/regression/tests/results/results_rls_R.csv b/statsmodels/regression/tests/results/results_rls_R.csv
new file mode 100644
--- /dev/null
+++ b/statsmodels/regression/tests/results/results_rls_R.csv
@@ -0,0 +1,202 @@
+"beta1","beta2","rec_resid","cusum"
+20.0003289473666,0.0651315789473821,0.244954345907551,0.00193563605335298
+27.2660687432794,0.01385606874334,0.203040661455013,0.00354006900657476
+38.67905442175,-0.0670068027209533,0.270280443387117,0.00567583258651052
+39.7751950254108,-0.0745053702654907,0.241841533101339,0.00758687110650389
+29.1331849912345,0.00178883071581329,0.415203521341092,0.010867820878631
+18.3066265462468,0.07927114677407,0.397784568256463,0.0140111255990758
+10.3433466424157,0.136079854809809,0.176046592258926,0.0154022506668258
+6.85989720582705,0.160900850388106,0.0938882276325083,0.0161441580525371
+8.20327148258485,0.151342100522645,-0.0456169627715057,0.0157836915441441
+10.2511427486456,0.136788154897673,-0.100272366304063,0.0149913365171122
+10.9957699951444,0.131503464575536,-0.0503494741056537,0.0145934735725493
+11.3175707763209,0.129219955362651,-0.0340711516639038,0.0143242423846087
+10.6654524361781,0.13384553399014,0.0916855645039212,0.0150487442650235
+11.1159086294258,0.130659898477266,-0.0628046014785767,0.0145524605575922
+12.156386729997,0.123317221871426,-0.163723398534085,0.0132587137093544
+12.7326996394925,0.119259115732779,-0.103746490236991,0.0124389060583918
+13.4410659854797,0.114279885481247,-0.153770453685824,0.0112238076571287
+13.6569896037784,0.112764484973186,-0.0567551736551511,0.010775326696588
+14.1144011142444,0.109559572375609,-0.142266908565485,0.00965112962877052
+14.8158432176534,0.104659089244761,-0.237517649981024,0.00777425855343773
+15.6507453043316,0.0988444881094996,-0.308861421756066,0.00533362700952327
+16.1607873501768,0.0952992037891843,-0.223226013059955,0.00356968884658018
+16.5385918090731,0.0926777890181332,-0.19441899414506,0.00203338454820357
+16.6498010517627,0.0919074166590858,-0.0667305825844362,0.0015060776288953
+17.014471754413,0.0893917183701445,-0.228436397901914,-0.000299033139965743
+17.3460818756217,0.0871131227705724,-0.222144348908167,-0.00205442396270116
+17.3843661628274,0.0868509149764685,-0.0285352009690891,-0.00227990991430085
+17.1280162311825,0.0886104513064945,0.25234647696406,-0.000285861035769862
+16.6667783624255,0.091771289167083,0.510364610726381,0.00374705431103636
+16.3226842126683,0.0941262202407745,0.426298113691983,0.00711567386045231
+16.0455621834406,0.0960146002312399,0.353662998951508,0.00991032871326247
+15.85854585646,0.0972808365656299,0.234197517696713,0.0117609640109543
+15.733990938833,0.0981199698632186,0.160737119802357,0.0130311132016079
+15.5472155607408,0.0993737343759857,0.259408244690296,0.0150809643650939
+15.359100577245,0.100632613316582,0.285291985514453,0.0173353495683793
+15.1438436622109,0.102065903516274,0.340269721055102,0.0200241703667954
+14.9504525606908,0.103348110227141,0.326521533003193,0.0226043526008987
+14.8392679441337,0.104081295077877,0.196271786313286,0.0241552977145134
+14.5735361313145,0.105829575321111,0.527326289405741,0.0283222447179351
+14.2099750496454,0.108218482052348,0.819366249673146,0.034796899608796
+13.7874175847374,0.110990470726413,1.06025114550419,0.0431750336453371
+13.3518043480087,0.113834887397852,1.14841860382417,0.0522498693893917
+12.8346569963456,0.117209228903986,1.52569799370484,0.0643059773641323
+12.3367186457394,0.120451583177883,1.5951492085586,0.0769108907700321
+11.9185230337974,0.123154390987991,1.33821597224724,0.0874855105974843
+11.5170468495664,0.125739357672303,1.36169336194335,0.0982456494112033
+11.266515248211,0.127342275661413,0.867738614053158,0.105102544070593
+11.0821361490573,0.128514376720014,0.653394030490114,0.11026568186991
+10.9290164719613,0.129484865504103,0.584740179652795,0.1148863150307
+10.8178656341354,0.130186652348879,0.450648727628674,0.118447353808616
+10.815540474966,0.130201229968511,0.00958837887806173,0.118523121445156
+10.8387662708446,0.130056193898848,-0.101729973630018,0.117719248364499
+10.945328087949,0.129395696123524,-0.474099262131996,0.113972902810797
+11.1085265304914,0.128391482155469,-0.742321517670144,0.108107057534779
+11.080805458722,0.128561827573399,0.139072384001631,0.109206011380655
+11.0781776871062,0.12857788516236,0.013749780020619,0.109314662524749
+10.8745917334063,0.129819717290388,1.15962459122506,0.118478048292875
+10.6044182322985,0.131458118663421,1.59589325604434,0.131088841182699
+10.2429003246852,0.133641433952644,2.25631514703675,0.148918306220968
+9.80473923277081,0.136280688202692,2.92799599642778,0.172055412010052
+9.25022363539014,0.139607925830564,3.90633237289927,0.202923359052794
+8.63802754477139,0.143273443914073,4.61111158580915,0.239360491035316
+8.05948905426918,0.146726439925444,4.59843304875835,0.275697436865482
+7.5349394264988,0.149825974640662,4.13302051978024,0.3083566799489
+7.00821909152587,0.152931416451406,4.42128714242792,0.343293813900952
+6.49494054475082,0.155945401893927,4.5048685993743,0.378891410850131
+6.0988192774942,0.15825221854616,3.50323010316598,0.406574032547477
+5.69919719959164,0.160571875780665,3.72091144830362,0.435976778287651
+5.32563871725321,0.162726735932826,3.5667996236933,0.464161728104023
+5.00672684903167,0.164551848094488,3.08141385283983,0.488511146060907
+4.71008920605094,0.166234013127225,2.87502733736156,0.511229692033032
+4.43756588678741,0.16777195359686,2.74961925042266,0.532957259814663
+4.21384227381131,0.169025609768913,2.30707054752226,0.551187795439038
+4.03276576900703,0.170031456375408,1.88475608800807,0.566081190460091
+3.84792927359839,0.171052217995977,1.99083244967449,0.581812803840935
+3.65581679781767,0.172104735954175,2.10565713861364,0.598451765106961
+3.43271594969956,0.173319986565316,2.53435920069987,0.618478341989467
+3.19551592377813,0.174605121939789,2.80068054404111,0.640609397533247
+2.94174918379039,0.175967331891713,3.02350395609635,0.664501209875634
+2.64977582541515,0.177521141926778,3.53325213433404,0.69242106649816
+2.27804795107949,0.179492726795484,4.75687341104855,0.730010012477248
+1.81207106307359,0.181952378499593,6.22749964103012,0.779219887822632
+1.20006257623398,0.185189423913085,8.93386320080931,0.849815523279275
+0.642700133221933,0.188106533453904,8.13393137937316,0.914090075237443
+0.162044794069806,0.190586654584145,6.84967429813633,0.968216392025478
+-0.388232157105243,0.193419998799898,8.33645466109985,1.03409128858787
+-0.830929504473958,0.195666868854773,6.55661128043321,1.08590181326015
+-1.36043838894161,0.198355531717544,8.48597347596573,1.15295821165562
+-1.91508980963037,0.201167866402037,9.45426514385162,1.22766607773816
+-2.33168037879134,0.203252287238571,7.00041047744203,1.28298351600692
+-2.6861602881266,0.205018905852968,6.23609678997191,1.33226132626232
+-3.11696629519342,0.207164092379631,8.06821700769127,1.39601660142799
+-3.380586761282,0.208457026967588,4.81782679243833,1.43408720272189
+-3.47550271526036,0.208917537856196,1.74135519064351,1.4478474398641
+-3.43518793631723,0.20872442486799,-0.734649496547034,1.44204221911235
+-3.29063290033468,0.20803953649519,-2.65151294543016,1.42108989008103
+-3.11250951812149,0.20720138448128,-3.37731816971661,1.39440222798332
+-2.93711028388834,0.206380820700801,-3.45884008330213,1.36707037745739
+-2.73891519437972,0.205460331097488,-4.02680824821257,1.33525042669758
+-2.54407370559115,0.204559368575348,-4.14575468573189,1.30249055788068
+-2.39053644758188,0.203850927117784,-3.45709420414838,1.27517250334038
+-2.15879795354626,0.202792578446064,-5.27251552247232,1.23350893905721
+-1.88564352691945,0.201557490834808,-6.29472546535112,1.18376784337854
+-1.50072985975385,0.199840034833206,-8.83289566450933,1.11397005620157
+-1.01998602735263,0.197721642567706,-11.0512754269927,1.02664257045003
+-0.530741824639045,0.195584625082435,-11.5172616043355,0.935632848966399
+0.126087783919003,0.192750446012934,-15.5482513416115,0.812770134480102
+0.946410908708773,0.189270304626056,-19.0555049685026,0.662193004300093
+1.86205593933885,0.185436633202073,-21.3782931153237,0.493261137588954
+2.94062744320269,0.180998996286405,-24.7634112972474,0.297579972866728
+3.93046978361501,0.176952871875341,-23.8543722535639,0.109082059947364
+4.7236004422528,0.173709372038306,-20.7823421335204,-0.0551405855326269
+5.4675996884093,0.170687823308056,-20.2975626887207,-0.215532490358468
+6.07736475879185,0.168211429838999,-17.8991553796871,-0.356972113930467
+6.65108884942456,0.165897192554224,-17.4682758358605,-0.495006915364721
+7.19807710004944,0.163707606768037,-17.1913300740697,-0.630853283687656
+7.64857293059005,0.161906308033814,-15.0423509578624,-0.749718359272868
+8.02400343300067,0.160407406629327,-13.2511610676823,-0.854429402534267
+8.27697105170115,0.159393021979789,-9.64098997283795,-0.930612773408131
+8.46390802433729,0.158642656136866,-7.55191041918006,-0.990288179553298
+8.64850604379691,0.157906542316743,-7.6524659626792,-1.05075817839957
+8.8032807335877,0.157293428596627,-6.57881654114708,-1.10274416965953
+8.95446233254623,0.15669939876546,-6.54733003621288,-1.15448135368188
+9.07158780840541,0.15624055040851,-5.26781371598019,-1.1961077641594
+9.1515744845053,0.155929537510679,-3.66905719708923,-1.22510075616445
+9.22290256262306,0.155653781395773,-3.35718417818991,-1.25162931890121
+9.33420751522502,0.155228728073273,-5.2451661212583,-1.29307676745542
+9.47688301586101,0.154691209393847,-6.70164388358391,-1.34603334379105
+9.64454275757949,0.154066975097651,-7.90326731732296,-1.40848518191631
+9.89285589272581,0.153162353373709,-11.3158347636997,-1.4979032229037
+10.22362756085,0.151981737573662,-14.650501628764,-1.61367189419335
+10.5746835692363,0.150742722870912,-15.6856020630384,-1.73761995789998
+11.0290368525466,0.149174121064393,-19.6221318472333,-1.89267458943318
+11.521525763197,0.147496618191182,-21.3110872218343,-2.06107539330502
+12.0097551690352,0.145847744373501,-21.6105380566535,-2.2318424660011
+12.5809716926057,0.143952306536491,-24.9283948322494,-2.42882733520498
+13.1820104607476,0.14198353337863,-26.3928121759554,-2.63738407098313
+13.7756629255838,0.140055020571329,-26.7444571357352,-2.84871951501245
+14.3220873542119,0.13828754257827,-25.6466882741224,-3.05138035896919
+14.8207276171901,0.13668165953396,-24.3380973264228,-3.24370068086631
+15.2481159052797,0.135304978578286,-22.0327586677785,-3.41780415257313
+15.6181829346188,0.134113727245657,-20.031239855858,-3.57609156696746
+15.9254888584187,0.133123389933797,-17.5347244815675,-3.71465144744797
+16.182155368207,0.132294410691331,-15.4547144169649,-3.83677503055452
+16.3834151607406,0.131641182247482,-12.8669102455973,-3.93844971313409
+16.5204878574538,0.131192230298616,-9.39682632316524,-4.01270369605823
+16.63080123604,0.130831250954667,-7.83625560015622,-4.0746260057309
+16.6978177388739,0.130609927017838,-5.07710225196627,-4.11474540891623
+16.701984857604,0.130595862027892,-0.347660671770127,-4.11749263321274
+16.6854899363512,0.130651769492848,1.43643763310767,-4.10614186308969
+16.6411402735019,0.1308047247492,4.1675554165708,-4.07320972429145
+16.5970272924035,0.130956482780881,4.23560772792058,-4.03973983423644
+16.5434552195032,0.131140920196373,5.29688512305167,-3.99788370069179
+16.4980678409499,0.131295685501156,4.49788774495942,-3.96234126664833
+16.4550533870677,0.131442089660633,4.35700314664811,-3.92791210661862
+16.4034997284221,0.131617772322878,5.37560925845634,-3.88543389276675
+16.3600988160341,0.131763871175012,4.50159941501498,-3.84986212900329
+16.3247393980859,0.131881384461121,3.64239912793104,-3.82107978980207
+16.2845302752546,0.132014427026232,4.20163109569142,-3.78787838403979
+16.2354404393816,0.132177427292469,5.30582134897501,-3.74595163618939
+16.1798773405667,0.132361174237754,6.09144757743956,-3.697816848034
+16.1369672582067,0.132500057996333,4.56732062189424,-3.66172575346613
+16.0729892527298,0.132709048388341,7.13935329958349,-3.60531038515291
+15.9868687893218,0.132993853448599,10.1342793363433,-3.52522902801063
+15.8911356599786,0.133312141181979,11.6651084758568,-3.43305101643673
+15.785185002403,0.133664851955115,13.2307698405521,-3.32850110521918
+15.6905768998578,0.133973040333207,11.4667001799168,-3.23789092151739
+15.6133391245925,0.134217442405614,8.91912747186154,-3.16741172820068
+15.5640712059174,0.13436901282478,5.42501709786526,-3.1245430922278
+15.5443250145019,0.134428290724675,2.0902928832644,-3.10802553973481
+15.5052812966114,0.134546068190848,4.28655544877293,-3.07415305937281
+15.4770175815522,0.134630205681346,3.07016536757968,-3.04989252725926
+15.4444079562019,0.134726861461024,3.58608824982803,-3.02155515818277
+15.4270366339921,0.134777267890843,1.85031118379933,-3.00693394775266
+15.4316347413337,0.134764175609174,-0.477068569778691,-3.01070375685796
+15.485223969148,0.134616348041038,-5.22556993240566,-3.0519963557829
+15.5377720379079,0.134472416205775,-5.15548845343067,-3.09273516891207
+15.5864363653653,0.134339998564693,-4.8096502231334,-3.13074115872793
+15.6597153950738,0.134144325745554,-7.04643728132906,-3.18642230208099
+15.7245766055152,0.133972149207703,-6.29666671180733,-3.23617873754313
+15.7994245842376,0.133776242802017,-7.16413965150234,-3.29278996829851
+15.868636133975,0.133595774179426,-6.72874484385436,-3.34596069717585
+15.9023375683116,0.133507356204151,-3.42152965903116,-3.37299771969162
+15.9376135653167,0.133415388659391,-3.60800397413116,-3.40150826743101
+15.9393738216715,0.133410826950409,-0.181502904584986,-3.4029425084305
+15.946239217832,0.133393106351883,-0.717507717778203,-3.40861227436972
+15.9406611757132,0.133407505145555,0.597384933183044,-3.40389172201489
+15.90456013034,0.133501495817669,4.05122277456845,-3.37187884698887
+15.8771653544801,0.133572808058213,3.14316508126937,-3.34704146910768
+15.8412573150016,0.133665967099135,4.17494417410317,-3.31405094414194
+15.7935359722084,0.133789167478241,5.59834503955845,-3.26981266595369
+15.7268054682595,0.133962833301512,8.16726022036199,-3.20527474856718
+15.6569651811967,0.13414318189471,8.55473897390291,-3.1376749632947
+15.5618045186239,0.134389293082345,11.9466775679243,-3.04327198492507
+15.4642740054136,0.134640099683783,12.314988670809,-2.94595860197799
+15.3556432691781,0.134913622023805,13.2582887882595,-2.84119123527038
+15.3324002785773,0.134969408327903,2.54655478870887,-2.82106828851228
+15.4991813437282,0.134592165940367,-15.7222457854785,-2.94530591193266
+15.6789718790788,0.134188841013154,-16.9736944902724,-3.07943251782015
+15.9359054652393,0.133628746925532,-22.7675615343864,-3.25934242221321
+16.1941692821757,0.133070741096307,-22.8916803003747,-3.44023311654675
diff --git a/statsmodels/regression/tests/results/results_rls_stata.csv b/statsmodels/regression/tests/results/results_rls_stata.csv
new file mode 100644
--- /dev/null
+++ b/statsmodels/regression/tests/results/results_rls_stata.csv
@@ -0,0 +1,204 @@
+rr,cusum,cusum2,uw,lw,uww,lww
+,,,,,,
+,,,,,,
+,,,,,,
+.20304158,.0226941,2.44e-06,13.54081,-13.54081,.12823,-.12823
+.2702816,.0529036,6.77e-06,13.67488,-13.67488,.1332551,-.1232049
+.24184042,.0799343,.0000102,13.80895,-13.80895,.1382803,-.1181797
+.41520333,.1263418,.0000205,13.94301,-13.94301,.1433054,-.1131546
+.39778414,.1708024,.0000298,14.07708,-14.07708,.1483305,-.1081295
+.17604447,.190479,.0000317,14.21115,-14.21115,.1533556,-.1031044
+.0938885,.200973,.0000322,14.34522,-14.34522,.1583808,-.0980792
+-.04561871,.1958742,.0000323,14.47928,-14.47928,.1634059,-.0930541
+-.10027123,.1846668,.0000329,14.61335,-14.61335,.168431,-.088029
+-.05035009,.1790391,.0000331,14.74742,-14.74742,.1734561,-.0830039
+-.03407203,.1752309,.0000331,14.88149,-14.88149,.1784813,-.0779787
+.09168515,.1854786,.0000336,15.01555,-15.01555,.1835064,-.0729536
+-.0628044,.1784589,.0000339,15.14962,-15.14962,.1885315,-.0679285
+-.16372342,.1601594,.0000354,15.28369,-15.28369,.1935566,-.0629034
+-.10374629,.1485636,.0000361,15.41776,-15.41776,.1985818,-.0578782
+-.15377103,.1313765,.0000375,15.55182,-15.55182,.2036069,-.0528531
+-.0567542,.1250331,.0000377,15.68589,-15.68589,.208632,-.047828
+-.14226659,.1091319,.0000389,15.81996,-15.81996,.2136571,-.0428029
+-.23751768,.0825844,.0000422,15.95403,-15.95403,.2186823,-.0377777
+-.3088606,.0480628,.0000479,16.08809,-16.08809,.2237074,-.0327526
+-.2232256,.0231128,.0000508,16.22216,-16.22216,.2287325,-.0277275
+-.19442033,.0013823,.0000531,16.35623,-16.35623,.2337576,-.0227024
+-.06673149,-.0060764,.0000533,16.4903,-16.4903,.2387828,-.0176772
+-.22843692,-.0316089,.0000564,16.62436,-16.62436,.2438079,-.0126521
+-.22214566,-.0564383,.0000593,16.75843,-16.75843,.248833,-.007627
+-.0285363,-.0596278,.0000594,16.8925,-16.8925,.2538581,-.0026019
+.25234739,-.0314227,.0000632,17.02657,-17.02657,.2588833,.0024233
+.51036376,.0256209,.0000786,17.16063,-17.16063,.2639084,.0074484
+.42630054,.0732688,.0000894,17.2947,-17.2947,.2689335,.0124735
+.35366206,.1127979,.0000968,17.42877,-17.42877,.2739587,.0174986
+.23419873,.1389744,.0001,17.56284,-17.56284,.2789838,.0225238
+.16073747,.1569401,.0001015,17.6969,-17.6969,.2840089,.0275489
+.25940672,.1859342,.0001055,17.83097,-17.83097,.289034,.032574
+.28529326,.2178216,.0001104,17.96504,-17.96504,.2940592,.0375992
+.34027138,.255854,.0001172,18.09911,-18.09911,.2990843,.0426243
+.32652083,.2923494,.0001235,18.23317,-18.23317,.3041094,.0476494
+.1962729,.3142869,.0001258,18.36724,-18.36724,.3091345,.0526745
+.52732584,.3732265,.0001423,18.50131,-18.50131,.3141596,.0576996
+.81936588,.4648075,.0001821,18.63537,-18.63537,.3191848,.0627248
+1.0602511,.5833125,.0002487,18.76944,-18.76944,.3242099,.0677499
+1.1484205,.7116722,.0003268,18.90351,-18.90351,.329235,.072775
+1.5256982,.8822004,.0004647,19.03758,-19.03758,.3342601,.0778001
+1.5951505,1.060491,.0006155,19.17164,-19.17164,.3392853,.0828253
+1.3382179,1.210065,.0007216,19.30571,-19.30571,.3443104,.0878504
+1.3616943,1.362262,.0008314,19.43978,-19.43978,.3493355,.0928755
+.86773648,1.45925,.0008761,19.57385,-19.57385,.3543606,.0979007
+.65339285,1.53228,.0009014,19.70791,-19.70791,.3593858,.1029258
+.58474164,1.597637,.0009216,19.84198,-19.84198,.3644109,.1079509
+.45064816,1.648006,.0009336,19.97605,-19.97605,.369436,.112976
+.00958712,1.649078,.0009337,20.11012,-20.11012,.3744611,.1180012
+-.10173146,1.637707,.0009343,20.24418,-20.24418,.3794863,.1230263
+-.47409877,1.584717,.0009476,20.37825,-20.37825,.3845114,.1280514
+-.7423211,1.501747,.0009802,20.51232,-20.51232,.3895365,.1330765
+.13907311,1.517291,.0009814,20.64639,-20.64639,.3945617,.1381017
+.01375003,1.518828,.0009814,20.78045,-20.78045,.3995868,.1431268
+1.1596226,1.64844,.0010611,20.91452,-20.91452,.4046119,.1481519
+1.5958938,1.826814,.001212,21.04859,-21.04859,.409637,.153177
+2.2563117,2.079004,.0015136,21.18266,-21.18266,.4146622,.1582021
+2.9279967,2.406268,.0020215,21.31672,-21.31672,.4196873,.1632273
+3.9063339,2.842881,.0029256,21.45079,-21.45079,.4247124,.1682524
+4.6111115,3.358268,.0041854,21.58486,-21.58486,.4297375,.1732775
+4.5984313,3.872237,.0054382,21.71893,-21.71893,.4347627,.1783026
+4.1330187,4.334188,.0064503,21.85299,-21.85299,.4397878,.1833278
+4.4212893,4.828358,.0076085,21.98706,-21.98706,.4448129,.1883529
+4.5048686,5.33187,.0088109,22.12113,-22.12113,.449838,.193378
+3.5032266,5.723428,.009538,22.2552,-22.2552,.4548632,.1984031
+3.7209096,6.139317,.0103583,22.38926,-22.38926,.4598883,.2034283
+3.5668012,6.53798,.0111121,22.52333,-22.52333,.4649134,.2084534
+3.0814165,6.882392,.0116746,22.6574,-22.6574,.4699385,.2134785
+2.8750272,7.203735,.0121644,22.79147,-22.79147,.4749637,.2185037
+2.7496164,7.511062,.0126123,22.92553,-22.92553,.4799888,.2235288
+2.3070702,7.768925,.0129277,23.0596,-23.0596,.4850139,.2285539
+1.8847579,7.979585,.0131381,23.19367,-23.19367,.4900391,.233579
+1.9908349,8.202102,.013373,23.32774,-23.32774,.4950642,.2386042
+2.105656,8.437452,.0136356,23.4618,-23.4618,.5000893,.2436293
+2.5343597,8.720719,.0140162,23.59587,-23.59587,.5051144,.2486544
+2.8006793,9.033753,.0144809,23.72994,-23.72994,.5101395,.2536795
+3.0235025,9.371693,.0150226,23.86401,-23.86401,.5151647,.2587047
+3.53325,9.766605,.0157622,23.99807,-23.99807,.5201898,.2637298
+4.7568726,10.29828,.0171029,24.13214,-24.13214,.5252149,.2687549
+6.227502,10.99434,.0194006,24.26621,-24.26621,.5302401,.27378
+8.9338671,11.99288,.0241295,24.40027,-24.40027,.5352651,.2788052
+8.1339298,12.90201,.0280494,24.53434,-24.53434,.5402903,.2838303
+6.8496713,13.66761,.0308292,24.66841,-24.66841,.5453154,.2888554
+8.3364544,14.59938,.0349468,24.80248,-24.80248,.5503405,.2938806
+6.5566113,15.33221,.0374938,24.93654,-24.93654,.5553657,.2989057
+8.4859752,16.2807,.0417604,25.07061,-25.07061,.5603908,.3039308
+9.4542677,17.33741,.0470562,25.20468,-25.20468,.5654159,.3089559
+7.0004094,18.11985,.0499597,25.33875,-25.33875,.5704411,.3139811
+6.2360956,18.81686,.0522638,25.47281,-25.47281,.5754662,.3190062
+8.0682169,19.71865,.0561206,25.60688,-25.60688,.5804913,.3240313
+4.8178254,20.25714,.0574959,25.74095,-25.74095,.5855164,.3290564
+1.7413544,20.45177,.0576755,25.87502,-25.87502,.5905415,.3340816
+-.7346488,20.36966,.0577075,26.00908,-26.00908,.5955667,.3391067
+-2.6515074,20.0733,.0581241,26.14315,-26.14315,.6005918,.3441318
+-3.3773178,19.69582,.0587999,26.27722,-26.27722,.6056169,.3491569
+-3.4588369,19.30922,.0595087,26.41129,-26.41129,.6106421,.3541821
+-4.0268058,18.85914,.0604694,26.54535,-26.54535,.6156672,.3592072
+-4.1457617,18.39577,.0614877,26.67942,-26.67942,.6206923,.3642323
+-3.4570982,18.00936,.0621959,26.81349,-26.81349,.6257174,.3692574
+-5.2725184,17.42005,.0638429,26.94756,-26.94756,.6307425,.3742826
+-6.2947299,16.71649,.0661906,27.08162,-27.08162,.6357676,.3793077
+-8.8328932,15.72923,.0708131,27.21569,-27.21569,.6407928,.3843328
+-11.051272,14.49402,.0780492,27.34976,-27.34976,.6458179,.389358
+-11.517264,13.20673,.0859083,27.48383,-27.48383,.650843,.394383
+-15.548254,11.46889,.1002315,27.61789,-27.61789,.6558682,.3994082
+-19.055501,9.339044,.1217453,27.75196,-27.75196,.6608933,.4044333
+-21.3783,6.949577,.1488237,27.88603,-27.88603,.6659184,.4094584
+-24.763411,4.181755,.1851564,28.0201,-28.0201,.6709436,.4144836
+-23.854377,1.515535,.2188706,28.15416,-28.15416,.6759686,.4195087
+-20.782338,-.8073198,.2444603,28.28823,-28.28823,.6809938,.4245338
+-20.297563,-3.075991,.2688701,28.4223,-28.4223,.6860189,.429559
+-17.899156,-5.076591,.2878521,28.55637,-28.55637,.691044,.4345841
+-17.468273,-7.029032,.3059312,28.69043,-28.69043,.6960692,.4396092
+-17.191332,-8.950518,.3234415,28.8245,-28.8245,.7010943,.4446343
+-15.042346,-10.63181,.3368478,28.95857,-28.95857,.7061194,.4496594
+-13.251165,-12.1129,.3472514,29.09264,-29.09264,.7111446,.4546846
+-9.6409921,-13.19048,.3527585,29.2267,-29.2267,.7161697,.4597097
+-7.551907,-14.03456,.3561375,29.36077,-29.36077,.7211948,.4647348
+-7.6524592,-14.88988,.3596071,29.49484,-29.49484,.72622,.46976
+-6.5788193,-15.6252,.3621714,29.62891,-29.62891,.731245,.4747851
+-6.5473367,-16.357,.3647113,29.76297,-29.76297,.7362702,.4798102
+-5.2678084,-16.94579,.3663554,29.89704,-29.89704,.7412953,.4848353
+-3.66906,-17.35588,.367153,30.03111,-30.03111,.7463204,.4898604
+-3.3571877,-17.73112,.3678208,30.16517,-30.16517,.7513456,.4948856
+-5.2451606,-18.31737,.3694508,30.29924,-30.29924,.7563707,.4999107
+-6.7016457,-19.06642,.3721118,30.43331,-30.43331,.7613958,.5049359
+-7.9032685,-19.94977,.3758125,30.56738,-30.56738,.7664209,.5099609
+-11.315834,-21.21455,.3833991,30.70144,-30.70144,.771446,.5149861
+-14.650504,-22.85205,.3961161,30.83551,-30.83551,.7764712,.5200112
+-15.685601,-24.60524,.4106934,30.96958,-30.96958,.7814963,.5250363
+-19.622131,-26.79841,.4335057,31.10365,-31.10365,.7865214,.5300615
+-21.311085,-29.18037,.4604141,31.23771,-31.23771,.7915466,.5350866
+-21.610529,-31.59579,.488084,31.37178,-31.37178,.7965717,.5401117
+-24.928394,-34.38205,.5249025,31.50585,-31.50585,.8015968,.5451369
+-26.392804,-37.33199,.5661737,31.63992,-31.63992,.8066219,.550162
+-26.74445,-40.32124,.608552,31.77398,-31.77398,.8116471,.5551871
+-25.646688,-43.18779,.6475227,31.90805,-31.90805,.8166722,.5602123
+-24.338104,-45.90807,.6826182,32.04212,-32.04212,.8216973,.5652373
+-22.032761,-48.37069,.7113799,32.17619,-32.17619,.8267224,.5702625
+-20.031242,-50.60959,.7351533,32.31025,-32.31025,.8317476,.5752876
+-17.534726,-52.56946,.7533702,32.44432,-32.44432,.8367727,.5803127
+-15.45471,-54.29684,.7675216,32.57839,-32.57839,.8417978,.5853379
+-12.866916,-55.73499,.7773306,32.71246,-32.71246,.8468229,.590363
+-9.3968278,-56.78527,.7825623,32.84652,-32.84652,.8518481,.5953881
+-7.8362546,-57.66114,.7862005,32.98059,-32.98059,.8568732,.6004133
+-5.0771041,-58.22861,.7877278,33.11466,-33.11466,.8618983,.6054384
+-.34765929,-58.26747,.7877349,33.24873,-33.24873,.8669235,.6104635
+1.4364328,-58.10691,.7878572,33.38279,-33.38279,.8719485,.6154886
+4.1675445,-57.64111,.7888862,33.51686,-33.51686,.8769737,.6205137
+4.2356097,-57.16769,.7899492,33.65093,-33.65093,.8819988,.6255389
+5.2968869,-56.57565,.7916115,33.785,-33.785,.8870239,.630564
+4.4978863,-56.07292,.7928101,33.91906,-33.91906,.8920491,.6355891
+4.3570048,-55.58594,.7939349,34.05313,-34.05313,.8970742,.6406143
+5.3756079,-54.9851,.795647,34.1872,-34.1872,.9020993,.6456394
+4.5015951,-54.48196,.7968476,34.32127,-34.32127,.9071245,.6506645
+3.6423914,-54.07484,.7976336,34.45533,-34.45533,.9121495,.6556896
+4.2016237,-53.60522,.7986796,34.5894,-34.5894,.9171747,.6607147
+5.3058269,-53.01219,.8003476,34.72347,-34.72347,.9221998,.6657399
+6.0914494,-52.33134,.802546,34.85754,-34.85754,.9272249,.670765
+4.5673224,-51.82085,.803782,34.9916,-34.9916,.9322501,.6757901
+7.1393493,-51.02288,.8068019,35.12567,-35.12567,.9372752,.6808153
+10.134278,-49.89017,.812887,35.25974,-35.25974,.9423003,.6858404
+11.665111,-48.58635,.8209491,35.39381,-35.39381,.9473255,.6908655
+13.230785,-47.10754,.8313208,35.52787,-35.52787,.9523506,.6958906
+11.466692,-45.8259,.839111,35.66194,-35.66194,.9573757,.7009158
+8.9191298,-44.829,.8438243,35.79601,-35.79601,.9624009,.7059409
+5.4250249,-44.22264,.845568,35.93008,-35.93008,.9674259,.710966
+2.0902884,-43.98901,.8458269,36.06414,-36.06414,.9724511,.7159911
+4.2865572,-43.5099,.8469156,36.19821,-36.19821,.9774762,.7210162
+3.0701673,-43.16674,.847474,36.33228,-36.33228,.9825013,.7260414
+3.5860871,-42.76592,.848236,36.46635,-36.46635,.9875265,.7310665
+1.8503127,-42.55911,.8484389,36.60041,-36.60041,.9925516,.7360916
+-.47707287,-42.61243,.8484523,36.73448,-36.73448,.9975767,.7411168
+-5.2255741,-43.1965,.8500702,36.86855,-36.86855,1.002602,.7461419
+-5.1554985,-43.77274,.851645,37.00262,-37.00262,1.007627,.751167
+-4.8096412,-44.31031,.8530155,37.13668,-37.13668,1.012652,.7561921
+-7.0464378,-45.0979,.8559573,37.27075,-37.27075,1.017677,.7612172
+-6.2966584,-45.80168,.8583064,37.40482,-37.40482,1.022702,.7662424
+-7.1641344,-46.60242,.8613473,37.53888,-37.53888,1.027727,.7712675
+-6.7287364,-47.35449,.8640299,37.67295,-37.67295,1.032753,.7762926
+-3.4215312,-47.73692,.8647235,37.80702,-37.80702,1.037778,.7813178
+-3.6080018,-48.14019,.8654947,37.94109,-37.94109,1.042803,.7863429
+-.18150405,-48.16048,.8654967,38.07515,-38.07515,1.047828,.791368
+-.71750861,-48.24067,.8655272,38.20922,-38.20922,1.052853,.7963932
+.59738383,-48.1739,.8655483,38.34329,-38.34329,1.057878,.8014182
+4.0512283,-47.72109,.8665208,38.47736,-38.47736,1.062903,.8064434
+3.1431576,-47.36978,.8671061,38.61142,-38.61142,1.067929,.8114685
+4.1749543,-46.90314,.8681388,38.74549,-38.74549,1.072954,.8164936
+5.5983515,-46.27741,.8699957,38.87956,-38.87956,1.077979,.8215188
+8.1672592,-45.36455,.8739479,39.01363,-39.01363,1.083004,.8265439
+8.5547431,-44.40838,.8782839,39.14769,-39.14769,1.088029,.831569
+11.946671,-43.0731,.88674,39.28176,-39.28176,1.093054,.8365942
+12.314985,-41.69664,.8957255,39.41583,-39.41583,1.098079,.8416193
+13.258285,-40.21476,.9061403,39.5499,-39.5499,1.103104,.8466444
+2.5465709,-39.93012,.9065245,39.68396,-39.68396,1.10813,.8516695
+-15.722248,-41.68741,.9211701,39.81803,-39.81803,1.113155,.8566946
+-16.973694,-43.58457,.9382399,39.9521,-39.9521,1.11818,.8617198
+-22.767561,-46.12931,.9689521,40.08617,-40.08617,1.123205,.8667449
+-22.891686,-48.68793,1,40.22023,-40.22023,1.12823,.87177
diff --git a/statsmodels/regression/tests/results/test_rls.R b/statsmodels/regression/tests/results/test_rls.R
new file mode 100644
--- /dev/null
+++ b/statsmodels/regression/tests/results/test_rls.R
@@ -0,0 +1,15 @@
+library(quantreg)
+library(strucchange)
+
+macrodata <- read.csv('/Users/fulton/projects/statsmodels/statsmodels/datasets/macrodata/macrodata.csv')
+
+y <- macrodata$cpi
+X <- cbind(rep(1, nrow(macrodata)), macrodata$m1)
+rec_beta <- lm.fit.recursive(X, y, int=FALSE)
+rec_resid <- recresid(cpi ~ 1 + m1, data=macrodata)
+cusum <- efp(cpi ~ 1 + m1, data=macrodata, type=c("Rec-CUSUM"))
+
+output <- data.frame(cbind(t(rec_beta[,3:203]), rec_resid, cusum$process[2:202]))
+names(output) <- c('beta1', 'beta2', 'rec_resid', 'cusum')
+write.csv(output, '/Users/fulton/projects/statsmodels/statsmodels/regression/tests/results/results_rls_R.csv', row.names=FALSE)
+print(cusum$sigma)  # 9.163565
diff --git a/statsmodels/regression/tests/results/test_rls.do b/statsmodels/regression/tests/results/test_rls.do
new file mode 100644
--- /dev/null
+++ b/statsmodels/regression/tests/results/test_rls.do
@@ -0,0 +1,9 @@
+insheet using /Users/fulton/projects/statsmodels/statsmodels/datasets/macrodata/macrodata.csv, clear
+
+gen tq = yq(year, quarter)
+format tq %tq
+tsset tq
+
+cusum6 cpi m1, rr(rr) cs(cusum) lw(lw) uw(uw) cs2(cusum2) lww(lww) uww(uww) noplot
+
+outsheet rr-lww using results_rls_stata.csv, comma replace
diff --git a/statsmodels/regression/tests/test_recursive_ls.py b/statsmodels/regression/tests/test_recursive_ls.py
new file mode 100644
--- /dev/null
+++ b/statsmodels/regression/tests/test_recursive_ls.py
@@ -0,0 +1,259 @@
+"""
+Tests for recursive least squares models
+
+Author: Chad Fulton
+License: Simplified-BSD
+"""
+from __future__ import division, absolute_import, print_function
+
+import numpy as np
+import pandas as pd
+import os
+
+import warnings
+from statsmodels.datasets import macrodata
+from statsmodels.regression.linear_model import OLS
+from statsmodels.regression.recursive_ls import RecursiveLS
+from statsmodels.stats.diagnostic import recursive_olsresiduals
+from statsmodels.tools import add_constant
+from numpy.testing import assert_equal, assert_almost_equal, assert_raises, assert_allclose
+from nose.exc import SkipTest
+
+try:
+    import matplotlib.pyplot as plt
+    have_matplotlib = True
+except ImportError:
+    have_matplotlib = False
+
+
+current_path = os.path.dirname(os.path.abspath(__file__))
+
+results_R_path = 'results' + os.sep + 'results_rls_R.csv'
+results_R = pd.read_csv(current_path + os.sep + results_R_path)
+
+results_stata_path = 'results' + os.sep + 'results_rls_stata.csv'
+results_stata = pd.read_csv(current_path + os.sep + results_stata_path)
+
+dta = macrodata.load_pandas().data
+dta.index = pd.date_range(start='1959-01-01', end='2009-07-01', freq='QS')
+
+endog = dta['cpi']
+exog = add_constant(dta['m1'])
+
+
+def test_endog():
+    # Tests for numpy input
+    mod = RecursiveLS(endog.values, exog.values)
+    res = mod.fit()
+
+    # Test the RLS estimates against OLS estimates
+    mod_ols = OLS(endog, exog)
+    res_ols = mod_ols.fit()
+    assert_allclose(res.params, res_ols.params)
+
+    # Tests for 1-dim exog
+    mod = RecursiveLS(endog, dta['m1'].values)
+    res = mod.fit()
+
+    # Test the RLS estimates against OLS estimates
+    mod_ols = OLS(endog, dta['m1'])
+    res_ols = mod_ols.fit()
+    assert_allclose(res.params, res_ols.params)
+
+
+def test_filter():
+    # Basic test for filtering
+    mod = RecursiveLS(endog, exog)
+    res = mod.filter()
+
+    # Test the RLS estimates against OLS estimates
+    mod_ols = OLS(endog, exog)
+    res_ols = mod_ols.fit()
+    assert_allclose(res.params, res_ols.params, atol=1e-5, rtol=1e-4)
+
+
+def test_estimates():
+    mod = RecursiveLS(endog, exog)
+    res = mod.fit()
+
+    # Test for start_params
+    assert_equal(mod.start_params, 0)
+
+
+    # Test the RLS coefficient estimates against those from R (quantreg)
+    # Due to initialization issues, we get more agreement as we get
+    # farther from the initial values.
+    assert_allclose(res.recursive_coefficients.filtered[:, 2:10].T,
+                    results_R.ix[:7, ['beta1', 'beta2']], atol=1e-2, rtol=1e-3)
+    assert_allclose(res.recursive_coefficients.filtered[:, 9:20].T,
+                    results_R.ix[7:17, ['beta1', 'beta2']], atol=1e-3, rtol=1e-4)
+    assert_allclose(res.recursive_coefficients.filtered[:, 19:].T,
+                    results_R.ix[17:, ['beta1', 'beta2']], atol=1e-4, rtol=1e-4)
+
+    # Test the RLS estimates against OLS estimates
+    mod_ols = OLS(endog, exog)
+    res_ols = mod_ols.fit()
+    assert_allclose(res.params, res_ols.params)
+
+
+def test_plots():
+    if not have_matplotlib:
+        raise SkipTest
+
+    exog = add_constant(dta[['m1', 'pop']])
+    mod = RecursiveLS(endog, exog)
+    res = mod.fit()
+
+    # Basic plot
+    fig = res.plot_recursive_coefficient()
+    plt.close(fig)
+
+    # Specific variable
+    fig = res.plot_recursive_coefficient(variables=['m1'])
+    plt.close(fig)
+
+    # All variables
+    fig = res.plot_recursive_coefficient(variables=[0, 'm1', 'pop'])
+    plt.close(fig)
+
+    # Basic plot
+    fig = res.plot_cusum()
+    plt.close(fig)
+
+    # Other alphas
+    for alpha in [0.01, 0.10]:
+        fig = res.plot_cusum(alpha=alpha)
+        plt.close(fig)
+
+    # Invalid alpha
+    assert_raises(ValueError, res.plot_cusum, alpha=0.123)
+
+    # Basic plot
+    fig = res.plot_cusum_squares()
+    plt.close(fig)
+
+    # Numpy input (no dates)
+    mod = RecursiveLS(endog.values, exog.values)
+    res = mod.fit()
+
+    # Basic plot
+    fig = res.plot_recursive_coefficient()
+    plt.close(fig)
+
+    # Basic plot
+    fig = res.plot_cusum()
+    plt.close(fig)
+
+    # Basic plot
+    fig = res.plot_cusum_squares()
+    plt.close(fig)
+
+
+def test_from_formula():
+    mod = RecursiveLS.from_formula('cpi ~ m1', data=dta)
+    res = mod.fit()
+
+    # Test the RLS estimates against OLS estimates
+    mod_ols = OLS.from_formula('cpi ~ m1', data=dta)
+    res_ols = mod_ols.fit()
+    assert_allclose(res.params, res_ols.params)
+
+
+def test_resid_recursive():
+    mod = RecursiveLS(endog, exog)
+    res = mod.fit()
+
+    # Test the recursive residuals against those from R (strucchange)
+    # Due to initialization issues, we get more agreement as we get
+    # farther from the initial values.
+    assert_allclose(res.resid_recursive[2:10].T,
+                    results_R.ix[:7, 'rec_resid'], atol=1e-2, rtol=1e-3)
+    assert_allclose(res.resid_recursive[9:20].T,
+                    results_R.ix[7:17, 'rec_resid'], atol=1e-3, rtol=1e-4)
+    assert_allclose(res.resid_recursive[19:].T,
+                    results_R.ix[17:, 'rec_resid'], atol=1e-4, rtol=1e-4)
+
+    # Test the RLS estimates against those from Stata (cusum6)
+    assert_allclose(res.resid_recursive[3:],
+                    results_stata.ix[3:, 'rr'], atol=1e-3)
+
+    # Test the RLS estimates against statsmodels estimates
+    mod_ols = OLS(endog, exog)
+    res_ols = mod_ols.fit()
+    desired_resid_recursive = recursive_olsresiduals(res_ols)[4][2:]
+    assert_allclose(res.resid_recursive[2:], desired_resid_recursive,
+                    atol=1e-4, rtol=1e-4)
+
+
+def test_cusum():
+    mod = RecursiveLS(endog, exog)
+    res = mod.fit()
+
+    # Test the cusum statistics against those from R (strucchange)
+    # These values are not even close to ours, to Statas, or to the alternate
+    # statsmodels values
+    # assert_allclose(res.cusum, results_R['cusum'])
+
+    # Test the cusum statistics against Stata (cusum6)
+    # Note: cusum6 excludes the first 3 elements due to OLS initialization
+    # whereas we exclude only the first 2. Also there are initialization
+    # differences (as seen above in the recursive residuals).
+    # Here we explicitly reverse engineer our cusum to match their to show the
+    # equivalence
+    llb = res.loglikelihood_burn
+    cusum = res.cusum * np.std(res.resid_recursive[llb:], ddof=1)
+    cusum -= res.resid_recursive[llb]
+    cusum /= np.std(res.resid_recursive[llb+1:], ddof=1)
+    cusum = cusum[1:]
+    assert_allclose(cusum, results_stata.ix[3:, 'cusum'], atol=1e-3, rtol=1e-3)
+
+    # Test the cusum statistics against statsmodels estimates
+    mod_ols = OLS(endog, exog)
+    res_ols = mod_ols.fit()
+    desired_cusum = recursive_olsresiduals(res_ols)[-2][1:]
+    assert_allclose(res.cusum, desired_cusum, atol=1e-4, rtol=1e-4)
+
+    # Test the cusum bounds against Stata (cusum6)
+    # Again note that cusum6 excludes the first 3 elements, so we need to
+    # change the ddof and points.
+    actual_bounds = res._cusum_significance_bounds(
+        alpha=0.05, ddof=1, points=np.arange(llb+1, res.nobs))
+    desired_bounds = results_stata.ix[3:, ['lw', 'uw']].T
+    assert_allclose(actual_bounds, desired_bounds, atol=1e-4)
+
+    # Test the cusum bounds against statsmodels
+    actual_bounds = res._cusum_significance_bounds(
+        alpha=0.05, ddof=0, points=np.arange(llb, res.nobs))
+    desired_bounds = recursive_olsresiduals(res_ols)[-1]
+    assert_allclose(actual_bounds, desired_bounds)
+
+    # Test for invalid calls
+    assert_raises(ValueError, res._cusum_squares_significance_bounds,
+                  alpha=0.123)
+
+
+def test_stata():
+    # Test the cusum and cusumsq statistics against Stata (cusum6)
+    # Note that here we change the loglikelihood_burn variable to explicitly
+    # excude the first 3 elements as in Stata, so we can compare directly
+    mod = RecursiveLS(endog, exog, loglikelihood_burn=3)
+    res = mod.fit()
+    llb = res.loglikelihood_burn
+
+    assert_allclose(res.resid_recursive[3:], results_stata.ix[3:, 'rr'],
+                    atol=1e-4, rtol=1e-4)
+    assert_allclose(res.cusum, results_stata.ix[3:, 'cusum'], atol=1e-4)
+    assert_allclose(res.cusum_squares, results_stata.ix[3:, 'cusum2'],
+                    atol=1e-4)
+
+    actual_bounds = res._cusum_significance_bounds(
+        alpha=0.05, ddof=0, points=np.arange(llb+1, res.nobs+1))
+    desired_bounds = results_stata.ix[3:, ['lw', 'uw']].T
+    assert_allclose(actual_bounds, desired_bounds, atol=1e-4)
+
+    # Note: Stata uses a set of tabulated critical values whereas we use an
+    # approximation formula, so this test is quite imprecise
+    actual_bounds = res._cusum_squares_significance_bounds(
+        alpha=0.05, points=np.arange(llb+1, res.nobs+1))
+    desired_bounds = results_stata.ix[3:, ['lww', 'uww']].T
+    assert_allclose(actual_bounds, desired_bounds, atol=1e-2)