diff --git a/doc/source/whatsnew/v1.1.0.rst b/doc/source/whatsnew/v1.1.0.rst index f1a219ad232de..e90030a6cffb1 100644 --- a/doc/source/whatsnew/v1.1.0.rst +++ b/doc/source/whatsnew/v1.1.0.rst @@ -565,6 +565,7 @@ Indexing - Bug in :meth:`DatetimeIndex.insert` and :meth:`TimedeltaIndex.insert` causing index ``freq`` to be lost when setting an element into an empty :class:`Series` (:issue:33573`) - Bug in :meth:`Series.__setitem__` with an :class:`IntervalIndex` and a list-like key of integers (:issue:`33473`) - Bug in :meth:`Series.__getitem__` allowing missing labels with ``np.ndarray``, :class:`Index`, :class:`Series` indexers but not ``list``, these now all raise ``KeyError`` (:issue:`33646`) +- Bug in :meth:`DataFrame.truncate` and :meth:`Series.truncate` where index was assumed to be monotone increasing (:issue:`33756`) Missing ^^^^^^^ diff --git a/pandas/core/generic.py b/pandas/core/generic.py index c761bd8cb465a..ed421718c400d 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -9196,6 +9196,9 @@ def truncate( if before > after: raise ValueError(f"Truncate: {after} must be after {before}") + if ax.is_monotonic_decreasing: + before, after = after, before + slicer = [slice(None, None)] * self._AXIS_LEN slicer[axis] = slice(before, after) result = self.loc[tuple(slicer)] diff --git a/pandas/tests/frame/methods/test_truncate.py b/pandas/tests/frame/methods/test_truncate.py index ad86ee1266874..768a5f22fb063 100644 --- a/pandas/tests/frame/methods/test_truncate.py +++ b/pandas/tests/frame/methods/test_truncate.py @@ -87,3 +87,20 @@ def test_truncate_nonsortedindex(self): msg = "truncate requires a sorted index" with pytest.raises(ValueError, match=msg): df.truncate(before=2, after=20, axis=1) + + @pytest.mark.parametrize( + "before, after, indices", + [(1, 2, [2, 1]), (None, 2, [2, 1, 0]), (1, None, [3, 2, 1])], + ) + @pytest.mark.parametrize("klass", [pd.Int64Index, pd.DatetimeIndex]) + def test_truncate_decreasing_index(self, before, after, indices, klass): + # https://github.com/pandas-dev/pandas/issues/33756 + idx = klass([3, 2, 1, 0]) + if klass is pd.DatetimeIndex: + before = pd.Timestamp(before) if before is not None else None + after = pd.Timestamp(after) if after is not None else None + indices = [pd.Timestamp(i) for i in indices] + values = pd.DataFrame(range(len(idx)), index=idx) + result = values.truncate(before=before, after=after) + expected = values.loc[indices] + tm.assert_frame_equal(result, expected) diff --git a/pandas/tests/series/methods/test_truncate.py b/pandas/tests/series/methods/test_truncate.py index c97369b349f56..47947f0287494 100644 --- a/pandas/tests/series/methods/test_truncate.py +++ b/pandas/tests/series/methods/test_truncate.py @@ -80,6 +80,23 @@ def test_truncate_nonsortedindex(self): with pytest.raises(ValueError, match=msg): ts.sort_values(ascending=False).truncate(before="2011-11", after="2011-12") + @pytest.mark.parametrize( + "before, after, indices", + [(1, 2, [2, 1]), (None, 2, [2, 1, 0]), (1, None, [3, 2, 1])], + ) + @pytest.mark.parametrize("klass", [pd.Int64Index, pd.DatetimeIndex]) + def test_truncate_decreasing_index(self, before, after, indices, klass): + # https://github.com/pandas-dev/pandas/issues/33756 + idx = klass([3, 2, 1, 0]) + if klass is pd.DatetimeIndex: + before = pd.Timestamp(before) if before is not None else None + after = pd.Timestamp(after) if after is not None else None + indices = [pd.Timestamp(i) for i in indices] + values = pd.Series(range(len(idx)), index=idx) + result = values.truncate(before=before, after=after) + expected = values.loc[indices] + tm.assert_series_equal(result, expected) + def test_truncate_datetimeindex_tz(self): # GH 9243 idx = date_range("4/1/2005", "4/30/2005", freq="D", tz="US/Pacific")