Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions docs/src/whatsnew/latest.rst
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ This document explains the changes made to Iris for this release
#. `@wjbenfold`_ and `@stephenworsley`_ (reviewer) added a maximum run length
aggregator (:class:`~iris.analysis.MAX_RUN`). (:pull:`4676`)

#. `@wjbenfold`_ and `@rcomer`_ (reviewer) added a ``climatological`` keyword to
:meth:`~iris.cube.Cube.aggregated_by` that causes the climatological flag to
be set and the point for each cell to equal its first bound, thereby
preserving the time of year.


🐛 Bugs Fixed
=============
Expand Down
43 changes: 34 additions & 9 deletions lib/iris/analysis/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2209,7 +2209,9 @@ class _Groupby:

"""

def __init__(self, groupby_coords, shared_coords=None):
def __init__(
self, groupby_coords, shared_coords=None, climatological=False
):
"""
Determine the group slices over the group-by coordinates.

Expand All @@ -2225,6 +2227,12 @@ def __init__(self, groupby_coords, shared_coords=None):
that share the same group-by coordinate axis. The `int` identifies
which dimension of the coord is on the group-by coordinate axis.

* climatological (bool):
Indicates whether the output is expected to be climatological. For
any aggregated time coord(s), this causes the climatological flag to
be set and the point for each cell to equal its first bound, thereby
preserving the time of year.

"""
#: Group-by and shared coordinates that have been grouped.
self.coords = []
Expand Down Expand Up @@ -2253,6 +2261,13 @@ def __init__(self, groupby_coords, shared_coords=None):
for coord, dim in shared_coords:
self._add_shared_coord(coord, dim)

# Aggregation is climatological in nature
self.climatological = climatological

# Stores mapping from original cube coords to new ones, as metadata may
# not match
self.coord_replacement_mapping = []

def _add_groupby_coord(self, coord):
if coord.ndim != 1:
raise iris.exceptions.CoordinateMultiDimError(coord)
Expand Down Expand Up @@ -2411,6 +2426,9 @@ def _compute_shared_coords(self):

# Create new shared bounded coordinates.
for coord, dim in self._shared_coords:
climatological_coord = (
self.climatological and coord.units.is_time_reference()
)
if coord.points.dtype.kind in "SU":
if coord.bounds is None:
new_points = []
Expand Down Expand Up @@ -2449,6 +2467,7 @@ def _compute_shared_coords(self):
maxmin_axis = (dim, -1)
first_choices = coord.bounds.take(0, -1)
last_choices = coord.bounds.take(1, -1)

else:
# Derive new coord's bounds from points.
item = coord.points
Expand Down Expand Up @@ -2501,7 +2520,11 @@ def _compute_shared_coords(self):

# Now create the new bounded group shared coordinate.
try:
new_points = new_bounds.mean(-1)
if climatological_coord:
# Use the first bound as the point
new_points = new_bounds[..., 0]
else:
new_points = new_bounds.mean(-1)
except TypeError:
msg = (
"The {0!r} coordinate on the collapsing dimension"
Expand All @@ -2510,17 +2533,19 @@ def _compute_shared_coords(self):
raise ValueError(msg)

try:
self.coords.append(
coord.copy(points=new_points, bounds=new_bounds)
)
new_coord = coord.copy(points=new_points, bounds=new_bounds)
except ValueError:
# non monotonic points/bounds
self.coords.append(
iris.coords.AuxCoord.from_coord(coord).copy(
points=new_points, bounds=new_bounds
)
new_coord = iris.coords.AuxCoord.from_coord(coord).copy(
points=new_points, bounds=new_bounds
)

if climatological_coord:
new_coord.climatological = True
self.coord_replacement_mapping.append((coord, new_coord))

self.coords.append(new_coord)

def __len__(self):
"""Calculate the number of groups given the group-by coordinates."""

Expand Down
76 changes: 48 additions & 28 deletions lib/iris/cube.py
Original file line number Diff line number Diff line change
Expand Up @@ -3840,45 +3840,53 @@ def collapsed(self, coords, aggregator, **kwargs):
)
return result

def aggregated_by(self, coords, aggregator, **kwargs):
def aggregated_by(
self, coords, aggregator, climatological=False, **kwargs
):
"""
Perform aggregation over the cube given one or more "group
coordinates".
Perform aggregation over the cube given one or more "group coordinates".

