Skip to content

Preserve step in RangeIndex.arange and slicing#11362

Open
mokashang wants to merge 1 commit into
pydata:mainfrom
mokashang:fix/range-index-arange-step
Open

Preserve step in RangeIndex.arange and slicing#11362
mokashang wants to merge 1 commit into
pydata:mainfrom
mokashang:fix/range-index-arange-step

Conversation

@mokashang
Copy link
Copy Markdown

Description

Fixes #11325.

RangeIndex is stored lazily as (start, stop, size) and the spacing is re-derived on demand as (stop - start) / size. That derivation only equals the requested spacing when (stop - start) is an exact multiple of the step, so RangeIndex.arange silently dropped the user's step whenever it didn't evenly divide the interval:

>>> import numpy as np
>>> from xarray.indexes import RangeIndex
>>> np.arange(0.0, 1.0, 0.3)
array([0. , 0.3, 0.6, 0.9])
>>> idx = RangeIndex.arange(0.0, 1.0, 0.3, dim="x")
>>> idx.step
0.25                                  # requested 0.3
>>> np.asarray(idx.to_pandas_index())
array([0.  , 0.25, 0.5 , 0.75])       # expected [0. , 0.3, 0.6, 0.9]

While fixing this I found the same root cause affects strided slicing, which was previously masked in the test suite because both the slice result and the arange it was compared against shared the identical (wrong) derivation. For arange(0, 3, 0.1), isel(x=slice(4, None, 3)) returned [0.4, 0.6889, …] instead of the actually-selected [0.4, 0.7, 1.0, …].

Approach

Carry the exact step through RangeCoordinateTransform (the reporter's suggested alternative of representing the range in terms of start, stop and step directly):

  • RangeCoordinateTransform.__init__ gains an optional step; when omitted, the existing (stop - start) / size derivation is kept (correct for linspace, which divides the interval evenly by construction).
  • arange snaps stop to start + size * step and passes the exact step. Snapping stop keeps the class invariant step == (stop - start) / size intact, so equals, repr and slice stay internally consistent (e.g. index.equals(index[:]) remains true).
  • slice scales the parent step by the slice's own step ([::2] doubles it, etc.) and propagates it explicitly. This also subsumes the previous empty-slice special case.

Both arange and strided slicing now match numpy.arange across the cases I checked (positive/negative steps, evenly- and unevenly-dividing steps, forward/reverse and strided slices, slice-of-slice, empty slices).

Testing

  • Added test_range_index_arange_step_not_dividing_interval (the 0.3 regression case).
  • Updated the GH10441 strided-slice assertion in test_range_index_isel: the slice now genuinely matches numpy.arange, so it is checked via the index's equals (isclose-based) plus np.testing.assert_allclose against the numpy ground truth. Bit-exact assert_identical is no longer appropriate here because the slice computes its step as 0.1 * 3 while arange uses the literal 0.3; these are equal only up to floating-point round-off. This matches the rationale already documented on RangeIndex.equals, which uses np.isclose for exactly this reason.
  • Updated test_range_index_equals_exact to use a deliberately perturbed start, since the fix makes a sliced index bit-identical to the equivalent arange (the scenario the test previously relied on to introduce float error).
  • Full xarray/tests/test_range_index.py suite passes (27 passed); module doctests pass; ruff check/ruff format clean.

Checklist

RangeIndex stored only (start, stop, size) and re-derived the step as
(stop - start) / size. That derivation only equals the requested spacing
when (stop - start) is an exact multiple of it, so RangeIndex.arange and
strided slicing silently changed the step and produced values that did
not match numpy.arange. For example RangeIndex.arange(0.0, 1.0, 0.3)
yielded a step of 0.25 and values [0, 0.25, 0.5, 0.75] instead of the
expected [0, 0.3, 0.6, 0.9].

Carry the exact step through RangeCoordinateTransform: arange snaps stop
to start + size * step and passes the step explicitly, and slicing scales
the parent step by the slice's own step. Both now match numpy.arange.

Fixes pydata#11325
@dcherian dcherian requested a review from benbovy May 27, 2026 18:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

RangeIndex.arange does not preserve step, has different values than numpy.arange

1 participant