Repository: statsmodels/statsmodels

Issue:
RLM.fit() does not use start_params
I'm writing more generic tests, and saw that RLM.fit() doesn't use start_params

The problem was that I use start_params in the test suite. for `LikelihoodModel._fit_zeros`

This is just one more for refactoring RLM to make it more consistent with recent changes to discrete models and GLM. 



Discussion:
RLM.fit also doesn't have `method` and `warn_convergence` kwds

we don't have `**kwds` to swallow typos and extra keywords


Resolving patch:
diff --git a/statsmodels/regression/_tools.py b/statsmodels/regression/_tools.py
--- a/statsmodels/regression/_tools.py
+++ b/statsmodels/regression/_tools.py
@@ -100,7 +100,20 @@ def fit(self, method='pinv'):
         else:
             params, _, _, _ = np.linalg.lstsq(self.wexog, self.wendog,
                                               rcond=-1)
+        return self.results(params)
 
+    def results(self, params):
+        """
+        Construct results
+
+        params : array
+            Model parameters
+
+        Notes
+        -----
+        Allows results to be constructed from either existing parameters or
+        when estimated using using ``fit``
+        """
         fitted_values = self.exog.dot(params)
         resid = self.endog - fitted_values
         wresid = self.wendog - self.wexog.dot(params)
diff --git a/statsmodels/robust/robust_linear_model.py b/statsmodels/robust/robust_linear_model.py
--- a/statsmodels/robust/robust_linear_model.py
+++ b/statsmodels/robust/robust_linear_model.py
@@ -28,8 +28,8 @@
 
 
 def _check_convergence(criterion, iteration, tol, maxiter):
-    return not (np.any(np.fabs(criterion[iteration] -
-                criterion[iteration-1]) > tol) and iteration < maxiter)
+    cond = np.fabs(criterion[iteration] - criterion[iteration - 1])
+    return not (np.any(cond > tol) and iteration < maxiter)
 
 
 class RLM(base.LikelihoodModel):
@@ -102,16 +102,16 @@ class RLM(base.LikelihoodModel):
     >>> rlm_hamp_hub = mod.fit(scale_est=sm.robust.scale.HuberScale())
     >>> rlm_hamp_hub.params
     array([  0.73175452,   1.25082038,  -0.14794399, -40.27122257])
-    """ % {'params' : base._model_params_doc,
-            'extra_params' : base._missing_param_doc}
+    """ % {'params': base._model_params_doc,
+           'extra_params': base._missing_param_doc}
 
     def __init__(self, endog, exog, M=None, missing='none',
                  **kwargs):
         self.M = M if M is not None else norms.HuberT()
         super(base.LikelihoodModel, self).__init__(endog, exog,
-                missing=missing, **kwargs)
+                                                   missing=missing, **kwargs)
         self._initialize()
-        #things to remove_data
+        # things to remove_data
         self._data_attr.extend(['weights', 'pinv_wexog'])
 
     def _initialize(self):
@@ -122,10 +122,10 @@ def _initialize(self):
         """
         self.pinv_wexog = np.linalg.pinv(self.exog)
         self.normalized_cov_params = np.dot(self.pinv_wexog,
-                                        np.transpose(self.pinv_wexog))
+                                            np.transpose(self.pinv_wexog))
         self.df_resid = (np.float(self.exog.shape[0] -
-                         np.linalg.matrix_rank(self.exog)))
-        self.df_model = np.float(np.linalg.matrix_rank(self.exog)-1)
+                                  np.linalg.matrix_rank(self.exog)))
+        self.df_model = np.float(np.linalg.matrix_rank(self.exog) - 1)
         self.nobs = float(self.endog.shape[0])
 
     def score(self, params):
@@ -153,7 +153,7 @@ def predict(self, params, exog=None):
         -----
         If the model as not yet been fit, params is not optional.
         """
-        #copied from linear_model
+        # copied from linear_model
         if exog is None:
             exog = self.exog
         return np.dot(exog, params)
