Skip to content

Commit 8613007

Browse files
authored
ENH: preserve RangeIndex in insert, delete (#44086)
1 parent 7dd34ea commit 8613007

File tree

2 files changed

+124
-7
lines changed

2 files changed

+124
-7
lines changed

pandas/core/indexes/range.py

+43-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@
1515

1616
import numpy as np
1717

18-
from pandas._libs import index as libindex
18+
from pandas._libs import (
19+
index as libindex,
20+
lib,
21+
)
1922
from pandas._libs.lib import no_default
2023
from pandas._typing import (
2124
Dtype,
@@ -726,9 +729,41 @@ def symmetric_difference(self, other, result_name: Hashable = None, sort=None):
726729

727730
# --------------------------------------------------------------------
728731

732+
# error: Return type "Index" of "delete" incompatible with return type
733+
# "RangeIndex" in supertype "Index"
734+
def delete(self, loc) -> Index: # type: ignore[override]
735+
# In some cases we can retain RangeIndex, see also
736+
# DatetimeTimedeltaMixin._get_delete_Freq
737+
if is_integer(loc):
738+
if loc == 0 or loc == -len(self):
739+
return self[1:]
740+
if loc == -1 or loc == len(self) - 1:
741+
return self[:-1]
742+
743+
elif lib.is_list_like(loc):
744+
slc = lib.maybe_indices_to_slice(np.asarray(loc, dtype=np.intp), len(self))
745+
if isinstance(slc, slice) and slc.step is not None and slc.step < 0:
746+
rng = range(len(self))[slc][::-1]
747+
slc = slice(rng.start, rng.stop, rng.step)
748+
749+
if isinstance(slc, slice) and slc.step in [1, None]:
750+
# Note: maybe_indices_to_slice will never return a slice
751+
# with 'slc.start is None'; may have slc.stop None in cases
752+
# with negative step
753+
if slc.start == 0:
754+
return self[slc.stop :]
755+
elif slc.stop in [len(self), None]:
756+
return self[: slc.start]
757+
758+
# TODO: more generally, self.difference(self[slc]),
759+
# once _difference is better about retaining RangeIndex
760+
761+
return super().delete(loc)
762+
729763
def insert(self, loc: int, item) -> Index:
730764
if len(self) and (is_integer(item) or is_float(item)):
731-
# We can retain RangeIndex is inserting at the beginning or end
765+
# We can retain RangeIndex is inserting at the beginning or end,
766+
# or right in the middle.
732767
rng = self._range
733768
if loc == 0 and item == self[0] - self.step:
734769
new_rng = range(rng.start - rng.step, rng.stop, rng.step)
@@ -738,6 +773,12 @@ def insert(self, loc: int, item) -> Index:
738773
new_rng = range(rng.start, rng.stop + rng.step, rng.step)
739774
return type(self)._simple_new(new_rng, name=self.name)
740775

776+
elif len(self) == 2 and item == self[0] + self.step / 2:
777+
# e.g. inserting 1 into [0, 2]
778+
step = int(self.step / 2)
779+
new_rng = range(self.start, self.stop, step)
780+
return type(self)._simple_new(new_rng, name=self.name)
781+
741782
return super().insert(loc, item)
742783

743784
def _concat(self, indexes: list[Index], name: Hashable) -> Index:

pandas/tests/indexes/ranges/test_range.py

+81-5
Original file line numberDiff line numberDiff line change
@@ -127,25 +127,101 @@ def test_insert(self):
127127
expected = Index([0, pd.NaT, 1, 2, 3, 4], dtype=object)
128128
tm.assert_index_equal(result, expected)
129129

130+
def test_insert_edges_preserves_rangeindex(self):
131+
idx = Index(range(4, 9, 2))
132+
133+
result = idx.insert(0, 2)
134+
expected = Index(range(2, 9, 2))
135+
tm.assert_index_equal(result, expected, exact=True)
136+
137+
result = idx.insert(3, 10)
138+
expected = Index(range(4, 11, 2))
139+
tm.assert_index_equal(result, expected, exact=True)
140+
141+
def test_insert_middle_preserves_rangeindex(self):
142+
# insert in the middle
143+
idx = Index(range(0, 3, 2))
144+
result = idx.insert(1, 1)
145+
expected = Index(range(3))
146+
tm.assert_index_equal(result, expected, exact=True)
147+
148+
idx = idx * 2
149+
result = idx.insert(1, 2)
150+
expected = expected * 2
151+
tm.assert_index_equal(result, expected, exact=True)
152+
130153
def test_delete(self):
131154

132155
idx = RangeIndex(5, name="Foo")
133-
expected = idx[1:].astype(int)
156+
expected = idx[1:]
134157
result = idx.delete(0)
135-
# TODO: could preserve RangeIndex at the ends
136-
tm.assert_index_equal(result, expected, exact="equiv")
158+
tm.assert_index_equal(result, expected, exact=True)
137159
assert result.name == expected.name
138160

139-
expected = idx[:-1].astype(int)
161+
expected = idx[:-1]
140162
result = idx.delete(-1)
141-
tm.assert_index_equal(result, expected, exact="equiv")
163+
tm.assert_index_equal(result, expected, exact=True)
142164
assert result.name == expected.name
143165

144166
msg = "index 5 is out of bounds for axis 0 with size 5"
145167
with pytest.raises((IndexError, ValueError), match=msg):
146168
# either depending on numpy version
147169
result = idx.delete(len(idx))
148170

171+
def test_delete_preserves_rangeindex(self):
172+
idx = Index(range(2), name="foo")
173+
174+
result = idx.delete([1])
175+
expected = Index(range(1), name="foo")
176+
tm.assert_index_equal(result, expected, exact=True)
177+
178+
result = idx.delete(1)
179+
tm.assert_index_equal(result, expected, exact=True)
180+
181+
def test_delete_preserves_rangeindex_list_at_end(self):
182+
idx = RangeIndex(0, 6, 1)
183+
184+
loc = [2, 3, 4, 5]
185+
result = idx.delete(loc)
186+
expected = idx[:2]
187+
tm.assert_index_equal(result, expected, exact=True)
188+
189+
result = idx.delete(loc[::-1])
190+
tm.assert_index_equal(result, expected, exact=True)
191+
192+
def test_delete_preserves_rangeindex_list_middle(self):
193+
idx = RangeIndex(0, 6, 1)
194+
195+
loc = [1, 2, 3, 4]
196+
result = idx.delete(loc)
197+
expected = RangeIndex(0, 6, 5)
198+
tm.assert_index_equal(result, expected, exact="equiv") # TODO: retain!
199+
200+
result = idx.delete(loc[::-1])
201+
tm.assert_index_equal(result, expected, exact="equiv") # TODO: retain!
202+
203+
def test_delete_all_preserves_rangeindex(self):
204+
idx = RangeIndex(0, 6, 1)
205+
206+
loc = [0, 1, 2, 3, 4, 5]
207+
result = idx.delete(loc)
208+
expected = idx[:0]
209+
tm.assert_index_equal(result, expected, exact=True)
210+
211+
result = idx.delete(loc[::-1])
212+
tm.assert_index_equal(result, expected, exact=True)
213+
214+
def test_delete_not_preserving_rangeindex(self):
215+
idx = RangeIndex(0, 6, 1)
216+
217+
loc = [0, 3, 5]
218+
result = idx.delete(loc)
219+
expected = Int64Index([1, 2, 4])
220+
tm.assert_index_equal(result, expected, exact=True)
221+
222+
result = idx.delete(loc[::-1])
223+
tm.assert_index_equal(result, expected, exact=True)
224+
149225
def test_view(self):
150226
i = RangeIndex(0, name="Foo")
151227
i_view = i.view()

0 commit comments

Comments
 (0)