diff --git a/doc/source/whatsnew/v0.21.0.txt b/doc/source/whatsnew/v0.21.0.txt index f50052347cfb5..f3e0119545aeb 100644 --- a/doc/source/whatsnew/v0.21.0.txt +++ b/doc/source/whatsnew/v0.21.0.txt @@ -325,6 +325,7 @@ Other API Changes - Compression defaults in HDF stores now follow pytable standards. Default is no compression and if ``complib`` is missing and ``complevel`` > 0 ``zlib`` is used (:issue:`15943`) - ``Index.get_indexer_non_unique()`` now returns a ndarray indexer rather than an ``Index``; this is consistent with ``Index.get_indexer()`` (:issue:`16819`) - Removed the ``@slow`` decorator from ``pandas.util.testing``, which caused issues for some downstream packages' test suites. Use ``@pytest.mark.slow`` instead, which achieves the same thing (:issue:`16850`) +- func:`pandas.MultiIndex.set_levels()` now allows the setting of empty levels and, when `level` is a list-like object, each element of levels is confirmed to be list-like (:issue:`16147`) - Moved definition of ``MergeError`` to the ``pandas.errors`` module. - The signature of :func:`Series.set_axis` and :func:`DataFrame.set_axis` has been changed from ``set_axis(axis, labels)`` to ``set_axis(labels, axis=0)``, for consistency with the rest of the API. The old signature is deprecated and will show a ``FutureWarning`` (:issue:`14636`) - :func:`Series.argmin` and :func:`Series.argmax` will now raise a ``TypeError`` when used with ``object`` dtypes, instead of a ``ValueError`` (:issue:`13595`) diff --git a/pandas/core/indexes/multi.py b/pandas/core/indexes/multi.py index 8b2cf0e7c0b40..3c3f6e4e0d161 100644 --- a/pandas/core/indexes/multi.py +++ b/pandas/core/indexes/multi.py @@ -229,16 +229,28 @@ def set_levels(self, levels, level=None, inplace=False, labels=[[0, 0, 1, 1], [0, 1, 0, 1]], names=[u'foo', u'bar']) """ - if level is not None and not is_list_like(level): - if not is_list_like(levels): - raise TypeError("Levels must be list-like") - if is_list_like(levels[0]): - raise TypeError("Levels must be list-like") + + level_list_like = level is None or is_list_like(level) + levels_list_like = (is_list_like(levels) and + all(is_list_like(x) for x in levels) and + levels != [] ) + + if level_list_like: + # level is a list-like object of scalars + levels_error = ("`levels` and `level` are incompatible. " + "When `level` is list-like, `levels` must be a " + "list-like object containing list-like objects") + if not levels_list_like: + raise TypeError(levels_error) + elif not level_list_like: + # level is a scalar + levels_error = ("`levels` and `level` are incompatible. " + "When `level` is a scalar, `levels` must be a " + " list-like object of scalars.") + if levels_list_like: + raise TypeError(levels_error) level = [level] levels = [levels] - elif level is None or is_list_like(level): - if not is_list_like(levels) or not is_list_like(levels[0]): - raise TypeError("Levels must be list of lists-like") if inplace: idx = self diff --git a/pandas/tests/indexing/test_multiindex.py b/pandas/tests/indexing/test_multiindex.py index c12bb8910ffc9..09f450463b51a 100644 --- a/pandas/tests/indexing/test_multiindex.py +++ b/pandas/tests/indexing/test_multiindex.py @@ -428,6 +428,50 @@ def test_xs_multiindex(self): expected.columns = expected.columns.droplevel('lvl1') tm.assert_frame_equal(result, expected) + def test_set_level_checkall(self): + + idx = MultiIndex.from_tuples([(1, u'one'), (1, u'two'), + (2, u'one'), (2, u'two')], + names=['foo', 'bar']) + result = idx.set_levels([['a', 'b'], [1, 2]]) + expected = MultiIndex(levels=[[u'a', u'b'], [1, 2]], + labels=[[0, 0, 1, 1], [0, 1, 0, 1]], + names=[u'foo', u'bar']) + tm.assert_index_equal(result, expected) + + result = idx.set_levels(['a', 'b'], level=0) + expected = MultiIndex(levels=[[u'a', u'b'], [u'one', u'two']], + labels=[[0, 0, 1, 1], [0, 1, 0, 1]], + names=[u'foo', u'bar']) + tm.assert_index_equal(result, expected) + + result = idx.set_levels(['a', 'b'], level='bar') + expected = MultiIndex(levels=[[1, 2], [u'a', u'b']], + labels=[[0, 0, 1, 1], [0, 1, 0, 1]], + names=[u'foo', u'bar']) + tm.assert_index_equal(result, expected) + + result = idx.set_levels([['a', 'b'], [1, 2]], level=[0, 1]) + expected = MultiIndex(levels=[[u'a', u'b'], [1, 2]], + labels=[[0, 0, 1, 1], [0, 1, 0, 1]], + names=[u'foo', u'bar']) + tm.assert_index_equal(result, expected) + + # setting empty levels are allowed + idx = MultiIndex(levels=[['L1'], ['L2']], labels=[[], []], + names=['a', 'b']) + result = idx.set_levels([], level='a') + expected = MultiIndex(levels=[[], ['L2']], labels=[[], []], + names=['a', 'b']) + tm.assert_index_equal(result, expected) + + idx = MultiIndex(levels=[['L1'], ['L2']], labels=[[], []], + names=['a', 'b']) + result = idx.set_levels([[], []], level=['a', 'b']) + expected = MultiIndex(levels=[[], []], labels=[[], []], + names=['a', 'b']) + tm.assert_index_equal(result, expected) + def test_multiindex_setitem(self): # GH 3738