Repository: statsmodels/statsmodels

Issue:
GLSAR add history and convergence criteria
GLSAR doesn't save the iteration history like RLM, GLM

neither GLSAR nor GLSHet has stopping rules except for number of iterations



Discussion:
Moving milestone.


Resolving patch:
diff --git a/examples/incomplete/glsar.py b/examples/incomplete/glsar.py
--- a/examples/incomplete/glsar.py
+++ b/examples/incomplete/glsar.py
@@ -21,7 +21,7 @@
 if 0 in examples:
     print('\n Example 0')
     X = np.arange(1, 8)
-    X = sm.add_constant(X)
+    X = sm.add_constant(X, prepend=False)
     Y = np.array((1, 3, 4, 5, 8, 10, 9))
     rho = 2
     model = GLSAR(Y, X, 2)
@@ -150,3 +150,7 @@
     model3b = GLSAR(Y, rho=0.1)
     for i in range(6):
         print(i, model3b.iterative_fit(2).params, model3b.rho)
+
+
+print(np.array(res.history['params']))
+print(np.array(res.history['rho']))
diff --git a/statsmodels/regression/linear_model.py b/statsmodels/regression/linear_model.py
--- a/statsmodels/regression/linear_model.py
+++ b/statsmodels/regression/linear_model.py
@@ -38,11 +38,13 @@
 __all__ = ['GLS', 'WLS', 'OLS', 'GLSAR']
 
 import numpy as np
+import pandas as pd
 from scipy.linalg import toeplitz
 from scipy import stats
 from scipy import optimize
 
 from statsmodels.compat.numpy import np_matrix_rank
