Skip to content

Commit 0074b76

Browse files
keewisbenbovydcherian
authored
modification methods on Coordinates (pydata#10318)
* implement `Coordinates` methods modifying coords * allow using the binary-or operator (`|`) for merging * tests for `drop_vars` * tests for `rename_dims` and `rename_vars` * tests for the merge operator * Apply suggestions from code review Co-authored-by: Benoit Bovy <[email protected]> * attempt to fix the typing * make sure we always return a `Coordinates` object * replace docstring by a reference to `Coordinates.merge` * changelog * create docs pages * add back the docstring for `__or__` * add `drop_dims` * typing * undo a bad merge [skip-ci] * document `drop_dims` [skip-ci] --------- Co-authored-by: Benoit Bovy <[email protected]> Co-authored-by: Deepak Cherian <[email protected]>
1 parent 9ed0102 commit 0074b76

File tree

4 files changed

+224
-3
lines changed

4 files changed

+224
-3
lines changed

doc/api/coordinates.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ and values given by ``DataArray`` objects.
3838
Coordinates.__getitem__
3939
Coordinates.__setitem__
4040
Coordinates.__delitem__
41+
Coordinates.__or__
4142
Coordinates.update
4243
Coordinates.get
4344
Coordinates.items
@@ -53,8 +54,12 @@ Coordinates contents
5354
Coordinates.to_dataset
5455
Coordinates.to_index
5556
Coordinates.assign
57+
Coordinates.drop_dims
58+
Coordinates.drop_vars
5659
Coordinates.merge
5760
Coordinates.copy
61+
Coordinates.rename_vars
62+
Coordinates.rename_dims
5863

5964
Comparisons
6065
-----------

doc/whats-new.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ v2025.08.1 (unreleased)
1212

1313
New Features
1414
~~~~~~~~~~~~
15-
15+
- Add convenience methods to :py:class:`~xarray.Coordinates` (:pull:`10318`)
16+
By `Justus Magin <https://github.com/keewis>`_.
1617
- Added :py:func:`load_datatree` for loading ``DataTree`` objects into memory
1718
from disk. It has the same relationship to :py:func:`open_datatree`, as
1819
:py:func:`load_dataset` has to :py:func:`open_dataset`.

xarray/core/coordinates.py

Lines changed: 133 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from __future__ import annotations
22

3-
from collections.abc import Hashable, Iterator, Mapping, Sequence
3+
from collections.abc import Callable, Hashable, Iterable, Iterator, Mapping, Sequence
44
from contextlib import contextmanager
55
from typing import (
66
TYPE_CHECKING,
@@ -21,7 +21,7 @@
2121
assert_no_index_corrupted,
2222
create_default_index_implicit,
2323
)
24-
from xarray.core.types import DataVars, Self, T_DataArray, T_Xarray
24+
from xarray.core.types import DataVars, ErrorOptions, Self, T_DataArray, T_Xarray
2525
from xarray.core.utils import (
2626
Frozen,
2727
ReprObject,
@@ -561,6 +561,35 @@ def merge(self, other: Mapping[Any, Any] | None) -> Dataset:
561561
variables=coords, coord_names=coord_names, indexes=indexes
562562
)
563563

564+
def __or__(self, other: Mapping[Any, Any] | None) -> Coordinates:
565+
"""Merge two sets of coordinates to create a new Coordinates object
566+
567+
The method implements the logic used for joining coordinates in the
568+
result of a binary operation performed on xarray objects:
569+
570+
- If two index coordinates conflict (are not equal), an exception is
571+
raised. You must align your data before passing it to this method.
572+
- If an index coordinate and a non-index coordinate conflict, the non-
573+
index coordinate is dropped.
574+
- If two non-index coordinates conflict, both are dropped.
575+
576+
Parameters
577+
----------
578+
other : dict-like, optional
579+
A :py:class:`Coordinates` object or any mapping that can be turned
580+
into coordinates.
581+
582+
Returns
583+
-------
584+
merged : Coordinates
585+
A new Coordinates object with merged coordinates.
586+
587+
See Also
588+
--------
589+
Coordinates.merge
590+
"""
591+
return self.merge(other).coords
592+
564593
def __setitem__(self, key: Hashable, value: Any) -> None:
565594
self.update({key: value})
566595

@@ -719,6 +748,108 @@ def copy(
719748
),
720749
)
721750

751+
def drop_vars(
752+
self,
753+
names: str
754+
| Iterable[Hashable]
755+
| Callable[
756+
[Coordinates | Dataset | DataArray | DataTree],
757+
str | Iterable[Hashable],
758+
],
759+
*,
760+
errors: ErrorOptions = "raise",
761+
) -> Self:
762+
"""Drop variables from this Coordinates object.
763+
764+
Note that indexes that depend on these variables will also be dropped.
765+
766+
Parameters
767+
----------
768+
names : hashable or iterable or callable
769+
Name(s) of variables to drop. If a callable, this is object is passed as its
770+
only argument and its result is used.
771+
errors : {"raise", "ignore"}, default: "raise"
772+
Error treatment.
773+
774+
- ``'raise'``: raises a :py:class:`ValueError` error if any of the variable
775+
passed are not in the dataset
776+
- ``'ignore'``: any given names that are in the dataset are dropped and no
777+
error is raised.
778+
"""
779+
return cast(Self, self.to_dataset().drop_vars(names, errors=errors).coords)
780+
781+
def drop_dims(
782+
self,
783+
drop_dims: str | Iterable[Hashable],
784+
*,
785+
errors: ErrorOptions = "raise",
786+
) -> Self:
787+
"""Drop dimensions and associated variables from this dataset.
788+
789+
Parameters
790+
----------
791+
drop_dims : str or Iterable of Hashable
792+
Dimension or dimensions to drop.
793+
errors : {"raise", "ignore"}, default: "raise"
794+
If 'raise', raises a ValueError error if any of the
795+
dimensions passed are not in the dataset. If 'ignore', any given
796+
dimensions that are in the dataset are dropped and no error is raised.
797+
798+
Returns
799+
-------
800+
obj : Coordinates
801+
Coordinates object without the given dimensions (or any coordinates
802+
containing those dimensions).
803+
"""
804+
return cast(Self, self.to_dataset().drop_dims(drop_dims, errors=errors).coords)
805+
806+
def rename_dims(
807+
self,
808+
dims_dict: Mapping[Any, Hashable] | None = None,
809+
**dims: Hashable,
810+
) -> Self:
811+
"""Returns a new object with renamed dimensions only.
812+
813+
Parameters
814+
----------
815+
dims_dict : dict-like, optional
816+
Dictionary whose keys are current dimension names and
817+
whose values are the desired names. The desired names must
818+
not be the name of an existing dimension or Variable in the Coordinates.
819+
**dims : optional
820+
Keyword form of ``dims_dict``.
821+
One of dims_dict or dims must be provided.
822+
823+
Returns
824+
-------
825+
renamed : Coordinates
826+
Coordinates object with renamed dimensions.
827+
"""
828+
return cast(Self, self.to_dataset().rename_dims(dims_dict, **dims).coords)
829+
830+
def rename_vars(
831+
self,
832+
name_dict: Mapping[Any, Hashable] | None = None,
833+
**names: Hashable,
834+
) -> Coordinates:
835+
"""Returns a new object with renamed variables.
836+
837+
Parameters
838+
----------
839+
name_dict : dict-like, optional
840+
Dictionary whose keys are current variable or coordinate names and
841+
whose values are the desired names.
842+
**names : optional
843+
Keyword form of ``name_dict``.
844+
One of name_dict or names must be provided.
845+
846+
Returns
847+
-------
848+
renamed : Coordinates
849+
Coordinates object with renamed variables
850+
"""
851+
return cast(Self, self.to_dataset().rename_vars(name_dict, **names).coords)
852+
722853

