ERA5 time series¶
In this notebook, we will explore how to:
Extract ERA5 reanalysis time series data for a single point from the C3S Climate Data Store (CDS)
Calculate temporal statistics (e.g. daily minima, maxima and means)
Visualise your results with earthkit-plots
Components of earthkit¶
This tutorial uses the following earthkit components - click any logo to open the package documentation:
Note: some of the examples in this notebook require an optional dependency - the Python package reverse-geocode. This lets you easily look up the nearest location to a latitude-longitude point. To install it, uncomment the cell below and run it.
[1]:
#!pip install reverse-geocode
[2]:
import earthkit.data as ekd
import earthkit.plots as ekp
import earthkit.transforms as ekt
1. Retrieving ERA5 time series data¶
In this example, we’ll examine Northern Hemisphere Summer 2025 for a single point location. By default, we’ll use Reading, UK — one of ECMWF’s sites - but feel free to choose your own location by changing the latitude and longitude values in the CDS request.
In order to access ERA5 renalysis data, you will need an account on the Copernicus Climate Data Store (CDS). If you do not have an account, please visit the CDS website and register for an account. Then, follow these instructions (step 1 only) to set up your API key.
If you do not wish to set up a CDS account right now, you can still follow along with this notebook by accessing the Reading data sample from an open URL. To do this, simply uncomment the cell below and run it.
[3]:
# ds = ekd.from_source("sample", "era5-timeseries-nh-summer-2025.nc")
If you do have a CDS account, you can access the ERA5 hourly time-series data on single levels from 1940 to present dataset, which provides super-fast access to point-based time series from ERA5 reanalysis from 1940 to present.
The following request will get you data for the grid point containing Reading, UK for summer 2025:
[4]:
dataset = "reanalysis-era5-single-levels-timeseries"
request = {
"variable": ["2m_temperature"],
"location": {"longitude": -1, "latitude": 51.5},
"date": ["2025-06-01/2025-09-30"],
"data_format": "netcdf"
}
ds = ekd.from_source("cds", dataset, request)
2026-04-15 17:56:17,336 INFO [2026-02-16T00:00:00] - To generate this ERA5 hourly time series dataset, **homogenisation conventions have been applied to the ERA5 source GRIB data** to ensure consistency, usability, and alignment across chosen variables and time steps. The processed data were then written to an **ARCO Zarr archive**, enabling efficient cloud-optimised access and scalable data retrieval. Please refer to the [user guide](https://confluence.ecmwf.int/x/R6cfHg) for details.
- The dataset presented here is a subset of selected parameters from the full [CDS ERA5 hourly data on single levels (1940–present)](https://cds.climate.copernicus.eu/datasets/reanalysis-era5-single-levels?tab=overview). **Requirements for additional parameters may be considered**. Please raise your request with ECMWF Support [here](https://jira.ecmwf.int/plugins/servlet/desk/portal/1/create/202).
2026-04-15 17:56:17,337 INFO Request ID is 0f020656-c4ef-4d3c-b4b5-e625bca50719
2026-04-15 17:56:17,620 INFO status has been updated to accepted
2026-04-15 17:56:26,442 INFO status has been updated to running
2026-04-15 17:56:31,574 INFO status has been updated to successful
Since this is netCDF data, converting it to xarray gives us a good view of our data.
[5]:
data = ds.to_xarray()
data
[5]:
<xarray.Dataset> Size: 35kB
Dimensions: (valid_time: 2928)
Coordinates:
latitude float64 8B ...
longitude float64 8B ...
* valid_time (valid_time) datetime64[ns] 23kB 2025-06-01 ... 2025-09-30T23...
Data variables:
t2m (valid_time) float32 12kB ...
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 Forecasts2. Plotting a time series¶
As well as geographical plots, earthkit-plots also has support for statistical plots. To get started, we can instantiate a TimeSeries() object just like we would a Map().
[6]:
chart = ekp.TimeSeries()
# Plot our hourly data as a line
chart.line(data)
chart.show()
Notice that earthkit-plots identified the time dimension of the data and put it on the x-axis by default, reducing the padding on the left and right edges to contain the data.
3. Customising the plot¶
Now let’s:
convert the units to celsius
change the line colour
add a title and a label for the y-axis
[7]:
chart = ekp.TimeSeries()
chart.line(
data,
units="celsius",
color="red",
)
# The label string is automatic, based on the data and the units
chart.ylabel()
# You can use metadata templating in titles just like with map plots
chart.title("ERA5 hourly {variable_name}")
chart.show()
4. Changing the plot orientation¶
In some cases, you may wish to flip your axes. With earthkit-plots, this is as straightforward as choosing which variable/coordinate/dimension you would like on each axis.
Note that you only need to specify one axis, like below where we specify
y="valid_time". Try experimenting withx="t2m"- it works just the same.
[8]:
chart = ekp.TimeSeries(size=(4, 8))
chart.line(
data,
y="valid_time", # put valid_time on the y-axis
units="celsius",
color="red",
)
chart.xlabel()
chart.title("ERA5 hourly {variable_name}")
chart.show()
/Users/ecm7348/Documents/Coding/earthkit-plots/src/earthkit/plots/sources/__init__.py:280: UserWarning: Unit conversion failed for y: K -> celsius. Error: ufunc 'multiply' cannot use operands with types dtype('<M8[ns]') and dtype('int64'). Returning original values.
warnings.warn(
5. Enhancing ticks and formatting¶
Now let’s add a nicer title and format the time ticks better for clarity.
Here we are adding more metadata keys ot the title, including:
latitude and longitude, with magic formatters (
%Ltand%Ln) to easily add cardinal directionslocation (requires the
reverse-geocodelibrary to have been installed at the top of this notebook!), which can be formatted to show the closest city (%c) or country (%C) to the latitude and longitude point
We are also formatting the x (time) axis with:
frequency="M": show one tick every monthformat="%B": show the full month nameperiod=True: place the labels in the center of each period
[9]:
chart = ekp.TimeSeries()
chart.line(data, units="celsius", color="red")
chart.title("ERA5 hourly {variable_name} at {latitude:%Lt} {longitude:%Ln} ({location:%c}, {location:%C})")
chart.ylabel()
chart.xticks(
frequency="M",
format="%B",
period=True,
)
chart.show()
6. Adding daily statistics¶
Finally, let’s do something a bit more interesting by calculating daily minimum and maximum temperatures, and adding them to the plot. We can do this easily with earthkit-transforms:
[10]:
daily_max = ekt.temporal.daily_max(data).rename({"t2m": "daily max"})
daily_min = ekt.temporal.daily_min(data).rename({"t2m": "daily min"})
chart = ekp.TimeSeries()
chart.line(daily_max, units="celsius", label="{name}")
chart.line(data, units="celsius", label="hourly")
chart.line(daily_min, units="celsius", label="{name}")
chart.title("ERA5 hourly {variable_name} at {latitude:%Lt} {longitude:%Ln} ({location:%c}, {location:%C})")
chart.ylabel()
chart.xticks(
frequency="M",
format="%B",
period=True,
)
chart.legend()
chart.show()
7. Produce a time series plot for a different location for summer 2025.¶
Let’s do a time series plot for Bologna, Italy - home to ECMWF’s computing facilities.
[11]:
dataset = "reanalysis-era5-single-levels-timeseries"
request = {
"variable": ["2m_temperature"],
"location": {"longitude": 11.25, "latitude": 44.5},
"date": ["2025-06-01/2025-09-30"],
"data_format": "netcdf"
}
ds = ekd.from_source("cds", dataset, request)
data = ds.to_xarray()
2026-04-15 17:56:34,221 INFO [2026-02-16T00:00:00] - To generate this ERA5 hourly time series dataset, **homogenisation conventions have been applied to the ERA5 source GRIB data** to ensure consistency, usability, and alignment across chosen variables and time steps. The processed data were then written to an **ARCO Zarr archive**, enabling efficient cloud-optimised access and scalable data retrieval. Please refer to the [user guide](https://confluence.ecmwf.int/x/R6cfHg) for details.
- The dataset presented here is a subset of selected parameters from the full [CDS ERA5 hourly data on single levels (1940–present)](https://cds.climate.copernicus.eu/datasets/reanalysis-era5-single-levels?tab=overview). **Requirements for additional parameters may be considered**. Please raise your request with ECMWF Support [here](https://jira.ecmwf.int/plugins/servlet/desk/portal/1/create/202).
2026-04-15 17:56:34,222 INFO Request ID is c98831a4-a284-4f8b-9894-2ef4f6fac397
2026-04-15 17:56:34,274 INFO status has been updated to accepted
2026-04-15 17:56:47,769 INFO status has been updated to running
2026-04-15 17:56:55,417 INFO status has been updated to successful
[12]:
daily_max = ekt.temporal.daily_max(data).rename({"t2m": "daily max"})
daily_min = ekt.temporal.daily_min(data).rename({"t2m": "daily min"})
chart = ekp.TimeSeries()
chart.line(daily_max, units="celsius", label="{name}")
chart.line(data, units="celsius", label="hourly")
chart.line(daily_min, units="celsius", label="{name}")
chart.title("ERA5 hourly {variable_name} at {latitude:%Lt} {longitude:%Ln} ({location:%c}, {location:%C})")
chart.ylabel()
chart.xticks(
frequency="M",
format="%B",
period=True,
)
chart.legend()
chart.show()
8. Calculate the daily mean temperature, and plot that between the daily min and daily max (instead of the hourly temperature).¶
[13]:
daily_mean = ekt.temporal.daily_mean(data).rename({"t2m": "daily mean"})
chart = ekp.TimeSeries()
chart.line(daily_max, units="celsius", label="{name}")
chart.line(daily_mean, units="celsius", label="{name}")
chart.line(daily_min, units="celsius", label="{name}")
chart.title("ERA5 hourly {variable_name} at {latitude:%Lt} {longitude:%Ln} ({location:%c}, {location:%C})")
chart.ylabel()
chart.xticks(
frequency="M",
format="%B",
period=True,
)
chart.legend()
chart.show()
9. Request total_precipitation from ERA5 and calculate the daily mean precipitation and plot as a bar chart.¶
[14]:
dataset = "reanalysis-era5-single-levels-timeseries"
request = {
"variable": ["total_precipitation"],
"location": {"longitude": -1, "latitude": 51.5},
"date": ["2025-06-01/2025-09-30"],
"data_format": "netcdf"
}
ds = ekd.from_source("cds", dataset, request)
data = ds.to_xarray()
2026-04-15 17:56:56,976 INFO [2026-02-16T00:00:00] - To generate this ERA5 hourly time series dataset, **homogenisation conventions have been applied to the ERA5 source GRIB data** to ensure consistency, usability, and alignment across chosen variables and time steps. The processed data were then written to an **ARCO Zarr archive**, enabling efficient cloud-optimised access and scalable data retrieval. Please refer to the [user guide](https://confluence.ecmwf.int/x/R6cfHg) for details.
- The dataset presented here is a subset of selected parameters from the full [CDS ERA5 hourly data on single levels (1940–present)](https://cds.climate.copernicus.eu/datasets/reanalysis-era5-single-levels?tab=overview). **Requirements for additional parameters may be considered**. Please raise your request with ECMWF Support [here](https://jira.ecmwf.int/plugins/servlet/desk/portal/1/create/202).
2026-04-15 17:56:56,977 INFO Request ID is b2d38f51-c307-47e7-b006-3f81f2449435
2026-04-15 17:56:57,019 INFO status has been updated to accepted
2026-04-15 17:57:18,088 INFO status has been updated to successful
[15]:
import numpy as np
daily_precip = ekt.temporal.daily_mean(ds)
daily_precip = daily_precip.assign_coords(valid_time=daily_precip.valid_time+np.timedelta64(12, "h"))
[16]:
chart = ekp.TimeSeries()
chart.bar(daily_precip, units="mm")
chart.title("ERA5 hourly {variable_name} at {latitude:%Lt} {longitude:%Ln} ({location:%c}, {location:%C})")
chart.ylabel()
chart.xticks(
frequency="M",
format="%B",
period=True,
)
chart.show()