+from statsmodels.tools.data import _is_using_pandas
 from statsmodels.tools.tools import add_constant, chain_dot, pinv_extended
 from statsmodels.tools.decorators import (resettable_cache,
                                           cache_readonly,
@@ -225,7 +227,8 @@ def fit(self, method="pinv", cov_type='nonrobust', cov_kwds=None,
         else:
             lfit = RegressionResults(self, beta,
                        normalized_cov_params=self.normalized_cov_params,
-                       cov_type=cov_type, cov_kwds=cov_kwds, use_t=use_t)
+                       cov_type=cov_type, cov_kwds=cov_kwds, use_t=use_t,
+                       **kwargs)
         return RegressionResultsWrapper(lfit)
 
     def fit_regularized(self, method="coord_descent", maxiter=1000,
@@ -805,7 +808,7 @@ def __init__(self, endog, exog=None, rho=1, missing='none', **kwargs):
             super(GLSAR, self).__init__(endog, exog, missing=missing,
                                         **kwargs)
 
-    def iterative_fit(self, maxiter=3):
+    def iterative_fit(self, maxiter=3, rtol=1e-4, **kwds):
         """
         Perform an iterative two-stage procedure to estimate a GLS model.
 
@@ -816,21 +819,55 @@ def iterative_fit(self, maxiter=3):
         ----------
         maxiter : integer, optional
             the number of iterations
+        rtol : float, optional
+            Relative tolerance between estimated coefficients to stop the
+            estimation.  Stops if
+
+            max(abs(last - current) / abs(last)) < rtol
+
         """
-        #TODO: update this after going through example.
-        for i in range(maxiter-1):
+        # TODO: update this after going through example.
+        converged = False
+        i = -1  # need to initialize for maxiter < 1 (skip loop)
+        history = {'params': [], 'rho':[self.rho]}
+        for i in range(maxiter - 1):
             if hasattr(self, 'pinv_wexog'):
                 del self.pinv_wexog
             self.initialize()
             results = self.fit()
+            history['params'].append(results.params)
+            if i == 0:
+                last = results.params
+            else:
+                diff = np.max(np.abs(last - results.params) / np.abs(last))
+                if diff < rtol:
+                    converged = True
+                    break
+                last = results.params
             self.rho, _ = yule_walker(results.resid,
                                       order=self.order, df=None)
-        #why not another call to self.initialize
-        if hasattr(self, 'pinv_wexog'):
-            del self.pinv_wexog
-        self.initialize()
-        results = self.fit() #final estimate
-        return results # add missing return
+            history['rho'].append(self.rho)
+
+        # why not another call to self.initialize
+        # Use kwarg to insert history
+        if not converged and maxiter > 0:
+            # maxiter <= 0 just does OLS
+            if hasattr(self, 'pinv_wexog'):
+                del self.pinv_wexog
+            self.initialize()
+
+        # if converged then this is a duplicate fit, because we didn't update rho
+        results = self.fit(history=history, **kwds)
+        results.iter = i + 1
+        # add last fit to history, not if duplicate fit
+        if not converged:
+            results.history['params'].append(results.params)
+            results.iter += 1
+
+        results.converged = converged
+
+        return results
+
 
     def whiten(self, X):
         """
@@ -994,6 +1031,8 @@ class RegressionResults(base.LikelihoodModelResults):
         adjusted squared residuals for heteroscedasticity robust standard
         errors. Is only available after `HC#_se` or `cov_HC#` is called.
         See HC#_se for more information.
+    history
+        Estimation history for iterative estimators
     HC0_se
         White's (1980) heteroskedasticity robust standard errors.
         Defined as sqrt(diag(X.T X)^(-1)X.T diag(e_i^(2)) X(X.T X)^(-1)
@@ -1076,7 +1115,7 @@ class RegressionResults(base.LikelihoodModelResults):
     _cache = {} # needs to be a class attribute for scale setter?
 
     def __init__(self, model, params, normalized_cov_params=None, scale=1.,
-                       cov_type='nonrobust', cov_kwds=None, use_t=None):
+                       cov_type='nonrobust', cov_kwds=None, use_t=None, **kwargs):
         super(RegressionResults, self).__init__(model, params,
                                                 normalized_cov_params,
                                                 scale)
@@ -1110,7 +1149,8 @@ def __init__(self, model, params, normalized_cov_params=None, scale=1.,
                 # TODO: warn or not?
             self.get_robustcov_results(cov_type=cov_type, use_self=True,
                                        use_t=use_t, **cov_kwds)
-
+        for key in kwargs:
+            setattr(self, key, kwargs[key])
 
     def __str__(self):
         self.summary()


Test patch:
diff --git a/statsmodels/regression/tests/test_glsar_stata.py b/statsmodels/regression/tests/test_glsar_stata.py
--- a/statsmodels/regression/tests/test_glsar_stata.py
+++ b/statsmodels/regression/tests/test_glsar_stata.py
@@ -7,7 +7,7 @@
 """
 
 import numpy as np
-from numpy.testing import assert_almost_equal, assert_allclose
+from numpy.testing import assert_almost_equal, assert_allclose, assert_equal
 
 from statsmodels.regression.linear_model import GLSAR
 from statsmodels.tools.tools import add_constant
@@ -55,6 +55,44 @@ def test_rho(self):
         assert_almost_equal(self.res.llf, self.results.ll, 4)
 
 
+    def test_glsar_arima(self):
+        from statsmodels.tsa.arima_model import ARMA
+
+        endog = self.res.model.endog
+        exog = self.res.model.exog
+        mod1 = GLSAR(endog, exog, 3)
+        res = mod1.iterative_fit(10)
+        mod_arma = ARMA(endog, order=(3,0), exog=exog[:, :-1])
+        res_arma = mod_arma.fit(method='css', iprint=0, disp=0)
+        assert_allclose(res.params, res_arma.params[[1,2,0]], atol=0.01, rtol=1e-3)
+        assert_allclose(res.model.rho, res_arma.params[3:], atol=0.05, rtol=1e-3)
+        assert_allclose(res.bse, res_arma.bse[[1,2,0]], atol=0.015, rtol=1e-3)
+
+        assert_equal(len(res.history['params']), 5)
+        # this should be identical, history has last fit
+        assert_equal(res.history['params'][-1], res.params)
+
+        res2 = mod1.iterative_fit(4, rtol=0)
+        assert_equal(len(res2.history['params']), 4)
+        assert_equal(len(res2.history['rho']), 4)
+
+
+    def test_glsar_iter0(self):
+        endog = self.res.model.endog
+        exog = self.res.model.exog
+
+        rho = np.array([ 0.207,  0.275,  1.033])
+        mod1 = GLSAR(endog, exog, rho)
+        res1 = mod1.fit()
+        res0 = mod1.iterative_fit(0)
+        res0b = mod1.iterative_fit(1)
+        # check iterative_fit(0) or iterative_fit(1) doesn't update rho
+        assert_allclose(res0.params, res1.params, rtol=1e-11)
+        assert_allclose(res0b.params, res1.params, rtol=1e-11)
+        assert_allclose(res0.model.rho, rho, rtol=1e-11)
+        assert_allclose(res0b.model.rho, rho, rtol=1e-11)
+
+
 if __name__=="__main__":
     import nose
     nose.runmodule(argv=[__file__,'-vvs','-x',#'--pdb', '--pdb-failure'