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.
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.
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.
Why the baseline matters for trends
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.
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.
Internal links
- Heat exposure explained — who is affected by those days
- How to compare air temperature with LST — stations against satellites
- How to analyse an air quality time series — trends that are steps
- NetCDF and gridded data explained — what a reanalysis cell is
- How to resample a time series with xarray — the same counting on grids
- Growing degree days explained — another threshold-based temperature count
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.