Skip to content

Add RangeIndex #10076

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 27 commits into from
Apr 18, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
dcc8f5b
add RangeIndex
benbovy Feb 25, 2025
04827db
Merge branch 'main' into range-index
benbovy Mar 20, 2025
adbd3d4
refactor RangeIndex
benbovy Mar 21, 2025
e7f6476
fix error raised during coords diff formatting
benbovy Mar 21, 2025
fb1b10b
assert_allclose: add support for Coordinates
benbovy Mar 21, 2025
76f58c0
add tests (wip)
benbovy Mar 21, 2025
53a02c8
assert invariants: skip check IndexVariable ...
benbovy Mar 14, 2025
9eaa530
more tests and fixes
benbovy Mar 21, 2025
e6709a1
no support for set_xindex (error msg)
benbovy Mar 21, 2025
e18725c
add public API documentation
benbovy Mar 21, 2025
cc3601f
fix doctests
benbovy Mar 21, 2025
b5c5207
add docstring examples
benbovy Mar 21, 2025
b48ed9d
[skip-ci] update whats new
benbovy Mar 21, 2025
2a27198
[skip-ci] doc: add RangeIndex factories to API (hidden)
benbovy Mar 21, 2025
f819846
add repr + start, stop, step properties
benbovy Mar 21, 2025
cd0a396
add support for rename vars and/or dims
benbovy Mar 21, 2025
4655934
step: attr -> property
benbovy Mar 24, 2025
ee11665
Merge branch 'main' into range-index
benbovy Mar 27, 2025
212a9ae
doc: list RangeIndex constructors in API ref
benbovy Mar 31, 2025
8ad8349
CoordinateTransformIndex.rename: deep copy transform
benbovy Mar 31, 2025
efe3e81
update arange and linspace signatures
benbovy Mar 31, 2025
38c8a6c
RangeIndex repr: show size, coord_name and dim props
benbovy Mar 31, 2025
24c6b55
fix doctests
benbovy Mar 31, 2025
911d76c
Merge branch 'main' into range-index
benbovy Mar 31, 2025
4a128d0
RangeIndex.arange: mimic pandas/numpy API
benbovy Apr 16, 2025
6541c98
Merge branch 'main' into range-index
benbovy Apr 16, 2025
78d0490
Merge branch 'main' into range-index
dcherian Apr 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
more tests and fixes
  • Loading branch information
benbovy committed Mar 21, 2025
commit 9eaa5301da18a4000e99d72b5d1a39475718c7d8
33 changes: 21 additions & 12 deletions xarray/indexes/range_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ class RangeCoordinateTransform(CoordinateTransform):
coord_name: Hashable
dim: str

__slots__ = ("coord_name", "dim", "size", "start", "stop")

