diff --git a/doc/source/whatsnew/v0.18.1.txt b/doc/source/whatsnew/v0.18.1.txt index 0aeeb281566ac..ee186aadb3156 100644 --- a/doc/source/whatsnew/v0.18.1.txt +++ b/doc/source/whatsnew/v0.18.1.txt @@ -233,3 +233,4 @@ Bug Fixes - Bug in ``.describe()`` resets categorical columns information (:issue:`11558`) - Bug where ``loffset`` argument was not applied when calling ``resample().count()`` on a timeseries (:issue:`12725`) - ``pd.read_excel()`` now accepts path objects (e.g. ``pathlib.Path``, ``py.path.local``) for the file path, in line with other ``read_*`` functions (:issue:`12655`) +- Bug in ``.loc`` raises inconsistent error when called on an unsorted ``MultiIndex`` (:issue:12660) diff --git a/pandas/indexes/multi.py b/pandas/indexes/multi.py index de3a67ebc1abf..2fc3d102dd55a 100644 --- a/pandas/indexes/multi.py +++ b/pandas/indexes/multi.py @@ -1595,6 +1595,8 @@ def get_loc_level(self, key, level=0, drop_level=True): ---------- key : label or tuple level : int/level name or list thereof + drop_level : bool + drop a level from the index if only a single element is selected Returns ------- @@ -1638,6 +1640,18 @@ def maybe_droplevels(indexer, levels, drop_level): if isinstance(key, list): key = tuple(key) + # must be lexsorted to at least as many levels as the level parameter, + # or the number of items in the key tuple. + # Note: level is 0-based + required_lexsort_depth = level + 1 + if isinstance(key, tuple): + required_lexsort_depth = max(required_lexsort_depth, len(key)) + if self.lexsort_depth < required_lexsort_depth: + raise KeyError('MultiIndex Slicing requires the index to be ' + 'fully lexsorted tuple len ({0}), lexsort depth ' + '({1})'.format(required_lexsort_depth, + self.lexsort_depth)) + if isinstance(key, tuple) and level == 0: try: diff --git a/pandas/tests/indexing/test_indexing.py b/pandas/tests/indexing/test_indexing.py index e5be2bb08f605..601504b34b3b6 100644 --- a/pandas/tests/indexing/test_indexing.py +++ b/pandas/tests/indexing/test_indexing.py @@ -2300,6 +2300,36 @@ def f(): 'lexsorted tuple len \(2\), lexsort depth \(0\)'): df.loc[(slice(None), df.loc[:, ('a', 'bar')] > 5), :] + def test_multiindex_slicers_raise_key_error(self): + + # GH6134 + # Test that mi slicers raise a KeyError with the proper error message + # on unsorted indices regardless of the invocation method + iterables1 = [['a', 'b'], [2, 1]] + iterables2 = [['c', 'd'], [4, 3]] + rows = pd.MultiIndex.from_product(iterables1, + names=['row1', 'row2']) + columns = pd.MultiIndex.from_product(iterables2, + names=['col1', 'col2']) + df = pd.DataFrame(np.random.randn(4, 4), index=rows, columns=columns) + + # In this example rows are not sorted at all, + # columns are sorted to the first level + self.assertEqual(df.index.lexsort_depth, 1) + self.assertEqual(df.columns.lexsort_depth, 0) + + with tm.assertRaisesRegexp( + KeyError, + 'MultiIndex Slicing requires the index to be fully ' + 'lexsorted tuple len \(\d\), lexsort depth \(\d\)'): + df.loc[('a', slice(None)), 'b'] + + with tm.assertRaisesRegexp( + KeyError, + 'MultiIndex Slicing requires the index to be fully ' + 'lexsorted tuple len \(\d\), lexsort depth \(\d\)'): + df.loc['a', 'b'] + def test_multiindex_slicers_non_unique(self): # GH 7106