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.

Step-by-step solution

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.

Code examples

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.

Explanation

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.

Edge cases or notes

  • 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.