Anomaly calculations

[1]:
from earthkit import data as ekd
from earthkit import plots as ekp
from earthkit import transforms as ekt
from earthkit.transforms._tools import earthkit_remote_test_data_file

Load some test data

In this example we will use hourly ERA5 2m temperature data on a 3˚ X 3˚ spatial grid for the years 2015 to 2017 as our physical data.

All earthkit-transforms methods can be called with earthkit-data objects (Readers and Wrappers) or with a pre-loaded xarray. To reduce the number of conversions in the example, we will convert to xarray in the first cell and use that data object for all subsequent steps. This also allows us to set some dataset specific options in the conversion to xarray instead of relying on the default options.

[2]:
# Get some demonstration ERA5 data, this could be any url or path to an ERA5 grib or netCDF file.
remote_era5_file = earthkit_remote_test_data_file("ERA5-Reading-2m-temperature-1940-2025.nc")
era5_data = ekd.from_source("url", remote_era5_file)
# convert to xarray to save repeated conversion in further steps.
# the .compute loads everything into memory which is safe for this example
era5_xr = era5_data.to_xarray().compute()
era5_xr

[2]:
<xarray.Dataset> Size: 9MB
Dimensions:     (valid_time: 753888)
Coordinates:
  * valid_time  (valid_time) datetime64[ns] 6MB 1940-01-01 ... 2025-12-31T23:...
    latitude    float64 8B 51.5
    longitude   float64 8B -1.0
Data variables:
    t2m         (valid_time) float32 3MB 270.2 270.4 269.6 ... 272.4 272.2 272.3
Attributes:
    Conventions:             CF-1.7
    GRIB_centre:             ecmf
    GRIB_centreDescription:  European Centre for Medium-Range Weather Forecasts
    GRIB_edition:            1
    GRIB_subCentre:          0
    history:                 2024-09-02T04:48 GRIB to CDM+CF via cfgrib-0.9.1...
    institution:             European Centre for Medium-Range Weather Forecasts

Calculate the daily climatology of the ERA5 data

[3]:
climatology_mean = ekt.climatology.mean(era5_xr)
climatology_monthly_mean = ekt.climatology.monthly_mean(era5_xr)
climatology_mean
[3]:
<xarray.Dataset> Size: 20B
Dimensions:    ()
Coordinates:
    latitude   float64 8B 51.5
    longitude  float64 8B -1.0
Data variables:
    t2m        float32 4B 283.2
Attributes:
    Conventions:             CF-1.7
    GRIB_centre:             ecmf
    GRIB_centreDescription:  European Centre for Medium-Range Weather Forecasts
    GRIB_edition:            1
    GRIB_subCentre:          0
    history:                 2024-09-02T04:48 GRIB to CDM+CF via cfgrib-0.9.1...
    institution:             European Centre for Medium-Range Weather Forecasts

Calculate anomalies and relative anomalies

[4]:
# The anomaly of the daily mean
anomaly_original_time_dim = ekt.climatology.anomaly(era5_xr, climatology_mean)
anomaly_original_time_dim
[4]:
<xarray.Dataset> Size: 9MB
Dimensions:     (valid_time: 753888)
Coordinates:
  * valid_time  (valid_time) datetime64[ns] 6MB 1940-01-01 ... 2025-12-31T23:...
    latitude    float64 8B 51.5
    longitude   float64 8B -1.0
Data variables:
    t2m         (valid_time) float32 3MB -12.99 -12.76 -13.6 ... -10.93 -10.9
Attributes:
    Conventions:             CF-1.7
    GRIB_centre:             ecmf
    GRIB_centreDescription:  European Centre for Medium-Range Weather Forecasts
    GRIB_edition:            1
    GRIB_subCentre:          0
    history:                 2024-09-02T04:48 GRIB to CDM+CF via cfgrib-0.9.1...
    institution:             European Centre for Medium-Range Weather Forecasts
[5]:
anomaly_annual = ekt.climatology.anomaly(era5_xr, climatology_mean, frequency="year")
anomaly_annual
[5]:
<xarray.Dataset> Size: 1kB
Dimensions:     (valid_time: 86)
Coordinates:
  * valid_time  (valid_time) datetime64[ns] 688B 1940-12-31 ... 2025-12-31
    latitude    float64 8B 51.5
    longitude   float64 8B -1.0