def __init__(
self,
start: float,
Expand All @@ -44,21 +46,14 @@ def __init__(
self.dim = dim
self.size = size

def _replace(
self, start: float, stop: float, size: int
) -> "RangeCoordinateTransform":
return type(self)(
start, stop, size, self.coord_name, self.dim, dtype=self.dtype
)

def forward(self, dim_positions: dict[str, Any]) -> dict[Hashable, Any]:
positions = dim_positions[self.dim]
labels = self.start + positions * self.step
return {self.dim: labels}

def reverse(self, coord_labels: dict[Hashable, Any]) -> dict[str, Any]:
labels = coord_labels[self.coord_names[0]]
positions = (labels - self.start) - self.step
positions = (labels - self.start) / self.step
return {self.dim: positions}

def equals(self, other: CoordinateTransform) -> bool:
Expand All @@ -77,11 +72,13 @@ def slice(self, sl: slice) -> "RangeCoordinateTransform":
# TODO: support reverse transform (i.e., start > stop)?
assert sl.start < sl.stop

new_size = (sl.stop - sl.start) / sl.step
new_size = (sl.stop - sl.start) // sl.step
new_start = self.start + sl.start * self.step
new_stop = new_start + new_size * sl.step * self.step

return self._replace(new_start, new_stop, new_size)
return type(self)(
new_start, new_stop, new_size, self.coord_name, self.dim, dtype=self.dtype
)


class RangeIndex(CoordinateTransformIndex):
Expand Down Expand Up @@ -149,18 +146,30 @@ def isel(
return None
else:
# otherwise convert to a PandasIndex
values = self.transform.forward({self.dim: idxer})[self.coord_name]
values = self.transform.forward({self.dim: np.asarray(idxer)})[
self.coord_name
]
if isinstance(idxer, Variable):
new_dim = idxer.dims[0]
else:
new_dim = self.dim
return PandasIndex(values, new_dim, coord_dtype=values.dtype)
pd_index = pd.Index(values, name=self.coord_name)
return PandasIndex(pd_index, new_dim, coord_dtype=values.dtype)

def sel(
self, labels: dict[Any, Any], method=None, tolerance=None
) -> IndexSelResult:
label = labels[self.dim]

if method != "nearest":
raise ValueError("RangeIndex only supports selection with method='nearest'")

# TODO: for RangeIndex it might not be too hard to support tolerance
if tolerance is not None:
raise ValueError(
"RangeIndex doesn't support selection with a given tolerance value yet"
)

if isinstance(label, slice):
if label.step is None:
# continuous interval slice indexing (preserves the index)
Expand Down
104 changes: 101 additions & 3 deletions xarray/tests/test_range_index.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import numpy as np
import pandas as pd
import pytest

import xarray as xr
from xarray.indexes import RangeIndex
from xarray.indexes import PandasIndex, RangeIndex
from xarray.tests import assert_allclose, assert_equal, assert_identical


def create_dataset_arange(start: float, stop: float, step: float, dim: str = "x"):
def create_dataset_arange(
start: float, stop: float, step: float, dim: str = "x"
) -> xr.Dataset:
index = RangeIndex.arange(dim, dim, start, stop, step)
return xr.Dataset(coords=xr.Coordinates.from_xindex(index))

Expand All @@ -21,17 +25,111 @@ def test_range_index_linspace() -> None:
index = RangeIndex.linspace("x", "x", 0.0, 1.0, num=10, endpoint=False)
actual = xr.Coordinates.from_xindex(index)
expected = xr.Coordinates({"x": np.linspace(0.0, 1.0, num=10, endpoint=False)})
assert_equal(actual, expected)
assert_equal(actual, expected, check_default_indexes=False)

index = RangeIndex.linspace("x", "x", 0.0, 1.0, num=11, endpoint=True)
actual = xr.Coordinates.from_xindex(index)
expected = xr.Coordinates({"x": np.linspace(0.0, 1.0, num=11, endpoint=True)})
assert_allclose(actual, expected, check_default_indexes=False)


def test_range_index_dtype() -> None:
index = RangeIndex.arange("x", "x", 0.0, 1.0, 0.1, dtype=np.float32)
coords = xr.Coordinates.from_xindex(index)
assert coords["x"].dtype == np.dtype(np.float32)


def test_range_index_isel() -> None:
ds = create_dataset_arange(0.0, 1.0, 0.1)

# slicing
actual = ds.isel(x=slice(None))
assert_identical(actual, ds, check_default_indexes=False)

actual = ds.isel(x=slice(1, None))
expected = create_dataset_arange(0.1, 1.0, 0.1)
assert_identical(actual, expected, check_default_indexes=False)

actual = ds.isel(x=slice(None, 2))
expected = create_dataset_arange(0.0, 0.2, 0.1)
assert_identical(actual, expected, check_default_indexes=False)

actual = ds.isel(x=slice(1, 3))
expected = create_dataset_arange(0.1, 0.3, 0.1)
assert_identical(actual, expected, check_default_indexes=False)

actual = ds.isel(x=slice(None, None, 2))
expected = create_dataset_arange(0.0, 1.0, 0.2)
assert_identical(actual, expected, check_default_indexes=False)

# scalar
actual = ds.isel(x=0)
expected = xr.Dataset(coords={"x": 0.0})
assert_identical(actual, expected)

# outer indexing with arbitrary array values
actual = ds.isel(x=[0, 2])
expected = xr.Dataset(coords={"x": [0.0, 0.2]})
assert_identical(actual, expected)
assert isinstance(actual.xindexes["x"], PandasIndex)

# fancy indexing with 1-d Variable
actual = ds.isel(x=xr.Variable("y", [0, 2]))
expected = xr.Dataset(coords={"x": ("y", [0.0, 0.2])}).set_xindex("x")
assert_identical(actual, expected, check_default_indexes=False)
assert isinstance(actual.xindexes["x"], PandasIndex)

# fancy indexing with n-d Variable
actual = ds.isel(x=xr.Variable(("u", "v"), [[0, 0], [2, 2]]))
expected = xr.Dataset(coords={"x": (("u", "v"), [[0.0, 0.0], [0.2, 0.2]])})
assert_identical(actual, expected)


def test_range_index_sel() -> None:
ds = create_dataset_arange(0.0, 1.0, 0.1)

# start-stop slice
actual = ds.sel(x=slice(0.12, 0.28), method="nearest")
expected = create_dataset_arange(0.1, 0.3, 0.1)
assert_identical(actual, expected, check_default_indexes=False)

# start-stop-step slice
actual = ds.sel(x=slice(0.0, 1.0, 0.2), method="nearest")
expected = ds.isel(x=range(0, 10, 2))
assert_identical(actual, expected, check_default_indexes=False)

# basic indexing
actual = ds.sel(x=0.52, method="nearest")
expected = xr.Dataset(coords={"x": 0.5})
assert_allclose(actual, expected)

actual = ds.sel(x=0.58, method="nearest")
expected = xr.Dataset(coords={"x": 0.6})
assert_allclose(actual, expected)

# 1-d array indexing
actual = ds.sel(x=[0.52, 0.58], method="nearest")
expected = xr.Dataset(coords={"x": [0.5, 0.6]})
assert_allclose(actual, expected)

actual = ds.sel(x=xr.Variable("y", [0.52, 0.58]), method="nearest")
expected = xr.Dataset(coords={"x": ("y", [0.5, 0.6])}).set_xindex("x")
assert_allclose(actual, expected, check_default_indexes=False)

actual = ds.sel(x=xr.DataArray([0.52, 0.58], dims="y"), method="nearest")
expected = xr.Dataset(coords={"x": ("y", [0.5, 0.6])}).set_xindex("x")
assert_allclose(actual, expected, check_default_indexes=False)

with pytest.raises(ValueError, match="RangeIndex only supports.*method.*nearest"):
ds.sel(x=0.1)

with pytest.raises(ValueError, match="RangeIndex doesn't support.*tolerance"):
ds.sel(x=0.1, method="nearest", tolerance=1e-3)


def test_range_index_to_pandas_index() -> None:
ds = create_dataset_arange(0.0, 1.0, 0.1)

actual = ds.indexes["x"]
expected = pd.Index(np.arange(0.0, 1.0, 0.1))
assert actual.equals(expected)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy