Repository: pandas-dev/pandas

Issue:
ERR: invalid MultiIndex construction
#### Code Sample, a copy-pastable example if possible

```python
import pandas as pd
import numpy as np
idx0 = range(2)
idx1 = np.repeat(range(2), 2)

midx = pd.MultiIndex(
    levels=[idx0, idx1],
    labels=[
        np.repeat(range(len(idx0)), len(idx1)),
        np.tile(range(len(idx1)), len(idx0))
    ],
    names=['idx0', 'idx1']
)

df = pd.DataFrame(
    [
        [i**2/float(j), 'example{}'.format(i), i**3/float(j)]
        for j in range(1, len(idx0) + 1)
        for i in range(1, len(idx1) + 1)
    ],
    columns=['col0', 'col1', 'col2'],
    index=midx
)

example = df.loc[[(0, 1)]]
```

For display
```python
In [13]: example
Out[13]:
           col0      col1  col2
idx0 idx1
0    1     16.0  example4  64.0
```
#### Problem description

Taken from this StackOverflow post:
 - https://stackoverflow.com/q/46098097/4013571

Given the following dataframe
```python
           col0      col1  col2
idx0 idx1
0    0      1.0  example1   1.0
     0      4.0  example2   8.0
     1      9.0  example3  27.0
     1     16.0  example4  64.0
1    0      0.5  example1   0.5
     0      2.0  example2   4.0
     1      4.5  example3  13.5
     1      8.0  example4  32.0
```

