Skip to content

Add CFIntervalIndex #10296

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

Open
wants to merge 25 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
9282fc4
add IntervalIndex
benbovy May 7, 2025
f71f767
add index description (docstrings)
benbovy May 7, 2025
f7041fd
add type annotations
benbovy May 7, 2025
9401774
expose IntervalIndex publicly via xarray.indexes
benbovy May 7, 2025
781d33f
add a few TODOs
benbovy May 7, 2025
48dc0bd
clean-up
benbovy May 8, 2025
b424b12
better docstrings
benbovy May 8, 2025
8d80e71
refactor: use two sub-indexes
benbovy May 8, 2025
e60a1a4
check consistent central values vs. intervals
benbovy May 8, 2025
8918fe8
fix mypy
benbovy May 8, 2025
c722a2e
implement join and reindex_like
benbovy May 8, 2025
23fb18b
add mid_index and bounds_index properties
benbovy May 8, 2025
de4f5d8
clean-up indexing.PandasIndexingAdapter typing
benbovy May 9, 2025
e1bf896
streamline PandasIndexingAdapter indexing logic
benbovy May 9, 2025
06a3b92
add xarray indexing adapater for pd.IntervalIndex
benbovy May 9, 2025
80f496f
clean-up PandasIndexingAdapter dtype handling
benbovy May 9, 2025
67d8f6c
fix mypy
benbovy May 9, 2025
a8015aa
IntervalIndex sel / isel: handle boundary dim & coord
benbovy May 9, 2025
5b5cbee
more clean-up
benbovy May 9, 2025
fdc1943
rename IntervalIndex -> CFIntervalIndex
benbovy Jul 3, 2025
3a8fd3c
Merge branch 'main' into add-interval-index
benbovy Jul 3, 2025
edfa435
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 3, 2025
bc20226
fix circular import
benbovy Jul 3, 2025
3ec2c65
Merge branch 'main' into add-interval-index
dcherian Jul 8, 2025
4cabb7c
Fix bad merge
dcherian Jul 8, 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
7 changes: 1 addition & 6 deletions xarray/core/indexes.py
Original file line number Diff line number Diff line change
Expand Up @@ -632,9 +632,6 @@ def get_indexer_nd(index: pd.Index, labels, method=None, tolerance=None) -> np.n
return indexer


T_PandasIndex = TypeVar("T_PandasIndex", bound="PandasIndex")


class PandasIndex(Index):
"""Wrap a pandas.Index as an xarray compatible index."""

Expand Down Expand Up @@ -929,9 +926,7 @@ def rename(self, name_dict, dims_dict):
new_dim = dims_dict.get(self.dim, self.dim)
return self._replace(index, dim=new_dim)

def _copy(
self: T_PandasIndex, deep: bool = True, memo: dict[int, Any] | None = None
) -> T_PandasIndex:
def _copy(self, deep: bool = True, memo: dict[int, Any] | None = None) -> Self:
if deep:
# pandas is not using the memo
index = self.index.copy(deep=True)
Expand Down
89 changes: 88 additions & 1 deletion xarray/core/indexing.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
get_valid_numpy_dtype,
is_duck_array,
is_duck_dask_array,
is_full_slice,
is_scalar,
is_valid_numpy_dtype,
to_0d_array,
Expand Down Expand Up @@ -1889,7 +1890,7 @@ def __getitem__(
) -> PandasIndexingAdapter | np.ndarray:
return self._index_get(indexer, "__getitem__")

def transpose(self, order) -> pd.Index:
def transpose(self, order) -> Self | pd.Index:
return self.array # self.array should be always one-dimensional

def _repr_inline_(self, max_width: int) -> str:
Expand Down Expand Up @@ -2005,6 +2006,92 @@ def copy(self, deep: bool = True) -> Self:
return type(self)(array, self._dtype, self.level)


class PandasIntervalIndexingAdapter(PandasIndexingAdapter):
"""Wraps a pandas.IntervalIndex as a 2-dimensional coordinate array.

When the array is not transposed, left and right interval boundaries are on
the 2nd axis, i.e., shape is (N, 2).

"""

__slots__ = ("_bounds_axis", "_dtype", "array")

array: pd.IntervalIndex
_dtype: np.dtype | pd.api.extensions.ExtensionDtype
_bounds_axis: int

def __init__(
self,
array: pd.IntervalIndex,
dtype: DTypeLike | pd.api.extensions.ExtensionDtype | None = None,
transpose: bool = False,
):
super().__init__(array, dtype=dtype)

if transpose:
self._bounds_axis = 0
else:
self._bounds_axis = -1

@property
def shape(self) -> _Shape:
if self._bounds_axis == 0:
return (2, len(self.array))
else:
return (len(self.array), 2)

def __array__(
self,
dtype: np.typing.DTypeLike | None = None,
/,
*,
copy: bool | None = None,
) -> np.ndarray:
dtype = self._get_numpy_dtype(dtype)

return np.stack(
[self.array.left, self.array.right], axis=self._bounds_axis, dtype=dtype
)

def get_duck_array(self) -> np.ndarray:
return np.asarray(self)

def _index_get(
self, indexer: ExplicitIndexer, func_name: str
) -> PandasIndexingAdapter | np.ndarray:
key: tuple | Any = indexer.tuple

if len(key) == 1:
# unpack key so it can index a pandas.Index object (pandas.Index
# objects don't like tuples)
(key,) = key
elif len(key) == 2 and is_full_slice(key[self._bounds_axis]):
# OK to index the pandas.IntervalIndex and keep it wrapped
# (drop the bounds axis key)
key = key[self._bounds_axis + 1]

# if length-2 or multidimensional key, convert the index to numpy array
# and index the latter
if (isinstance(key, tuple) and len(key) == 2) or getattr(key, "ndim", 0) > 1:
indexable = NumpyIndexingAdapter(np.asarray(self))
return getattr(indexable, func_name)(indexer)

# otherwise index the pandas IntervalIndex then re-wrap or convert the result
result = self.array[key]

if isinstance(result, pd.IntervalIndex):
return type(self)(result, dtype=self.dtype)
elif isinstance(result, pd.Interval):
dtype = self._get_numpy_dtype()
return np.array([result.left, result.right], dtype=dtype)
else:
return self._convert_scalar(result)

def transpose(self, order: Iterable[int]) -> Self:
transpose = tuple(order) == (1, 0)
return type(self)(self.array, dtype=self.dtype, transpose=transpose)


class CoordinateTransformIndexingAdapter(ExplicitlyIndexedNDArrayMixin):
"""Wrap a CoordinateTransform as a lazy coordinate array.

Expand Down
2 changes: 2 additions & 0 deletions xarray/indexes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@
PandasIndex,
PandasMultiIndex,
)
from xarray.indexes.cf_interval_index import CFIntervalIndex
from xarray.indexes.nd_point_index import NDPointIndex
from xarray.indexes.range_index import RangeIndex

__all__ = [
"CFIntervalIndex",
"CoordinateTransform",
"CoordinateTransformIndex",
"Index",
Expand Down
Loading
Loading