Repository: statsmodels/statsmodels

Issue:
ENH/BUG: ContrastResults chisquare missing `denom`, distargs attribute
general
ContrastResults might need another review, 
and there is also WaldTestResults which seems to be used only in wald_test_terms
new t_test_pairwise doesn't return Results instance (?)

specific:
`wald_test` in a model results with use_f=False, i.e. chisquare, does not attach `denom` or `distargs`, and doesn't show denom in summary (`__str__`, `__repr__`)

(I started to change this in #4576 but it should be a separate PR, I was trying out pinv for #4588 in my branch for a fit_constrained example.)


Discussion:
maybe fix still for 0.9. fix does not change any numeric results.

Resolving patch:
diff --git a/statsmodels/base/model.py b/statsmodels/base/model.py
--- a/statsmodels/base/model.py
+++ b/statsmodels/base/model.py
@@ -1505,6 +1505,7 @@ def wald_test(self, r_matrix, cov_p=None, scale=1.0, invcov=None,
 
         cparams = np.dot(r_matrix, self.params[:, None])
         J = float(r_matrix.shape[0])  # number of restrictions
+
         if q_matrix is None:
             q_matrix = np.zeros(J)
         else:
@@ -1521,7 +1522,15 @@ def wald_test(self, r_matrix, cov_p=None, scale=1.0, invcov=None,
                 raise ValueError("r_matrix performs f_test for using "
                                  "dimensions that are asymptotically "
                                  "non-normal")
-            invcov = np.linalg.inv(cov_p)
+            invcov = np.linalg.pinv(cov_p)
+            J_ = np.linalg.matrix_rank(cov_p)
+            if J_ < J:
+                import warnings
+                from statsmodels.tools.sm_exceptions import ValueWarning
+                warnings.warn('covariance of constraints does not have full '
+                              'rank. The number of constraints is %d, but '
+                              'rank is %d' % (J, J_), ValueWarning)
+                J = J_
 
         if (hasattr(self, 'mle_settings') and
                 self.mle_settings['optimizer'] in ['l1', 'l1_cvxopt_cp']):
@@ -1533,7 +1542,7 @@ def wald_test(self, r_matrix, cov_p=None, scale=1.0, invcov=None,
         if use_f:
             F /= J
             return ContrastResults(F=F, df_denom=df_resid,
-                                   df_num=invcov.shape[0])
+                                   df_num=J) #invcov.shape[0])
         else:
             return ContrastResults(chi2=F, df_denom=J, statistic=F,
                                    distribution='chi2', distargs=(J,))
diff --git a/statsmodels/stats/contrast.py b/statsmodels/stats/contrast.py
--- a/statsmodels/stats/contrast.py
+++ b/statsmodels/stats/contrast.py
@@ -51,9 +51,10 @@ def __init__(self, t=None, F=None, sd=None, effect=None, df_denom=None,
             # TODO: for results instance we decided to use tvalues also for normal
             self.sd = sd
             self.dist = getattr(stats, self.distribution)
-            self.dist_args = ()
+            self.dist_args = kwds.get('dist_args', ())
             if self.distribution is 'chi2':
                 self.pvalue = self.dist.sf(self.statistic, df_denom)
+                self.df_denom = df_denom
             else:
                 "normal"
                 self.pvalue = np.full_like(value, np.nan)
@@ -157,6 +158,9 @@ def summary(self, xname=None, alpha=0.05, title=None):
             # TODO: create something nicer for these casee
             return '<F test: F=%s, p=%s, df_denom=%d, df_num=%d>' % \
                    (repr(self.fvalue), self.pvalue, self.df_denom, self.df_num)
+        elif self.distribution == 'chi2':
+            return '<Wald test (%s): statistic=%s, p-value=%s, df_denom=%d>' % \
+                   (self.distribution, self.statistic, self.pvalue, self.df_denom)
         else:
             # generic
             return '<Wald test: statistic=%s, p-value=%s>' % \


Test patch:
diff --git a/statsmodels/genmod/tests/test_constrained.py b/statsmodels/genmod/tests/test_constrained.py
--- a/statsmodels/genmod/tests/test_constrained.py
+++ b/statsmodels/genmod/tests/test_constrained.py
@@ -9,12 +9,15 @@
 Author: Josef Perktold
 """
 
+import warnings
 
 import numpy as np
 from numpy.testing import assert_allclose, assert_equal
 from statsmodels.tools.tools import add_constant
 from statsmodels.regression.linear_model import OLS, WLS
 from statsmodels.genmod.generalized_linear_model import GLM
+from statsmodels.genmod.families import family
+from statsmodels.tools.sm_exceptions import ValueWarning
 
 
 class ConstrainedCompareMixin(object):
@@ -52,7 +55,7 @@ def test_se(self):
         assert_allclose(res1.scale, res2.scale, rtol=1e-10)
         assert_allclose(res1.bse[self.idx_p_uc], res2.bse, rtol=1e-10)
         assert_allclose(res1.cov_params()[self.idx_p_uc[:, None],
-                        self.idx_p_uc], res2.cov_params(), rtol=1e-10)
+                        self.idx_p_uc], res2.cov_params(), rtol=1e-10, atol=1e-15)
 
     def test_resid(self):
         res1 = self.res1
@@ -176,3 +179,85 @@ def init(cls):
         mod = GLM(cls.endog, cls.exog, var_weights=cls.aweights)
         mod.exog_names[:] = ['const', 'x1', 'x2', 'x3', 'x4']
         cls.res1 = mod.fit_constrained('x1=0.5', cov_type=cov_type)
+
+
+class TestGLMBinomialCountConstrained(ConstrainedCompareMixin):
+
+    @classmethod
+    def setup_class(cls):
+        from statsmodels.datasets.star98 import load
+        #from statsmodels.genmod.tests.results.results_glm import Star98
+        data = load()
+        exog = add_constant(data.exog, prepend=True)
+        offset = np.ones(len(data.endog))
+        exog_keep = exog[:, :-5]
+        cls.mod2 = GLM(data.endog, exog_keep, family=family.Binomial(),
+                       offset=offset)
+
+        cls.mod1 = GLM(data.endog, exog, family=family.Binomial(),
+                       offset=offset)
+        cls.init()
+
+    @classmethod
+    def init(cls):
+        cls.res2 = cls.mod2.fit()
+        k = cls.mod1.exog.shape[1]
+        cls.idx_p_uc = np.arange(k - 5)
+        constraints = np.eye(k)[-5:]
+        cls.res1 = cls.mod1.fit_constrained(constraints)
+
+    def test_resid(self):
+        # need to override because res2 does not have resid
+        res1 = self.res1
+        res2 = self.res2
+        assert_allclose(res1.resid_response, res2.resid_response, rtol=1e-8)
+
+    def test_glm_attr(self):
+        for attr in ['llf', 'null_deviance', 'aic', 'bic', 'df_resid',
+                     'df_model', 'pearson_chi2', 'scale']:
+            assert_allclose(getattr(self.res1, attr),
+                            getattr(self.res2, attr), rtol=1e-10)
+
+    def test_wald(self):
+        res1 = self.res1
+        res2 = self.res2
+        k1 = len(res1.params)
+        k2 = len(res2.params)
+
+        use_f = False
+        with warnings.catch_warnings():
+            warnings.simplefilter('ignore', ValueWarning)
+            wt2 = res2.wald_test(np.eye(k2)[1:], use_f=use_f)
+            wt1 = res1.wald_test(np.eye(k1)[1:], use_f=use_f)
+        assert_allclose(wt2.pvalue, wt1.pvalue, atol=1e-20) # pvalue = 0
+        assert_allclose(wt2.statistic, wt1.statistic, rtol=1e-8)
+        assert_equal(wt2.df_denom, wt1.df_denom)
+
+        use_f = True
+        with warnings.catch_warnings():
+            warnings.simplefilter('ignore', ValueWarning)
+            wt2 = res2.wald_test(np.eye(k2)[1:], use_f=use_f)
+            wt1 = res1.wald_test(np.eye(k1)[1:], use_f=use_f)
+        assert_allclose(wt2.pvalue, wt1.pvalue, rtol=1) # pvalue = 8e-273
+        assert_allclose(wt2.statistic, wt1.statistic, rtol=1e-8)
+        assert_equal(wt2.df_denom, wt1.df_denom)
+        assert_equal(wt2.df_num, wt1.df_num)
+        assert_equal(wt2.summary()[-30:], wt1.summary()[-30:])
+
+        # smoke
+        with warnings.catch_warnings():
+            warnings.simplefilter('ignore', ValueWarning)
+            # RuntimeWarnings because of truedivide and scipy distributions
+            warnings.simplefilter('ignore', RuntimeWarning)
+            self.res1.summary()
+            self.res1.summary2()
+
+
+class TestGLMBinomialCountConstrainedHC(TestGLMBinomialCountConstrained):
+    @classmethod
+    def init(cls):
+        cls.res2 = cls.mod2.fit(cov_type='HC0')
+        k = cls.mod1.exog.shape[1]
+        cls.idx_p_uc = np.arange(k - 5)
+        constraints = np.eye(k)[-5:]
+        cls.res1 = cls.mod1.fit_constrained(constraints, cov_type='HC0')
diff --git a/statsmodels/regression/tests/test_cov.py b/statsmodels/regression/tests/test_cov.py
--- a/statsmodels/regression/tests/test_cov.py
+++ b/statsmodels/regression/tests/test_cov.py
@@ -5,7 +5,7 @@
 import numpy as np
 import statsmodels.api as sm
 
-from numpy.testing import assert_almost_equal
+from numpy.testing import assert_almost_equal, assert_allclose
 
 def test_HC_use():
     np.random.seed(0)
@@ -36,4 +36,4 @@ def test_HC_use():
     idx = np.array([0,1])
     cov_slopes = results.cov_HC0[idx[:,None], idx]
     fval = np.dot(slopes, np.dot(np.linalg.inv(cov_slopes), slopes))/len(idx)
-    assert_almost_equal(ftest.fvalue, fval, decimal=12)
+    assert_allclose(ftest.fvalue, fval, rtol=12)
diff --git a/statsmodels/regression/tests/test_robustcov.py b/statsmodels/regression/tests/test_robustcov.py
--- a/statsmodels/regression/tests/test_robustcov.py
+++ b/statsmodels/regression/tests/test_robustcov.py
@@ -842,8 +842,8 @@ def test_all(self):
             mat = np.eye(len(res1.params))
             ft1 = res1.f_test(mat)
             ft2 = res2.f_test(mat)
-            assert_allclose(ft1.fvalue, ft2.fvalue, rtol=1e-13)
-            assert_allclose(ft1.pvalue, ft2.pvalue, rtol=1e-12)
+            assert_allclose(ft1.fvalue, ft2.fvalue, rtol=1e-12)
+            assert_allclose(ft1.pvalue, ft2.pvalue, rtol=5e-11)
 
     def test_fixed_scale(self):
         cov_type = 'fixed_scale'