Introduction to Temperature-based Climate Indices¶
This notebook demonstrates how to compute and visualize temperature-based climate indices from CMIP6 datasets using the earthkit-climate and xclim packages.
We use:
Temperature-based indices:
DTR: Daily Temperature Range (Tmax - Tmin)
WSDI: Warm Spell Duration Index (≥6 consecutive days above 90th percentile)
HDD: Heating Degree Days (based on temperature below threshold)
We load ACCESS-CM2 CMIP6 data for both historical and SSP585 scenarios.
[1]:
import warnings
from typing import Any, List, Union
import cartopy.crs as ccrs
import earthkit.data as ekd
import earthkit.plots as ekp
import matplotlib.pyplot as plt
import xarray as xr
from earthkit.climate.atmos import temperature
from earthkit.climate.utils.percentile import calculate_percentile_doy
warnings.filterwarnings("ignore")
plt.rcParams["figure.figsize"] = (8, 5)
Loading CMIP6 data¶
We’ll use daily gridded data from the ACCESS-CM2 model for maximum (tasmax) and minimum (tasmin) temperature, for both historical and SSP585 future scenarios.
[2]:
# Load temperature
tasmin_hist = ekd.from_source("earthkit-climate-sample", "tasmin_ACCESS-CM2_historical_reference").to_xarray()
tasmin_ssp585 = ekd.from_source("earthkit-climate-sample", "tasmin_ACCESS-CM2_ssp585_far_future").to_xarray()
tasmax_hist = ekd.from_source("earthkit-climate-sample", "tasmax_ACCESS-CM2_historical_reference").to_xarray()
tasmax_ssp585 = ekd.from_source("earthkit-climate-sample", "tasmax_ACCESS-CM2_ssp585_far_future").to_xarray()
Inspect and visualize the raw data¶
Before computing indices, let’s plot a few example grids to see how the raw variables look.
[3]:
def plot_climatology(
figure: Any, data: xr.DataArray, row: int, col: int, title: str, colors: Union[str, List[str]], units: str
) -> None:
"""
Plots a climatological map based on the mean over time.
Parameters
----------
figure : Any
The figure object where the map will be added. Typically a
custom plotting canvas or layout manager.
data : xarray.DataArray
The dataset containing atmospheric or oceanic variables.
Must include a 'time' dimension to calculate the mean.
row : int
The row index in the figure layout where the plot will be placed.
col : int
The column index in the figure layout where the plot will be placed.
title : str
The title text to display above the individual map.
colors : str or list of str
The color scale or list of colors to be used by the style engine.
units : str
The measurement units for the data (e.g., 'K', 'm/s'), used
for the style and legend.
Returns
-------
None
"""
# Se asume que 'domain' está definido globalmente o es accesible
# en el scope de la función original.
map_plot = figure.add_map(row=row, column=col, domain=domain)
style = ekp.styles.Style(colors=colors, units=units)
# Cálculo de la climatología (media temporal) y graficado
map_plot.quickplot(data.mean("time"), style=style)
# Elementos geográficos y estéticos
map_plot.coastlines()
map_plot.gridlines()
map_plot.legend(location="right")
map_plot.title(title)
# Define domain (Southern Spain)
domain = [-7.5, 0, 35.8, 40.2]
# Create the figure with 2x2 maps
figure = ekp.Figure(
crs=ccrs.NearsidePerspective(central_longitude=-3.75, central_latitude=38.0), rows=2, columns=2, size=(15, 10)
)
# HISTORICAL
plot_climatology(figure, tasmin_hist, 0, 0, "tasmin Climatology (Historical)", "autumn", "celsius")
plot_climatology(figure, tasmax_hist, 0, 1, "tasmax Climatology (Historical)", "autumn", "celsius")
# SSP585
plot_climatology(figure, tasmin_ssp585, 1, 0, "tasmin Climatology (SSP585)", "autumn_r", "celsius")
plot_climatology(figure, tasmax_ssp585, 1, 1, "tasmax Climatology (SSP585)", "autumn_r", "celsius")
figure.show()
Compute Daily Temperature Range (DTR)¶
DTR is simply the difference between the daily maximum and minimum temperature. We’ll compute it for the SSP585 scenario.
[4]:
dtr = temperature.daily_temperature_range(tasmax=tasmax_ssp585, tasmin=tasmin_ssp585)
dtr
[4]:
<xarray.DataArray 'dtr' (time: 30, lat: 88, lon: 150)> Size: 2MB
array([[[ nan, nan, nan, ..., nan,
nan, nan],
[ nan, nan, nan, ..., nan,
nan, nan],
[ nan, nan, nan, ..., nan,
nan, nan],
...,
[ nan, nan, nan, ..., 11.322295 ,
10.918469 , 10.447549 ],
[ nan, nan, nan, ..., 11.648677 ,
11.34013 , 10.961644 ],
[ nan, nan, nan, ..., 11.894479 ,
11.664554 , 11.338991 ]],
[[ nan, nan, nan, ..., nan,
nan, nan],
[ nan, nan, nan, ..., nan,
nan, nan],
[ nan, nan, nan, ..., nan,
nan, nan],
...
[ nan, nan, nan, ..., 12.406018 ,
11.98628 , 11.499621 ],
[ nan, nan, nan, ..., 12.7879505,
12.463803 , 12.047528 ],
[ nan, nan, nan, ..., 13.074625 ,
12.839722 , 12.490876 ]],
[[ nan, nan, nan, ..., nan,
nan, nan],
[ nan, nan, nan, ..., nan,
nan, nan],
[ nan, nan, nan, ..., nan,
nan, nan],
...,
[ nan, nan, nan, ..., 12.111836 ,
11.678537 , 11.171482 ],
[ nan, nan, nan, ..., 12.507235 ,
12.176954 , 11.7508 ],
[ nan, nan, nan, ..., 12.805477 ,
12.561137 , 12.2005005]]], shape=(30, 88, 150), dtype=float32)
Coordinates:
* time (time) datetime64[ns] 240B 2071-01-01 2072-01-01 ... 2100-01-01
* lat (lat) float64 704B 35.82 35.87 35.92 35.97 ... 40.07 40.12 40.17
* lon (lon) float64 1kB -7.475 -7.425 -7.375 ... -0.125 -0.075 -0.025
height float64 8B 2.0
Attributes:
units_metadata: temperature: difference
units: K
cell_methods: time range within days time: mean over days
history: [2026-06-02 16:43:36] dtr: DTR(tasmin=tasmin, tasmax=tas...
standard_name: air_temperature
long_name: Mean diurnal temperature range
description: Annual mean diurnal temperature range.Compute Warm Spell Duration Index (WSDI)¶
WSDI identifies periods of at least 6 consecutive days where the daily maximum temperature exceeds the 90th percentile of the historical reference period (calculated per day of year).
[5]:
# WSDI (using historical baseline)
# Calculate 90th percentile from historical data
tasmax_per = calculate_percentile_doy(tasmax_hist, variable="tasmax", percentile=90)
# Merge percentile with target dataset
ds_merged = tasmax_ssp585.merge(tasmax_per)
wsdi = temperature.warm_spell_duration_index(ds=ds_merged)
wsdi
[5]:
<xarray.DataArray 'warm_spell_duration_index' (time: 30, lat: 88, lon: 150)> Size: 2MB
array([[[ nan, nan, nan, ..., nan, nan, nan],
[ nan, nan, nan, ..., nan, nan, nan],
[ nan, nan, nan, ..., nan, nan, nan],
...,
[ nan, nan, nan, ..., 222., 223., 223.],
[ nan, nan, nan, ..., 222., 222., 220.],
[ nan, nan, nan, ..., 222., 221., 222.]],
[[ nan, nan, nan, ..., nan, nan, nan],
[ nan, nan, nan, ..., nan, nan, nan],
[ nan, nan, nan, ..., nan, nan, nan],
...,
[ nan, nan, nan, ..., 226., 226., 226.],
[ nan, nan, nan, ..., 225., 226., 224.],
[ nan, nan, nan, ..., 225., 225., 225.]],
[[ nan, nan, nan, ..., nan, nan, nan],
[ nan, nan, nan, ..., nan, nan, nan],
[ nan, nan, nan, ..., nan, nan, nan],
...,
...
[ nan, nan, nan, ..., 300., 300., 300.],
[ nan, nan, nan, ..., 300., 300., 300.],
[ nan, nan, nan, ..., 300., 301., 301.]],
[[ nan, nan, nan, ..., nan, nan, nan],
[ nan, nan, nan, ..., nan, nan, nan],
[ nan, nan, nan, ..., nan, nan, nan],
...,
[ nan, nan, nan, ..., 313., 312., 311.],
[ nan, nan, nan, ..., 312., 312., 310.],
[ nan, nan, nan, ..., 310., 311., 312.]],
[[ nan, nan, nan, ..., nan, nan, nan],
[ nan, nan, nan, ..., nan, nan, nan],
[ nan, nan, nan, ..., nan, nan, nan],
...,
[ nan, nan, nan, ..., 294., 295., 295.],
[ nan, nan, nan, ..., 294., 294., 293.],
[ nan, nan, nan, ..., 294., 293., 292.]]],
shape=(30, 88, 150), dtype=float32)
Coordinates:
* time (time) datetime64[ns] 240B 2071-01-01 2072-01-01 ... 2100-01-01
* lat (lat) float64 704B 35.82 35.87 35.92 35.97 ... 40.07 40.12 40.17
* lon (lon) float64 1kB -7.475 -7.425 -7.375 ... -0.125 -0.075 -0.025
height float64 8B 2.0
Attributes:
climatology_bounds: ['1971-01-01', '2000-12-31']
window: 5
alpha: 0.3333333333333333
beta: 0.3333333333333333
history: [2026-06-02 16:43:51] warm_spell_duration_index: WAR...
units: days
cell_methods: time: sum over days
standard_name: number_of_days_with_air_temperature_above_threshold
long_name: Number of days with at least 6 consecutive days wher...
description: Annual number of days with at least 6 consecutive da...Compute Heating Degree Days (HDD)¶
HDD is the annual cumulative sum of temperature degrees for daily mean temperatures below a threshold (commonly 17.0°C). This version uses an approximation based on Tmin and Tmax.
[6]:
tas_da = (tasmax_ssp585["tasmax"] + tasmin_ssp585["tasmin"]) / 2
hdd = temperature.heating_degree_days_approximation(
tasmax_ssp585.tasmax,
tasmin=tasmin_ssp585.tasmin,
tas=tas_da,
)
hdd
[6]:
<xarray.DataArray 'heating_degree_days_approximation' (time: 30, lat: 88,
lon: 150)> Size: 2MB
array([[[ nan, nan, nan, ..., nan, nan,
nan],
[ nan, nan, nan, ..., nan, nan,
nan],
[ nan, nan, nan, ..., nan, nan,
nan],
...,
[ nan, nan, nan, ..., 658.39233, 660.7792 ,
641.38904],
[ nan, nan, nan, ..., 694.26166, 689.26843,
679.48315],
[ nan, nan, nan, ..., 708.51685, 692.3627 ,
698.4987 ]],
[[ nan, nan, nan, ..., nan, nan,
nan],
[ nan, nan, nan, ..., nan, nan,
nan],
[ nan, nan, nan, ..., nan, nan,
nan],
...
[ nan, nan, nan, ..., 432.9406 , 433.8806 ,
417.95914],
[ nan, nan, nan, ..., 460.21143, 456.7092 ,
448.42825],
[ nan, nan, nan, ..., 472.38202, 460.5053 ,
463.57053]],
[[ nan, nan, nan, ..., nan, nan,
nan],
[ nan, nan, nan, ..., nan, nan,
nan],
[ nan, nan, nan, ..., nan, nan,
nan],
...,
[ nan, nan, nan, ..., 460.51578, 460.17325,
444.713 ],
[ nan, nan, nan, ..., 486.55493, 482.00055,
474.72467],
[ nan, nan, nan, ..., 498.27454, 486.6211 ,
489.0348 ]]], shape=(30, 88, 150), dtype=float32)
Coordinates:
* time (time) datetime64[ns] 240B 2071-01-01 2072-01-01 ... 2100-01-01
* lat (lat) float64 704B 35.82 35.87 35.92 35.97 ... 40.07 40.12 40.17
* lon (lon) float64 1kB -7.475 -7.425 -7.375 ... -0.125 -0.075 -0.025
height float64 8B 2.0
Attributes:
units: K days
units_metadata: temperature: difference
cell_methods: time: sum over days
history: [2026-06-02 16:43:54] heating_degree_days_approximation:...
standard_name: integral_of_air_temperature_deficit_wrt_time
long_name: Cumulative sum of temperature degrees for daily temperat...
description: Annual cumulative heating degree days (temperature below...Visualization of Temperature Indices¶
Finally, let’s visualize the climatologies of these temperature indices across our domain.
[7]:
# Create the figure with 1x3 maps
figure = ekp.Figure(
crs=ccrs.NearsidePerspective(central_longitude=-3.75, central_latitude=38.0), rows=1, columns=3, size=(15, 6)
)
# PLOT: Each index climatology using the helper function
plot_climatology(figure, dtr, 0, 0, "DTR Climatology (SSP585)", "autumn_r", "K")
plot_climatology(figure, wsdi, 0, 1, "WSDI Climatology (SSP585)", "autumn_r", "days")
plot_climatology(figure, hdd, 0, 2, "HDD Climatology (SSP585)", "autumn_r", "K days")
figure.show()