You can run this notebook in a live session or view it on Github.

# Calculating Seasonal Averages from Time Series of Monthly Means

Author: Joe Hamman

The data used for this example can be found in the xarray-data repository. You may need to change the path to `rasm.nc` below.

Suppose we have a netCDF or `xarray.Dataset` of monthly mean data and we want to calculate the seasonal average. To do this properly, we need to calculate the weighted average considering that each month has a different number of days.

    [1]:

    %matplotlib inline
    import numpy as np
    import pandas as pd
    import xarray as xr
    import matplotlib.pyplot as plt

## Open the `Dataset`

    [2]:

    ds = xr.tutorial.open_dataset("rasm").load()
    ds

    ---------------------------------------------------------------------------
    PermissionError                           Traceback (most recent call last)
    File /usr/lib/python3/dist-packages/pooch/utils.py:262, in make_local_storage(path, env)
        258 if action == "create":
        259     # When running in parallel, it's possible that multiple jobs will
        260     # try to create the path at the same time. Use exist_ok to avoid
        261     # raising an error.
    --> 262     os.makedirs(path, exist_ok=True)
        263 else:

    File /usr/lib/python3.13/os.py:217, in makedirs(name, mode, exist_ok)
        216 try:
    --> 217     makedirs(head, exist_ok=exist_ok)
        218 except FileExistsError:
        219     # Defeats race condition when another thread created the path

    File /usr/lib/python3.13/os.py:217, in makedirs(name, mode, exist_ok)
        216 try:
    --> 217     makedirs(head, exist_ok=exist_ok)
        218 except FileExistsError:
        219     # Defeats race condition when another thread created the path

    File /usr/lib/python3.13/os.py:227, in makedirs(name, mode, exist_ok)
        226 try:
    --> 227     mkdir(name, mode)
        228 except OSError:
        229     # Cannot rely on checking for EEXIST, since the operating system
        230     # could give priority to other errors like EACCES or EROFS

    PermissionError: [Errno 13] Permission denied: '/sbuild-nonexistent'

    The above exception was the direct cause of the following exception:

    PermissionError                           Traceback (most recent call last)
    Cell In[2], line 1
    ----> 1 ds = xr.tutorial.open_dataset("rasm").load()
          2 ds

    File /usr/lib/python3/dist-packages/xarray/tutorial.py:165, in open_dataset(name, cache, cache_dir, engine, **kws)
        162 downloader = pooch.HTTPDownloader(headers=headers)
        164 # retrieve the file
    --> 165 filepath = pooch.retrieve(
        166     url=url, known_hash=None, path=cache_dir, downloader=downloader
        167 )
        168 ds = _open_dataset(filepath, engine=engine, **kws)
        169 if not cache:

    File /usr/lib/python3/dist-packages/pooch/core.py:227, in retrieve(url, known_hash, fname, path, processor, downloader, progressbar)
        222 action, verb = download_action(full_path, known_hash)
        224 if action in ("download", "update"):
        225     # We need to write data, so create the local data directory if it
        226     # doesn't already exist.
    --> 227     make_local_storage(path)
        229     get_logger().info(
        230         "%s data from '%s' to file '%s'.",
        231         verb,
        232         url,
        233         str(full_path),
        234     )
        236     if downloader is None:

    File /usr/lib/python3/dist-packages/pooch/utils.py:276, in make_local_storage(path, env)
        272 if env is not None:
        273     message.append(
        274         f"Use environment variable '{env}' to specify a different location."
        275     )
    --> 276 raise PermissionError(" ".join(message)) from error

    PermissionError: [Errno 13] Permission denied: '/sbuild-nonexistent' | Pooch could not create data cache folder '/sbuild-nonexistent/.cache/xarray_tutorial_data'. Will not be able to download data files.

## Now for the heavy lifting:

We first have to come up with the weights, - calculate the month length for each monthly data record - calculate weights using `groupby('time.season')`

