feat: hgb-v3 — Open-Meteo rain features clear the 12h warning gate
CI/CD Pipeline - Northern Thailand Ping River Monitor / Deploy to Production (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Performance Test (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Code Quality (push) Successful in 13s
Documentation / Validate Documentation (push) Failing after 7s
Documentation / Generate API Documentation (push) Successful in 8s
Documentation / Build Sphinx Documentation (push) Successful in 17s
CI/CD Pipeline - Northern Thailand Ping River Monitor / Cleanup (push) Successful in 0s
Documentation / Documentation Summary (push) Successful in 2s
CI/CD Pipeline - Northern Thailand Ping River Monitor / Test Suite (3.11) (push) Failing after 24s
CI/CD Pipeline - Northern Thailand Ping River Monitor / Build Docker Image (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Integration Test with Services (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Deploy to Staging (push) Skipped

The rolling-origin harness (models/eval_rain.json) showed catchment rain
halving flood-year Brier scores, cutting flood-regime MAE 20-40%, and
extending the hard 2024 leads (+6h -> +11h at P.1, +10h -> +19h at
P.103). Ported: train_all loads the catchment-mean series (use_rain /
--no-rain to opt out; without it bundles train as v2), predict fetches
live rain hourly and passes an empty series on failure so rain-trained
bundles serve with NaN features instead of tripping the feature guard,
and the leader worker persists hourly per-point + catchment-mean rows to
a new openmeteo_rain table.

Regenerated backtest: the 2024 record flood now gets a 13-HOUR WARNING
(alert 04:00 vs 17:00 crossing, river at 2.9m at alert time) — the >=12h
acceptance gate PASSES for the first time. Journey on that crossing:
v1 -18h, v2 +6h, v3 +13h. The marginal 2025 double-crest trades its
artifact +46h latch for a calibrated +2h with zero false alarms. P.1
MAE 4.9/7.2/8.7 cm at 6/12/24h. Docs updated throughout.
This commit is contained in:
2026-08-12 17:05:01 +07:00
parent cbb3bf7369
commit df0ae8cda3
12 changed files with 646 additions and 53 deletions
+21 -4
View File
@@ -127,6 +127,7 @@ def _model_forecast(
bundle: dict,
as_of: pd.Timestamp,
current_level: float,
rain: Optional[pd.Series] = None,
) -> List[dict]:
warn_thr = bundle["thresholds"]["warning"]
danger_thr = bundle["thresholds"]["danger"]
@@ -145,7 +146,7 @@ def _model_forecast(
)
warn_thr, danger_thr = cfg_warn, cfg_danger
feature_row = features.build_features(grid, station_code).loc[[as_of]]
feature_row = features.build_features(grid, station_code, rain=rain).loc[[as_of]]
expected_columns = bundle["feature_names"]
missing = [c for c in expected_columns if c not in feature_row.columns]
if missing:
@@ -229,6 +230,7 @@ def _forecast_station(
models_dir: Path,
now: pd.Timestamp,
horizons: Tuple[int, ...],
rain: Optional[pd.Series] = None,
) -> List[dict]:
level_col = (station_code, "water_level")
if level_col not in grid.observed.columns:
@@ -264,7 +266,9 @@ def _forecast_station(
)
bundle = _load_bundle(bundle_path)
model_results = _model_forecast(station_code, grid, bundle, as_of, current_level)
model_results = _model_forecast(
station_code, grid, bundle, as_of, current_level, rain=rain
)
if model_results is None:
return _heuristic_forecast(
station_code,
@@ -301,6 +305,7 @@ def get_forecasts(
readings_by_station: Dict[str, List[dict]],
models_dir: Union[str, Path] = DEFAULT_MODELS_DIR,
now: Optional[Union[datetime.datetime, str]] = None,
rain: Optional[pd.Series] = None,
) -> List[dict]:
"""Produce flood forecasts for every station present in `readings_by_station`.
@@ -323,7 +328,9 @@ def get_forecasts(
for station_code in readings_by_station.keys():
try:
results.extend(
_forecast_station(station_code, grid, models_dir, now, DEFAULT_HORIZONS)
_forecast_station(
station_code, grid, models_dir, now, DEFAULT_HORIZONS, rain=rain
)
)
except Exception as error:
logger.error(f"Forecast failed for station {station_code}: {error}")
@@ -359,4 +366,14 @@ def get_latest_forecasts(
f"No recent data for station {missing_station}; omitting from forecasts"
)
return get_forecasts(readings_by_station, models_dir=models_dir)
# Live rain: trailing days + next-48h forecast. On fetch failure pass an
# EMPTY series (not None) so rain-trained bundles still find their columns
# (as NaN) and serve model output instead of tripping the feature guard.
from .rain import serving_series
rain = serving_series()
if rain is None:
logger.warning("live rain unavailable; rain features will be NaN")
rain = pd.Series(dtype=float)
return get_forecasts(readings_by_station, models_dir=models_dir, rain=rain)