Reinforcement Learning
stable-baselines3
deep-reinforcement-learning
agricultural-ai
weather-modelling
curriculum-learning
edge-ai
Instructions to use DHDRL/monsoon-rl with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- stable-baselines3
How to use DHDRL/monsoon-rl with stable-baselines3:
from huggingface_sb3 import load_from_hub checkpoint = load_from_hub( repo_id="DHDRL/monsoon-rl", filename="{MODEL FILENAME}.zip", ) - Notebooks
- Google Colab
- Kaggle
Update backtest_indonesia.py
Browse files- backtest_indonesia.py +2 -78
backtest_indonesia.py
CHANGED
|
@@ -3,17 +3,6 @@ backtest_indonesia.py
|
|
| 3 |
=====================
|
| 4 |
Historical replay / evaluation harness for the Indonesia weather-risk stack.
|
| 5 |
|
| 6 |
-
WHAT THIS IS FOR
|
| 7 |
-
----------------
|
| 8 |
-
The Optuna sweep (train_kaggle.py) evaluates the RL AGENT's hyperparameters.
|
| 9 |
-
This harness answers a different, more operational question: "fed real
|
| 10 |
-
historical weather for Indonesian zones, does the deterministic scoring
|
| 11 |
-
stack (climatology anomalies -> ZoneObs signals -> crop_risk_scorer alert
|
| 12 |
-
levels) actually fire on the days when the climate says something bad was
|
| 13 |
-
happening -- and with how much lead time?" It is the missing link between
|
| 14 |
-
"the modules pass unit tests" and "the system demonstrably works as a
|
| 15 |
-
weather modelling / prediction / planning tool for Indonesia".
|
| 16 |
-
|
| 17 |
PIPELINE PER REPLAY STEP (per zone, per date)
|
| 18 |
---------------------------------------------
|
| 19 |
1. obs: live mode -> era5_data_pipeline.fetch_zone_obs (Open-Meteo archive
|
|
@@ -24,37 +13,10 @@ PIPELINE PER REPLAY STEP (per zone, per date)
|
|
| 24 |
event blocks (offline, CI-friendly).
|
| 25 |
2. anomalies: climatology.apply_climatology_anomalies with a PINNED
|
| 26 |
climatology whose period ends BEFORE the replay window starts
|
| 27 |
-
(end_year = replay_start.year - 1).
|
| 28 |
-
guarantee; fetching anomalies through cfg.use_climatology_anomalies
|
| 29 |
-
would instead use the most-recent years and leak the replayed period.
|
| 30 |
3. forecast: timesfm_wrapper 'baseline' backend (persistence /
|
| 31 |
climatology-reverting, derived from the obs only -- no look-ahead).
|
| 32 |
-
A real NWP hindcast archive would be the strict upgrade; the baseline
|
| 33 |
-
keeps the replay honest and reproducible.
|
| 34 |
4. score: crop_risk_scorer.compute_risk_score -> alert level per day.
|
| 35 |
-
|
| 36 |
-
GROUND TRUTH
|
| 37 |
-
------------
|
| 38 |
-
Default (proxy): climatological percentiles from the SAME pinned climatology:
|
| 39 |
-
drought day: obs.precip_30d_mm < mean_30d - 0.84 * std_30d
|
| 40 |
-
flood day: obs.precip_7d_mm > mean_7d + 1.28 * std_7d
|
| 41 |
-
L1 (optional): --impact-labels PATH loads impact_labels JSON (e.g.
|
| 42 |
-
impact_labels_java_v1.json). When a zone-day has an L1 drought/flood
|
| 43 |
-
event, that overrides the proxy flags for that day only. Days without
|
| 44 |
-
L1 coverage keep the proxy. This is the documented production path
|
| 45 |
-
toward BNPB/provincial catalogues without inventing live APIs.
|
| 46 |
-
|
| 47 |
-
PRODUCT EMIT (optional)
|
| 48 |
-
-----------------------
|
| 49 |
-
--emit-product-alerts: after each compute_risk_score, call
|
| 50 |
-
product_alert_service.emit_product_alert (idempotent, WARNING+ or
|
| 51 |
-
hazard gate). Trusted backend path only; uses LocalTransport by default.
|
| 52 |
-
|
| 53 |
-
SUGGESTED LIVE DEMO WINDOWS (historically documented events):
|
| 54 |
-
* 2023 El Nino + positive-IOD dry season, Java:
|
| 55 |
-
--zones karawang_rice,indramayu_rice --start 2023-07-01 --end 2023-11-30
|
| 56 |
-
* 2020-21 La Nina wet season (Jan 2021 Java floods):
|
| 57 |
-
--zones karawang_rice --start 2020-12-01 --end 2021-02-28
|
| 58 |
"""
|
| 59 |
|
| 60 |
from __future__ import annotations
|
|
@@ -132,7 +94,6 @@ class DayRecord:
|
|
| 132 |
|
| 133 |
@dataclass
|
| 134 |
class BacktestMetrics:
|
| 135 |
-
"""Day-level confusion + event-level detection/lead time."""
|
| 136 |
n_days: int = 0
|
| 137 |
tp: int = 0
|
| 138 |
fp: int = 0
|
|
@@ -174,17 +135,6 @@ class BacktestMetrics:
|
|
| 174 |
|
| 175 |
|
| 176 |
def compute_metrics(records: Sequence[DayRecord]) -> BacktestMetrics:
|
| 177 |
-
"""Day-level confusion + event-run detection with lead time.
|
| 178 |
-
|
| 179 |
-
Day-level: TP = alert on an event day, FP = alert on a non-event day,
|
| 180 |
-
FN = missed event day, TN = quiet day correctly quiet.
|
| 181 |
-
Run-level: contiguous event days (merged across gaps <=
|
| 182 |
-
_RUN_MERGE_GAP_DAYS) form one event run. A run is DETECTED
|
| 183 |
-
if any alert fires inside the run or in the
|
| 184 |
-
_LEAD_WINDOW_DAYS before its first day. Lead time = days
|
| 185 |
-
from that first qualifying alert to the run start (0 for
|
| 186 |
-
alerts landing on day 1 of the run).
|
| 187 |
-
"""
|
| 188 |
m = BacktestMetrics(n_days=len(records))
|
| 189 |
for r in records:
|
| 190 |
if r.event and r.alert:
|
|
@@ -243,13 +193,6 @@ def compute_metrics(records: Sequence[DayRecord]) -> BacktestMetrics:
|
|
| 243 |
|
| 244 |
|
| 245 |
def compute_product_l1_metrics(records: Sequence[Dict[str, Any]]) -> Dict[str, Any]:
|
| 246 |
-
"""
|
| 247 |
-
Primary product metrics: EMITTED vs L1 event days (not ADVISORY vs proxy).
|
| 248 |
-
|
| 249 |
-
Uses record dicts (as written to result JSON). A day is:
|
| 250 |
-
product_positive = product_emit == 'EMITTED'
|
| 251 |
-
l1_event = gt_source == 'l1' and (event_drought or event_flood)
|
| 252 |
-
"""
|
| 253 |
n = len(records)
|
| 254 |
tp = fp = fn = tn = 0
|
| 255 |
n_l1 = 0
|
|
@@ -299,7 +242,6 @@ def compute_product_l1_metrics(records: Sequence[Dict[str, Any]]) -> Dict[str, A
|
|
| 299 |
# ---------------------------------------------------------------------------
|
| 300 |
|
| 301 |
def _classify_events(obs: ZoneObs, clim: ZoneClimatology) -> Tuple[bool, bool]:
|
| 302 |
-
"""Proxy ground truth from the pinned climatology (see module docstring)."""
|
| 303 |
event_drought = False
|
| 304 |
event_flood = False
|
| 305 |
if obs.precip_30d_mm > 0.0:
|
|
@@ -324,18 +266,6 @@ def replay_zone(
|
|
| 324 |
emission_ledger: Any = None,
|
| 325 |
transport: Any = None,
|
| 326 |
) -> List[DayRecord]:
|
| 327 |
-
"""Replay one zone over [start, end] at `step_days` resolution.
|
| 328 |
-
|
| 329 |
-
mode='live': obs via era5_data_pipeline (Open-Meteo archive for
|
| 330 |
-
historical dates), forecast via baseline backend.
|
| 331 |
-
mode='synthetic': deterministic synthetic obs; `planted_events` maps
|
| 332 |
-
'drought'/'flood' -> (start, end) blocks during which
|
| 333 |
-
the synthetic generator's event flag is forced on.
|
| 334 |
-
impact_store: optional ImpactLabelStore; L1 flags override proxy
|
| 335 |
-
when present for that zone-day.
|
| 336 |
-
emit_product: if True, call product_alert_service after each score
|
| 337 |
-
(requires emission_ledger + transport).
|
| 338 |
-
"""
|
| 339 |
if start.tzinfo is None:
|
| 340 |
start = start.replace(tzinfo=timezone.utc)
|
| 341 |
if end.tzinfo is None:
|
|
@@ -378,9 +308,6 @@ def replay_zone(
|
|
| 378 |
seed=_zo._stable_seed(f"{zone_id}|{vt.date().isoformat()}"),
|
| 379 |
**flag,
|
| 380 |
)
|
| 381 |
-
# The factory draws its own random valid_time from the seed;
|
| 382 |
-
# the replay clock is authoritative -- override via the
|
| 383 |
-
# codebase's to_dict/from_dict idiom (never mutate).
|
| 384 |
_d = obs.to_dict()
|
| 385 |
_d.pop("_schema_version", None)
|
| 386 |
_d["valid_time"] = vt.isoformat()
|
|
@@ -397,7 +324,6 @@ def replay_zone(
|
|
| 397 |
ev_drought, ev_flood = _classify_events(obs, clim)
|
| 398 |
gt_source = "proxy"
|
| 399 |
|
| 400 |
-
# L1 override: when store has a label for this zone-day, use it.
|
| 401 |
if impact_store is not None:
|
| 402 |
try:
|
| 403 |
l1_d, l1_f = impact_store.labels_for_day(zone_id, vt.date())
|
|
@@ -427,7 +353,6 @@ def replay_zone(
|
|
| 427 |
except RuntimeError:
|
| 428 |
loop = None
|
| 429 |
if loop and loop.is_running():
|
| 430 |
-
# Nested running loop (e.g. notebook): schedule carefully
|
| 431 |
import concurrent.futures
|
| 432 |
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
|
| 433 |
product_emit = pool.submit(lambda: asyncio.run(_one())).result().outcome_code
|
|
@@ -467,7 +392,6 @@ def run_backtest(
|
|
| 467 |
impact_labels_path: Optional[str] = None,
|
| 468 |
emit_product_alerts: bool = False,
|
| 469 |
) -> Dict[str, Any]:
|
| 470 |
-
"""Replay several zones; return per-zone + overall metrics and records."""
|
| 471 |
if mode == "live":
|
| 472 |
register_indonesia_zones()
|
| 473 |
|
|
@@ -737,4 +661,4 @@ if __name__ == "__main__":
|
|
| 737 |
if len(sys.argv) > 1:
|
| 738 |
sys.exit(_main())
|
| 739 |
else:
|
| 740 |
-
sys.exit(_self_test())
|
|
|
|
| 3 |
=====================
|
| 4 |
Historical replay / evaluation harness for the Indonesia weather-risk stack.
|
| 5 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
PIPELINE PER REPLAY STEP (per zone, per date)
|
| 7 |
---------------------------------------------
|
| 8 |
1. obs: live mode -> era5_data_pipeline.fetch_zone_obs (Open-Meteo archive
|
|
|
|
| 13 |
event blocks (offline, CI-friendly).
|
| 14 |
2. anomalies: climatology.apply_climatology_anomalies with a PINNED
|
| 15 |
climatology whose period ends BEFORE the replay window starts
|
| 16 |
+
(end_year = replay_start.year - 1).
|
|
|
|
|
|
|
| 17 |
3. forecast: timesfm_wrapper 'baseline' backend (persistence /
|
| 18 |
climatology-reverting, derived from the obs only -- no look-ahead).
|
|
|
|
|
|
|
| 19 |
4. score: crop_risk_scorer.compute_risk_score -> alert level per day.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
"""
|
| 21 |
|
| 22 |
from __future__ import annotations
|
|
|
|
| 94 |
|
| 95 |
@dataclass
|
| 96 |
class BacktestMetrics:
|
|
|
|
| 97 |
n_days: int = 0
|
| 98 |
tp: int = 0
|
| 99 |
fp: int = 0
|
|
|
|
| 135 |
|
| 136 |
|
| 137 |
def compute_metrics(records: Sequence[DayRecord]) -> BacktestMetrics:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
m = BacktestMetrics(n_days=len(records))
|
| 139 |
for r in records:
|
| 140 |
if r.event and r.alert:
|
|
|
|
| 193 |
|
| 194 |
|
| 195 |
def compute_product_l1_metrics(records: Sequence[Dict[str, Any]]) -> Dict[str, Any]:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 196 |
n = len(records)
|
| 197 |
tp = fp = fn = tn = 0
|
| 198 |
n_l1 = 0
|
|
|
|
| 242 |
# ---------------------------------------------------------------------------
|
| 243 |
|
| 244 |
def _classify_events(obs: ZoneObs, clim: ZoneClimatology) -> Tuple[bool, bool]:
|
|
|
|
| 245 |
event_drought = False
|
| 246 |
event_flood = False
|
| 247 |
if obs.precip_30d_mm > 0.0:
|
|
|
|
| 266 |
emission_ledger: Any = None,
|
| 267 |
transport: Any = None,
|
| 268 |
) -> List[DayRecord]:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 269 |
if start.tzinfo is None:
|
| 270 |
start = start.replace(tzinfo=timezone.utc)
|
| 271 |
if end.tzinfo is None:
|
|
|
|
| 308 |
seed=_zo._stable_seed(f"{zone_id}|{vt.date().isoformat()}"),
|
| 309 |
**flag,
|
| 310 |
)
|
|
|
|
|
|
|
|
|
|
| 311 |
_d = obs.to_dict()
|
| 312 |
_d.pop("_schema_version", None)
|
| 313 |
_d["valid_time"] = vt.isoformat()
|
|
|
|
| 324 |
ev_drought, ev_flood = _classify_events(obs, clim)
|
| 325 |
gt_source = "proxy"
|
| 326 |
|
|
|
|
| 327 |
if impact_store is not None:
|
| 328 |
try:
|
| 329 |
l1_d, l1_f = impact_store.labels_for_day(zone_id, vt.date())
|
|
|
|
| 353 |
except RuntimeError:
|
| 354 |
loop = None
|
| 355 |
if loop and loop.is_running():
|
|
|
|
| 356 |
import concurrent.futures
|
| 357 |
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
|
| 358 |
product_emit = pool.submit(lambda: asyncio.run(_one())).result().outcome_code
|
|
|
|
| 392 |
impact_labels_path: Optional[str] = None,
|
| 393 |
emit_product_alerts: bool = False,
|
| 394 |
) -> Dict[str, Any]:
|
|
|
|
| 395 |
if mode == "live":
|
| 396 |
register_indonesia_zones()
|
| 397 |
|
|
|
|
| 661 |
if len(sys.argv) > 1:
|
| 662 |
sys.exit(_main())
|
| 663 |
else:
|
| 664 |
+
sys.exit(_self_test())
|