Skip to content

Climatology & Anomalies

Compute climatologies and seasonal cycles, then form anomalies by removing them. For a field \(x(t)\) with climatology \(\bar{x}(\tau)\) over the cycle phase \(\tau\) (e.g. day-of-year), the anomaly is

\[ x'(t) = x(t) - \bar{x}\bigl(\tau(t)\bigr). \]

The *_smoothed variants apply a harmonic / rolling smoother to \(\bar{x}(\tau)\) before subtraction.

Operators

CalculateClimatology

CalculateClimatology(freq: str = 'day', time: str = 'time')

Bases: Operator

Compute a climatology grouped at freq from the time axis.

Parameters:

Name Type Description Default
freq str

Climatology grouping — e.g. "day" (day-of-year), "month", "season" (one of CLIMATOLOGY_DIMS).

'day'
time str

Name of the time coordinate.

'time'

Returns:

Type Description

A dataset of the per-group climatological means, with time

replaced by the grouping dimension.

Source code in packages/xrtoolz-ops/src/xrops/anomaly/operators.py
def __init__(self, freq: str = "day", time: str = "time"):
    if freq not in _detrend.CLIMATOLOGY_DIMS:
        raise ValueError(
            f"Unsupported climatology frequency {freq!r}; expected one of "
            f"{sorted(_detrend.CLIMATOLOGY_DIMS)}."
        )
    self.freq = freq
    self.time = time

CalculateClimatologySmoothed

CalculateClimatologySmoothed(window: int = 60, time: str = 'time')

Bases: Operator

Compute a day-of-year climatology smoothed with a rolling window.

Parameters:

Name Type Description Default
window int

Width (in days) of the centred rolling-mean smoothing applied around the day-of-year cycle.

60
time str

Name of the time coordinate.

'time'

Returns:

Type Description

A smoothed day-of-year climatology dataset.

Source code in packages/xrtoolz-ops/src/xrops/anomaly/operators.py
def __init__(self, window: int = 60, time: str = "time"):
    self.window = window
    self.time = time

RemoveClimatology

RemoveClimatology(climatology, time: str = 'time')

Bases: Operator

Subtract a precomputed climatology to form anomalies.

Parameters:

Name Type Description Default
climatology

Climatology dataset (e.g. from :class:CalculateClimatology) to subtract.

required
time str

Name of the time coordinate.

'time'

Returns:

Type Description

The anomaly dataset (input minus the matched climatology).

Source code in packages/xrtoolz-ops/src/xrops/anomaly/operators.py
def __init__(self, climatology, time: str = "time"):
    self.climatology = climatology
    self.time = time

AddClimatology

AddClimatology(climatology, time: str = 'time')

Bases: Operator

Add a climatology back to anomalies (inverse of :class:RemoveClimatology).

Parameters:

Name Type Description Default
climatology

Climatology dataset to add.

required
time str

Name of the time coordinate.

'time'

Returns:

Type Description

The reconstructed dataset (anomaly plus climatology).

Source code in packages/xrtoolz-ops/src/xrops/anomaly/operators.py
def __init__(self, climatology, time: str = "time"):
    self.climatology = climatology
    self.time = time

RemoveMean

RemoveMean(dim: str | Sequence[str])

Bases: Operator

Subtract the mean over dim (a cheap anomaly without a climatology).

Parameters:

Name Type Description Default
dim str | Sequence[str]

Dimension or dimensions to average over before subtracting.

required

Returns:

Type Description

The input dataset with the per-dim mean removed.

Source code in packages/xrtoolz-ops/src/xrops/anomaly/operators.py
def __init__(self, dim: str | Sequence[str]):
    self.dim = (dim,) if isinstance(dim, str) else tuple(dim)

Functional primitives (Layer 0)

These pure functions back the operators above; each takes xr.DataArray/xr.Dataset and a dim: argument.

calculate_climatology

calculate_climatology(data: DataArray | Dataset, freq: str = 'day', time: str = 'time') -> DataArray | Dataset

Compute the climatology by grouping time at a given frequency.

Parameters:

Name Type Description Default
data DataArray | Dataset

Input DataArray or Dataset with a time coordinate.

required
freq str

One of "day", "month", "year". Maps to xarray's time.dayofyear, time.month, time.year.

'day'
time str

Name of the time coordinate (default "time").

'time'

Returns:

Type Description
DataArray | Dataset

Climatology, same container type as data, with time

DataArray | Dataset

replaced by the grouping dimension

DataArray | Dataset

(dayofyear / month / year).

Source code in packages/xrtoolz-ops/src/xrops/anomaly/_src/detrend.py
def calculate_climatology(
    data: xr.DataArray | xr.Dataset,
    freq: str = "day",
    time: str = "time",
) -> xr.DataArray | xr.Dataset:
    """Compute the climatology by grouping ``time`` at a given frequency.

    Args:
        data: Input DataArray or Dataset with a ``time`` coordinate.
        freq: One of ``"day"``, ``"month"``, ``"year"``. Maps to
            xarray's ``time.dayofyear``, ``time.month``, ``time.year``.
        time: Name of the time coordinate (default ``"time"``).

    Returns:
        Climatology, same container type as ``data``, with ``time``
        replaced by the grouping dimension
        (``dayofyear`` / ``month`` / ``year``).
    """
    dim = _climatology_dim(freq)
    return data.groupby(f"{time}.{dim}").mean(time)

calculate_climatology_season

calculate_climatology_season(data: DataArray | Dataset, time: str = 'time') -> DataArray | Dataset

Climatology grouped by meteorological season (DJF/MAM/JJA/SON).

Parameters:

Name Type Description Default
data DataArray | Dataset

Input DataArray or Dataset with a time coordinate.

required
time str

Name of the time coordinate.

'time'

Returns:

Type Description
DataArray | Dataset

Climatology with season as the grouping dimension.

Source code in packages/xrtoolz-ops/src/xrops/anomaly/_src/detrend.py
def calculate_climatology_season(
    data: xr.DataArray | xr.Dataset,
    time: str = "time",
) -> xr.DataArray | xr.Dataset:
    """Climatology grouped by meteorological season (DJF/MAM/JJA/SON).

    Args:
        data: Input DataArray or Dataset with a ``time`` coordinate.
        time: Name of the time coordinate.

    Returns:
        Climatology with ``season`` as the grouping dimension.
    """
    season = data[time].dt.month.copy(
        data=[SEASONS[int(m)] for m in data[time].dt.month.values]
    )
    with_season = data.assign_coords(season=(time, season.values))
    return with_season.groupby("season").mean(time)

calculate_climatology_smoothed

calculate_climatology_smoothed(data: DataArray | Dataset, window: int = 60, time: str = 'time') -> DataArray | Dataset

Daily climatology smoothed with a circular rolling window.

Pads the climatology using wrap mode so the smoothing doesn't bias the January / December edges, then applies a centered rolling mean.

Parameters:

Name Type Description Default
data DataArray | Dataset

Input DataArray or Dataset with a time coordinate.

required
window int

Length of the rolling window in days (must be even so that the half-window window // 2 padding is symmetric).

60
time str

Name of the time coordinate.

'time'

Returns:

Type Description
DataArray | Dataset

Smoothed day-of-year climatology.

Source code in packages/xrtoolz-ops/src/xrops/anomaly/_src/detrend.py
def calculate_climatology_smoothed(
    data: xr.DataArray | xr.Dataset,
    window: int = 60,
    time: str = "time",
) -> xr.DataArray | xr.Dataset:
    """Daily climatology smoothed with a circular rolling window.

    Pads the climatology using wrap mode so the smoothing doesn't bias
    the January / December edges, then applies a centered rolling mean.

    Args:
        data: Input DataArray or Dataset with a ``time`` coordinate.
        window: Length of the rolling window in days (must be even so
            that the half-window ``window // 2`` padding is symmetric).
        time: Name of the time coordinate.

    Returns:
        Smoothed day-of-year climatology.
    """
    if window <= 0 or window % 2 != 0:
        raise ValueError(f"window must be a positive even integer, got {window}.")

    pad = window // 2
    clim = calculate_climatology(data, freq="day", time=time)
    clim = clim.pad(dayofyear=(pad, pad), mode="wrap")
    clim = clim.rolling(dayofyear=window, center=True, min_periods=1).mean()
    return clim.isel(dayofyear=slice(pad, -pad))

calculate_anomaly

calculate_anomaly(data: DataArray | Dataset, freq: str = 'day', time: str = 'time') -> DataArray | Dataset

Convenience: compute climatology at freq and subtract it.

Source code in packages/xrtoolz-ops/src/xrops/anomaly/_src/detrend.py
def calculate_anomaly(
    data: xr.DataArray | xr.Dataset,
    freq: str = "day",
    time: str = "time",
) -> xr.DataArray | xr.Dataset:
    """Convenience: compute climatology at ``freq`` and subtract it."""
    clim = calculate_climatology(data, freq=freq, time=time)
    return remove_climatology(data, clim, time=time)

calculate_anomaly_smoothed

calculate_anomaly_smoothed(data: DataArray | Dataset, window: int = 60, time: str = 'time') -> DataArray | Dataset

Convenience: smoothed daily climatology then subtract it.

Source code in packages/xrtoolz-ops/src/xrops/anomaly/_src/detrend.py
def calculate_anomaly_smoothed(
    data: xr.DataArray | xr.Dataset,
    window: int = 60,
    time: str = "time",
) -> xr.DataArray | xr.Dataset:
    """Convenience: smoothed daily climatology then subtract it."""
    clim = calculate_climatology_smoothed(data, window=window, time=time)
    return remove_climatology(data, clim, time=time)

remove_climatology

remove_climatology(data: DataArray | Dataset, climatology: DataArray | Dataset, time: str = 'time') -> DataArray | Dataset

Subtract a precomputed climatology from data.

The grouping frequency is inferred from climatology: it must carry exactly one of the dimensions dayofyear, month, or year.

Parameters:

Name Type Description Default
data DataArray | Dataset

Input with a time coordinate.

required
climatology DataArray | Dataset

Output of :func:calculate_climatology or friends.

required
time str

Name of the time coordinate.

'time'

Returns:

Type Description
DataArray | Dataset

Anomalies data - climatology.

Source code in packages/xrtoolz-ops/src/xrops/anomaly/_src/detrend.py
def remove_climatology(
    data: xr.DataArray | xr.Dataset,
    climatology: xr.DataArray | xr.Dataset,
    time: str = "time",
) -> xr.DataArray | xr.Dataset:
    """Subtract a precomputed climatology from ``data``.

    The grouping frequency is inferred from ``climatology``: it must
    carry exactly one of the dimensions ``dayofyear``, ``month``, or
    ``year``.

    Args:
        data: Input with a ``time`` coordinate.
        climatology: Output of :func:`calculate_climatology` or friends.
        time: Name of the time coordinate.

    Returns:
        Anomalies ``data - climatology``.
    """
    dim = _infer_climatology_dim(climatology)
    return data.groupby(f"{time}.{dim}") - climatology

add_climatology

add_climatology(data: DataArray | Dataset, climatology: DataArray | Dataset, time: str = 'time') -> DataArray | Dataset

Add a climatology back onto data (inverse of :func:remove_climatology).

Parameters:

Name Type Description Default
data DataArray | Dataset

Anomaly field with a time coordinate.

required
climatology DataArray | Dataset

Climatology computed via :func:calculate_climatology.

required
time str

Name of the time coordinate.

'time'

Returns:

Type Description
DataArray | Dataset

Reconstructed field data + climatology.

Source code in packages/xrtoolz-ops/src/xrops/anomaly/_src/detrend.py
def add_climatology(
    data: xr.DataArray | xr.Dataset,
    climatology: xr.DataArray | xr.Dataset,
    time: str = "time",
) -> xr.DataArray | xr.Dataset:
    """Add a climatology back onto ``data`` (inverse of
    :func:`remove_climatology`).

    Args:
        data: Anomaly field with a ``time`` coordinate.
        climatology: Climatology computed via :func:`calculate_climatology`.
        time: Name of the time coordinate.

    Returns:
        Reconstructed field ``data + climatology``.
    """
    dim = _infer_climatology_dim(climatology)
    return data.groupby(f"{time}.{dim}") + climatology

remove_mean

remove_mean(data: DataArray | Dataset, dims: str | tuple[str, ...]) -> DataArray | Dataset

Subtract the mean over dims (per-variable, NaN-aware).

Useful for cheap anomaly fields when no climatology is available (e.g. spatial mean removal per timestep, or zonal-mean removal). Distinct from :func:calculate_anomaly, which subtracts a time-grouped climatology.

Source code in packages/xrtoolz-ops/src/xrops/anomaly/_src/detrend.py
def remove_mean(
    data: xr.DataArray | xr.Dataset,
    dims: str | tuple[str, ...],
) -> xr.DataArray | xr.Dataset:
    """Subtract the mean over ``dims`` (per-variable, NaN-aware).

    Useful for cheap anomaly fields when no climatology is available
    (e.g. spatial mean removal per timestep, or zonal-mean removal).
    Distinct from :func:`calculate_anomaly`, which subtracts a
    time-grouped climatology.
    """
    dim_list = [dims] if isinstance(dims, str) else list(dims)
    return data - data.mean(dim=dim_list)