04 — Identify heat waves

Learning objectives: aggregate hourly LST weather to daily maxima, identify contiguous threshold events, and report duration, peak, and cumulative excess heat.

SMHI’s Swedish climatological definition is at least five consecutive days with daily maximum temperature at or above 25.0°C. Definitions vary internationally, so the detector keeps both values configurable. Read the SMHI definition.

Network: SMHI and potentially Open-Meteo if the 2018 AMY is absent. Expected runtime: 2–8 minutes initially.

[ ]:
from pathlib import Path

import matplotlib.pyplot as plt
import pandas as pd

from smhi2epw import EPWConfig, compile_epw, read_epw

OUTPUT_DIR = Path("examples/output") if Path("examples").exists() else Path("output")
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
EPW_PATH = OUTPUT_DIR / "gothenburg_2018.epw"
if not EPW_PATH.exists():
    compile_epw(
        EPWConfig(
            2018,
            str(EPW_PATH),
            city="Gothenburg",
            latitude=57.7156,
            longitude=11.9924,
            utc_offset=1,
        )
    )
weather = read_epw(EPW_PATH)
[ ]:
dates = pd.to_datetime(
    {"year": weather.year, "month": weather.month, "day": weather.day}
)
daily_max = weather.assign(date=dates).groupby("date").dry_bulb.max()
daily_max.describe()
[ ]:
def detect_heatwaves(daily_temperature, threshold=25.0, minimum_days=5):
    """Return contiguous hot periods satisfying a threshold and duration."""
    hot = daily_temperature.ge(threshold)
    groups = hot.ne(hot.shift(fill_value=False)).cumsum()
    events = []
    for _, values in daily_temperature[hot].groupby(groups[hot]):
        if len(values) >= minimum_days:
            events.append(
                {
                    "start": values.index.min(),
                    "end": values.index.max(),
                    "duration_days": len(values),
                    "peak_C": values.max(),
                    "cumulative_excess_C_days": (values - threshold).sum(),
                }
            )
    return pd.DataFrame(events)


events = detect_heatwaves(daily_max)
events
[ ]:
fig, ax = plt.subplots(figsize=(11, 4))
ax.plot(daily_max.index, daily_max, color="#0072B2", linewidth=1)
ax.axhline(25, color="#D55E00", linestyle="--", label="25°C threshold")
for event in events.itertuples():
    ax.axvspan(
        event.start, event.end + pd.Timedelta(days=1), color="#E69F00", alpha=0.25
    )
ax.set(ylabel="Daily maximum temperature (°C)", xlabel="Local date")
ax.legend()
fig.tight_layout()

Key takeaways, cautions, and exercises

This detects events in the station-informed AMY, not necessarily at a specific façade or urban canyon. Gap-filled hours can influence a daily maximum, so cross-check event dates against processing diagnostics and source observations.

Try it: detect tropical days from daily minimum ≥20°C, or compare a percentile-based definition. Explain why changing the definition changes the scientific question rather than merely changing a plotting option.