diff --git a/doc/source/whatsnew/v0.24.0.rst b/doc/source/whatsnew/v0.24.0.rst index 1fe5e4e6e7087..da1ea62e6f49a 100644 --- a/doc/source/whatsnew/v0.24.0.rst +++ b/doc/source/whatsnew/v0.24.0.rst @@ -1538,6 +1538,7 @@ Missing MultiIndex ^^^^^^^^^^ +- Bug in :func:`io.formats.style.Styler.applymap` where ``subset=`` with :class:`MultiIndex` slice would reduce to :class:`Series` (:issue:`19861`) - Removed compatibility for :class:`MultiIndex` pickles prior to version 0.8.0; compatibility with :class:`MultiIndex` pickles from version 0.13 forward is maintained (:issue:`21654`) - :meth:`MultiIndex.get_loc_level` (and as a consequence, ``.loc`` on a ``Series`` or ``DataFrame`` with a :class:`MultiIndex` index) will now raise a ``KeyError``, rather than returning an empty ``slice``, if asked a label which is present in the ``levels`` but is unused (:issue:`22221`) - :class:`MultiIndex` has gained the :meth:`MultiIndex.from_frame`, it allows constructing a :class:`MultiIndex` object from a :class:`DataFrame` (:issue:`22420`) diff --git a/pandas/core/indexing.py b/pandas/core/indexing.py index ab4ad693a462e..3504c6e12b896 100755 --- a/pandas/core/indexing.py +++ b/pandas/core/indexing.py @@ -2732,8 +2732,10 @@ def _non_reducing_slice(slice_): slice_ = IndexSlice[:, slice_] def pred(part): - # true when slice does *not* reduce - return isinstance(part, slice) or is_list_like(part) + # true when slice does *not* reduce, False when part is a tuple, + # i.e. MultiIndex slice + return ((isinstance(part, slice) or is_list_like(part)) + and not isinstance(part, tuple)) if not is_list_like(slice_): if not isinstance(slice_, slice): diff --git a/pandas/tests/indexing/test_indexing.py b/pandas/tests/indexing/test_indexing.py index 03f1975c50d2a..2224c3ab9935a 100644 --- a/pandas/tests/indexing/test_indexing.py +++ b/pandas/tests/indexing/test_indexing.py @@ -812,6 +812,23 @@ def test_non_reducing_slice(self): tslice_ = _non_reducing_slice(slice_) assert isinstance(df.loc[tslice_], DataFrame) + def test_non_reducing_slice_on_multiindex(self): + # GH 19861 + dic = { + ('a', 'd'): [1, 4], + ('a', 'c'): [2, 3], + ('b', 'c'): [3, 2], + ('b', 'd'): [4, 1] + } + df = pd.DataFrame(dic, index=[0, 1]) + idx = pd.IndexSlice + slice_ = idx[:, idx['b', 'd']] + tslice_ = _non_reducing_slice(slice_) + + result = df.loc[tslice_] + expected = pd.DataFrame({('b', 'd'): [4, 1]}) + tm.assert_frame_equal(result, expected) + def test_list_slice(self): # like dataframe getitem slices = [['A'], Series(['A']), np.array(['A'])] diff --git a/pandas/tests/io/formats/test_style.py b/pandas/tests/io/formats/test_style.py index fa8bd91dce939..3432d686a9fd6 100644 --- a/pandas/tests/io/formats/test_style.py +++ b/pandas/tests/io/formats/test_style.py @@ -274,6 +274,32 @@ def f(x): col in self.df.loc[slice_].columns} assert result == expected + def test_applymap_subset_multiindex(self): + # GH 19861 + # Smoke test for applymap + def color_negative_red(val): + """ + Takes a scalar and returns a string with + the css property `'color: red'` for negative + strings, black otherwise. + """ + color = 'red' if val < 0 else 'black' + return 'color: %s' % color + + dic = { + ('a', 'd'): [-1.12, 2.11], + ('a', 'c'): [2.78, -2.88], + ('b', 'c'): [-3.99, 3.77], + ('b', 'd'): [4.21, -1.22], + } + + idx = pd.IndexSlice + df = pd.DataFrame(dic, index=[0, 1]) + + (df.style + .applymap(color_negative_red, subset=idx[:, idx['b', 'd']]) + .render()) + def test_where_with_one_style(self): # GH 17474 def f(x):