Data variables:
    t2m         (valid_time) float32 344B -0.4996 -0.6321 -1.028 ... 1.303 1.521
Attributes:
    Conventions:             CF-1.7
    GRIB_centre:             ecmf
    GRIB_centreDescription:  European Centre for Medium-Range Weather Forecasts
    GRIB_edition:            1
    GRIB_subCentre:          0
    history:                 2024-09-02T04:48 GRIB to CDM+CF via cfgrib-0.9.1...
    institution:             European Centre for Medium-Range Weather Forecasts
[6]:
anomaly_monthly = ekt.climatology.anomaly(era5_xr, climatology_monthly_mean, frequency="month")
anomaly_monthly
[6]:
<xarray.Dataset> Size: 12kB
Dimensions:     (valid_time: 1032)
Coordinates:
  * valid_time  (valid_time) datetime64[ns] 8kB 1940-01-01 ... 2025-12-01
Data variables:
    t2m         (valid_time) float32 4kB -5.153 -0.5941 -0.4915 ... 1.752 2.078
Attributes:
    Conventions:             CF-1.7
    GRIB_centre:             ecmf
    GRIB_centreDescription:  European Centre for Medium-Range Weather Forecasts
    GRIB_edition:            1
    GRIB_subCentre:          0
    history:                 2024-09-02T04:48 GRIB to CDM+CF via cfgrib-0.9.1...
    institution:             European Centre for Medium-Range Weather Forecasts
[7]:
relative_anomaly_monthly = ekt.climatology.relative_anomaly(era5_xr, climatology_monthly_mean, frequency="month")
relative_anomaly_monthly
[7]:
<xarray.Dataset> Size: 12kB
Dimensions:     (valid_time: 1032)
Coordinates:
  * valid_time  (valid_time) datetime64[ns] 8kB 1940-01-01 ... 2025-12-01
Data variables:
    t2m         (valid_time) float32 4kB -1.859 -0.2141 -0.176 ... 0.6254 0.7472
Attributes:
    Conventions:             CF-1.7
    GRIB_centre:             ecmf
    GRIB_centreDescription:  European Centre for Medium-Range Weather Forecasts
    GRIB_edition:            1
    GRIB_subCentre:          0
    history:                 2024-09-02T04:48 GRIB to CDM+CF via cfgrib-0.9.1...
    institution:             European Centre for Medium-Range Weather Forecasts

Plot the output

Climate stripes

We can use the annual anomaly to plot the climate strips for our time-series.

[8]:
chart = ekp.timeseries.stripes(anomaly_annual, cmap="RdBu_r")

chart.xticks(frequency="10Y")

chart.title("ERA5 {variable_name} anomaly\nLocation: {latitude:%Lt}, {longitude:%Ln} • Baseline: 1991–2020")
chart.attribution("Credit: C3S/ECMWF", location="lower right")

chart.show()
../../_images/tutorials_climatology_02-era5-anomaly_14_0.png

Traditional time-series on a single plot

[9]:
fig = ekp.TimeSeries()

# Add the absolute anomalies
# # Uncomment the line below to plot the anomaly calculated using the original time-dimension (e.g. daily)
# # data, but this will be very noisy and not very informative.
# fig.line(anomaly_original_time_dim, label="Original time-dimension anomaly", color="seagreen", alpha=0.5)
fig.line(anomaly_monthly, label="Monthly anomaly", color="goldenrod")
fig.line(anomaly_annual, label="Annual anomaly", color="cornflowerblue")
fig.ylabel()

ax2 = fig.ax.twinx()
ax2.plot(
    relative_anomaly_monthly["valid_time"],
    relative_anomaly_monthly["t2m"],
    label="Relative anomaly",
    color="orchid",
    linestyle=":",
)
ax2.set_ylabel("Monthly relative anomaly (%)")

handles, labels = fig.ax.get_legend_handles_labels()
handles2, labels2 = ax2.get_legend_handles_labels()
fig.ax.legend(handles2 + handles, labels2 + labels, loc="lower right")

fig.title("Daily anomaly for {variable_name}\n Reading (51.5°N, 1.0°W) • Baseline: 1991–2020")

fig.show()
../../_images/tutorials_climatology_02-era5-anomaly_16_0.png
[ ]: