Repository: statsmodels/statsmodels

Issue:
TypeError for Tukey Test
#### Bug
A type error occurs if the tukey test is performed using specific data.

#### Code Sample
Following code snippet triggers the error:
```
from statsmodels.stats.multicomp import MultiComparison
data=[23.0, 23.0, 12.0, 11.0, 23.0, 23.0, 12.0, 11.0]
labels=['DAPI', 'DAPI', 'DAPI', 'DAPI', 'ATTO', 'ATTO', 'ATTO', 'ATTO']
res = MultiComparison(data, labels)
tukey = res.tukeyhsd()
print(tukey.summary())
```
If one of the 23s is changed to 26, the bug does not occur:
```
from statsmodels.stats.multicomp import MultiComparison
data=[26.0, 23.0, 12.0, 11.0, 23.0, 23.0, 12.0, 11.0]
labels=['DAPI', 'DAPI', 'DAPI', 'DAPI', 'ATTO', 'ATTO', 'ATTO', 'ATTO']
res = MultiComparison(data, labels)
tukey = res.tukeyhsd()
print(tukey.summary())
```
The other values can also be changed in a way that will make the code work. For example changing one of the 12s to 16 will work as well.

<details>

```
TypeError                                 Traceback (most recent call last)
<ipython-input-35-3b34488f294a> in <module>
      3 labels=['DAPI', 'DAPI', 'DAPI', 'DAPI', 'ATTO', 'ATTO', 'ATTO', 'ATTO']
      4 res = MultiComparison(data, labels)
----> 5 tukey = res.tukeyhsd()
      6 print(tukey.summary())

~\.conda\envs\tf_gpu\lib\site-packages\statsmodels\sandbox\stats\multicomp.py in tukeyhsd(self, alpha)
   1011                                np.round(res[4][:, 0], 4),
   1012                                np.round(res[4][:, 1], 4),
-> 1013                                res[1]),
   1014                           dtype=[('group1', object),
   1015                                  ('group2', object),

~\.conda\envs\tf_gpu\lib\site-packages\statsmodels\compat\python.py in lzip(*args, **kwargs)
     60 
     61     def lzip(*args, **kwargs):
---> 62         return list(zip(*args, **kwargs))
     63 
     64     def lmap(*args, **kwargs):

TypeError: zip argument #4 must support iteration
```

</details>


#### Expected Output

Well, i expect the function to work.

#### Output of ``import statsmodels.api as sm; sm.show_versions()``

<details>
INSTALLED VERSIONS
------------------
Python: 3.6.8.final.0

Statsmodels
===========

Installed: 0.10.1

Required Dependencies
=====================

cython: 0.29.13
numpy: 1.16.3 
scipy: 1.2.1 
pandas: 0.25.0 
    dateutil: 2.8.0 
patsy: 0.5.1 

Optional Dependencies
=====================

matplotlib: 3.0.3 
    backend: module://ipykernel.pylab.backend_inline 
cvxopt: Not installed
joblib: Not installed

Developer Tools
================

IPython: 7.5.0 
    jinja2: 2.10.1 
sphinx: Not installed
    pygments: 2.4.0 
pytest: Not installed
virtualenv: Not installed

</details>



Discussion:
I guess that's a corner case that is (currently) not supported.

You only have two levels in the group variable, and so there is no multiple comparison, just a single comparison.

It looks like that if there is only a single comparison, then some values are converted to scalars instead of a 1 element iterable.
However, I don't know whether the inference and tukey pvalues and critical values support a single comparison. If not, then it would be necessary to handle the single comparison just like a two sample t-test

I'm facing the same error with the same situation: Two levels in group variables. Maybe the [R implementation](https://www.rdocumentation.org/packages/stats/versions/3.6.2/topics/TukeyHSD) of TukeyHSD is a good reference to update the function? It works for the given situation.
2 sample case works without exception, e.g. original example with one 26 instead of 23.
So it's not there is something special here
exception shows when first value is between 21 and 25, but not for values outside a fractionally larger interval than this. (I didn't try many fractional cases, 20.5 runs ok, 20.9 doesn't)

weird

