Skip to content

Commit caf482d

Browse files
authored
BUG: Subset slicer on Styler failed on MultiIndex with slice(None) (#39421)
1 parent 34191dd commit caf482d

File tree

4 files changed

+62
-25
lines changed

4 files changed

+62
-25
lines changed

doc/source/whatsnew/v1.3.0.rst

+2
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,8 @@ Other
438438
- Bug in :class:`Styler` which caused CSS to duplicate on multiple renders. (:issue:`39395`)
439439
- :meth:`Index.where` behavior now mirrors :meth:`Index.putmask` behavior, i.e. ``index.where(mask, other)`` matches ``index.putmask(~mask, other)`` (:issue:`39412`)
440440
- 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`)
441+
- Bug in :class:`Styler` where ``subset`` arg in methods raised an error for some valid multiindex slices (:issue:`33562`)
442+
-
441443
-
442444

443445
.. ---------------------------------------------------------------------------

pandas/core/indexing.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -2407,9 +2407,11 @@ def pred(part) -> bool:
24072407
"""
24082408
# true when slice does *not* reduce, False when part is a tuple,
24092409
# i.e. MultiIndex slice
2410-
return (isinstance(part, slice) or is_list_like(part)) and not isinstance(
2411-
part, tuple
2412-
)
2410+
if isinstance(part, tuple):
2411+
# GH#39421 check for sub-slice:
2412+
return any((isinstance(s, slice) or is_list_like(s)) for s in part)
2413+
else:
2414+
return isinstance(part, slice) or is_list_like(part)
24132415

24142416
if not is_list_like(slice_):
24152417
if not isinstance(slice_, slice):

pandas/tests/indexing/multiindex/test_slice.py

+36
Original file line numberDiff line numberDiff line change
@@ -780,6 +780,42 @@ def test_non_reducing_slice_on_multiindex(self):
780780
expected = DataFrame({("b", "d"): [4, 1]})
781781
tm.assert_frame_equal(result, expected)
782782

783+
@pytest.mark.parametrize(
784+
"slice_",
785+
[
786+
pd.IndexSlice[:, :],
787+
# check cols
788+
pd.IndexSlice[:, pd.IndexSlice[["a"]]], # inferred deeper need list
789+
pd.IndexSlice[:, pd.IndexSlice[["a"], ["c"]]], # inferred deeper need list
790+
pd.IndexSlice[:, pd.IndexSlice["a", "c", :]],
791+
pd.IndexSlice[:, pd.IndexSlice["a", :, "e"]],
792+
pd.IndexSlice[:, pd.IndexSlice[:, "c", "e"]],
793+
pd.IndexSlice[:, pd.IndexSlice["a", ["c", "d"], :]], # check list
794+
pd.IndexSlice[:, pd.IndexSlice["a", ["c", "d", "-"], :]], # allow missing
795+
pd.IndexSlice[:, pd.IndexSlice["a", ["c", "d", "-"], "e"]], # no slice
796+
# check rows
797+
pd.IndexSlice[pd.IndexSlice[["U"]], :], # inferred deeper need list
798+
pd.IndexSlice[pd.IndexSlice[["U"], ["W"]], :], # inferred deeper need list
799+
pd.IndexSlice[pd.IndexSlice["U", "W", :], :],
800+
pd.IndexSlice[pd.IndexSlice["U", :, "Y"], :],
801+
pd.IndexSlice[pd.IndexSlice[:, "W", "Y"], :],
802+
pd.IndexSlice[pd.IndexSlice[:, "W", ["Y", "Z"]], :], # check list
803+
pd.IndexSlice[pd.IndexSlice[:, "W", ["Y", "Z", "-"]], :], # allow missing
804+
pd.IndexSlice[pd.IndexSlice["U", "W", ["Y", "Z", "-"]], :], # no slice
805+
# check simultaneous
806+
pd.IndexSlice[pd.IndexSlice[:, "W", "Y"], pd.IndexSlice["a", "c", :]],
807+
],
808+
)
809+
def test_non_reducing_multi_slice_on_multiindex(self, slice_):
810+
# GH 33562
811+
cols = pd.MultiIndex.from_product([["a", "b"], ["c", "d"], ["e", "f"]])
812+
idxs = pd.MultiIndex.from_product([["U", "V"], ["W", "X"], ["Y", "Z"]])
813+
df = DataFrame(np.arange(64).reshape(8, 8), columns=cols, index=idxs)
814+
815+
expected = df.loc[slice_]
816+
result = df.loc[non_reducing_slice(slice_)]
817+
tm.assert_frame_equal(result, expected)
818+
783819
def test_loc_slice_negative_stepsize(self):
784820
# GH#38071
785821
mi = MultiIndex.from_product([["a", "b"], [0, 1]])

pandas/tests/io/formats/test_style.py

+19-22
Original file line numberDiff line numberDiff line change
@@ -377,29 +377,26 @@ def f(x):
377377
}
378378
assert result == expected
379379

380-
def test_applymap_subset_multiindex(self):
380+
@pytest.mark.parametrize(
381+
"slice_",
382+
[
383+
pd.IndexSlice[:, pd.IndexSlice["x", "A"]],
384+
pd.IndexSlice[:, pd.IndexSlice[:, "A"]],
385+
pd.IndexSlice[:, pd.IndexSlice[:, ["A", "C"]]], # missing col element
386+
pd.IndexSlice[pd.IndexSlice["a", 1], :],
387+
pd.IndexSlice[pd.IndexSlice[:, 1], :],
388+
pd.IndexSlice[pd.IndexSlice[:, [1, 3]], :], # missing row element
389+
pd.IndexSlice[:, ("x", "A")],
390+
pd.IndexSlice[("a", 1), :],
391+
],
392+
)
393+
def test_applymap_subset_multiindex(self, slice_):
381394
# GH 19861
382-
# Smoke test for applymap
383-
def color_negative_red(val):
384-
"""
385-
Takes a scalar and returns a string with
386-
the css property `'color: red'` for negative
387-
strings, black otherwise.
388-
"""
389-
color = "red" if val < 0 else "black"
390-
return f"color: {color}"
391-
392-
dic = {
393-
("a", "d"): [-1.12, 2.11],
394-
("a", "c"): [2.78, -2.88],
395-
("b", "c"): [-3.99, 3.77],
396-
("b", "d"): [4.21, -1.22],
397-
}
398-
399-
idx = pd.IndexSlice
400-
df = DataFrame(dic, index=[0, 1])
401-
402-
(df.style.applymap(color_negative_red, subset=idx[:, idx["b", "d"]]).render())
395+
# edited for GH 33562
396+
idx = pd.MultiIndex.from_product([["a", "b"], [1, 2]])
397+
col = pd.MultiIndex.from_product([["x", "y"], ["A", "B"]])
398+
df = DataFrame(np.random.rand(4, 4), columns=col, index=idx)
399+
df.style.applymap(lambda x: "color: red;", subset=slice_).render()
403400

404401
def test_applymap_subset_multiindex_code(self):
405402
# https://github.com/pandas-dev/pandas/issues/25858

0 commit comments

Comments
 (0)