climate-risk-engine / src /prediction /forecast_trigger.py
jtlevine's picture
Replace neural actuarial model with GraphCast triggers + empirical burn analysis
277cbcf
"""Forecast-based parametric insurance trigger decision.
Given a 5-day WBGT forecast, decides whether to issue no trigger,
an alert cash transfer, or a full insurance payout. Used by the
LastMileBench insurance benchmark to test forecast-driven trigger
accuracy against ERA5 actuals ("perfect forecast") and GraphCast.
The logic counts consecutive days from the start of the window
where forecast WBGT meets the heat-wave threshold. Duration +
severity determine the trigger level.
"""
from __future__ import annotations
import sys
from pathlib import Path
# Import CRE's calculate_wbgt
_CRE_ROOT = str(Path(__file__).resolve().parent.parent.parent)
if _CRE_ROOT not in sys.path:
sys.path.insert(0, _CRE_ROOT)
from src.indexing.heat_index import calculate_wbgt
def forecast_trigger_decision(
forecast_wbgt: list[float], # 5 days of forecast WBGT values
alert_duration_days: int = 2,
payout_duration_days: int = 5,
window_threshold_c: float = 35.1, # threshold for "heat wave day"
payout_severity_c: float = 30.7, # min peak WBGT for full payout (always met in practice)
) -> str:
"""Predict trigger action from a 5-day WBGT forecast.
Counts consecutive forecast days (from day 1) above window_threshold_c.
Returns 'no_trigger', 'alert_cash', or 'full_payout'.
Args:
forecast_wbgt: List of daily WBGT values for the forecast window
(typically 5 days starting from window_start + 1).
alert_duration_days: Minimum consecutive days above threshold
for an alert_cash trigger (default 2).
payout_duration_days: Minimum consecutive days above threshold
for a full_payout trigger (default 5).
window_threshold_c: WBGT threshold for counting a day as a
heat-wave day (default 35.1, the ERA5-based P90).
payout_severity_c: Minimum peak WBGT required for full payout
in addition to the duration requirement (default 30.7,
always met in practice for Dar es Salaam).
Returns:
One of 'no_trigger', 'alert_cash', or 'full_payout'.
"""
# Count consecutive days from day 1 where WBGT >= threshold
consecutive_days = 0
for wbgt in forecast_wbgt:
if wbgt >= window_threshold_c:
consecutive_days += 1
else:
break
peak_wbgt = max(forecast_wbgt) if forecast_wbgt else 0.0
if (
consecutive_days >= payout_duration_days
and peak_wbgt >= payout_severity_c
):
return "full_payout"
if consecutive_days >= alert_duration_days:
return "alert_cash"
return "no_trigger"
def compute_wbgt_from_forecast(temp_c: float, humidity_pct: float) -> float:
"""Compute WBGT from forecast temperature and humidity.
Thin wrapper around the CRE project's calculate_wbgt function,
which uses the Liljegren simplified outdoor WBGT formula.
Args:
temp_c: Air temperature in degrees Celsius.
humidity_pct: Relative humidity in percent (0-100).
Returns:
Estimated outdoor WBGT in degrees Celsius.
"""
return calculate_wbgt(temp_c, humidity_pct)