Skip to content

BUG: Subset slicer on Styler failed on MultiIndex with slice(None) #39421

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Feb 4, 2021
2 changes: 2 additions & 0 deletions doc/source/whatsnew/v1.3.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,8 @@ Other
- Bug in constructing a :class:`Series` from a list and a :class:`PandasDtype` (:issue:`39357`)
- Bug in :class:`Styler` which caused CSS to duplicate on multiple renders. (:issue:`39395`)
- 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`)
-

.. ---------------------------------------------------------------------------

Expand Down
11 changes: 8 additions & 3 deletions pandas/core/indexing.py
Original file line number Diff line number Diff line change
Expand Up @@ -2407,9 +2407,14 @@ 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):
# check for sub-slice:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you add "GH#39421" here

instead of a for loop can you do this with an any(...)

for subpart in part:
if isinstance(subpart, slice) or is_list_like(subpart):
return True
return False
else:
return isinstance(part, slice) or is_list_like(part)

if not is_list_like(slice_):
if not isinstance(slice_, slice):
Expand Down
36 changes: 36 additions & 0 deletions pandas/tests/indexing/multiindex/test_slice.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]])
Expand Down
41 changes: 19 additions & 22 deletions pandas/tests/io/formats/test_style.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down