How to Count Heatwave Days in Python

On this page

Problem statement

"Heatwaves are becoming more frequent" is a claim about counts, and counting heatwaves needs a definition: how hot, relative to what, for how many days. Absolute thresholds (days above 35 °C) are easy to understand and do not travel between climates; relative ones (days above the local 95th percentile) travel well and change meaning when the baseline period changes. Most national weather services use a relative threshold with a minimum duration, and the choice changes both the number of heatwave days and the trend.

With seventy years of daily reanalysis maxima for any point on Earth, the counting is a short pandas exercise. The work is in fixing the definition and saying which one was used.

Quick answer

Get daily maximum temperatures, set a threshold from a baseline period, and count runs of consecutive hot days:

import requests, numpy as np, pandas as pd

j = requests.get("https://archive-api.open-meteo.com/v1/archive", params={
    "latitude": 40.4168, "longitude": -3.7038, "start_date": "1950-01-01",
    "end_date": "2024-12-31", "daily": "temperature_2m_max", "timezone": "Europe/Madrid"},
    timeout=300).json()
tmax = pd.Series(j["daily"]["temperature_2m_max"], index=pd.to_datetime(j["daily"]["time"]))

summer = tmax[tmax.index.month.isin([7, 8])]
base = summer["1971":"2000"]
threshold = base.quantile(0.95)                                  # a percentile-based threshold
print(f"ERA5 cell at {j['latitude']:.2f}, {j['longitude']:.2f} ({j['elevation']:.0f} m)")
print(f"95th percentile of July–August Tmax, 1971–2000: {threshold:.1f} °C")

def heatwave_days(series, thr, min_len=3):
    hot = series > thr
    run_id = (hot != hot.shift()).cumsum()
    lengths = hot.groupby(run_id).transform("size")
    return hot & (lengths >= min_len)

hw = heatwave_days(tmax, threshold)
per_year = hw.groupby(hw.index.year).sum()
for a, b in ((1951, 1980), (1981, 2010), (2011, 2024)):
    print(f"{a}–{b}: mean {per_year.loc[a:b].mean():4.1f} heatwave days a year, "
          f"max {per_year.loc[a:b].max()} ({per_year.loc[a:b].idxmax()})")
ERA5 cell at 40.39, -3.68 (666 m)
95th percentile of July–August Tmax, 1971–2000: 36.3 °C
1951–1980: mean  0.4 heatwave days a year, max 3 (1954)
1981–2010: mean  1.8 heatwave days a year, max 14 (1991)
2011–2024: mean 10.1 heatwave days a year, max 30 (2022)

A heatwave here is at least three consecutive days above the baseline's 95th percentile of summer maxima. The count per year, averaged over periods, shows the change without leaning on any single extreme summer: from 0.4 heatwave days a year in 1951–80 to 10.1 in 2011–24, with 30 in 2022.

Variable, threshold type, baseline, duration, data checks

1. Choose the variable

Daily maximum temperature is standard for heatwaves; daily minimum captures hot nights, which matter as much for health. Some definitions require both.

2. Choose the threshold type

Absolute (35 °C, 40 °C) for communication in one place; relative (a percentile of a baseline period and season) for comparing places and times.

3. Fix the baseline period

1961–1990, 1971–2000 and 1991–2020 give different thresholds. A recent baseline absorbs recent warming into "normal" and shows fewer heatwaves.

4. Require a minimum duration

Two or three consecutive days is common. Single hot days are counted separately as hot days, not heatwave days.

5. Check the data against a station

Reanalysis cells are tens of kilometres wide and smooth out urban heat. Compare with a local station before quoting absolute temperatures.

Daily maximum temperatures crossing a percentile threshold, with runs of three or more days marked as heatwaves. threshold, duration, countday 1above 36.3 °Cday 2aboveday 3above: a heatwaveday 4belowday 5alone: no95th percentile of July–August Tmax, 1971–2000, at least three days spatialworkflow.io
Threshold, duration, count: a heatwave is a definition.

How the baseline decides the count

Example 1 — the baseline decides the count

for a, b in (("1961", "1990"), ("1971", "2000"), ("1991", "2020")):
    thr = summer[a:b].quantile(0.95)
    n = heatwave_days(tmax, thr).groupby(tmax.index.year).sum().loc[2011:2024].mean()
    print(f"baseline {a}–{b}: threshold {thr:.1f} °C → {n:.1f} heatwave days a year in 2011–24")
baseline 1961–1990: threshold 35.7 °C → 14.5 heatwave days a year in 2011–24
baseline 1971–2000: threshold 36.3 °C → 10.1 heatwave days a year in 2011–24
baseline 1991–2020: threshold 36.9 °C → 7.7 heatwave days a year in 2011–24