Finally, we just need to multiply our weights by the `Dataset` and sum along the time dimension. Creating a `DataArray` for the month length is as easy as using the `days_in_month` accessor on the time coordinate. The calendar type, in this case `'noleap'`, is automatically considered in this operation.

    [3]:

    month_length = ds.time.dt.days_in_month
    month_length

    ---------------------------------------------------------------------------
    NameError                                 Traceback (most recent call last)
    Cell In[3], line 1
    ----> 1 month_length = ds.time.dt.days_in_month
          2 month_length

    NameError: name 'ds' is not defined

    [4]:

    # Calculate the weights by grouping by 'time.season'.
    weights = (
        month_length.groupby("time.season") / month_length.groupby("time.season").sum()
    )

    # Test that the sum of the weights for each season is 1.0
    np.testing.assert_allclose(weights.groupby("time.season").sum().values, np.ones(4))

    # Calculate the weighted average
    ds_weighted = (ds * weights).groupby("time.season").sum(dim="time")

    ---------------------------------------------------------------------------
    NameError                                 Traceback (most recent call last)
    Cell In[4], line 3
          1 # Calculate the weights by grouping by 'time.season'.
          2 weights = (
    ----> 3     month_length.groupby("time.season") / month_length.groupby("time.season").sum()
          4 )
          6 # Test that the sum of the weights for each season is 1.0
          7 np.testing.assert_allclose(weights.groupby("time.season").sum().values, np.ones(4))

    NameError: name 'month_length' is not defined

    [5]:

    ds_weighted

    ---------------------------------------------------------------------------
    NameError                                 Traceback (most recent call last)
    Cell In[5], line 1
    ----> 1 ds_weighted

    NameError: name 'ds_weighted' is not defined

    [6]:

    # only used for comparisons
    ds_unweighted = ds.groupby("time.season").mean("time")
    ds_diff = ds_weighted - ds_unweighted

    ---------------------------------------------------------------------------
    NameError                                 Traceback (most recent call last)
    Cell In[6], line 2
          1 # only used for comparisons
    ----> 2 ds_unweighted = ds.groupby("time.season").mean("time")
          3 ds_diff = ds_weighted - ds_unweighted

    NameError: name 'ds' is not defined

    [7]:

    # Quick plot to show the results
    notnull = pd.notnull(ds_unweighted["Tair"][0])

    fig, axes = plt.subplots(nrows=4, ncols=3, figsize=(14, 12))
    for i, season in enumerate(("DJF", "MAM", "JJA", "SON")):
        ds_weighted["Tair"].sel(season=season).where(notnull).plot.pcolormesh(
            ax=axes[i, 0],
            vmin=-30,
            vmax=30,
            cmap="Spectral_r",
            add_colorbar=True,
            extend="both",
        )

        ds_unweighted["Tair"].sel(season=season).where(notnull).plot.pcolormesh(
            ax=axes[i, 1],
            vmin=-30,
            vmax=30,
            cmap="Spectral_r",
            add_colorbar=True,
            extend="both",
        )

        ds_diff["Tair"].sel(season=season).where(notnull).plot.pcolormesh(
            ax=axes[i, 2],
            vmin=-0.1,
            vmax=0.1,
            cmap="RdBu_r",
            add_colorbar=True,
            extend="both",
        )

        axes[i, 0].set_ylabel(season)
        axes[i, 1].set_ylabel("")
        axes[i, 2].set_ylabel("")

    for ax in axes.flat:
        ax.axes.get_xaxis().set_ticklabels([])
        ax.axes.get_yaxis().set_ticklabels([])
        ax.axes.axis("tight")
        ax.set_xlabel("")

    axes[0, 0].set_title("Weighted by DPM")
    axes[0, 1].set_title("Equal Weighting")
    axes[0, 2].set_title("Difference")

    plt.tight_layout()

    fig.suptitle("Seasonal Surface Air Temperature", fontsize=16, y=1.02)

    ---------------------------------------------------------------------------
    NameError                                 Traceback (most recent call last)
    Cell In[7], line 2
          1 # Quick plot to show the results
    ----> 2 notnull = pd.notnull(ds_unweighted["Tair"][0])
          4 fig, axes = plt.subplots(nrows=4, ncols=3, figsize=(14, 12))
          5 for i, season in enumerate(("DJF", "MAM", "JJA", "SON")):

    NameError: name 'ds_unweighted' is not defined

    [8]:

    # Wrap it into a simple function
    def season_mean(ds, calendar="standard"):
        # Make a DataArray with the number of days in each month, size = len(time)
        month_length = ds.time.dt.days_in_month

        # Calculate the weights by grouping by 'time.season'
        weights = (
            month_length.groupby("time.season") / month_length.groupby("time.season").sum()
        )

        # Test that the sum of the weights for each season is 1.0
        np.testing.assert_allclose(weights.groupby("time.season").sum().values, np.ones(4))

        # Calculate the weighted average
        return (ds * weights).groupby("time.season").sum(dim="time")