Climate change — temperature anomalies vs a baseline¶
Climate-change communication usually centres on anomalies — how much warmer or cooler a recent period is than a long-term reference. ERA5 covers 1940–present at monthly resolution, plenty for the WMO 30-year baseline.
Domain context. The standard WMO climatological normal period is 1991–2020 (or 1961–1990 for older comparisons). To evaluate recent warming we'll compute monthly 2 m temperature averaged over Europe for two windows — a 1991–2000 decade and a 2014–2023 decade — and show the difference.
Setup¶
Consolidate the imports up front. earthlens provides the unified
EarthLens entry point plus AggregationConfig (the temporal aggregator);
pyramids provides Dataset for reading the written GeoTIFFs; numpy,
pandas, and matplotlib handle the climatology maths and the plots.
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from pyramids.dataset import Dataset
from earthlens import AggregationConfig, EarthLens
Step 1 — pull two decades, monthly¶
Box: Europe (35°–60°N, 10°W–30°E). Two retrievals — one per decade — to keep the per-call queue request small. Each retrieval covers 120 monthly samples.
The request, shared across both decades¶
The spatial box, the dataset/variable mapping, and the two output directories are the same for both retrievals, so we define them once.
EUR = {"lat_lim": [35.0, 60.0], "lon_lim": [-10.0, 30.0]}
VAR = {"reanalysis-era5-single-levels-monthly-means": ["2m-temperature"]}
OUT_OLD = Path("data/era5-europe-1991-2000")
OUT_NEW = Path("data/era5-europe-2014-2023")
Download each decade¶
Loop over the two windows: build the EarthLens request, then call
download() with a monthly-mean AggregationConfig as a separate step.
Each call queues a CDS request and writes the aggregated GeoTIFFs under
the decade's output directory.
for path, start, end in [
(OUT_OLD, "1991-01-01", "2000-12-01"),
(OUT_NEW, "2014-01-01", "2023-12-01"),
]:
path.mkdir(parents=True, exist_ok=True)
earthlens = EarthLens(
data_source="ecmwf",
cadence="monthly",
start=start,
end=end,
variables=VAR,
**EUR,
path=str(path),
)
earthlens.download(aggregate=AggregationConfig(freq="1MS", op="mean"))
Step 2 — compute the climatology of each decade¶
Group the 120 monthly samples in each decade by calendar month and average. Result: a 12-month seasonal cycle per decade.
A helper for one decade's climatology¶
decade_climatology() reads every aggregated GeoTIFF in a decade's
directory, stacks them into a cube, takes the per-month spatial mean (in
°C), and groups by calendar month to give the 12-value seasonal cycle.
def decade_climatology(out_dir: Path, start_year: int) -> pd.Series:
paths = sorted((out_dir / "aggregated").glob("2m_temperature_1MS_*.tif"))
cube = np.stack([Dataset.read_file(str(p)).read_array() for p in paths])
site_mean = np.nanmean(cube, axis=(1, 2)) - 273.15 # to °C
months = pd.date_range(f"{start_year}-01-01", periods=len(site_mean), freq="MS")
s = pd.Series(site_mean, index=months)
return s.groupby(s.index.month).mean()
Run it for both decades¶
Compute the seasonal cycle for each decade, then difference them to get the monthly anomaly.
old = decade_climatology(OUT_OLD, 1991)
new = decade_climatology(OUT_NEW, 2014)
anom = new - old
Tabulate the two decades and their difference¶
Lay the two seasonal cycles and their anomaly side by side, indexed by month name.
names = [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
]
df = pd.DataFrame(
{
"1991–2000 [°C]": old.round(2).to_numpy(),
"2014–2023 [°C]": new.round(2).to_numpy(),
"Δ [°C]": anom.round(2).to_numpy(),
},
index=names,
)
df
Step 3 — plot the seasonal cycles and anomaly¶
European summer warming has accelerated more than winter — visible as a larger anomaly in JJA than DJF on the right panel.
fig, axes = plt.subplots(1, 2, figsize=(11, 4))
axes[0].plot(names, old, marker="o", label="1991–2000", color="tab:blue")
axes[0].plot(names, new, marker="s", label="2014–2023", color="tab:red")
axes[0].set_ylabel("T_2m [°C]")
axes[0].set_title("European seasonal cycle, two decades")
axes[0].legend()
axes[0].grid(alpha=0.3)
axes[1].bar(names, anom, color=["tab:red" if v > 0 else "tab:blue" for v in anom])
axes[1].axhline(0, color="gray", lw=0.5)
axes[1].set_ylabel("Anomaly Δ [°C]")
axes[1].set_title("2014–2023 minus 1991–2000")
axes[1].grid(alpha=0.3)
plt.tight_layout()
plt.show()
Notes¶
- Pick the right baseline. WMO 1991–2020 is the current standard; IPCC reports use 1850–1900 (pre-industrial). The choice changes the numerical anomaly substantially.
- Spatial vs temporal averaging order matters. This notebook computes the per-month spatial mean first, then averages across years per calendar month. For a true climatological grid, do it the other way around — average each pixel through time first, then display the spatial pattern.
- CDS-Beta data assimilation upgrades. ERA5 was reprocessed in CDS-Beta with consistent satellite assimilation. The single-levels product has a known small jump at 1979 (start of ERA5 satellite era) that you should be aware of for cross-decadal comparisons.
- For higher resolution. ERA5-Land is 0.1° but only available from 1950. ERA5 single-levels at 0.25° goes back to 1940 — choose based on whether the spatial detail or the time depth matters more.