Skip to content

Commit 8561eb5

Browse files
authored
BUG: MultiIndex slicing with negative step (#46156)
1 parent a49e0f5 commit 8561eb5

File tree

3 files changed

+59
-15
lines changed

3 files changed

+59
-15
lines changed

doc/source/whatsnew/v1.5.0.rst

+3
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,9 @@ Missing
409409

410410
MultiIndex
411411
^^^^^^^^^^
412+
- Bug in :meth:`DataFrame.loc` returning empty result when slicing a :class:`MultiIndex` with a negative step size and non-null start/stop values (:issue:`46156`)
413+
- Bug in :meth:`DataFrame.loc` raising when slicing a :class:`MultiIndex` with a negative step size other than -1 (:issue:`46156`)
414+
- Bug in :meth:`DataFrame.loc` raising when slicing a :class:`MultiIndex` with a negative step size and slicing a non-int labeled index level (:issue:`46156`)
412415
- Bug in :meth:`Series.to_numpy` where multiindexed Series could not be converted to numpy arrays when an ``na_value`` was supplied (:issue:`45774`)
413416
- Bug in :class:`MultiIndex.equals` not commutative when only one side has extension array dtype (:issue:`46026`)
414417
-

pandas/core/indexes/multi.py

+12-7
Original file line numberDiff line numberDiff line change
@@ -3162,9 +3162,6 @@ def convert_indexer(start, stop, step, indexer=indexer, codes=level_codes):
31623162
# given the inputs and the codes/indexer, compute an indexer set
31633163
# if we have a provided indexer, then this need not consider
31643164
# the entire labels set
3165-
if step is not None and step < 0:
3166-
# Switch elements for negative step size
3167-
start, stop = stop - 1, start - 1
31683165
r = np.arange(start, stop, step)
31693166

31703167
if indexer is not None and len(indexer) != len(codes):
@@ -3196,19 +3193,25 @@ def convert_indexer(start, stop, step, indexer=indexer, codes=level_codes):
31963193
if isinstance(key, slice):
31973194
# handle a slice, returning a slice if we can
31983195
# otherwise a boolean indexer
3196+
step = key.step
3197+
is_negative_step = step is not None and step < 0
31993198

32003199
try:
32013200
if key.start is not None:
32023201
start = level_index.get_loc(key.start)
3202+
elif is_negative_step:
3203+
start = len(level_index) - 1
32033204
else:
32043205
start = 0
3206+
32053207
if key.stop is not None:
32063208
stop = level_index.get_loc(key.stop)
3209+
elif is_negative_step:
3210+
stop = 0
32073211
elif isinstance(start, slice):
32083212
stop = len(level_index)
32093213
else:
32103214
stop = len(level_index) - 1
3211-
step = key.step
32123215
except KeyError:
32133216

32143217
# we have a partial slice (like looking up a partial date
@@ -3228,8 +3231,9 @@ def convert_indexer(start, stop, step, indexer=indexer, codes=level_codes):
32283231
elif level > 0 or self._lexsort_depth == 0 or step is not None:
32293232
# need to have like semantics here to right
32303233
# searching as when we are using a slice
3231-
# so include the stop+1 (so we include stop)
3232-
return convert_indexer(start, stop + 1, step)
3234+
# so adjust the stop by 1 (so we include stop)
3235+
stop = (stop - 1) if is_negative_step else (stop + 1)
3236+
return convert_indexer(start, stop, step)
32333237
else:
32343238
# sorted, so can return slice object -> view
32353239
i = algos.searchsorted(level_codes, start, side="left")
@@ -3520,7 +3524,8 @@ def _reorder_indexer(
35203524

35213525
new_order = key_order_map[self.codes[i][indexer]]
35223526
elif isinstance(k, slice) and k.step is not None and k.step < 0:
3523-
new_order = np.arange(n)[k][indexer]
3527+
# flip order for negative step
3528+
new_order = np.arange(n)[::-1][indexer]
35243529
elif isinstance(k, slice) and k.start is None and k.stop is None:
35253530
# slice(None) should not determine order GH#31330
35263531
new_order = np.ones((n,))[indexer]

pandas/tests/indexing/multiindex/test_slice.py

+44-8
Original file line numberDiff line numberDiff line change
@@ -758,12 +758,48 @@ def test_int_series_slicing(self, multiindex_year_month_day_dataframe_random_dat
758758
expected = ymd.reindex(s.index[5:])
759759
tm.assert_frame_equal(result, expected)
760760

761-
def test_loc_slice_negative_stepsize(self):
761+
@pytest.mark.parametrize(
762+
"dtype, loc, iloc",
763+
[
764+
# dtype = int, step = -1
765+
("int", slice(None, None, -1), slice(None, None, -1)),
766+
("int", slice(3, None, -1), slice(3, None, -1)),
767+
("int", slice(None, 1, -1), slice(None, 0, -1)),
768+
("int", slice(3, 1, -1), slice(3, 0, -1)),
769+
# dtype = int, step = -2
770+
("int", slice(None, None, -2), slice(None, None, -2)),
771+
("int", slice(3, None, -2), slice(3, None, -2)),
772+
("int", slice(None, 1, -2), slice(None, 0, -2)),
773+
("int", slice(3, 1, -2), slice(3, 0, -2)),
774+
# dtype = str, step = -1
775+
("str", slice(None, None, -1), slice(None, None, -1)),
776+
("str", slice("d", None, -1), slice(3, None, -1)),
777+
("str", slice(None, "b", -1), slice(None, 0, -1)),
778+
("str", slice("d", "b", -1), slice(3, 0, -1)),
779+
# dtype = str, step = -2
780+
("str", slice(None, None, -2), slice(None, None, -2)),
781+
("str", slice("d", None, -2), slice(3, None, -2)),
782+
("str", slice(None, "b", -2), slice(None, 0, -2)),
783+
("str", slice("d", "b", -2), slice(3, 0, -2)),
784+
],
785+
)
786+
def test_loc_slice_negative_stepsize(self, dtype, loc, iloc):
762787
# GH#38071
763-
mi = MultiIndex.from_product([["a", "b"], [0, 1]])
764-
df = DataFrame([[1, 2], [3, 4], [5, 6], [7, 8]], index=mi)
765-
result = df.loc[("a", slice(None, None, -1)), :]
766-
expected = DataFrame(
767-
[[3, 4], [1, 2]], index=MultiIndex.from_tuples([("a", 1), ("a", 0)])
768-
)
769-
tm.assert_frame_equal(result, expected)
788+
labels = {
789+
"str": list("abcde"),
790+
"int": range(5),
791+
}[dtype]
792+
793+
mi = MultiIndex.from_arrays([labels] * 2)
794+
df = DataFrame(1.0, index=mi, columns=["A"])
795+
796+
SLC = pd.IndexSlice
797+
798+
expected = df.iloc[iloc, :]
799+
result_get_loc = df.loc[SLC[loc], :]
800+
result_get_locs_level_0 = df.loc[SLC[loc, :], :]
801+
result_get_locs_level_1 = df.loc[SLC[:, loc], :]
802+
803+
tm.assert_frame_equal(result_get_loc, expected)
804+
tm.assert_frame_equal(result_get_locs_level_0, expected)
805+
tm.assert_frame_equal(result_get_locs_level_1, expected)

0 commit comments

Comments
 (0)