It looks like the problem is in ` statsmodels.stats.libqsturng.qsturng._psturng` which returns scalar if p-value is at bounds.

The exception is in zip of the last value of the return of the tukeyhsd function which is called by the tukeyhsd method. That return are the pvalues.

update maybe not, it looks like psturng has consistent return dimension (scalar or array independent of values). (need debugger to look more closely and not a notebook)

quickfix would be to add `atleast_1d`  in the zip, curing the symptom but not the underlying bug/inconsistency
The following is the same for `psturng` as for `_psturng`

It only happens with the first argument, i.e. if that is a 1 element, 1d, (1,) array
If any of the other two have shape (1,), then the return is scalar in all cases of the loop

```
for q_ in range(10):
    q = q_ / 9
    print(q, qs._psturng(np.array([q]), 1.3, 3))
0.0 0.9
0.1111111111111111 0.9
0.2222222222222222 0.9
0.3333333333333333 0.9
0.4444444444444444 [0.37832146]
0.5555555555555556 [0.34315768]
0.6666666666666666 [0.31123725]
0.7777777777777778 [0.2822719]
0.8888888888888888 [0.25629549]
1.0 [0.23274786]
```

but scalar case is consistent:

```
for q_ in range(10):
    q = q_ / 9
    print(q, qs._psturng(np.array(q), 1.3, 3))
0.0 0.9
0.1111111111111111 0.9
0.2222222222222222 0.9
0.3333333333333333 0.9
0.4444444444444444 0.3783214598083995
0.5555555555555556 0.34315768092019705
0.6666666666666666 0.31123724658719576
0.7777777777777778 0.28227190048295125
0.8888888888888888 0.2562954851383107
1.0 0.23274786051515117
```

`qsturng._isfloat` checks for 1 float element, not for scalar, so this case does not go through `np.vectorize`

```
list(map(qs._isfloat, [np.array(q), np.array([1.3]), np.array(3)]))
[True, True, True]
```
I think we go with the quickfix for now. That's faster than deciding on the proper shape of qstrung function returns.

i.e. add atleast_1d on line 993
`np.round(np.atleast_1d(res[8]), 4),

summary goes through, pvalues look continuous on the switch in code path

however, attribute `pvalues` still has inconsistent dimension with only the atleast_1d fix in the summary table


```
-
#
for v1 in np.linspace(20, 21, 11): 
    data[0] = v1
    labels=['DAPI', 'DAPI', 'DAPI', 'DAPI', 'ATTO', 'ATTO', 'ATTO', 'ATTO']
    res = MultiComparison(data, labels)
    tukey = res.tukeyhsd()
    print(v1, tukey.pvalues)
    #print(tukey.summary())
20.0 [0.87447738]
20.1 [0.87847191]
20.2 [0.88245892]
20.3 [0.88643625]
20.4 [0.89040577]
20.5 [0.89436469]
20.6 [0.89831465]
20.7 0.9
20.8 0.9
20.9 0.9
21.0 0.9
```

better fix, leaving qsturng alone, is in adding atleast_1d in the standalone tukeyhsd function 

In tukeyhsd everything is designed for multiple comparison, so we return also a array,list or iterable in the case when we only have 1 comparison.

```
    pvalues = get_tukey_pvalue(n_means, df_total, st_range)
    # we need pvalues to be atleast_1d for iteration. see 6132
    pvalues = np.atleast_1d(pvalues)
```

Resolving patch:
diff --git a/statsmodels/sandbox/stats/multicomp.py b/statsmodels/sandbox/stats/multicomp.py
--- a/statsmodels/sandbox/stats/multicomp.py
+++ b/statsmodels/sandbox/stats/multicomp.py
@@ -1295,6 +1295,8 @@ def tukeyhsd(mean_all, nobs_all, var_all, df=None, alpha=0.05, q_crit=None):
         q_crit = get_tukeyQcrit2(n_means, df_total, alpha=alpha)
 
     pvalues = get_tukey_pvalue(n_means, df_total, st_range)
+    # we need pvalues to be atleast_1d for iteration. see #6132
+    pvalues = np.atleast_1d(pvalues)
 
     reject = st_range > q_crit
     crit_int = std_pairs * q_crit


Test patch: