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 ForecastsCalculate 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 ForecastsCalculate 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 ForecastsPlot 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()
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()
[ ]: