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()
../_images/how-tos_intro_temperature_indices_5_0.png

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()
../_images/how-tos_intro_temperature_indices_13_0.png