feat: Mae Ngat dam features — built, evaluated, defaulted OFF
CI/CD Pipeline - Northern Thailand Ping River Monitor / Code Quality (push) Successful in 15s
Documentation / Validate Documentation (push) Failing after 8s
Documentation / Generate API Documentation (push) Successful in 9s
Documentation / Build Sphinx Documentation (push) Successful in 15s
CI/CD Pipeline - Northern Thailand Ping River Monitor / Cleanup (push) Successful in 1s
Documentation / Documentation Summary (push) Successful in 2s
CI/CD Pipeline - Northern Thailand Ping River Monitor / Test Suite (3.11) (push) Failing after 27s
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
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

src/ml/dam.py loads rid_reservoir_daily into a leakage-safe hourly frame
(daily row visible from 07:00 its own date, ffill capped at 48 h) and is
plumbed through features/train/predict/evaluate exactly like rain, gated
to the six mainstem stations below the Mae Ngat confluence.

The experiment concludes as a documented NEGATIVE result: on the 2024
record-flood backtest every dam-feature subset costs 1-3 h of first-alert
lead (13h -> 10-12h) for <=3 cm of peak-error gain, because the daily RID
report lags up to 31 h and describes yesterday's benign absorbing
reservoir during fast onset. Features therefore default OFF (--dam
opt-in on the training and backtest CLIs; rise_rain_dam/rise_dam harness
variants, excluded from the default variant set). The ablation also
isolated the HII gap-fill as lead-neutral: the acceptance gate holds at
13 h with fill enabled, and docs/img charts are regenerated with the
shipping configuration. Full table in docs/FLOOD_FORECASTING.md §5.

Review-swarm fixes: evaluate.py skips variants whose feature family is
absent instead of crashing the run; --dam forwards --db-url and warns
loudly when no dam history loads; an empty DB result can no longer wipe
a good dam cache; run-level metrics version claims v4 only when a dam
station is actually in the set.
This commit is contained in:
2026-08-13 20:42:21 +07:00
parent 6af6fbe02c
commit 28b62e5a36
12 changed files with 464 additions and 25 deletions
+30 -4
View File
@@ -128,6 +128,7 @@ def _model_forecast(
as_of: pd.Timestamp,
current_level: float,
rain: Optional[pd.Series] = None,
dam: Optional[pd.DataFrame] = None,
) -> List[dict]:
warn_thr = bundle["thresholds"]["warning"]
danger_thr = bundle["thresholds"]["danger"]
@@ -146,7 +147,9 @@ def _model_forecast(
)
warn_thr, danger_thr = cfg_warn, cfg_danger
feature_row = features.build_features(grid, station_code, rain=rain).loc[[as_of]]
feature_row = features.build_features(grid, station_code, rain=rain, dam=dam).loc[
[as_of]
]
expected_columns = bundle["feature_names"]
missing = [c for c in expected_columns if c not in feature_row.columns]
if missing:
@@ -231,6 +234,7 @@ def _forecast_station(
now: pd.Timestamp,
horizons: Tuple[int, ...],
rain: Optional[pd.Series] = None,
dam: Optional[pd.DataFrame] = None,
) -> List[dict]:
level_col = (station_code, "water_level")
if level_col not in grid.observed.columns:
@@ -267,7 +271,7 @@ def _forecast_station(
bundle = _load_bundle(bundle_path)
model_results = _model_forecast(
station_code, grid, bundle, as_of, current_level, rain=rain
station_code, grid, bundle, as_of, current_level, rain=rain, dam=dam
)
if model_results is None:
return _heuristic_forecast(
@@ -306,6 +310,7 @@ def get_forecasts(
models_dir: Union[str, Path] = DEFAULT_MODELS_DIR,
now: Optional[Union[datetime.datetime, str]] = None,
rain: Optional[pd.Series] = None,
dam: Optional[pd.DataFrame] = None,
) -> List[dict]:
"""Produce flood forecasts for every station present in `readings_by_station`.
@@ -329,7 +334,13 @@ def get_forecasts(
try:
results.extend(
_forecast_station(
station_code, grid, models_dir, now, DEFAULT_HORIZONS, rain=rain
station_code,
grid,
models_dir,
now,
DEFAULT_HORIZONS,
rain=rain,
dam=dam,
)
)
except Exception as error:
@@ -376,4 +387,19 @@ def get_latest_forecasts(
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)
# Recent Mae Ngat reservoir state; same empty-not-None contract so
# dam-trained bundles keep their columns (NaN) when the DB read fails.
from . import dam as dam_mod
try:
dam = dam_mod.serving_frame(db_url=db_url)
except Exception as error:
logger.warning(f"dam serving frame failed: {error}")
dam = None
if dam is None:
logger.warning("dam state unavailable; dam features will be NaN")
dam = pd.DataFrame()
return get_forecasts(
readings_by_station, models_dir=models_dir, rain=rain, dam=dam
)