723854
class DatasetCoordinates(Coordinates):
724855
"""Dictionary like container for Dataset coordinates (variables + indexes).

xarray/tests/test_coordinates.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,3 +208,87 @@ def test_dataset_from_coords_with_multidim_var_same_name(self):
208208
coords = Coordinates(coords={"x": var}, indexes={})
209209
ds = Dataset(coords=coords)
210210
assert ds.coords["x"].dims == ("x", "y")
211+
212+
def test_drop_vars(self):
213+
coords = Coordinates(
214+
coords={
215+
"x": Variable("x", range(3)),
216+
"y": Variable("y", list("ab")),
217+
"a": Variable(["x", "y"], np.arange(6).reshape(3, 2)),
218+
},
219+
indexes={},
220+
)
221+
222+
actual = coords.drop_vars("x")
223+
assert isinstance(actual, Coordinates)
224+
assert set(actual.variables) == {"a", "y"}
225+
226+
actual = coords.drop_vars(["x", "y"])
227+
assert isinstance(actual, Coordinates)
228+
assert set(actual.variables) == {"a"}
229+
230+
def test_drop_dims(self) -> None:
231+
coords = Coordinates(
232+
coords={
233+
"x": Variable("x", range(3)),
234+
"y": Variable("y", list("ab")),
235+
"a": Variable(["x", "y"], np.arange(6).reshape(3, 2)),
236+
},
237+
indexes={},
238+
)
239+
240+
actual = coords.drop_dims("x")
241+
assert isinstance(actual, Coordinates)
242+
assert set(actual.variables) == {"y"}
243+
244+
actual = coords.drop_dims(["x", "y"])
245+
assert isinstance(actual, Coordinates)
246+
assert set(actual.variables) == set()
247+
248+
def test_rename_dims(self) -> None:
249+
coords = Coordinates(
250+
coords={
251+
"x": Variable("x", range(3)),
252+
"y": Variable("y", list("ab")),
253+
"a": Variable(["x", "y"], np.arange(6).reshape(3, 2)),
254+
},
255+
indexes={},
256+
)
257+
258+
actual = coords.rename_dims({"x": "X"})
259+
assert isinstance(actual, Coordinates)
260+
assert set(actual.dims) == {"X", "y"}
261+
assert set(actual.variables) == {"a", "x", "y"}
262+
263+
actual = coords.rename_dims({"x": "u", "y": "v"})
264+
assert isinstance(actual, Coordinates)
265+
assert set(actual.dims) == {"u", "v"}
266+
assert set(actual.variables) == {"a", "x", "y"}
267+
268+
def test_rename_vars(self) -> None:
269+
coords = Coordinates(
270+
coords={
271+
"x": Variable("x", range(3)),
272+
"y": Variable("y", list("ab")),
273+
"a": Variable(["x", "y"], np.arange(6).reshape(3, 2)),
274+
},
275+
indexes={},
276+
)
277+
278+
actual = coords.rename_vars({"x": "X"})
279+
assert isinstance(actual, Coordinates)
280+
assert set(actual.dims) == {"x", "y"}
281+
assert set(actual.variables) == {"a", "X", "y"}
282+
283+
actual = coords.rename_vars({"x": "u", "y": "v"})
284+
assert isinstance(actual, Coordinates)
285+
assert set(actual.dims) == {"x", "y"}
286+
assert set(actual.variables) == {"a", "u", "v"}
287+
288+
def test_operator_merge(self) -> None:
289+
coords1 = Coordinates({"x": ("x", [0, 1, 2])})
290+
coords2 = Coordinates({"y": ("y", [3, 4, 5])})
291+
expected = Dataset(coords={"x": [0, 1, 2], "y": [3, 4, 5]})
292+
293+
actual = coords1 | coords2
294+
assert_identical(Dataset(coords=actual), expected)

0 commit comments

Comments
 (0)