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