Repository: statsmodels/statsmodels

Issue:
BUG: df in wald_test_terms for single column items
I'm not sure what's going on with the df in wald_test_terms results
I'm working with a GAM example in a notebook, and the df in the results is equal to the number of coefficients and not 1 as it should be for a single constraint.

The constraint matrix (returned in `temp`) are 1-D for single column constraints and not 2-D as for joint hypothesis. So there might be a incorrect df = constraint.shape[0] or missing atleast_2d.

Why doesn't this show up in the unit tests?

```
res_bsf.wald_test_terms(skip_single=False, combine_terms=['fuel', 'drive', 'weight', 'hp'])
<class 'statsmodels.stats.contrast.WaldTestResults'>
                    chi2         P>chi2  df constraint
Intercept     677.786660  2.024193e-149             24
fuel[T.gas]    63.823523   1.360790e-15             24
drive[T.fwd]    2.887571   8.926542e-02             24
drive[T.rwd]    1.594575   2.066736e-01             24
weight_s0      13.741523   2.097653e-04             24
weight_s1      26.966268   2.070372e-07             24
weight_s2      50.869767   9.870223e-13             24
weight_s3      59.216023   1.412788e-14             24
weight_s4      64.379100   1.026412e-15             24
weight_s5      66.065909   4.360938e-16             24
weight_s6      66.597275   3.330462e-16             24
weight_s7      69.019581   9.749068e-17             24
weight_s8      66.816994   2.979188e-16             24
weight_s9      71.769472   2.418654e-17             24
weight_s10     72.534338   1.641508e-17             24
hp_s0           6.717964   9.544654e-03             24
hp_s1          11.429164   7.230019e-04             24
hp_s2          22.246778   2.397608e-06             24
hp_s3          28.663073   8.613056e-08             24
hp_s4          43.307451   4.677986e-11             24
hp_s5          42.858347   5.885046e-11             24
hp_s6          35.971865   2.001873e-09             24
hp_s7          18.913675   1.367687e-05             24
hp_s8          21.947246   2.802488e-06             24
fuel           63.823523   1.360790e-15              1
drive           2.982567   2.250835e-01              2
weight         97.733732   5.013118e-16             11
hp             74.837143   1.701728e-12              9
```


Discussion:
Needs investigating to see whether this is the case in general or whether there is something specific to my model. (There should not be anything specific to GAM and my GAM example for this.)
confirmed bug in results table for wald_test_terms

I'm using `constraint.shape[0]` for df_constraint instead of the value that wald_test returns.
The p-values are correct and the correct test is used because wald_test handles 1-D constraint correctly.
But the returned df_constraint is wrong if constraint is 1-D.

```
        for name, constraint in constraints + combined_constraints + extra_constraints:
            wt = result.wald_test(constraint)
            row = [wt.statistic.item(), wt.pvalue.item(), constraint.shape[0]]
            if use_t:
                row.append(wt.df_denom)
            res_wald.append(row)
            index.append(name)
```

unit tests in base test_generic_methods

`compare_waldres` checks the df and uses `df = c.shape[0] if c.ndim == 2 else 1`
However, AFAICS there is no test class with a single column variable, i.e. with single, simple constraint.

names for df
I don't like df_num, df_denom because I never remember what they are and which is which

it looks like `chi2` has `df_constraints` in `df_denom`, and `F` has it in `df_num`

```
vars(res_bsf.wald_test(row, use_f=True))
{'df_denom': 189.13394496903189,
 'df_num': 1.0,
 'dist': <scipy.stats._continuous_distns.f_gen at 0xb73c908>,
 'dist_args': (1.0, 189.13394496903189),
 'distribution': 'F',
 'effect': None,
 'fvalue': array([[ 21.9472461]]),
 'pvalue': array(5.343056530859978e-06),
 'statistic': array([[ 21.9472461]])}

vars(res_bsf.wald_test(row, use_f=False))
{'df_denom': 1.0,
 'dist': <scipy.stats._continuous_distns.chi2_gen at 0xb737080>,
 'dist_args': (),
 'distribution': 'chi2',
 'effect': None,
 'pvalue': array(2.802487582963751e-06),
 'sd': None,
 'statistic': array([[ 21.9472461]]),
 'tvalue': array([[ 21.9472461]])}
```

`wald_test_terms` uses `df_constraints` and additionally `df_denom` in case of F


