diff --git a/doc/source/whatsnew/v1.3.0.rst b/doc/source/whatsnew/v1.3.0.rst index 6abe70e56b9b9..e19c98039f279 100644 --- a/doc/source/whatsnew/v1.3.0.rst +++ b/doc/source/whatsnew/v1.3.0.rst @@ -437,6 +437,8 @@ Other - Bug in :class:`Styler` which caused CSS to duplicate on multiple renders. (:issue:`39395`) - :meth:`Index.where` behavior now mirrors :meth:`Index.putmask` behavior, i.e. ``index.where(mask, other)`` matches ``index.putmask(~mask, other)`` (:issue:`39412`) - Bug in :func:`pandas.testing.assert_series_equal`, :func:`pandas.testing.assert_frame_equal`, :func:`pandas.testing.assert_index_equal` and :func:`pandas.testing.assert_extension_array_equal` incorrectly raising when an attribute has an unrecognized NA type (:issue:`39461`) +- Bug in :class:`Styler` where ``subset`` arg in methods raised an error for some valid multiindex slices (:issue:`33562`) +- - .. --------------------------------------------------------------------------- diff --git a/pandas/core/indexing.py b/pandas/core/indexing.py index ce7d5b511e811..cc7c5f666feda 100644 --- a/pandas/core/indexing.py +++ b/pandas/core/indexing.py @@ -2407,9 +2407,11 @@ def pred(part) -> bool: """ # 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 isinstance(part, tuple): + # GH#39421 check for sub-slice: + return any((isinstance(s, slice) or is_list_like(s)) for s in part) + else: + return isinstance(part, slice) or is_list_like(part) if not is_list_like(slice_): if not isinstance(slice_, slice): diff --git a/pandas/tests/indexing/multiindex/test_slice.py b/pandas/tests/indexing/multiindex/test_slice.py index b60135a802b8e..6c7d5f06ac355 100644 --- a/pandas/tests/indexing/multiindex/test_slice.py +++ b/pandas/tests/indexing/multiindex/test_slice.py @@ -780,6 +780,42 @@ def test_non_reducing_slice_on_multiindex(self): expected = DataFrame({("b", "d"): [4, 1]}) tm.assert_frame_equal(result, expected) + @pytest.mark.parametrize( + "slice_", + [ + pd.IndexSlice[:, :], + # check cols + pd.IndexSlice[:, pd.IndexSlice[["a"]]], # inferred deeper need list + pd.IndexSlice[:, pd.IndexSlice[["a"], ["c"]]], # inferred deeper need list + pd.IndexSlice[:, pd.IndexSlice["a", "c", :]], + pd.IndexSlice[:, pd.IndexSlice["a", :, "e"]], + pd.IndexSlice[:, pd.IndexSlice[:, "c", "e"]], + pd.IndexSlice[:, pd.IndexSlice["a", ["c", "d"], :]], # check list + pd.IndexSlice[:, pd.IndexSlice["a", ["c", "d", "-"], :]], # allow missing + pd.IndexSlice[:, pd.IndexSlice["a", ["c", "d", "-"], "e"]], # no slice + # check rows + pd.IndexSlice[pd.IndexSlice[["U"]], :], # inferred deeper need list + pd.IndexSlice[pd.IndexSlice[["U"], ["W"]], :], # inferred deeper need list + pd.IndexSlice[pd.IndexSlice["U", "W", :], :], + pd.IndexSlice[pd.IndexSlice["U", :, "Y"], :], + pd.IndexSlice[pd.IndexSlice[:, "W", "Y"], :], + pd.IndexSlice[pd.IndexSlice[:, "W", ["Y", "Z"]], :], # check list + pd.IndexSlice[pd.IndexSlice[:, "W", ["Y", "Z", "-"]], :], # allow missing + pd.IndexSlice[pd.IndexSlice["U", "W", ["Y", "Z", "-"]], :], # no slice + # check simultaneous + pd.IndexSlice[pd.IndexSlice[:, "W", "Y"], pd.IndexSlice["a", "c", :]], + ], + ) + def test_non_reducing_multi_slice_on_multiindex(self, slice_): + # GH 33562 + cols = pd.MultiIndex.from_product([["a", "b"], ["c", "d"], ["e", "f"]]) + idxs = pd.MultiIndex.from_product([["U", "V"], ["W", "X"], ["Y", "Z"]]) + df = DataFrame(np.arange(64).reshape(8, 8), columns=cols, index=idxs) + + expected = df.loc[slice_] + result = df.loc[non_reducing_slice(slice_)] + tm.assert_frame_equal(result, expected) + def test_loc_slice_negative_stepsize(self): # GH#38071 mi = MultiIndex.from_product([["a", "b"], [0, 1]]) diff --git a/pandas/tests/io/formats/test_style.py b/pandas/tests/io/formats/test_style.py index 6556075272308..0e732ddc0a27b 100644 --- a/pandas/tests/io/formats/test_style.py +++ b/pandas/tests/io/formats/test_style.py @@ -377,29 +377,26 @@ def f(x): } assert result == expected - def test_applymap_subset_multiindex(self): + @pytest.mark.parametrize( + "slice_", + [ + pd.IndexSlice[:, pd.IndexSlice["x", "A"]], + pd.IndexSlice[:, pd.IndexSlice[:, "A"]], + pd.IndexSlice[:, pd.IndexSlice[:, ["A", "C"]]], # missing col element + pd.IndexSlice[pd.IndexSlice["a", 1], :], + pd.IndexSlice[pd.IndexSlice[:, 1], :], + pd.IndexSlice[pd.IndexSlice[:, [1, 3]], :], # missing row element + pd.IndexSlice[:, ("x", "A")], + pd.IndexSlice[("a", 1), :], + ], + ) + def test_applymap_subset_multiindex(self, slice_): # 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 f"color: {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 = DataFrame(dic, index=[0, 1]) - - (df.style.applymap(color_negative_red, subset=idx[:, idx["b", "d"]]).render()) + # edited for GH 33562 + idx = pd.MultiIndex.from_product([["a", "b"], [1, 2]]) + col = pd.MultiIndex.from_product([["x", "y"], ["A", "B"]]) + df = DataFrame(np.random.rand(4, 4), columns=col, index=idx) + df.style.applymap(lambda x: "color: red;", subset=slice_).render() def test_applymap_subset_multiindex_code(self): # https://github.com/pandas-dev/pandas/issues/25858