API: Have MultiIndex consturctors always return a MI
State: closed

This removes the special case for MultiIndex constructors returning
an Index if all the levels are length-1. Now this will return a
MultiIndex with a single level.

This is a backwards incompatabile change, with no clear method for
deprecation, so we're making a clean break.

Old Behavior:

```python
In [1]: import pandas as pd

In [2]: pd.MultiIndex.from_tuples([('a',), ('b',)])
Out[2]: Index(['a', 'b'], dtype='object')

```

New:

```python
In [1]: import pandas as pd
In [2]: pd.MultiIndex.from_tuples([('a',), ('b',)])
Out[2]:
MultiIndex(levels=[['a', 'b']],
           labels=[[0, 1]])
```

Closes #17178


Labels:
name: Indexing
description: Related to indexing on series/frames, not to indexes themselves
name: API Design
name: MultiIndex

Comments:
# [Codecov](https://codecov.io/gh/pandas-dev/pandas/pull/17236?src=pr&el=h1) Report
> Merging [#17236](https://codecov.io/gh/pandas-dev/pandas/pull/17236?src=pr&el=desc) into [master](https://codecov.io/gh/pandas-dev/pandas/commit/77bfe21c7229e724d01721bb84861283baf7e9d3?src=pr&el=desc) will **decrease** coverage by `0.01%`.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/pandas-dev/pandas/pull/17236/graphs/tree.svg?src=pr&token=eZ4WkYLtcO&width=650&height=150)](https://codecov.io/gh/pandas-dev/pandas/pull/17236?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master   #17236      +/-   ##
==========================================
- Coverage   91.01%   90.99%   -0.02%     
==========================================
  Files         163      163              
  Lines       49570    49580      +10     
==========================================
  Hits        45116    45116              
- Misses       4454     4464      +10
```

| Flag | Coverage Δ | |
|---|---|---|
| #multiple | `88.77% <100%> (ø)` | :arrow_up: |
| #single | `40.26% <53.12%> (-0.06%)` | :arrow_down: |

| [Impacted Files](https://codecov.io/gh/pandas-dev/pandas/pull/17236?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [pandas/util/testing.py](https://codecov.io/gh/pandas-dev/pandas/pull/17236?src=pr&el=tree#diff-cGFuZGFzL3V0aWwvdGVzdGluZy5weQ==) | `100% <ø> (ø)` | :arrow_up: |
| [pandas/core/indexes/multi.py](https://codecov.io/gh/pandas-dev/pandas/pull/17236?src=pr&el=tree#diff-cGFuZGFzL2NvcmUvaW5kZXhlcy9tdWx0aS5weQ==) | `96.9% <ø> (-0.03%)` | :arrow_down: |
| [pandas/core/reshape/reshape.py](https://codecov.io/gh/pandas-dev/pandas/pull/17236?src=pr&el=tree#diff-cGFuZGFzL2NvcmUvcmVzaGFwZS9yZXNoYXBlLnB5) | `99.29% <100%> (ø)` | :arrow_up: |
| [pandas/io/parsers.py](https://codecov.io/gh/pandas-dev/pandas/pull/17236?src=pr&el=tree#diff-cGFuZGFzL2lvL3BhcnNlcnMucHk=) | `95.46% <100%> (ø)` | :arrow_up: |
| [pandas/core/strings.py](https://codecov.io/gh/pandas-dev/pandas/pull/17236?src=pr&el=tree#diff-cGFuZGFzL2NvcmUvc3RyaW5ncy5weQ==) | `98.46% <100%> (ø)` | :arrow_up: |
| [pandas/core/sparse/scipy\_sparse.py](https://codecov.io/gh/pandas-dev/pandas/pull/17236?src=pr&el=tree#diff-cGFuZGFzL2NvcmUvc3BhcnNlL3NjaXB5X3NwYXJzZS5weQ==) | `97.05% <100%> (+0.08%)` | :arrow_up: |
| [pandas/core/indexes/base.py](https://codecov.io/gh/pandas-dev/pandas/pull/17236?src=pr&el=tree#diff-cGFuZGFzL2NvcmUvaW5kZXhlcy9iYXNlLnB5) | `95.84% <100%> (-0.04%)` | :arrow_down: |
| [pandas/core/frame.py](https://codecov.io/gh/pandas-dev/pandas/pull/17236?src=pr&el=tree#diff-cGFuZGFzL2NvcmUvZnJhbWUucHk=) | `97.72% <100%> (-0.1%)` | :arrow_down: |
| [pandas/core/indexes/api.py](https://codecov.io/gh/pandas-dev/pandas/pull/17236?src=pr&el=tree#diff-cGFuZGFzL2NvcmUvaW5kZXhlcy9hcGkucHk=) | `98.78% <100%> (ø)` | :arrow_up: |
| [pandas/io/gbq.py](https://codecov.io/gh/pandas-dev/pandas/pull/17236?src=pr&el=tree#diff-cGFuZGFzL2lvL2dicS5weQ==) | `25% <0%> (-58.34%)` | :arrow_down: |
| ... and [2 more](https://codecov.io/gh/pandas-dev/pandas/pull/17236?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/pandas-dev/pandas/pull/17236?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/pandas-dev/pandas/pull/17236?src=pr&el=footer). Last update [77bfe21...5c8205c](https://codecov.io/gh/pandas-dev/pandas/pull/17236?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

I don't think this is a good idea at all. Why are you not simply converting these 1-tuples? This is just adding complexity.
> I don't think this is a good idea at all. Why are you not simply converting these 1-tuples?

For the categorical case, we need to treat `categories=[1, 2, 3]` differently than `categories=[(1,), (2,), (3,)]`. I could add additional code there to special case categories of length 1, but the fix here is much cleaner.

> This is just adding complexity.

Perhaps a bit? I'm not sure an additional keyword + a one line change to the MI constructor is a maintenance burden.

We've been trying to remove all the areas where the return type is dependent on the values, like it is here. That said, I don't think this is worth breaking API over, since it's been this way for so long. It's the mirror behavior of `Index` with tuples:

```python
In [9]: pd.Index([(1, 2), (3, 4)])
Out[9]:
MultiIndex(levels=[[1, 3], [2, 4]],
           labels=[[0, 1], [0, 1]])

In [10]: pd.Index([(1, 2), (3, 4)], tupleize_cols=False)
Out[10]: Index([(1, 2), (3, 4)], dtype='object')
```

The constructors try to be smart about what you want, but give you control if you know better what you want.
The ability to have a 1-level MI would be nice.  It's a step towards making MI and non-MI quack alike.
the ``tupleize_cols`` kw to ``Index`` is not a good example. that should be removed as well. 
This is just not a good solution here. If its a problem in ``Categorical``, then more introspection is warranted.

Adding a keyword IS adding more complexity in the constructor here.


> the tupleize_cols kw to Index is not a good example. that should be removed as well.

I disagree, but opened #17246 for the overall issue.

> If its a problem in Categorical, then more introspection is warranted.

Categorical is an *instance* of this problem surfacing. I just really don't think it should be on the user to ensure that their tuples are all length > 1 every time they want to use a MultiIndex. e.g. fixing `hash_tuples` to work on tuples of length 1. I'm not sure what you mean by "more introspection is warranted."


> the tupleize_cols kw to Index is not a good example. that should be removed as well.

kwargs just adding technical debt here. I am -1 on this soln.
@TomAugspurger the problem here is you are trying to use a well-defined public API internally. Instead you merely need to use (and possibly modify) ``ensure_index`` which is the right place for this.


I want to change the public API; I don't think the return type should depend on the data. But I don't want to break existing code, hence the keyword.
my point is why don't you just change it. I don't think

```
In [1]: import pandas as pd
In [2]: pd.MultiIndex.from_tuples([('a',), ('b',)])
Out[2]: Index(['a', 'b'], dtype='object')
```

make any senses anyhow.

adding a kw just creates *more* techincal debt here.
@jreback just to be sure I understand you correctly: you are opposed to adding a new kwarg (which indeed adds complexity), but you are OK with actually changing the behaviour so that `MultiIndex.from_tuples` with 1-length tuples returns a MultiIndex instead of an Index ?

Because in https://github.com/pandas-dev/pandas/issues/17178#issuecomment-320459446 you said that the current implementation (return plain Index from `MultiIndex.from_tuples` given 1-length tuples) "is correct" (and it is indeed also tested explicitly, so at the time this behaviour was added on purpose)
I think its clear that adding a keyword to simply avoid an existing special case is very odd. better to simply fix ``MultiIndex.from_tuples`` to simply always return a ``MultiIndex``, which eliminates the current special case.
OK, thanks for the clarification! Then I agree with just fixing the special case (although this is a back incompatible change)
Hello @TomAugspurger! Thanks for updating the PR.























Cheers ! There are no PEP8 issues in this Pull Request. :beers: 

##### Comment last updated on August 30, 2017 at 16:34 Hours UTC
Updated, if anyone has a chance to look (updated the original post to reflect the change in the PR's purpose from adding a keyword to just removing the behavior) .

We made use of this in quite a few places, and all the extra if checks are a bit annoying, but I think worth it in the end to know that `MultiIndex` will always return a MultiIndex.
If we find the if/else statements too annoying, we could also have a private 'squeeze' method on a MultiIndex that returns a plain Index if it has only one level and otherwise leaves it as a MultiIndex. 
But it seems it are not that many places that you needed to change it.
Yeah, I think not enough places to warrant a private method.

On Wed, Aug 23, 2017 at 3:22 PM, Joris Van den Bossche <
notifications@github.com> wrote:

> If we find the if/else statements too annoying, we could also have a
> private 'squeeze' method on a MultiIndex that returns a plain Index if it
> has only one level and otherwise leaves it as a MultiIndex.
> But it seems it are not that many places that you needed to change it.
>
> —
> You are receiving this because you were mentioned.
> Reply to this email directly, view it on GitHub
> <https://github.com/pandas-dev/pandas/pull/17236#issuecomment-324451265>,
> or mute the thread
> <https://github.com/notifications/unsubscribe-auth/ABQHIslQkD1lPIBkC5KWyzTGMp-wMUTwks5sbInrgaJpZM4O1Yv7>
> .
>

Updated. Instead of using `_ensure_index` I added a small helper method `_index_from_sequences` that does the `if len(x) == 1` checking. `_ensure_index` seemed to be sufficiently different from what I needed to warrant a new method.
I think all the comments have been addressed.