```
wtt3 = res_bsf.wald_test_terms(skip_single=True, combine_terms=['fuel', 'drive', 'weight', 'hp'])
vars(wtt3)
{'df_constraints': array([ 1,  2, 11,  9], dtype=int64),
 'dist_args': None,
 'distribution': 'chi2',
 'pvalues': array([  1.36079010e-15,   2.25083535e-01,   5.01311810e-16,
          1.70172799e-12]),
 'statistic': array([ 63.82352279,   2.98256735,  97.73373168,  74.83714306]),
 'table':         statistic        pvalue  df_constraint
 fuel    63.823523  1.360790e-15              1
 drive    2.982567  2.250835e-01              2
 weight  97.733732  5.013118e-16             11
 hp      74.837143  1.701728e-12              9,
 'temp': [('fuel',
   array([[ 0.,  1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,
            0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.]])),
  ('drive',
   array([[ 0.,  0.,  1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,
            0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.],
          [ 0.,  0.,  0.,  1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,
            0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.]])),
  ('weight',
   array([[ 0.,  0.,  0.,  0.,  1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,
            0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.],
          [ 0.,  0.,  0.,  0.,  0.,  1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,
            0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.],
          [ 0.,  0.,  0.,  0.,  0.,  0.,  1.,  0.,  0.,  0.,  0.,  0.,  0.,
            0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.],
          [ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  1.,  0.,  0.,  0.,  0.,  0.,
            0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.],
          [ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  1.,  0.,  0.,  0.,  0.,
            0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.],
          [ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  1.,  0.,  0.,  0.,
            0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.],
          [ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  1.,  0.,  0.,
            0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.],
          [ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  1.,  0.,
            0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.],
          [ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  1.,
            0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.],
          [ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,
            1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.],
          [ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,
            0.,  1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.]])),
  ('hp',
   array([[ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,
            0.,  0.,  1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.],
          [ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,
            0.,  0.,  0.,  1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.],
          [ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,
            0.,  0.,  0.,  0.,  1.,  0.,  0.,  0.,  0.,  0.,  0.],
          [ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,
            0.,  0.,  0.,  0.,  0.,  1.,  0.,  0.,  0.,  0.,  0.],
          [ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,
            0.,  0.,  0.,  0.,  0.,  0.,  1.,  0.,  0.,  0.,  0.],
          [ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,
            0.,  0.,  0.,  0.,  0.,  0.,  0.,  1.,  0.,  0.,  0.],
          [ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,
            0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  1.,  0.,  0.],
          [ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,
            0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  1.,  0.],
          [ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,
            0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  1.]]))]}
```

wald_test_terms does not have a `use_f` option.
so, cheating

```

res_bsf._results.use_t = True
wtt3 = res_bsf.wald_test_terms(skip_single=True, combine_terms=['fuel', 'drive', 'weight', 'hp'])
res_bsf._results.use_t = False
vars(wtt3)

{'df_constraints': array([ 1,  2, 11,  9], dtype=int64),
 'df_denom': array([ 189.13394497,  189.13394497,  189.13394497,  189.13394497]),
 'dist_args': None,
 'distribution': 'F',
 'pvalues': array([  1.29954437e-13,   2.27717973e-01,   1.13680464e-12,
          2.07948129e-10]),
 'statistic': array([ 63.82352279,   1.49128368,   8.8848847 ,   8.31523812]),
 'table':         statistic        pvalue  df_constraint    df_denom
 fuel    63.823523  1.299544e-13              1  189.133945
 drive    1.491284  2.277180e-01              2  189.133945
 weight   8.884885  1.136805e-12             11  189.133945
 hp       8.315238  2.079481e-10              9  189.133945,
 'temp': [('fuel',
   array([[ 0.,  1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,
            0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.]])),
  ('drive',
   array([[ 0.,  0.,  1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,
            0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.],
          [ 0.,  0.,  0.,  1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,
            0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.]])),
  ('weight',
   array([[ 0.,  0.,  0.,  0.,  1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,
            0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.],
          [ 0.,  0.,  0.,  0.,  0.,  1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,
            0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.],
          [ 0.,  0.,  0.,  0.,  0.,  0.,  1.,  0.,  0.,  0.,  0.,  0.,  0.,
            0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.],
          [ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  1.,  0.,  0.,  0.,  0.,  0.,
            0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.],
          [ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  1.,  0.,  0.,  0.,  0.,
            0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.],
          [ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  1.,  0.,  0.,  0.,
            0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.],
          [ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  1.,  0.,  0.,
            0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.],
          [ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  1.,  0.,
            0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.],
          [ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  1.,
            0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.],
          [ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,
            1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.],
          [ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,
            0.,  1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.]])),
  ('hp',
   array([[ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,
            0.,  0.,  1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.],
          [ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,
            0.,  0.,  0.,  1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.],
          [ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,
            0.,  0.,  0.,  0.,  1.,  0.,  0.,  0.,  0.,  0.,  0.],
          [ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,
            0.,  0.,  0.,  0.,  0.,  1.,  0.,  0.,  0.,  0.,  0.],
          [ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,
            0.,  0.,  0.,  0.,  0.,  0.,  1.,  0.,  0.,  0.,  0.],
          [ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,
            0.,  0.,  0.,  0.,  0.,  0.,  0.,  1.,  0.,  0.,  0.],
          [ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,
            0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  1.,  0.,  0.],
          [ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,
            0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  1.,  0.],
          [ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,
            0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  1.]]))]}

​```

quickfix for `wald_test_terms`: make simple constraint matrix into 2-D array (one 2-D row)

cleaner version:
unify and add/rename  `df_constraints` in wald_test, reuse df_constraint for wald_test_terms return table.
I was using this uncommitted change in the GAM PR #5370 (I will not include it there. PR not recently rebased)
This worked fine in my examples, but there are no unit tests yet.

```
@@ -1814,11 +1814,11 @@ class LikelihoodModelResults(Results):
                         combined[cname].append(constraint_matrix)
 
                 if skip_single:
                     continue
 
-                constraints.append((name, constraint_matrix))
+                constraints.append((name, np.atleast_2d(constraint_matrix)))
 
             combined_constraints = []
             for cname in combine_terms:
                 combined_constraints.append((cname, np.vstack(combined[cname])))
```
simple example with problem using GLM with data from gam tests

wrong in model created with pandas data

```
res1a.wald_test_terms(combine_terms=['drive', 'fuel'])
​
<class 'statsmodels.stats.contrast.WaldTestResults'>
                    chi2         P>chi2  df constraint
Intercept     553.340987  2.361532e-122              6
fuel[T.gas]    72.626605   1.566529e-17              6
drive[T.fwd]    2.440157   1.182644e-01              6
drive[T.rwd]    1.063788   3.023527e-01              6
weight         83.153705   7.591496e-20              6
hp             23.361817   1.342164e-06              6
drive           2.718371   2.568699e-01              2
fuel           72.626605   1.566529e-17              1
```

 model created with formulas shows correct df

```
modf = GLM.from_formula('city_mpg ~ fuel + drive + weight + hp', df_autos)
res1f = modf.fit()  # default is pirls with use_t=False
f
res1f.wald_test_terms(combine_terms=['drive', 'fuel'])
<class 'statsmodels.stats.contrast.WaldTestResults'>
                 chi2         P>chi2  df constraint
Intercept  553.340987  2.361532e-122              1
fuel        72.626605   1.566529e-17              1
drive        2.718371   2.568699e-01              2
weight      83.153705   7.591496e-20              1
hp          23.361817   1.342164e-06              1
drive        2.718371   2.568699e-01              2
fuel        72.626605   1.566529e-17              1
```
In the formula path of wald_test_terms all constraint matrices are 2-dim,
so adding `atleast_2d` to the non-formula path makes this the same

from `vars(wtf)`

```
'temp': [('Intercept', array([[ 1.,  0.,  0.,  0.,  0.,  0.]])),
  ('fuel', array([[ 0.,  1.,  0.,  0.,  0.,  0.]])),
  ('drive', array([[ 0.,  0.,  1.,  0.,  0.,  0.],
          [ 0.,  0.,  0.,  1.,  0.,  0.]])),
  ('weight', array([[ 0.,  0.,  0.,  0.,  1.,  0.]])),
  ('hp', array([[ 0.,  0.,  0.,  0.,  0.,  1.]])),
  ('drive', array([[ 0.,  0.,  1.,  0.,  0.,  0.],
          [ 0.,  0.,  0.,  1.,  0.,  0.]])),
  ('fuel', array([[ 0.,  1.,  0.,  0.,  0.,  0.]]))]
```

Resolving patch:
diff --git a/statsmodels/base/model.py b/statsmodels/base/model.py
--- a/statsmodels/base/model.py
+++ b/statsmodels/base/model.py
@@ -1842,7 +1842,7 @@ def wald_test_terms(self, skip_single=False, extra_constraints=None,
         else:
             # check by exog/params names if there is no formula info
             for col, name in enumerate(result.model.exog_names):
-                constraint_matrix = identity[col]
+                constraint_matrix = np.atleast_2d(identity[col])
 
                 # check if in combined
                 for cname in combine_terms:


Test patch:
diff --git a/statsmodels/base/tests/test_generic_methods.py b/statsmodels/base/tests/test_generic_methods.py
--- a/statsmodels/base/tests/test_generic_methods.py
+++ b/statsmodels/base/tests/test_generic_methods.py
@@ -531,18 +531,26 @@ def initialize(cls):
         cls.res = mod.fit(use_t=False)
 
     def test_noformula(self):
+        # this verifies single and composite constraints against explicit
+        #     wald test
         endog = self.res.model.endog
         exog = self.res.model.data.orig_exog
         exog = pd.DataFrame(exog)
 
         res = sm.OLS(endog, exog).fit()
-        wa = res.wald_test_terms(skip_single=True,
+        wa = res.wald_test_terms(skip_single=False,
                                  combine_terms=['Duration', 'Weight'])
         eye = np.eye(len(res.params))
+
+        c_single = [row for row in eye]
         c_weight = eye[2:6]
         c_duration = eye[[1, 4, 5]]
 
-        compare_waldres(res, wa, [c_duration, c_weight])
+        compare_waldres(res, wa, c_single + [c_duration, c_weight])
+
+        # assert correct df_constraints, see #5475 for bug in single constraint
+        df_constraints = [1] * len(c_single) + [3, 4]
+        assert_equal(wa.df_constraints, df_constraints)
 
 
 class TestWaldAnovaOLSF(CheckAnovaMixin):