Repository: statsmodels/statsmodels

Issue:
get_robustcov_results kernel keyword issue
The documentation incorrectly says that the 'kernel' keyword argument in `get_robustcov_results` should be a string instead of a callable.  See here:

http://www.statsmodels.org/devel/generated/statsmodels.regression.linear_model.OLSResults.get_robustcov_results.html?highlight=get_robustcov_results

```python
In [3]: model = sm.OLS(np.random.normal(size=(10,1)), np.random.normal(size=(10,2)))

In [4]: model.fit().get_robustcov_results(cov_type='HAC', kernel='bartlett',maxlags=3)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-64-7041f059add2> in <module>()
----> 1 model.fit().get_robustcov_results(cov_type='HAC', kernel='bartlett',maxlags=3)

~/anaconda3/lib/python3.6/site-packages/statsmodels/regression/linear_model.py in get_robustcov_results(self, cov_type, use_t, **kwds)
   2029             res.cov_params_default = sw.cov_hac_simple(self, nlags=maxlags,
   2030                                                  weights_func=weights_func,
-> 2031                                                  use_correction=use_correction)
   2032         elif cov_type.lower() == 'cluster':
   2033             #cluster robust standard errors, one- or two-way

~/anaconda3/lib/python3.6/site-packages/statsmodels/stats/sandwich_covariance.py in cov_hac_simple(results, nlags, weights_func, use_correction)
    696     '''
    697     xu, hessian_inv = _get_sandwich_arrays(results)
--> 698     sigma = S_hac_simple(xu, nlags=nlags, weights_func=weights_func)
    699 
    700     cov_hac = _HCCM2(hessian_inv, sigma)

~/anaconda3/lib/python3.6/site-packages/statsmodels/stats/sandwich_covariance.py in S_hac_simple(x, nlags, weights_func)
    394         nlags = int(np.floor(4 * (n_periods / 100.)**(2./9.)))
    395 
--> 396     weights = weights_func(nlags)
    397 
    398     S = weights[0] * np.dot(x.T, x)  #weights[0] just for completeness, is 1

TypeError: 'str' object is not callable
```
I am happy make a PR to either fix the documentation (or to change the code to allow for ['bartlett', 'uniform', callable].


Discussion:
@eph Thanks for reporting, That is a mistake that needs to be fixed.

as reminder/reference: previous consistency fix for hac/nw was #3032

I would prefer that the code is fixed, and the docstring adjusted to whatever that will be. 
In the medium/long run, we want to add more kernels and more options, and "finding" the kernel functions is not so easy for a user (especially because I use weights_func in the code.). 
So, being able to use a string to choose will be a lot more user friendly.

I think the best would be to define a dict in stats.sandwich_covariance that maps names to functions. That would be easily extentable as we add more weights_xxx kernel functions to the module and dict.

I guess the best place would be add the initial keyword normalization, e.g. after 
https://github.com/statsmodels/statsmodels/blob/master/statsmodels/regression/linear_model.py#L2127

Note: This code is still almost exactly duplicated and needs to be changed twice. Second place is
https://github.com/statsmodels/statsmodels/blob/master/statsmodels/base/covtype.py#L136




Not having duplicated code is also an option.

Resolving patch:
diff --git a/statsmodels/base/covtype.py b/statsmodels/base/covtype.py
--- a/statsmodels/base/covtype.py
+++ b/statsmodels/base/covtype.py
@@ -56,7 +56,9 @@ def get_robustcov_results(self, cov_type='HC1', use_t=None, **kwds):
     - 'HAC' and keywords
 
         - `maxlag` integer (required) : number of lags to use
-        - `kernel` string (optional) : kernel, default is Bartlett
+        - `kernel` callable or str (optional) : kernel
+              currently available kernels are ['bartlett', 'uniform'],
+              default is Bartlett
         - `use_correction` bool (optional) : If true, use small sample
               correction
 
@@ -85,7 +87,9 @@ def get_robustcov_results(self, cov_type='HC1', use_t=None, **kwds):
 
         - `time` array_like (required) : index of time periods
         - `maxlag` integer (required) : number of lags to use
-        - `kernel` string (optional) : kernel, default is Bartlett
+        - `kernel` callable or str (optional) : kernel
+              currently available kernels are ['bartlett', 'uniform'],
+              default is Bartlett
         - `use_correction` False or string in ['hac', 'cluster'] (optional) :
               If False the the sandwich covariance is calulated without
               small sample correction.
@@ -109,7 +113,9 @@ def get_robustcov_results(self, cov_type='HC1', use_t=None, **kwds):
           `groups` : indicator for groups
           `time` : index of time periods
         - `maxlag` integer (required) : number of lags to use
-        - `kernel` string (optional) : kernel, default is Bartlett
+        - `kernel` callable or str (optional) : kernel
+              currently available kernels are ['bartlett', 'uniform'],
+              default is Bartlett
         - `use_correction` False or string in ['hac', 'cluster'] (optional) :
               If False the the sandwich covariance is calulated without
               small sample correction.
@@ -134,7 +140,9 @@ def get_robustcov_results(self, cov_type='HC1', use_t=None, **kwds):
     if cov_type == 'nw-groupsum':
         cov_type = 'hac-groupsum'
     if 'kernel' in kwds:
-            kwds['weights_func'] = kwds.pop('kernel')
+        kwds['weights_func'] = kwds.pop('kernel')
+    if 'weights_func' in kwds and not callable(kwds['weights_func']):
+        kwds['weights_func'] = sw.kernel_dict[kwds['weights_func']]
 
     # pop because HCx raises if any kwds
     sc_factor = kwds.pop('scaling_factor', None)
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
@@ -2047,7 +2047,9 @@ def get_robustcov_results(self, cov_type='HC1', use_t=None, **kwds):
         - 'HAC' and keywords
 
             - `maxlag` integer (required) : number of lags to use
-            - `kernel` string (optional) : kernel, default is Bartlett
+            - `kernel` callable or str (optional) : kernel
+                  currently available kernels are ['bartlett', 'uniform'],
+                  default is Bartlett
             - `use_correction` bool (optional) : If true, use small sample
                   correction
 
@@ -2076,7 +2078,9 @@ def get_robustcov_results(self, cov_type='HC1', use_t=None, **kwds):
 
             - `time` array_like (required) : index of time periods
             - `maxlag` integer (required) : number of lags to use
-            - `kernel` string (optional) : kernel, default is Bartlett
+            - `kernel` callable or str (optional) : kernel
+                  currently available kernels are ['bartlett', 'uniform'],
+                  default is Bartlett
             - `use_correction` False or string in ['hac', 'cluster'] (optional) :
                   If False the the sandwich covariance is calulated without
                   small sample correction.
@@ -2101,7 +2105,9 @@ def get_robustcov_results(self, cov_type='HC1', use_t=None, **kwds):
               `groups` : indicator for groups
               `time` : index of time periods
             - `maxlag` integer (required) : number of lags to use
-            - `kernel` string (optional) : kernel, default is Bartlett
+            - `kernel` callable or str (optional) : kernel
+                  currently available kernels are ['bartlett', 'uniform'],
+                  default is Bartlett
             - `use_correction` False or string in ['hac', 'cluster'] (optional) :
                   If False the sandwich covariance is calculated without
                   small sample correction.
@@ -2116,7 +2122,6 @@ def get_robustcov_results(self, cov_type='HC1', use_t=None, **kwds):
         TODO: Currently there is no check for extra or misspelled keywords,
         except in the case of cov_type `HCx`
         """
-
         import statsmodels.stats.sandwich_covariance as sw
 
         # normalize names
@@ -2126,6 +2131,8 @@ def get_robustcov_results(self, cov_type='HC1', use_t=None, **kwds):
             cov_type = 'hac-groupsum'
         if 'kernel' in kwds:
             kwds['weights_func'] = kwds.pop('kernel')
+        if 'weights_func' in kwds and not callable(kwds['weights_func']):
+            kwds['weights_func'] = sw.kernel_dict[kwds['weights_func']]
 
         # TODO: make separate function that returns a robust cov plus info
         use_self = kwds.pop('use_self', False)
diff --git a/statsmodels/stats/sandwich_covariance.py b/statsmodels/stats/sandwich_covariance.py
--- a/statsmodels/stats/sandwich_covariance.py
+++ b/statsmodels/stats/sandwich_covariance.py
@@ -357,6 +357,11 @@ def weights_uniform(nlags):
     #with lag zero
     return np.ones(nlags+1)
 
+
+kernel_dict = {'bartlett': weights_bartlett,
+               'uniform': weights_uniform}
+
+
 def S_hac_simple(x, nlags=None, weights_func=weights_bartlett):
     '''inner covariance matrix for HAC (Newey, West) sandwich
 


Test patch:
diff --git a/statsmodels/discrete/tests/test_sandwich_cov.py b/statsmodels/discrete/tests/test_sandwich_cov.py
--- a/statsmodels/discrete/tests/test_sandwich_cov.py
+++ b/statsmodels/discrete/tests/test_sandwich_cov.py
@@ -571,6 +571,23 @@ def setup_class(cls):
         cls.res2 = mod2.fit(cov_type='HAC', cov_kwds=kwds)
 
 
+class TestGLMGaussHAC2(CheckDiscreteGLM):
+
+    @classmethod
+    def setup_class(cls):
+
+        cls.cov_type = 'HAC'
+
+        # check kernel specified as string
+        kwds = {'kernel': 'bartlett', 'maxlags': 2}
+        mod1 = GLM(endog, exog, family=families.Gaussian())
+        cls.res1 = mod1.fit(cov_type='HAC', cov_kwds=kwds)
+
+        mod2 = OLS(endog, exog)
+        kwds2 = {'maxlags': 2}
+        cls.res2 = mod2.fit(cov_type='HAC', cov_kwds=kwds2)
+
+
 class TestGLMGaussHACUniform(CheckDiscreteGLM):
 
     @classmethod
@@ -592,7 +609,7 @@ def setup_class(cls):
     def test_cov_options(self):
 
         # check keyword `weights_func
-        kwdsa = {'weights_func':sw.weights_uniform, 'maxlags':2}
+        kwdsa = {'weights_func': sw.weights_uniform, 'maxlags': 2}
         res1a = self.res1.model.fit(cov_type='HAC', cov_kwds=kwdsa)
         res2a = self.res2.model.fit(cov_type='HAC', cov_kwds=kwdsa)
         assert_allclose(res1a.bse, self.res1.bse, rtol=1e-12)
@@ -604,7 +621,7 @@ def test_cov_options(self):
 
         assert_(res1a.cov_kwds['weights_func'] is sw.weights_uniform)
 
-        kwdsb = {'kernel':sw.weights_bartlett, 'maxlags':2}
+        kwdsb = {'kernel': sw.weights_bartlett, 'maxlags': 2}
         res1a = self.res1.model.fit(cov_type='HAC', cov_kwds=kwdsb)
         res2a = self.res2.model.fit(cov_type='HAC', cov_kwds=kwdsb)
         assert_allclose(res1a.bse, res2a.bse, rtol=1e-12)
@@ -614,6 +631,24 @@ def test_cov_options(self):
         assert_allclose(res1a.bse, bse, rtol=1e-6)
 
 
+
+class TestGLMGaussHACUniform2(TestGLMGaussHACUniform):
+
+    @classmethod
+    def setup_class(cls):
+
+        cls.cov_type = 'HAC'
+
+        kwds={'kernel': sw.weights_uniform, 'maxlags': 2}
+        mod1 = GLM(endog, exog, family=families.Gaussian())
+        cls.res1 = mod1.fit(cov_type='HAC', cov_kwds=kwds)
+
+        # check kernel as string
+        mod2 = OLS(endog, exog)
+        kwds2 = {'kernel': 'uniform', 'maxlags': 2}
+        cls.res2 = mod2.fit(cov_type='HAC', cov_kwds=kwds)
+
+
 class TestGLMGaussHACPanel(CheckDiscreteGLM):
 
     @classmethod