A "group coordinate" is a coordinate where repeating values represent a
single group, such as a month coordinate on a daily time slice.
Repeated values will form a group even if they are not consecutive.
single group, such as a month coordinate on a daily time slice. Repeated
values will form a group even if they are not consecutive.

The group coordinates must all be over the same cube dimension. Each
common value group identified over all the group-by coordinates is
collapsed using the provided aggregator.

Weighted aggregations (:class:`iris.analysis.WeightedAggregator`) may
also be supplied. These include :data:`~iris.analysis.MEAN` and
sum :data:`~iris.analysis.SUM`.
:data:`~iris.analysis.SUM`.

Weighted aggregations support an optional *weights* keyword argument.
If set, this should be supplied as an array of weights whose shape
matches the cube or as 1D array whose length matches the dimension over
which is aggregated.
Weighted aggregations support an optional *weights* keyword argument. If
set, this should be supplied as an array of weights whose shape matches
the cube or as 1D array whose length matches the dimension over which is
aggregated.

Args:

* coords (list of coord names or :class:`iris.coords.Coord` instances):
Parameters
----------
coords : (list of coord names or :class:`iris.coords.Coord` instances)
One or more coordinates over which group aggregation is to be
performed.
* aggregator (:class:`iris.analysis.Aggregator`):
aggregator : :class:`iris.analysis.Aggregator`
Aggregator to be applied to each group.

Kwargs:

* kwargs:
climatological : bool
Indicates whether the output is expected to be climatological. For
any aggregated time coord(s), this causes the climatological flag to
be set and the point for each cell to equal its first bound, thereby
preserving the time of year

Returns
-------
:class:`iris.cube.Cube`

Other Parameters
----------------
kwargs:
Aggregator and aggregation function keyword arguments.

Returns:
:class:`iris.cube.Cube`.

For example:
Examples
--------

>>> import iris
>>> import iris.analysis
Expand Down Expand Up @@ -3981,7 +3989,9 @@ def aggregated_by(self, coords, aggregator, **kwargs):

# Create the aggregation group-by instance.
groupby = iris.analysis._Groupby(
groupby_coords, shared_coords_and_dims
groupby_coords,
shared_coords_and_dims,
climatological=climatological,
)

# Create the resulting aggregate-by cube and remove the original
Expand Down Expand Up @@ -4103,17 +4113,27 @@ def aggregated_by(self, coords, aggregator, **kwargs):
dimensions=dimension_to_groupby, dim_coords=True
) or [None]
for coord in groupby.coords:
new_coord = coord.copy()

# The metadata may have changed (e.g. climatology), so check if
# there's a better coord to pass to self.coord_dims
lookup_coord = coord
for (
cube_coord,
groupby_coord,
) in groupby.coord_replacement_mapping:
if coord == groupby_coord:
lookup_coord = cube_coord

if (
dim_coord is not None
and dim_coord.metadata == coord.metadata
and dim_coord.metadata == lookup_coord.metadata
and isinstance(coord, iris.coords.DimCoord)
):
aggregateby_cube.add_dim_coord(
coord.copy(), dimension_to_groupby
)
aggregateby_cube.add_dim_coord(new_coord, dimension_to_groupby)
else:
aggregateby_cube.add_aux_coord(
coord.copy(), self.coord_dims(coord)
new_coord, self.coord_dims(lookup_coord)
)

# Attach the aggregate-by data into the aggregate-by cube.
Expand Down
Loading