@@ -166,7 +166,7 @@ def deviance(self, tmp_results):
         Returns the (unnormalized) log-likelihood from the M estimator.
         """
         return self.M((self.endog - tmp_results.fittedvalues) /
-                          tmp_results.scale).sum()
+                      tmp_results.scale).sum()
 
     def _update_history(self, tmp_results, history, conv):
         history['params'].append(tmp_results.params)
@@ -174,7 +174,7 @@ def _update_history(self, tmp_results, history, conv):
         if conv == 'dev':
             history['deviance'].append(self.deviance(tmp_results))
         elif conv == 'sresid':
-            history['sresid'].append(tmp_results.resid/tmp_results.scale)
+            history['sresid'].append(tmp_results.resid / tmp_results.scale)
         elif conv == 'weights':
             history['weights'].append(tmp_results.model.weights)
         return history
@@ -192,10 +192,10 @@ def _estimate_scale(self, resid):
         elif isinstance(self.scale_est, scale.HuberScale):
             return self.scale_est(self.df_resid, self.nobs, resid)
         else:
-            return scale.scale_est(self, resid)**2
+            return scale.scale_est(self, resid) ** 2
 
     def fit(self, maxiter=50, tol=1e-8, scale_est='mad', init=None, cov='H1',
-            update_scale=True, conv='dev'):
+            update_scale=True, conv='dev', start_params=None):
         """
         Fits the model using iteratively reweighted least squares.
 
@@ -234,37 +234,51 @@ def fit(self, maxiter=50, tol=1e-8, scale_est='mad', init=None, cov='H1',
             If `update_scale` is False then the scale estimate for the
             weights is held constant over the iteration.  Otherwise, it
             is updated for each fit in the iteration.  Default is True.
+        start_params : array-like, optional
+            Initial guess of the solution of the optimizer. If not provided,
+            the initial parameters are computed using OLS.
 
         Returns
         -------
         results : object
             statsmodels.rlm.RLMresults
         """
-        if cov.upper() not in ["H1","H2","H3"]:
+        if cov.upper() not in ["H1", "H2", "H3"]:
             raise ValueError("Covariance matrix %s not understood" % cov)
         else:
             self.cov = cov.upper()
         conv = conv.lower()
-        if conv not in ["weights","coefs","dev","sresid"]:
-            raise ValueError("Convergence argument %s not understood" \
-                % conv)
+        if conv not in ["weights", "coefs", "dev", "sresid"]:
+            raise ValueError("Convergence argument %s not understood" % conv)
         self.scale_est = scale_est
 
-        wls_results = lm.WLS(self.endog, self.exog).fit()
+        if start_params is None:
+            wls_results = lm.WLS(self.endog, self.exog).fit()
+        else:
+            start_params = np.asarray(start_params, dtype=np.double).squeeze()
+            if (start_params.shape[0] != self.exog.shape[1] or
+                    start_params.ndim != 1):
+                raise ValueError('start_params must by a 1-d array with {0} '
+                                 'values'.format(self.exog.shape[1]))
+            fake_wls = reg_tools._MinimalWLS(self.endog, self.exog,
+                                             weights=np.ones_like(self.endog),
+                                             check_weights=False)
+            wls_results = fake_wls.results(start_params)
+
         if not init:
             self.scale = self._estimate_scale(wls_results.resid)
 
-        history = dict(params = [np.inf], scale = [])
+        history = dict(params=[np.inf], scale=[])
         if conv == 'coefs':
             criterion = history['params']
         elif conv == 'dev':
-            history.update(dict(deviance = [np.inf]))
+            history.update(dict(deviance=[np.inf]))
             criterion = history['deviance']
         elif conv == 'sresid':
-            history.update(dict(sresid = [np.inf]))
+            history.update(dict(sresid=[np.inf]))
             criterion = history['sresid']
         elif conv == 'weights':
-            history.update(dict(weights = [np.inf]))
+            history.update(dict(weights=[np.inf]))
             criterion = history['weights']
 
         # done one iteration so update
@@ -272,7 +286,7 @@ def fit(self, maxiter=50, tol=1e-8, scale_est='mad', init=None, cov='H1',
         iteration = 1
         converged = 0
         while not converged:
-            self.weights = self.M.weights(wls_results.resid/self.scale)
+            self.weights = self.M.weights(wls_results.resid / self.scale)
             wls_results = reg_tools._MinimalWLS(self.endog, self.exog,
                                                 weights=self.weights,
                                                 check_weights=True).fit()
@@ -282,19 +296,20 @@ def fit(self, maxiter=50, tol=1e-8, scale_est='mad', init=None, cov='H1',
             iteration += 1
             converged = _check_convergence(criterion, iteration, tol, maxiter)
         results = RLMResults(self, wls_results.params,
-                            self.normalized_cov_params, self.scale)
+                             self.normalized_cov_params, self.scale)
 
         history['iteration'] = iteration
         results.fit_history = history
         results.fit_options = dict(cov=cov.upper(), scale_est=scale_est,
                                    norm=self.M.__class__.__name__, conv=conv)
-        #norm is not changed in fit, no old state
+        # norm is not changed in fit, no old state
 
-        #doing the next causes exception
-        #self.cov = self.scale_est = None #reset for additional fits
-        #iteration and history could contain wrong state with repeated fit
+        # doing the next causes exception
+        # self.cov = self.scale_est = None #reset for additional fits
+        # iteration and history could contain wrong state with repeated fit
         return RLMResultsWrapper(results)
 
+
 class RLMResults(base.LikelihoodModelResults):
     """
     Class to contain RLM results
@@ -380,20 +395,19 @@ class RLMResults(base.LikelihoodModelResults):
     statsmodels.base.model.LikelihoodModelResults
     """
 
-
     def __init__(self, model, params, normalized_cov_params, scale):
         super(RLMResults, self).__init__(model, params,
-                normalized_cov_params, scale)
+                                         normalized_cov_params, scale)
         self.model = model
         self.df_model = model.df_model
         self.df_resid = model.df_resid
         self.nobs = model.nobs
         self._cache = {}
-        #for remove_data
+        # for remove_data
         self.data_in_cache = ['sresid']
 
         self.cov_params_default = self.bcov_scaled
-        #TODO: "pvals" should come from chisq on bse?
+        # TODO: "pvals" should come from chisq on bse?
 
     @cache_readonly
     def fittedvalues(self):
@@ -401,11 +415,11 @@ def fittedvalues(self):
 
     @cache_readonly
     def resid(self):
-        return self.model.endog - self.fittedvalues   # before bcov
+        return self.model.endog - self.fittedvalues  # before bcov
 
     @cache_readonly
     def sresid(self):
-        return self.resid/self.scale
+        return self.resid / self.scale
 
     @cache_readonly
     def bcov_unscaled(self):
@@ -420,34 +434,36 @@ def bcov_scaled(self):
         model = self.model
         m = np.mean(model.M.psi_deriv(self.sresid))
         var_psiprime = np.var(model.M.psi_deriv(self.sresid))
-        k = 1 + (self.df_model+1)/self.nobs * var_psiprime/m**2
+        k = 1 + (self.df_model + 1) / self.nobs * var_psiprime / m ** 2
 
         if model.cov == "H1":
-            return k**2 * (1/self.df_resid*\
-                np.sum(model.M.psi(self.sresid)**2)*self.scale**2)\
-                /((1/self.nobs*np.sum(model.M.psi_deriv(self.sresid)))**2)\
-                *model.normalized_cov_params
+            ss_psi = np.sum(model.M.psi(self.sresid) ** 2)
+            s_psi_deriv = np.sum(model.M.psi_deriv(self.sresid))
+            return k ** 2 * (1 / self.df_resid * ss_psi * self.scale ** 2) /\
+                ((1 / self.nobs * s_psi_deriv) ** 2) *\
+                model.normalized_cov_params
         else:
-            W = np.dot(model.M.psi_deriv(self.sresid)*model.exog.T,
-                    model.exog)
+            W = np.dot(model.M.psi_deriv(self.sresid) * model.exog.T,
+                       model.exog)
             W_inv = np.linalg.inv(W)
             # [W_jk]^-1 = [SUM(psi_deriv(Sr_i)*x_ij*x_jk)]^-1
             # where Sr are the standardized residuals
             if model.cov == "H2":
-            # These are correct, based on Huber (1973) 8.13
-                return k*(1/self.df_resid)*np.sum(\
-                    model.M.psi(self.sresid)**2)*self.scale**2\
-                    /((1/self.nobs)*np.sum(\
-                    model.M.psi_deriv(self.sresid)))*W_inv
+                # These are correct, based on Huber (1973) 8.13
+                return k * (1 / self.df_resid) * np.sum(
+                    model.M.psi(self.sresid) ** 2) * self.scale ** 2 \
+                       / ((1 / self.nobs) *
+                          np.sum(model.M.psi_deriv(self.sresid))) * W_inv
             elif model.cov == "H3":
-                return k**-1*1/self.df_resid*np.sum(\
-                    model.M.psi(self.sresid)**2)*self.scale**2\
-                    *np.dot(np.dot(W_inv, np.dot(model.exog.T,model.exog)),\
+                return k ** -1 * 1 / self.df_resid * np.sum(
+                    model.M.psi(self.sresid) ** 2) * self.scale ** 2 \
+                       * np.dot(
+                    np.dot(W_inv, np.dot(model.exog.T, model.exog)),
                     W_inv)
 
     @cache_readonly
     def pvalues(self):
-        return stats.norm.sf(np.abs(self.tvalues))*2
+        return stats.norm.sf(np.abs(self.tvalues)) * 2
 
     @cache_readonly
     def bse(self):
@@ -455,7 +471,7 @@ def bse(self):
 
     @cache_readonly
     def chisq(self):
-        return (self.params/self.bse)**2
+        return (self.params / self.bse) ** 2
 
     def summary(self, yname=None, xname=None, title=0, alpha=.05,
                 return_fmt='text'):
@@ -490,10 +506,9 @@ def summary(self, yname=None, xname=None, title=0, alpha=.05,
 
         # add warnings/notes, added to text format only
         etext = []
-        wstr = ("If the model instance has been used for another fit "
-                "with different fit\n"
-               "parameters, then the fit options might not be the correct "
-               "ones anymore .")
+        wstr = ("If the model instance has been used for another fit with "
+                "different fit parameters, then the fit options might not be "
+                "the correct ones anymore .")
         etext.append(wstr)
 
         if etext:
@@ -541,104 +556,6 @@ def summary2(self, xname=None, yname=None, title=None, alpha=.05,
 
 class RLMResultsWrapper(lm.RegressionResultsWrapper):
     pass
-wrap.populate_wrapper(RLMResultsWrapper, RLMResults)  # noqa:E305
 
 
-if __name__=="__main__":
-#NOTE: This is to be removed
-#Delivery Time Data is taken from Montgomery and Peck
-    import statsmodels.api as sm
-
-#delivery time(minutes)
-    endog = np.array([16.68, 11.50, 12.03, 14.88, 13.75, 18.11, 8.00, 17.83,
-    79.24, 21.50, 40.33, 21.00, 13.50, 19.75, 24.00, 29.00, 15.35, 19.00,
-    9.50, 35.10, 17.90, 52.32, 18.75, 19.83, 10.75])
-
-#number of cases, distance (Feet)
-    exog = np.array([[7, 3, 3, 4, 6, 7, 2, 7, 30, 5, 16, 10, 4, 6, 9, 10, 6,
-    7, 3, 17, 10, 26, 9, 8, 4], [560, 220, 340, 80, 150, 330, 110, 210, 1460,
-    605, 688, 215, 255, 462, 448, 776, 200, 132, 36, 770, 140, 810, 450, 635,
-    150]])
-    exog = exog.T
-    exog = sm.add_constant(exog)
-
-#    model_ols = models.regression.OLS(endog, exog)
-#    results_ols = model_ols.fit()
-
-#    model_ramsaysE = RLM(endog, exog, M=norms.RamsayE())
-#    results_ramsaysE = model_ramsaysE.fit(update_scale=False)
-
-#    model_andrewWave = RLM(endog, exog, M=norms.AndrewWave())
-#    results_andrewWave = model_andrewWave.fit(update_scale=False)
-
-#    model_hampel = RLM(endog, exog, M=norms.Hampel(a=1.7,b=3.4,c=8.5)) # convergence problems with scale changed, not with 2,4,8 though?
-#    results_hampel = model_hampel.fit(update_scale=False)
-
-#######################
-### Stack Loss Data ###
-#######################
-    from statsmodels.datasets.stackloss import load
-    data = load(as_pandas=False)
-    data.exog = sm.add_constant(data.exog)
-#############
-### Huber ###
-#############
-#    m1_Huber = RLM(data.endog, data.exog, M=norms.HuberT())
-#    results_Huber1 = m1_Huber.fit()
-#    m2_Huber = RLM(data.endog, data.exog, M=norms.HuberT())
-#    results_Huber2 = m2_Huber.fit(cov="H2")
-#    m3_Huber = RLM(data.endog, data.exog, M=norms.HuberT())
-#    results_Huber3 = m3_Huber.fit(cov="H3")
-##############
-### Hampel ###
-##############
-#    m1_Hampel = RLM(data.endog, data.exog, M=norms.Hampel())
-#    results_Hampel1 = m1_Hampel.fit()
-#    m2_Hampel = RLM(data.endog, data.exog, M=norms.Hampel())
-#    results_Hampel2 = m2_Hampel.fit(cov="H2")
-#    m3_Hampel = RLM(data.endog, data.exog, M=norms.Hampel())
-#    results_Hampel3 = m3_Hampel.fit(cov="H3")
-################
-### Bisquare ###
-################
-#    m1_Bisquare = RLM(data.endog, data.exog, M=norms.TukeyBiweight())
-#    results_Bisquare1 = m1_Bisquare.fit()
-#    m2_Bisquare = RLM(data.endog, data.exog, M=norms.TukeyBiweight())
-#    results_Bisquare2 = m2_Bisquare.fit(cov="H2")
-#    m3_Bisquare = RLM(data.endog, data.exog, M=norms.TukeyBiweight())
-#    results_Bisquare3 = m3_Bisquare.fit(cov="H3")
-
-
-##############################################
-# Huber's Proposal 2 scaling                 #
-##############################################
-
-################
-### Huber'sT ###
-################
-    m1_Huber_H = RLM(data.endog, data.exog, M=norms.HuberT())
-    results_Huber1_H = m1_Huber_H.fit(scale_est=scale.HuberScale())
-#    m2_Huber_H
-#    m3_Huber_H
-#    m4 = RLM(data.endog, data.exog, M=norms.HuberT())
-#    results4 = m1.fit(scale_est="Huber")
-#    m5 = RLM(data.endog, data.exog, M=norms.Hampel())
-#    results5 = m2.fit(scale_est="Huber")
-#    m6 = RLM(data.endog, data.exog, M=norms.TukeyBiweight())
-#    results6 = m3.fit(scale_est="Huber")
-
-
-
-
-#    print """Least squares fit
-#%s
-#Huber Params, t = 2.
-#%s
-#Ramsay's E Params
-#%s
-#Andrew's Wave Params
-#%s
-#Hampel's 17A Function
-#%s
-#""" % (results_ols.params, results_huber.params, results_ramsaysE.params,
-#            results_andrewWave.params, results_hampel.params)
+wrap.populate_wrapper(RLMResultsWrapper, RLMResults)  # noqa:E305


Test patch:
diff --git a/statsmodels/robust/tests/test_rlm.py b/statsmodels/robust/tests/test_rlm.py
--- a/statsmodels/robust/tests/test_rlm.py
+++ b/statsmodels/robust/tests/test_rlm.py
@@ -291,3 +291,26 @@ def test_missing():
 
     d = {'Foo': [1, 2, 10, 149], 'Bar': [1, 2, 3, np.nan]}
     smf.rlm('Foo ~ Bar', data=d)
+
+
+def test_rlm_start_values():
+    data = sm.datasets.stackloss.load_pandas()
+    exog = sm.add_constant(data.exog, prepend=False)
+    model = RLM(data.endog, exog, M=norms.HuberT())
+    results = model.fit()
+    start_params = [0.7156402, 1.29528612, -0.15212252, -39.91967442]
+    result_sv = model.fit(start_params=start_params)
+    assert_allclose(results.params, result_sv.params)
+
+
+def test_rlm_start_values_errors():
+    data = sm.datasets.stackloss.load_pandas()
+    exog = sm.add_constant(data.exog, prepend=False)
+    model = RLM(data.endog, exog, M=norms.HuberT())
+    start_params = [0.7156402, 1.29528612, -0.15212252]
+    with pytest.raises(ValueError):
+        model.fit(start_params=start_params)
+
+    start_params = np.array([start_params, start_params]).T
+    with pytest.raises(ValueError):
+        model.fit(start_params=start_params)