From 11f237a8688f333118cdf341fc80b6711e425125 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Thu, 1 Sep 2022 09:51:08 +0200 Subject: [PATCH 01/14] improve Index base class type annotations Use T_Index generic when possible. --- xarray/core/indexes.py | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/xarray/core/indexes.py b/xarray/core/indexes.py index 8ff0d40ff07..ff20498f844 100644 --- a/xarray/core/indexes.py +++ b/xarray/core/indexes.py @@ -35,7 +35,9 @@ class Index: """Base class inherited by all xarray-compatible indexes.""" @classmethod - def from_variables(cls, variables: Mapping[Any, Variable]) -> Index: + def from_variables( + cls: type[T_Index], variables: Mapping[Any, Variable] + ) -> T_Index: raise NotImplementedError() @classmethod @@ -48,7 +50,9 @@ def concat( raise NotImplementedError() @classmethod - def stack(cls, variables: Mapping[Any, Variable], dim: Hashable) -> Index: + def stack( + cls: type[T_Index], variables: Mapping[Any, Variable], dim: Hashable + ) -> T_Index: raise NotImplementedError( f"{cls!r} cannot be used for creating an index of stacked coordinates" ) @@ -76,8 +80,8 @@ def to_pandas_index(self) -> pd.Index: raise TypeError(f"{self!r} cannot be cast to a pandas.Index object") def isel( - self, indexers: Mapping[Any, int | slice | np.ndarray | Variable] - ) -> Index | None: + self: T_Index, indexers: Mapping[Any, int | slice | np.ndarray | Variable] + ) -> T_Index | None: return None def sel(self, labels: dict[Any, Any]) -> IndexSelResult: @@ -91,26 +95,28 @@ def join(self: T_Index, other: T_Index, how: str = "inner") -> T_Index: def reindex_like(self: T_Index, other: T_Index) -> dict[Hashable, Any]: raise NotImplementedError(f"{self!r} doesn't support re-indexing labels") - def equals(self, other): # pragma: no cover + def equals(self: T_Index, other: T_Index): raise NotImplementedError() - def roll(self, shifts: Mapping[Any, int]) -> Index | None: + def roll(self: T_Index, shifts: Mapping[Any, int]) -> T_Index | None: return None def rename( - self, name_dict: Mapping[Any, Hashable], dims_dict: Mapping[Any, Hashable] - ) -> Index: + self: T_Index, + name_dict: Mapping[Any, Hashable], + dims_dict: Mapping[Any, Hashable], + ) -> T_Index: return self - def __copy__(self) -> Index: + def __copy__(self: T_Index) -> T_Index: return self.copy(deep=False) - def __deepcopy__(self, memo=None) -> Index: + def __deepcopy__(self: T_Index, memo=None) -> T_Index: # memo does nothing but is required for compatibility with # copy.deepcopy return self.copy(deep=True) - def copy(self, deep: bool = True) -> Index: + def copy(self: T_Index, deep: bool = True) -> T_Index: cls = self.__class__ copied = cls.__new__(cls) if deep: @@ -120,7 +126,7 @@ def copy(self, deep: bool = True) -> Index: copied.__dict__.update(self.__dict__) return copied - def __getitem__(self, indexer: Any): + def __getitem__(self: T_Index, indexer: Any) -> T_Index: raise NotImplementedError() From f8da4fc550beb98fb4ab59cea1419adb08749906 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Thu, 1 Sep 2022 09:53:09 +0200 Subject: [PATCH 02/14] import Index base class in Xarray root namespace --- xarray/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/xarray/__init__.py b/xarray/__init__.py index 46dcf0e9b32..8ea955e7210 100644 --- a/xarray/__init__.py +++ b/xarray/__init__.py @@ -30,6 +30,7 @@ from .core.dataarray import DataArray from .core.dataset import Dataset from .core.extensions import register_dataarray_accessor, register_dataset_accessor +from .core.indexes import Index from .core.merge import Context, MergeError, merge from .core.options import get_options, set_options from .core.parallel import map_blocks @@ -99,6 +100,7 @@ "Coordinate", "DataArray", "Dataset", + "Index", "IndexVariable", "Variable", # Exceptions From 631ec7359d022418595b66bb9f1cb10e0545a373 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Thu, 1 Sep 2022 15:17:10 +0200 Subject: [PATCH 03/14] import IndexSelResult into Xarray root namespace --- xarray/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/xarray/__init__.py b/xarray/__init__.py index 8ea955e7210..e683d648450 100644 --- a/xarray/__init__.py +++ b/xarray/__init__.py @@ -31,6 +31,7 @@ from .core.dataset import Dataset from .core.extensions import register_dataarray_accessor, register_dataset_accessor from .core.indexes import Index +from .core.indexing import IndexSelResult from .core.merge import Context, MergeError, merge from .core.options import get_options, set_options from .core.parallel import map_blocks @@ -101,6 +102,7 @@ "DataArray", "Dataset", "Index", + "IndexSelResult", "IndexVariable", "Variable", # Exceptions From 909fdcb63e4c67c3b07969939e6c3ef1b6365392 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Thu, 1 Sep 2022 15:17:52 +0200 Subject: [PATCH 04/14] wip: Index API docstrings --- doc/api-hidden.rst | 10 +++ doc/api.rst | 2 + xarray/core/indexes.py | 189 ++++++++++++++++++++++++++++++++++++++++- 3 files changed, 197 insertions(+), 4 deletions(-) diff --git a/doc/api-hidden.rst b/doc/api-hidden.rst index 30bc9f858f2..b1174c92c00 100644 --- a/doc/api-hidden.rst +++ b/doc/api-hidden.rst @@ -461,6 +461,16 @@ CFTimeIndex.values CFTimeIndex.year + Index.from_variables + Index.concat + Index.stack + Index.unstack + Index.create_variables + Index.to_pandas_index + Index.isel + Index.sel + Index.join + backends.NetCDF4DataStore.close backends.NetCDF4DataStore.encode backends.NetCDF4DataStore.encode_attribute diff --git a/doc/api.rst b/doc/api.rst index 11ae5de8531..150e9a8adda 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -1080,6 +1080,8 @@ Advanced API Variable IndexVariable as_variable + Index + IndexSelResult Context register_dataset_accessor register_dataarray_accessor diff --git a/xarray/core/indexes.py b/xarray/core/indexes.py index ff20498f844..b03744d7e7c 100644 --- a/xarray/core/indexes.py +++ b/xarray/core/indexes.py @@ -32,12 +32,47 @@ class Index: - """Base class inherited by all xarray-compatible indexes.""" + """Base class inherited by all xarray-compatible indexes. + + Do not use this class directly for creating index objects. Instead, index + objects are created from subclasses via Xarray's public API (e.g., + via ``Dataset.set_xindex``). + + A subclass of ``Index`` either must, should or may (re)implement the methods + in this base class. + + The ``Index`` API closely follows the :py:meth:`Dataset` and + :py:meth:`DataArray` API, e.g., for an index to support ``.sel()`` it needs + to implement :py:meth:`Index.sel`, to support ``.stack()`` and + ``.unstack()`` it needs to implement :py:meth:`Index.stack` and + :py:meth:`Index.unstack`, etc. + + When a method is not (re)implemented, depending on the case the + corresponding operation on a :py:meth:`Dataset` or :py:meth:`DataArray` will + either raise a ``NotImplementedError`` or drop / pass / copy the index to + the result. + + """ @classmethod def from_variables( cls: type[T_Index], variables: Mapping[Any, Variable] ) -> T_Index: + """Create a new index object from one or more coordinate variables. + + This factory method must be implemented in all subclasses of Index. + + Parameters + ---------- + variables : dict-like + Mapping of :py:class:`Variable` objects holding the coordinate labels + to index. + + Returns + ------- + index + A new Index object. + """ raise NotImplementedError() @classmethod @@ -47,22 +82,101 @@ def concat( dim: Hashable, positions: Iterable[Iterable[int]] = None, ) -> T_Index: + """Create a new index by concatenating one or more indexes of the same + type. + + Implementation is optional but required in order to support + concatenation. Otherwise it will raise an error if the index needs to be + updated during the operation. + + Parameters + ---------- + indexes : sequence of Index objects + Indexes objects to concatenate together. All objects must be of the + same type. + dim : Hashable + Name of the dimension to concatenate along. + positions : None or list of integer arrays, optional + List of integer arrays which specifies the integer positions to which + to assign each dataset along the concatenated dimension. If not + supplied, objects are concatenated in the provided order. + + Returns + ------- + index + A new Index object. + + """ raise NotImplementedError() @classmethod def stack( cls: type[T_Index], variables: Mapping[Any, Variable], dim: Hashable ) -> T_Index: + """Create a new index by stacking coordinate variables into a single new + dimension. + + Implementation is optional but required in order to support stack. + Otherwise it will raise an error when trying to pass the Index subclass + as argument to :py:meth:`Dataset.stack`. + + Parameters + ---------- + variables : dict-like + Mapping of :py:class:`Variable` objects to stack together. + dim : Hashable + Name of the new, stacked dimension. + + Returns + ------- + index + A new Index object. + + """ raise NotImplementedError( f"{cls!r} cannot be used for creating an index of stacked coordinates" ) def unstack(self) -> tuple[dict[Hashable, Index], pd.MultiIndex]: + """Unstack a (multi-)index into multiple (single) indexes. + + Implementation is optional but required in order to support unstacking + the indexed coordinates. + + Returns + ------- + indexes : tuple + A 2-length tuple where the 1st item is a dictionary of unstacked + Index objects and the 2nd item is a :py:class`pandas.MultiIndex` + object used to unstack unindexed coordinate variables or data + variables. + + """ raise NotImplementedError() def create_variables( self, variables: Mapping[Any, Variable] | None = None ) -> IndexVars: + """Create new coordinate variables from this index. + + This method is useful if the index data can be reused as coordinate + variable data. + + The variables given as argument (if any) are either returned as-is + (default) or can be used in subclasses of Index to collect metadata and + copy it into the new returned coordinate variables. + + Parameters + ---------- + variables : dict-like, optional + Mapping of :py:class:`Variable` objects. + + Returns + ------- + index_variables : dict-like + Dictionary of :py:class:`Variable` objects. + + """ if variables is not None: # pass through return dict(**variables) @@ -70,24 +184,91 @@ def create_variables( return {} def to_pandas_index(self) -> pd.Index: - """Cast this xarray index to a pandas.Index object or raise a TypeError - if this is not supported. + """Cast this xarray index to a pandas.Index object or raise a + ``TypeError`` if this is not supported. - This method is used by all xarray operations that expect/require a + This method is used by all xarray operations that still expect/require a pandas.Index object. + By default it raises a ``TypeError``, unless it is re-implemented in + subclasses of Index. """ raise TypeError(f"{self!r} cannot be cast to a pandas.Index object") def isel( self: T_Index, indexers: Mapping[Any, int | slice | np.ndarray | Variable] ) -> T_Index | None: + """Maybe returns a new index from the current index itself indexed by + positional indexers. + + This method should be re-implemented in subclasses of Index if the + wrapped index structure supports indexing operations. For example, + indexing a ``pandas.Index`` is pretty straightforward as it behaves very + much like an array. By contrast, it may be harder doing so for a + structure like a kd-tree that differs much from a simple array. + + If not re-implemented in subclasses of Index, this method returns + ``None``, i.e., calling :py:meth:`Dataset.isel` will either drop the + index in the resulting dataset or pass it unchanged if its corresponding + coordinate(s) are not indexed. + + Parameters + ---------- + indexers : dict + A dictionary of positional indexers as passed from + :py:meth:`Dataset.isel` and where the entries have been filtered + for the current index. + + Returns + ------- + maybe_index + A new Index object or ``None``. + + """ return None def sel(self, labels: dict[Any, Any]) -> IndexSelResult: + """Perform label-based selection. + + Implementation is optional but required in order to support label-based + selection. Otherwise it will raise an error when trying to call + :py:meth:`Dataset.sel` with labels for one of the corresponding + coordinates. + + Parameters + ---------- + labels : dict + A dictionary of label indexers as passed from + :py:meth:`Dataset.sel` and where the entries have been filtered + for the current index. + + Returns + ------- + sel_results : :py:class:`IndexSelResult` + An index query result object that contains positional indexers and that + may also contain new indexes, coordinate variables, etc. + + """ raise NotImplementedError(f"{self!r} doesn't support label-based selection") def join(self: T_Index, other: T_Index, how: str = "inner") -> T_Index: + """Return a new index from the combination of this index with another + index of the same type. + + Implementation is optional but required in order to support alignment. + + Parameters + ---------- + other : Index + The other Index object to combine with this index. + join : {"outer", "inner", "left", "right", "exact", "override"}, optional + Method for joining the two indexes (see :py:func:`~xarray.align`). + + Returns + ------- + joined + A new Index object. + """ raise NotImplementedError( f"{self!r} doesn't support alignment with inner/outer join method" ) From 5755c580bee0a2b427b414af955f6565764baaae Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Thu, 1 Sep 2022 15:18:21 +0200 Subject: [PATCH 05/14] wip: doc: add how to add custom index section --- doc/internals/how-to-add-custom-index.rst | 30 +++++++++++++++++++++++ doc/internals/index.rst | 1 + 2 files changed, 31 insertions(+) create mode 100644 doc/internals/how-to-add-custom-index.rst diff --git a/doc/internals/how-to-add-custom-index.rst b/doc/internals/how-to-add-custom-index.rst new file mode 100644 index 00000000000..c07c3c4743c --- /dev/null +++ b/doc/internals/how-to-add-custom-index.rst @@ -0,0 +1,30 @@ +.. currentmodule:: xarray + +How to add a custom index +========================= + +.. warning:: + + This feature is highly experimental. Support for custom indexes + has been introduced in v2022.06.0 and is still incomplete. + +Xarray's built-in support for label-based indexing and alignment operations +relies on :py:class:`pandas.Index` objects. It is powerful and suitable for many +applications but it also has some limitations: + +- it only works with 1-dimensional coordinates +- it is hard to reuse it with irregular data for which there exist more + efficient, tree-based structures to perform data selection +- it doesn't support extra metadata that may be required for indexing and + alignment (e.g., a coordinate reference system) + +Fortunately, Xarray now allows extending this functionality with custom indexes, +which can be implemented in 3rd-party libraries. + +The Index base class +-------------------- + +Every Xarray index must inherit from the :py:class:`Index` base class. It is the +case of Xarray built-in ``PandasIndex`` and ``PandasMultiIndex`` subclasses, +which wrap :py:class:`pandas.Index` and :py:class:`pandas.MultiIndex` +respectively. diff --git a/doc/internals/index.rst b/doc/internals/index.rst index e4ca9779dd7..88c6f2290ea 100644 --- a/doc/internals/index.rst +++ b/doc/internals/index.rst @@ -18,3 +18,4 @@ compiled code to :ref:`optional dependencies`. extending-xarray zarr-encoding-spec how-to-add-new-backend + how-to-add-custom-index From ca498b3a1f35cef06263546d27899a3f3277eb85 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Fri, 2 Sep 2022 15:05:42 +0200 Subject: [PATCH 06/14] add Index method docstrings --- doc/api-hidden.rst | 5 + ...dex.rst => how-to-create-custom-index.rst} | 0 doc/internals/index.rst | 2 +- xarray/core/indexes.py | 165 ++++++++++++++---- 4 files changed, 138 insertions(+), 34 deletions(-) rename doc/internals/{how-to-add-custom-index.rst => how-to-create-custom-index.rst} (100%) diff --git a/doc/api-hidden.rst b/doc/api-hidden.rst index b1174c92c00..fea2b18818c 100644 --- a/doc/api-hidden.rst +++ b/doc/api-hidden.rst @@ -470,6 +470,11 @@ Index.isel Index.sel Index.join + Index.reindex_like + Index.equals + Index.roll + Index.rename + Index.copy backends.NetCDF4DataStore.close backends.NetCDF4DataStore.encode diff --git a/doc/internals/how-to-add-custom-index.rst b/doc/internals/how-to-create-custom-index.rst similarity index 100% rename from doc/internals/how-to-add-custom-index.rst rename to doc/internals/how-to-create-custom-index.rst diff --git a/doc/internals/index.rst b/doc/internals/index.rst index 88c6f2290ea..e2115edc7ff 100644 --- a/doc/internals/index.rst +++ b/doc/internals/index.rst @@ -18,4 +18,4 @@ compiled code to :ref:`optional dependencies`. extending-xarray zarr-encoding-spec how-to-add-new-backend - how-to-add-custom-index + how-to-create-custom-index diff --git a/xarray/core/indexes.py b/xarray/core/indexes.py index b03744d7e7c..3365597bc78 100644 --- a/xarray/core/indexes.py +++ b/xarray/core/indexes.py @@ -34,12 +34,14 @@ class Index: """Base class inherited by all xarray-compatible indexes. - Do not use this class directly for creating index objects. Instead, index - objects are created from subclasses via Xarray's public API (e.g., - via ``Dataset.set_xindex``). + Do not use this class directly for creating index objects. Xarray indexes + are created exclusively from subclasses of ``Index``, mostly via Xarray's + public API like ``Dataset.set_xindex``. - A subclass of ``Index`` either must, should or may (re)implement the methods - in this base class. + Every subclass must at least implement :py:meth:`Index.from_variables`. The + (re)implementation of the other methods of this base class is optional but + mostly required in order to support operations relying on indexes such as + label-based selection or alignment. The ``Index`` API closely follows the :py:meth:`Dataset` and :py:meth:`DataArray` API, e.g., for an index to support ``.sel()`` it needs @@ -48,9 +50,9 @@ class Index: :py:meth:`Index.unstack`, etc. When a method is not (re)implemented, depending on the case the - corresponding operation on a :py:meth:`Dataset` or :py:meth:`DataArray` will - either raise a ``NotImplementedError`` or drop / pass / copy the index to - the result. + corresponding operation on a :py:meth:`Dataset` or :py:meth:`DataArray` + either will raise a ``NotImplementedError`` or will simply drop/pass/copy + the index from/to the result. """ @@ -62,6 +64,10 @@ def from_variables( This factory method must be implemented in all subclasses of Index. + The coordinate variables may be passed here in an arbitrary number and + order and each with arbitrary dimensions. It is the responsibility of + the index to check the consistency and validity of these coordinates. + Parameters ---------- variables : dict-like @@ -70,7 +76,7 @@ def from_variables( Returns ------- - index + index : Index A new Index object. """ raise NotImplementedError() @@ -103,9 +109,8 @@ def concat( Returns ------- - index + index : Index A new Index object. - """ raise NotImplementedError() @@ -131,7 +136,6 @@ def stack( ------- index A new Index object. - """ raise NotImplementedError( f"{cls!r} cannot be used for creating an index of stacked coordinates" @@ -141,7 +145,7 @@ def unstack(self) -> tuple[dict[Hashable, Index], pd.MultiIndex]: """Unstack a (multi-)index into multiple (single) indexes. Implementation is optional but required in order to support unstacking - the indexed coordinates. + the coordinates from which this index has been built. Returns ------- @@ -150,21 +154,24 @@ def unstack(self) -> tuple[dict[Hashable, Index], pd.MultiIndex]: Index objects and the 2nd item is a :py:class`pandas.MultiIndex` object used to unstack unindexed coordinate variables or data variables. - """ raise NotImplementedError() def create_variables( self, variables: Mapping[Any, Variable] | None = None ) -> IndexVars: - """Create new coordinate variables from this index. + """Maybe create new coordinate variables from this index. This method is useful if the index data can be reused as coordinate - variable data. + variable data. It is often the case when the underlying index structure + has an array-like interface, like :py:class:`pandas.Index` objects. The variables given as argument (if any) are either returned as-is - (default) or can be used in subclasses of Index to collect metadata and - copy it into the new returned coordinate variables. + (default behavior) or can be used to copy their metadata (attributes and + encoding) into the new returned coordinate variables. + + Note: the input variables may or may not have been filtered for this + index. Parameters ---------- @@ -174,8 +181,8 @@ def create_variables( Returns ------- index_variables : dict-like - Dictionary of :py:class:`Variable` objects. - + Dictionary of :py:class:`Variable` or :py:class:`IndexVariable` + objects. """ if variables is not None: # pass through @@ -187,8 +194,8 @@ def to_pandas_index(self) -> pd.Index: """Cast this xarray index to a pandas.Index object or raise a ``TypeError`` if this is not supported. - This method is used by all xarray operations that still expect/require a - pandas.Index object. + This method is used by all xarray operations that still rely on + pandas.Index objects. By default it raises a ``TypeError``, unless it is re-implemented in subclasses of Index. @@ -221,33 +228,34 @@ def isel( Returns ------- - maybe_index + maybe_index : Index A new Index object or ``None``. - """ return None def sel(self, labels: dict[Any, Any]) -> IndexSelResult: - """Perform label-based selection. + """Query the index with arbitrary coordinate label indexers. Implementation is optional but required in order to support label-based selection. Otherwise it will raise an error when trying to call - :py:meth:`Dataset.sel` with labels for one of the corresponding - coordinates. + :py:meth:`Dataset.sel` with labels for this index coordinates. + + Coordinate label indexers can be of many kinds, e.g., scalar, list, + tuple, array-like, slice, :py:class:`Variable`, :py:class:`DataArray`, etc. + It is the responsibility of the index to handle those indexers properly. Parameters ---------- labels : dict - A dictionary of label indexers as passed from + A dictionary of coordinate label indexers passed from :py:meth:`Dataset.sel` and where the entries have been filtered for the current index. Returns ------- sel_results : :py:class:`IndexSelResult` - An index query result object that contains positional indexers and that - may also contain new indexes, coordinate variables, etc. - + An index query result object that contains dimension positional indexers. + It may also contain new indexes, coordinate variables, etc. """ raise NotImplementedError(f"{self!r} doesn't support label-based selection") @@ -266,7 +274,7 @@ def join(self: T_Index, other: T_Index, how: str = "inner") -> T_Index: Returns ------- - joined + joined : Index A new Index object. """ raise NotImplementedError( @@ -274,12 +282,63 @@ def join(self: T_Index, other: T_Index, how: str = "inner") -> T_Index: ) def reindex_like(self: T_Index, other: T_Index) -> dict[Hashable, Any]: + """Query the index with another index of the same type. + + Implementation is optional but required in order to support alignment. + + Parameters + ---------- + other : Index + The other Index object used to query this index. + + Returns + ------- + dim_positional_indexers : dict + A dictionary where keys are dimension names and values are positional + indexers. + """ raise NotImplementedError(f"{self!r} doesn't support re-indexing labels") - def equals(self: T_Index, other: T_Index): + def equals(self: T_Index, other: T_Index) -> bool: + """Compare this index with another index of the same type. + + Implemenation is optional but required in order to support alignment. + + Parameters + ---------- + other : Index + The other Index object to compare with this object. + + Returns + ------- + is_equal : bool + ``True`` if the indexes are equal, ``False`` otherwise. + """ raise NotImplementedError() def roll(self: T_Index, shifts: Mapping[Any, int]) -> T_Index | None: + """Roll this index by an offset along one or more dimensions. + + This method can be re-implemented in subclasses of Index, e.g., when the + index can be itself indexed. + + If not re-implemented, this method returns ``None``, i.e., calling + :py:meth:`Dataset.roll` will either drop the index in the resulting + dataset or pass it unchanged if its corresponding coordinate(s) are not + rolled. + + Parameters + ---------- + shifts : mapping of hashable to int, optional + A dict with keys matching dimensions and values given + by integers to rotate each of the given dimensions, as passed + :py:meth:`Dataset.roll`. + + Returns + ------- + rolled : Index + A new index with rolled data. + """ return None def rename( @@ -287,6 +346,30 @@ def rename( name_dict: Mapping[Any, Hashable], dims_dict: Mapping[Any, Hashable], ) -> T_Index: + """Maybe update the index with new coordinate and dimension names. + + This method should be re-implemented in subclasses of Index if it has + attributes that depends on coordinate or dimension names. + + By default (if not re-implemented), it returns the index itself. + + Warning: the input names are not filtered for this index, they may + correspond to any variable or dimension of a Dataset or a DataArray. + + Parameters + ---------- + name_dict : dict-like + Mapping of current variable or coordinate names to the desired names, + as passed from :py:meth:`Dataset.rename_vars`. + dims_dict : dict-like + Mapping of current dimension names to the desired names, as passed + from :py:meth:`Dataset.rename_dims`. + + Returns + ------- + renamed : Index + Index with renamed attributes. + """ return self def __copy__(self: T_Index) -> T_Index: @@ -298,6 +381,22 @@ def __deepcopy__(self: T_Index, memo=None) -> T_Index: return self.copy(deep=True) def copy(self: T_Index, deep: bool = True) -> T_Index: + """Return a (deep) copy of this index. + + Implementation is subclasses of Index is optional. The base class + implements the default (deep) copy semantics. + + Parameters + ---------- + deep : bool, optional + If true (default), a copy of the internal structures + (e.g., wrapped index) is returned with the new object. + + Returns + ------- + index : Index + A new Index object. + """ cls = self.__class__ copied = cls.__new__(cls) if deep: From d7f42207dc7bf63008c1a5e4defb0f19c0b4ce02 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Fri, 2 Sep 2022 15:07:33 +0200 Subject: [PATCH 07/14] add user guide on how to create a custom index --- doc/internals/how-to-create-custom-index.rst | 222 ++++++++++++++++++- 1 file changed, 213 insertions(+), 9 deletions(-) diff --git a/doc/internals/how-to-create-custom-index.rst b/doc/internals/how-to-create-custom-index.rst index c07c3c4743c..a4739b86800 100644 --- a/doc/internals/how-to-create-custom-index.rst +++ b/doc/internals/how-to-create-custom-index.rst @@ -1,18 +1,20 @@ .. currentmodule:: xarray -How to add a custom index -========================= +How to create a custom index +============================ .. warning:: - This feature is highly experimental. Support for custom indexes - has been introduced in v2022.06.0 and is still incomplete. + This feature is highly experimental. Support for custom indexes has been + introduced in v2022.06.0 and is still incomplete. API is subject to change + without deprecation notice. Xarray's built-in support for label-based indexing and alignment operations relies on :py:class:`pandas.Index` objects. It is powerful and suitable for many applications but it also has some limitations: -- it only works with 1-dimensional coordinates +- it only works with 1-dimensional coordinates where explicit labels + are fully loaded in memory - it is hard to reuse it with irregular data for which there exist more efficient, tree-based structures to perform data selection - it doesn't support extra metadata that may be required for indexing and @@ -24,7 +26,209 @@ which can be implemented in 3rd-party libraries. The Index base class -------------------- -Every Xarray index must inherit from the :py:class:`Index` base class. It is the -case of Xarray built-in ``PandasIndex`` and ``PandasMultiIndex`` subclasses, -which wrap :py:class:`pandas.Index` and :py:class:`pandas.MultiIndex` -respectively. +Every Xarray index must inherit from the :py:class:`Index` base class. It is for +example the case of Xarray built-in ``PandasIndex`` and ``PandasMultiIndex`` +subclasses, which wrap :py:class:`pandas.Index` and +:py:class:`pandas.MultiIndex` respectively. + +The ``Index`` API closely follows the :py:meth:`Dataset` and +:py:meth:`DataArray` API, e.g., for an index to support ``.sel()`` it needs to +implement :py:meth:`Index.sel`, to support ``.stack()`` and ``.unstack()`` it +needs to implement :py:meth:`Index.stack` and :py:meth:`Index.unstack`, etc. + +Some guidelines and examples are given below. More details can be found in the +documented :py:class:`Index` API. + +Minimal requirements +-------------------- + +Every index must at least implement the :py:meth:`Index.from_variables` class +method, which is used by Xarray to build a new index instance from one or more +existing coordinates in a Dataset or DataArray. + +Since any collection of coordinates can be passed to that method (i.e., the +number, order and dimensions of the coordinates are all arbitrary), it is the +responsibility of the index to check the consistency and validity of those input +coordinates. + +For example, ``PandasIndex`` accepts only one coordinate and +``PandasMultiIndex`` accepts one or more 1-dimensional coordinates that must all +share the same dimension. Other, custom indexes wouldn't have the same +constraints, e.g., + +- a georeferenced raster index which only accepts two 1-d coordinates with each + distinct dimensions +- a staggered grid index which takes coordinates with different dimension name + suffixes (e.g., "_c" and "_l" for center and left) + +Optional requirements +--------------------- + +Pretty much everything else is optional. Depending on the case, in the absence +of a (re)implementation, an index will either raise an error (operation not +supported) or won't do anything specific (just drop, pass or copy itself +from/to the resulting Dataset or DataArray). + +For example, you can just skip re-implementing :py:meth:`Index.rename` if there +is no internal attribute or object to rename according to the new desired +coordinate or dimension names. In the case of ``PandasIndex``, we rename the +underlying ``pandas.Index`` object and/or update the ``PandasIndex.dim`` +attribute. + +Wrap index data as coordinate data +---------------------------------- + +In some cases it is possible to reuse the index underlying object or structure +as coordinate data and hence avoid data duplication. + +It is the case of ``PandasIndex`` and ``PandasMultiIndex``, where we can +leverage the fact that ``pandas.Index`` objects expose some array-like API. In +Xarray we use some wrappers around those underlying objects as a thin +compatibility layer to preserve dtypes, handle explicit and n-dimensional +indexing, etc. + +Other structures like tree-based indexes (e.g., kd-tree) may differ too much +from arrays to reuse it as coordinate data. + +If the index data can be reused as coordinate data, the ``Index`` subclass +should implement :py:meth:`Index.create_variables`. This method accepts a +dictionary of :py:class:`Variable` objects as input (used for propagating +variable metadata) and should return a dictionary of new :py:class:`Variable` or +:py:class:`IndexVariable` objects. + +Data selection +-------------- + +For an index to support label-based selection, it needs to at least implement +:py:meth:`Index.sel`. This method accepts a dictionary of labels where the keys +are coordinate names (already filtered for the current index) and the values can +be pretty much anything (e.g., a slice, a tuple, a list, a numpy array, a +:py:class:`Variable` or a :py:class:`DataArray`). It is the responsibility of +the index to properly handle those input labels. + +:py:meth:`Index.sel` must return an instance of :py:class:`IndexSelResult`. The +latter is a small data class that holds positional indexers (indices) and that +may also hold new variables, new indexes, names of variables or indexes to drop, +names of dimensions to rename, etc. This is useful in the case of +``PandasMultiIndex`` as it allows to convert it into a single ``PandasIndex`` +when only one level remains after the selection. + +The ``IndexSelResult`` class is also used to merge results from label-based +selection performed by different indexes. Note that it is now possible to have +two distinct indexes for two 1-d coordinates sharing the same dimension, but it +is not currently possible to use those two indexes in the same call to +:py:meth:`Dataset.sel`. + +Optionally, the index may also implement :py:meth:`Index.isel`. In the case of +``PandasIndex`` we use it to create a new index object by just indexing the +underlying ``pandas.Index`` object. In other cases this may not be possible, +e.g., a kd-tree object may not be easily indexed. If ``Index.isel()`` is not +implemented, the index in just dropped in the DataArray or Dataset resulting +from the selection. + +Alignment +--------- + +For an index to support alignment, it needs to implement: + +- :py:meth:`Index.equals`, which compares the index with another index and + returns either ``True`` or ``False`` +- :py:meth:`Index.join`, which combines the index with another index and returns + a new Index object +- :py:meth:`Index.reindex_like`, which queries the index with another index and + returns positional indexers that are used to re-index Dataset or DataArray + variables along one or more dimensions + +Xarray ensures that those three methods are called with an index of the same +type as argument. + +Meta-indexes +------------ + +Nothing prevents writing a custom Xarray index that itself encapsulates other +Xarray index(es). We call such index a "meta-index". + +Here is a small example of a meta-index for geospatial, raster datasets (i.e., +regularly spaced 2-dimensional data) that internally relies on two +``PandasIndex`` instances for the x and y dimensions, respectively: + +.. code-block:: python + + from xarray import Index + from xarray.core.indexes import PandasIndex + from xarray.core.indexing import merge_sel_results + + + class RasterIndex(Index): + def __init__(self, xy_indexes): + + assert len(xy_indexes) == 2 + + # must have two distinct dimensions + dim = [idx.dim for idx in xy_indexes.values()] + assert dim[0] != dim[1] + + self._xy_indexes = xy_indexes + + @classmethod + def from_variables(cls, variables): + assert len(variables) == 2 + + xy_indexes = { + k: PandasIndex.from_variables({k: v}) for k, v in variables.items() + } + + return cls(xy_indexes) + + def create_variables(self, variables): + idx_variables = {} + + for index in self._xy_indexes.values(): + idx_variables.update(index.create_variables(variables)) + + return idx_variables + + def sel(self, labels): + results = [] + + for k, index in self._xy_indexes.items(): + if k in labels: + results.append(index.sel({k: labels[k]})) + + return merge_sel_results(results) + + +This basic index only supports label-based selection. Providing a full-featured +index by implementing the other ``Index`` methods should be pretty +straightforward for this example, though. + +This example is also not very useful unless we add some extra functionality on +top of the two encapsulated ``PandasIndex`` objects, such as a coordinate +reference system. + +How to use a custom index +------------------------- + +You can use ``Dataset.set_xindex()`` or ``DataArray.set_xindex()`` to assign a +custom index to a Dataset or DataArray, e.g., using the ``RasterIndex`` above: + +.. code-block:: python + + import numpy as np + import xarray as xr + + da = xr.DataArray( + np.random.uniform(size=(100, 50)), + coords={"x": ("x", np.arange(50)), "y": ("y", np.arange(100))}, + dims=("y", "x"), + ) + + # Xarray create default indexes for the 'x' and 'y' coordinates + # we first need to explicitly drop it + da.drop_indexes(["x", "y"]) + + # Build a RasterIndex from the 'x' and 'y' coordinates + da_raster = da.set_xindex(["x", "y"], RasterIndex) + + # RasterIndex now takes care of label-based selection + selected = da_raster.sel(x=10, y=slice(20, 50)) From 390635a84d7262873c063b982f396e1bb245ffed Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Mon, 5 Sep 2022 09:11:40 +0200 Subject: [PATCH 08/14] review comments + tweaks --- doc/internals/how-to-create-custom-index.rst | 4 ++-- xarray/core/indexes.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/internals/how-to-create-custom-index.rst b/doc/internals/how-to-create-custom-index.rst index a4739b86800..8b84f173d5b 100644 --- a/doc/internals/how-to-create-custom-index.rst +++ b/doc/internals/how-to-create-custom-index.rst @@ -78,7 +78,7 @@ attribute. Wrap index data as coordinate data ---------------------------------- -In some cases it is possible to reuse the index underlying object or structure +In some cases it is possible to reuse the index's underlying object or structure as coordinate data and hence avoid data duplication. It is the case of ``PandasIndex`` and ``PandasMultiIndex``, where we can @@ -150,7 +150,7 @@ Xarray index(es). We call such index a "meta-index". Here is a small example of a meta-index for geospatial, raster datasets (i.e., regularly spaced 2-dimensional data) that internally relies on two -``PandasIndex`` instances for the x and y dimensions, respectively: +``PandasIndex`` instances for the x and y dimensions respectively: .. code-block:: python diff --git a/xarray/core/indexes.py b/xarray/core/indexes.py index 3365597bc78..474e161d0a4 100644 --- a/xarray/core/indexes.py +++ b/xarray/core/indexes.py @@ -25,7 +25,7 @@ from .utils import Frozen, get_valid_numpy_dtype, is_dict_like, is_scalar if TYPE_CHECKING: - from .types import ErrorOptions, T_Index + from .types import ErrorOptions, JoinOptions, T_Index from .variable import Variable IndexVars = Dict[Any, "Variable"] @@ -259,7 +259,7 @@ def sel(self, labels: dict[Any, Any]) -> IndexSelResult: """ raise NotImplementedError(f"{self!r} doesn't support label-based selection") - def join(self: T_Index, other: T_Index, how: str = "inner") -> T_Index: + def join(self: T_Index, other: T_Index, how: JoinOptions = "inner") -> T_Index: """Return a new index from the combination of this index with another index of the same type. @@ -269,7 +269,7 @@ def join(self: T_Index, other: T_Index, how: str = "inner") -> T_Index: ---------- other : Index The other Index object to combine with this index. - join : {"outer", "inner", "left", "right", "exact", "override"}, optional + join : str, optional Method for joining the two indexes (see :py:func:`~xarray.align`). Returns From d7548932cfb4f8bdb7ba14322be9e253c960c914 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Mon, 5 Sep 2022 09:15:47 +0200 Subject: [PATCH 09/14] update what's new --- doc/whats-new.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/whats-new.rst b/doc/whats-new.rst index 9ce51e48983..9f89dc04270 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -63,6 +63,9 @@ Bug fixes Documentation ~~~~~~~~~~~~~ +- Add docstrings for the :py:class:`Index` base class and add some documentation on how to + create custom, Xarray-compatible indexes (:pull:`6975`) + By `BenoƮt Bovy `_. Internal Changes ~~~~~~~~~~~~~~~~ From e4e0e1f3ebf1ba36f3c5ae9f157192d62638cd03 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 15 Jul 2023 16:49:01 +0000 Subject: [PATCH 10/14] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- doc/internals/how-to-create-custom-index.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/internals/how-to-create-custom-index.rst b/doc/internals/how-to-create-custom-index.rst index 8b84f173d5b..7cdb3d39094 100644 --- a/doc/internals/how-to-create-custom-index.rst +++ b/doc/internals/how-to-create-custom-index.rst @@ -161,7 +161,6 @@ regularly spaced 2-dimensional data) that internally relies on two class RasterIndex(Index): def __init__(self, xy_indexes): - assert len(xy_indexes) == 2 # must have two distinct dimensions From ee4963b9c80dfc8b065f382cbf17a51b29f39b90 Mon Sep 17 00:00:00 2001 From: Tom Nicholas Date: Sat, 15 Jul 2023 13:04:53 -0400 Subject: [PATCH 11/14] Apply uncontroversial suggestions from Deepak's code review Co-authored-by: Deepak Cherian --- doc/internals/how-to-create-custom-index.rst | 38 ++++++++++---------- xarray/core/indexes.py | 12 +++---- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/doc/internals/how-to-create-custom-index.rst b/doc/internals/how-to-create-custom-index.rst index 7cdb3d39094..2bc2140e8d2 100644 --- a/doc/internals/how-to-create-custom-index.rst +++ b/doc/internals/how-to-create-custom-index.rst @@ -7,11 +7,11 @@ How to create a custom index This feature is highly experimental. Support for custom indexes has been introduced in v2022.06.0 and is still incomplete. API is subject to change - without deprecation notice. + without deprecation notice. However we encourage you to experiment and report issues that arise. -Xarray's built-in support for label-based indexing and alignment operations -relies on :py:class:`pandas.Index` objects. It is powerful and suitable for many -applications but it also has some limitations: +Xarray's built-in support for label-based indexing (e.g. `ds.sel(latitude=40, method="nearest")`) and alignment operations +relies on :py:class:`pandas.Index` objects. Pandas Indexes are powerful and suitable for many +applications but also have some limitations: - it only works with 1-dimensional coordinates where explicit labels are fully loaded in memory @@ -31,9 +31,9 @@ example the case of Xarray built-in ``PandasIndex`` and ``PandasMultiIndex`` subclasses, which wrap :py:class:`pandas.Index` and :py:class:`pandas.MultiIndex` respectively. -The ``Index`` API closely follows the :py:meth:`Dataset` and -:py:meth:`DataArray` API, e.g., for an index to support ``.sel()`` it needs to -implement :py:meth:`Index.sel`, to support ``.stack()`` and ``.unstack()`` it +The ``Index`` API closely follows the :py:class:`Dataset` and +:py:class:`DataArray` API, e.g., for an index to support :py:meth:`DataArray.sel` it needs to +implement :py:meth:`Index.sel`, to support :py:meth:`DataArray.stack` and :py:meth:`DataArray.unstack` it needs to implement :py:meth:`Index.stack` and :py:meth:`Index.unstack`, etc. Some guidelines and examples are given below. More details can be found in the @@ -53,10 +53,10 @@ coordinates. For example, ``PandasIndex`` accepts only one coordinate and ``PandasMultiIndex`` accepts one or more 1-dimensional coordinates that must all -share the same dimension. Other, custom indexes wouldn't have the same +share the same dimension. Other, custom indexes need not have the same constraints, e.g., -- a georeferenced raster index which only accepts two 1-d coordinates with each +- a georeferenced raster index which only accepts two 1-d coordinates with distinct dimensions - a staggered grid index which takes coordinates with different dimension name suffixes (e.g., "_c" and "_l" for center and left) @@ -64,16 +64,16 @@ constraints, e.g., Optional requirements --------------------- -Pretty much everything else is optional. Depending on the case, in the absence -of a (re)implementation, an index will either raise an error (operation not -supported) or won't do anything specific (just drop, pass or copy itself +Pretty much everything else is optional. Depending on the method, in the absence +of a (re)implementation, an index will either raise a `NotImplementedError` +or won't do anything specific (just drop, pass or copy itself from/to the resulting Dataset or DataArray). For example, you can just skip re-implementing :py:meth:`Index.rename` if there is no internal attribute or object to rename according to the new desired coordinate or dimension names. In the case of ``PandasIndex``, we rename the underlying ``pandas.Index`` object and/or update the ``PandasIndex.dim`` -attribute. +attribute since the associated dimension name has been changed. Wrap index data as coordinate data ---------------------------------- @@ -81,7 +81,7 @@ Wrap index data as coordinate data In some cases it is possible to reuse the index's underlying object or structure as coordinate data and hence avoid data duplication. -It is the case of ``PandasIndex`` and ``PandasMultiIndex``, where we can +For ``PandasIndex`` and ``PandasMultiIndex``, we leverage the fact that ``pandas.Index`` objects expose some array-like API. In Xarray we use some wrappers around those underlying objects as a thin compatibility layer to preserve dtypes, handle explicit and n-dimensional @@ -109,11 +109,11 @@ the index to properly handle those input labels. :py:meth:`Index.sel` must return an instance of :py:class:`IndexSelResult`. The latter is a small data class that holds positional indexers (indices) and that may also hold new variables, new indexes, names of variables or indexes to drop, -names of dimensions to rename, etc. This is useful in the case of -``PandasMultiIndex`` as it allows to convert it into a single ``PandasIndex`` +names of dimensions to rename, etc. For example, this is useful in the case of +``PandasMultiIndex`` as it allows Xarray to convert it into a single ``PandasIndex`` when only one level remains after the selection. -The ``IndexSelResult`` class is also used to merge results from label-based +The :py:class:`IndexSelResult` class is also used to merge results from label-based selection performed by different indexes. Note that it is now possible to have two distinct indexes for two 1-d coordinates sharing the same dimension, but it is not currently possible to use those two indexes in the same call to @@ -208,7 +208,7 @@ reference system. How to use a custom index ------------------------- -You can use ``Dataset.set_xindex()`` or ``DataArray.set_xindex()`` to assign a +You can use :py:meth:`Dataset.set_xindex` or :py:meth:`DataArray.set_xindex` to assign a custom index to a Dataset or DataArray, e.g., using the ``RasterIndex`` above: .. code-block:: python @@ -224,7 +224,7 @@ custom index to a Dataset or DataArray, e.g., using the ``RasterIndex`` above: # Xarray create default indexes for the 'x' and 'y' coordinates # we first need to explicitly drop it - da.drop_indexes(["x", "y"]) + da = da.drop_indexes(["x", "y"]) # Build a RasterIndex from the 'x' and 'y' coordinates da_raster = da.set_xindex(["x", "y"], RasterIndex) diff --git a/xarray/core/indexes.py b/xarray/core/indexes.py index 12f2f686ba0..bfa8b9da07a 100644 --- a/xarray/core/indexes.py +++ b/xarray/core/indexes.py @@ -97,7 +97,7 @@ def concat( type. Implementation is optional but required in order to support - concatenation. Otherwise it will raise an error if the index needs to be + ``concat``. Otherwise it will raise an error if the index needs to be updated during the operation. Parameters @@ -126,7 +126,7 @@ def stack( """Create a new index by stacking coordinate variables into a single new dimension. - Implementation is optional but required in order to support stack. + Implementation is optional but required in order to support ``stack``. Otherwise it will raise an error when trying to pass the Index subclass as argument to :py:meth:`Dataset.stack`. @@ -156,7 +156,7 @@ def unstack(self) -> tuple[dict[Hashable, Index], pd.MultiIndex]: ------- indexes : tuple A 2-length tuple where the 1st item is a dictionary of unstacked - Index objects and the 2nd item is a :py:class`pandas.MultiIndex` + Index objects and the 2nd item is a :py:class:`pandas.MultiIndex` object used to unstack unindexed coordinate variables or data variables. """ @@ -354,11 +354,11 @@ def rename( """Maybe update the index with new coordinate and dimension names. This method should be re-implemented in subclasses of Index if it has - attributes that depends on coordinate or dimension names. + attributes that depend on coordinate or dimension names. By default (if not re-implemented), it returns the index itself. - Warning: the input names are not filtered for this index, they may + Warning: the input names are not filtered for this method, they may correspond to any variable or dimension of a Dataset or a DataArray. Parameters @@ -380,7 +380,7 @@ def rename( def copy(self: T_Index, deep: bool = True) -> T_Index: """Return a (deep) copy of this index. - Implementation is subclasses of Index is optional. The base class + Implementation in subclasses of Index is optional. The base class implements the default (deep) copy semantics. Parameters From 7e2cecbed088049df1dbc92d4f2b527454646b8a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 15 Jul 2023 17:05:30 +0000 Subject: [PATCH 12/14] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- doc/internals/how-to-create-custom-index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/internals/how-to-create-custom-index.rst b/doc/internals/how-to-create-custom-index.rst index 2bc2140e8d2..d19f65c7be0 100644 --- a/doc/internals/how-to-create-custom-index.rst +++ b/doc/internals/how-to-create-custom-index.rst @@ -65,7 +65,7 @@ Optional requirements --------------------- Pretty much everything else is optional. Depending on the method, in the absence -of a (re)implementation, an index will either raise a `NotImplementedError` +of a (re)implementation, an index will either raise a `NotImplementedError` or won't do anything specific (just drop, pass or copy itself from/to the resulting Dataset or DataArray). From 0bbc1e521395de9b148e0f56552c61d4bc5ea095 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Mon, 17 Jul 2023 10:14:23 +0200 Subject: [PATCH 13/14] Apply more suggestions from code review Co-authored-by: Deepak Cherian --- doc/internals/how-to-create-custom-index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/internals/how-to-create-custom-index.rst b/doc/internals/how-to-create-custom-index.rst index d19f65c7be0..edb3f31fe10 100644 --- a/doc/internals/how-to-create-custom-index.rst +++ b/doc/internals/how-to-create-custom-index.rst @@ -92,7 +92,7 @@ from arrays to reuse it as coordinate data. If the index data can be reused as coordinate data, the ``Index`` subclass should implement :py:meth:`Index.create_variables`. This method accepts a -dictionary of :py:class:`Variable` objects as input (used for propagating +dictionary of variable names as keys and :py:class:`Variable` objects as values (used for propagating variable metadata) and should return a dictionary of new :py:class:`Variable` or :py:class:`IndexVariable` objects. From 07814bc579a0687ddc4deef0a1825c16ba02333e Mon Sep 17 00:00:00 2001 From: Tom Nicholas Date: Mon, 17 Jul 2023 17:01:36 -0400 Subject: [PATCH 14/14] Link to source code for PandasIndex and PandasMultiIndex --- doc/internals/how-to-create-custom-index.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/internals/how-to-create-custom-index.rst b/doc/internals/how-to-create-custom-index.rst index edb3f31fe10..93805229db1 100644 --- a/doc/internals/how-to-create-custom-index.rst +++ b/doc/internals/how-to-create-custom-index.rst @@ -51,8 +51,8 @@ number, order and dimensions of the coordinates are all arbitrary), it is the responsibility of the index to check the consistency and validity of those input coordinates. -For example, ``PandasIndex`` accepts only one coordinate and -``PandasMultiIndex`` accepts one or more 1-dimensional coordinates that must all +For example, :py:class:`~xarray.core.indexes.PandasIndex` accepts only one coordinate and +:py:class:`~xarray.core.indexes.PandasMultiIndex` accepts one or more 1-dimensional coordinates that must all share the same dimension. Other, custom indexes need not have the same constraints, e.g.,