Against a 1961–90 baseline, 2011–24 had 14.5 heatwave days a year; against 1991–2020, 7.7. The same recent summers contain fewer "heatwave days" against a more recent baseline, because the baseline has warmed. Neither count is wrong; they answer different questions — change against a fixed past, or against the current normal.

Example 2 — absolute thresholds

for t in (35, 38, 40):
    d = (tmax > t).groupby(tmax.index.year).sum()
    print(f"days above {t} °C: 1951–80 mean {d.loc[1951:1980].mean():4.1f}, "
          f"2011–24 mean {d.loc[2011:2024].mean():4.1f}")
days above 35 °C: 1951–80 mean  3.7, 2011–24 mean 24.6
days above 38 °C: 1951–80 mean  0.1, 2011–24 mean  5.4
days above 40 °C: 1951–80 mean  0.0, 2011–24 mean  0.7

Days above 35 °C went from under four a year to about 25. Absolute counts are easy to explain and highly sensitive to the data source: this reanalysis cell reports 0.7 days a year above 40 °C in 2011–24, while the Barajas station recorded six in the single summer of 2023. Near a threshold, a cell that runs a degree cooler than a station counts far fewer days.

Example 3 — warm nights

jn = requests.get("https://archive-api.open-meteo.com/v1/archive", params={
    "latitude": 40.4168, "longitude": -3.7038, "start_date": "1950-01-01",
    "end_date": "2024-12-31", "daily": "temperature_2m_min", "timezone": "Europe/Madrid"},
    timeout=300).json()
tmin = pd.Series(jn["daily"]["temperature_2m_min"], index=pd.to_datetime(jn["daily"]["time"]))
tropical = (tmin >= 20).groupby(tmin.index.year).sum()
print(f"nights with Tmin ≥ 20 °C: 1951–80 mean {tropical.loc[1951:1980].mean():.1f}, "
      f"2011–24 mean {tropical.loc[2011:2024].mean():.1f}")
nights with Tmin ≥ 20 °C: 1951–80 mean 6.8, 2011–24 mean 34.1

Tropical nights went from 7 a year to 34 — a fivefold rise, larger in proportion than the rise in hot days. Warm nights prevent recovery from daytime heat. In cities they are amplified by the night-time heat island, which a reanalysis cell barely resolves, so the count for the dense centre can differ from the cell's; check it against a station inside the city before quoting it.

Mean heatwave days per year in Madrid in three periods from 1951 to 2024. heatwave days a year, ERA5 cell over Madrid1951–19800.4 · max 3 (1954)1981–20101.8 · max 14 (1991)2011–202410.1 · max 30 (2022)0612Tropical nights: 7 a year in 1951–80, 34 in 2011–24 spatialworkflow.io
Heatwave days per year, averaged over three periods.

Relative thresholds and reanalysis limits

Why relative thresholds

A 35 °C day is ordinary in Seville and extreme in Stockholm. Percentile thresholds scale with the local climate, which makes heatwave counts comparable between places and ties them to what local people and infrastructure are adapted to.

A percentile threshold fixed on an old baseline measures how the present compares with the past; a moving baseline measures how the present compares with itself. Climate-change assessments use fixed baselines; operational warnings use recent ones.

Why reanalysis underestimates urban extremes

ERA5 represents a 25–30 km cell, mostly the surroundings of a city, and smooths peaks. Station records in the urban core are typically hotter in summer, especially at night. Trends are usually well captured; absolute values and threshold exceedances less so.

The heatwave threshold and 2011–24 heatwave days per year for three baseline periods. the baseline decides the countbaselinethresholddays/yr 2011–241961–199035.7 °C14.51971–200036.3 °C10.11991–202036.9 °C7.7Days above 40 °C: 0.7 a year in the cell, 6 at Barajas in 2023 alone spatialworkflow.io
The same summers, 7.7 or 14.5 heatwave days a year depending on the baseline.

Season windows, heat indices and gaps

  • Season window: July–August, June–September or the whole year give different percentiles.
  • Moving-window percentiles per calendar day (a 15-day window) are used by some agencies.
  • Heat indices combine temperature and humidity; in humid climates they matter more than temperature alone.
  • Data gaps in station records break runs; decide how to handle a missing day inside a heatwave.
  • Time zone: request daily aggregates in local time so days run midnight to midnight locally.

FAQ

How is a heatwave defined?

Typically as at least two or three consecutive days with maximum temperature above a local percentile threshold; definitions vary by agency.

Should I use an absolute or relative threshold?

Relative for comparing places and periods; absolute for communicating in one place.

Which baseline period should I use?

A fixed one (e.g. 1971–2000) to measure change; state it, because it changes the count.

Is ERA5 good enough for heatwave counts?

For trends, usually; for absolute temperatures in a city, it runs cooler than urban stations.

Should I count hot nights too?

Yes — nights with minimum temperature above 20 °C strongly affect health.

Why do heatwave statistics differ between sources?

Different thresholds, baselines, durations, seasons and data.