the [`.xs`](https://pandas.pydata.org/pandas-docs/stable/generated/pandas.DataFrame.xs.html) operation will select

```python
In [121]: df.xs((0,1), level=[0,1])
Out[121]:
           col0      col1  col2
idx0 idx1
0    1      9.0  example3  27.0
     1     16.0  example4  64.0
```

whilst the [`.loc`](https://pandas.pydata.org/pandas-docs/stable/generated/pandas.DataFrame.loc.html) operation will select

```python
In [125]: df.loc[[(0,1)]]
Out[125]:
           col0      col1  col2
idx0 idx1
0    1     16.0  example4  64.0
```

This is highlighted even further by the following

```python
In [149]: df.loc[pd.IndexSlice[:, 1], :]
Out[149]:
           col0      col1  col2
idx0 idx1
0    1      9.0  example3  27.0
     1     16.0  example4  64.0

In [150]: df.loc[pd.IndexSlice[0, 1], :]
Out[150]:
col0          16
col1    example4
col2          64
Name: (0, 1), dtype: object
```

#### Expected Output

Note that this only works for this minimal example because there is only one label in level 0 index axis
```python
In [8]: df.loc[pd.IndexSlice[:, 1], :]
Out[8]:
           col0      col1  col2
idx0 idx1
0    1      9.0  example3  27.0
     1     16.0  example4  64.0
```
#### Output of ``pd.show_versions()``

<details>

INSTALLED VERSIONS
------------------
commit: None
python: 2.7.13.final.0
python-bits: 64
OS: Windows
OS-release: 7
machine: AMD64
processor: Intel64 Family 6 Model 79 Stepping 1, GenuineIntel
byteorder: little
LC_ALL: None
LANG: None
LOCALE: None.None

pandas: 0.20.3
pytest: 3.0.7
pip: 9.0.1
setuptools: 36.2.7
Cython: 0.25.2
numpy: 1.13.1
scipy: 0.19.0
xarray: None
IPython: 5.3.0
sphinx: 1.5.6
patsy: 0.4.1
dateutil: 2.6.1
pytz: 2017.2
blosc: None
bottleneck: 1.2.1
tables: 3.2.2
numexpr: 2.6.2
feather: None
matplotlib: 2.0.2
openpyxl: 2.4.7
xlrd: 1.0.0
xlwt: 1.2.0
xlsxwriter: 0.9.6
lxml: 3.7.3
bs4: 4.6.0
html5lib: 0.999
sqlalchemy: 1.1.9
pymysql: None
psycopg2: None
jinja2: 2.9.6
s3fs: None
pandas_gbq: None
pandas_datareader: 0.5.0

</details>



Discussion:
The `midx` returned as constructed above is as follows:

```
MultiIndex(levels=[[0, 1], 
                   [0, 0, 1, 1]],
           labels=[[0, 0, 0, 0, 1, 1, 1, 1], 
                   [0, 0, 1, 1, 0, 0, 1, 1]],
           names=[u'idx0', u'idx1'])
```

Whereas typical construction would be:

```
midx = pd.MultiIndex.from_product([[0, 1], [0, 0, 1, 1]], names=['idx0', 'idx1'])
>>> midx
MultiIndex(levels=[[0, 1], 
                   [0, 1]],
           labels=[[0, 0, 0, 0, 1, 1, 1, 1], 
                   [0, 0, 1, 1, 0, 0, 1, 1]],
           names=[u'idx0', u'idx1'])
```
Using the typical construction,  indexing via `df.loc[[(0, 1)]]` appears to behave as expected.

I believe the issue here is how this indexing handles duplicates in one of the levels (i.e. the `1`).
your directly constructing the MultiIndex is violating guarantees, namely that the levels are each unique. We don't explicitly check this as the public constructors guarantee this. I suppose we could check this when ``verify_integrity=True``.

want to do a PR for this?

to clarify, you certainly can have a non-unique MultiIndex (though generally discouraged as they are not that performant), but you would have duplicate ``labels``, never ``level`` values.

Resolving patch:
diff --git a/pandas/core/indexes/multi.py b/pandas/core/indexes/multi.py
--- a/pandas/core/indexes/multi.py
+++ b/pandas/core/indexes/multi.py
@@ -177,8 +177,8 @@ def _verify_integrity(self, labels=None, levels=None):
         Raises
         ------
         ValueError
-            * if length of levels and labels don't match or any label would
-            exceed level bounds
+            If length of levels and labels don't match, if any label would
+            exceed level bounds, or there are any duplicate levels.
         """
         # NOTE: Currently does not check, among other things, that cached
         # nlevels matches nor that sortorder matches actually sortorder.
@@ -198,6 +198,11 @@ def _verify_integrity(self, labels=None, levels=None):
                                  " level  (%d). NOTE: this index is in an"
                                  " inconsistent state" % (i, label.max(),
                                                           len(level)))
+            if not level.is_unique:
+                raise ValueError("Level values must be unique: {values} on "
+                                 "level {level}".format(
+                                     values=[value for value in level],
+                                     level=i))
 
     @property
     def levels(self):


Test patch:
diff --git a/pandas/tests/groupby/test_functional.py b/pandas/tests/groupby/test_functional.py
--- a/pandas/tests/groupby/test_functional.py
+++ b/pandas/tests/groupby/test_functional.py
@@ -52,10 +52,10 @@ def test_frame_describe_multikey(self):
         desc_groups = []
         for col in self.tsframe:
             group = grouped[col].describe()
-            group_col = pd.MultiIndex([[col] * len(group.columns),
-                                       group.columns],
-                                      [[0] * len(group.columns),
-                                       range(len(group.columns))])
+            # GH 17464 - Remove duplicate MultiIndex levels
+            group_col = pd.MultiIndex(
+                levels=[[col], group.columns],
+                labels=[[0] * len(group.columns), range(len(group.columns))])
             group = pd.DataFrame(group.values,
                                  columns=group_col,
                                  index=group.index)
@@ -67,8 +67,9 @@ def test_frame_describe_multikey(self):
                                          'C': 1, 'D': 1}, axis=1)
         result = groupedT.describe()
         expected = self.tsframe.describe().T
-        expected.index = pd.MultiIndex([[0, 0, 1, 1], expected.index],
-                                       [range(4), range(len(expected.index))])
+        expected.index = pd.MultiIndex(
+            levels=[[0, 1], expected.index],
+            labels=[[0, 0, 1, 1], range(len(expected.index))])
         tm.assert_frame_equal(result, expected)
 
     def test_frame_describe_tupleindex(self):
diff --git a/pandas/tests/indexes/test_multi.py b/pandas/tests/indexes/test_multi.py
--- a/pandas/tests/indexes/test_multi.py
+++ b/pandas/tests/indexes/test_multi.py
@@ -1618,7 +1618,9 @@ def test_is_(self):
         # shouldn't change
         assert mi2.is_(mi)
         mi4 = mi3.view()
-        mi4.set_levels([[1 for _ in range(10)], lrange(10)], inplace=True)
+
+        # GH 17464 - Remove duplicate MultiIndex levels
+        mi4.set_levels([lrange(10), lrange(10)], inplace=True)
         assert not mi4.is_(mi3)
         mi5 = mi.view()
         mi5.set_levels(mi5.levels, inplace=True)
@@ -2450,13 +2452,11 @@ def test_isna_behavior(self):
             pd.isna(self.index)
 
     def test_level_setting_resets_attributes(self):
-        ind = MultiIndex.from_arrays([
+        ind = pd.MultiIndex.from_arrays([
             ['A', 'A', 'B', 'B', 'B'], [1, 2, 1, 2, 3]
         ])
         assert ind.is_monotonic
-        ind.set_levels([['A', 'B', 'A', 'A', 'B'], [2, 1, 3, -2, 5]],
-                       inplace=True)
-
+        ind.set_levels([['A', 'B'], [1, 3, 2]], inplace=True)
         # if this fails, probably didn't reset the cache correctly.
         assert not ind.is_monotonic
 
@@ -3083,3 +3083,16 @@ def test_million_record_attribute_error(self):
         with tm.assert_raises_regex(AttributeError,
                                     "'Series' object has no attribute 'foo'"):
             df['a'].foo()
+
+    def test_duplicate_multiindex_labels(self):
+        # GH 17464
+        # Make sure that a MultiIndex with duplicate levels throws a ValueError
+        with pytest.raises(ValueError):
+            ind = pd.MultiIndex([['A'] * 10, range(10)], [[0] * 10, range(10)])
+
+        # And that using set_levels with duplicate levels fails
+        ind = MultiIndex.from_arrays([['A', 'A', 'B', 'B', 'B'],
+                                      [1, 2, 1, 2, 3]])
+        with pytest.raises(ValueError):
+            ind.set_levels([['A', 'B', 'A', 'A', 'B'], [2, 1, 3, -2, 5]],
+                           inplace=True)