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
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:
+56
-4
@@ -204,9 +204,10 @@ def train_station(
|
||||
split_test_start: str = SPLIT_B_TEST_START,
|
||||
split_test_end: str = SPLIT_B_TEST_END,
|
||||
rain: Optional[pd.Series] = None,
|
||||
dam: Optional[pd.DataFrame] = None,
|
||||
) -> Tuple[Optional[dict], dict]:
|
||||
"""Train every head for one station. Returns (bundle_or_None, station_metrics)."""
|
||||
X, Y, meta = features.build_matrix(df_long, station, horizons, rain=rain)
|
||||
X, Y, meta = features.build_matrix(df_long, station, horizons, rain=rain, dam=dam)
|
||||
if meta["n_rows"] < MIN_ROWS_TO_TRAIN:
|
||||
return None, {
|
||||
"status": "failed",
|
||||
@@ -412,8 +413,13 @@ def train_station(
|
||||
] = f"only {n_pos} positives in train span (< {MIN_POSITIVES_FOR_CLASSIFIER})"
|
||||
final_heads[head_key] = None
|
||||
|
||||
# v3 = rise target + Open-Meteo rain features; v2 = rise target only
|
||||
version_prefix = "hgb-v3" if "rain_24h" in feature_names else "hgb-v2"
|
||||
# v4 = + Mae Ngat dam features; v3 = rise + rain; v2 = rise target only
|
||||
if "dam_storage_pct" in feature_names:
|
||||
version_prefix = "hgb-v4"
|
||||
elif "rain_24h" in feature_names:
|
||||
version_prefix = "hgb-v3"
|
||||
else:
|
||||
version_prefix = "hgb-v2"
|
||||
bundle = {
|
||||
"station_code": station,
|
||||
"model_version": f"{version_prefix}+{_git_short_sha()}",
|
||||
@@ -443,6 +449,8 @@ def train_all(
|
||||
skip_eval: bool = False,
|
||||
hgb_overrides: Optional[dict] = None,
|
||||
use_rain: bool = True,
|
||||
use_dam: bool = False,
|
||||
db_url: Optional[str] = None,
|
||||
) -> dict:
|
||||
"""Train and save every requested station's models. Returns the metrics.json payload."""
|
||||
models_dir = Path(models_dir)
|
||||
@@ -462,7 +470,42 @@ def train_all(
|
||||
logger.info(
|
||||
f"rain series: {rain_series.index.min()} .. {rain_series.index.max()}"
|
||||
)
|
||||
version_prefix = "hgb-v3" if rain_series is not None else "hgb-v2"
|
||||
|
||||
# Mae Ngat reservoir state (rid_reservoir_daily, 2018+). OFF by default:
|
||||
# the 2026-08-13 backtest ablation showed every dam-feature subset COSTS
|
||||
# 1-3 h of first-alert lead on the 2024 record flood (the daily report
|
||||
# lags up to 31 h, so during fast onset the columns describe yesterday's
|
||||
# benign reservoir and damp the alarm). Kept as an opt-in for post-monsoon
|
||||
# re-evaluation once the 2026 season adds dam-era flood events.
|
||||
dam_frame = None
|
||||
if use_dam:
|
||||
try:
|
||||
from . import dam as dam_mod
|
||||
|
||||
dam_frame = dam_mod.load_history(db_url=db_url)
|
||||
except Exception as error:
|
||||
logger.warning(f"dam history unavailable, training without it: {error}")
|
||||
if dam_frame is None:
|
||||
# load_history returns None (no raise) when both DB and cache
|
||||
# miss — an explicitly requested experiment must say so loudly.
|
||||
logger.warning(
|
||||
"--dam requested but no dam history available; "
|
||||
"training v3-style bundles WITHOUT dam features"
|
||||
)
|
||||
if dam_frame is not None:
|
||||
logger.info(
|
||||
f"dam series: {dam_frame.index.min()} .. {dam_frame.index.max()}"
|
||||
)
|
||||
|
||||
# Run-level version: v4 only if some requested station actually receives
|
||||
# dam columns (they are gated to DAM_STATIONS; per-bundle versions are
|
||||
# derived from each station's own feature_names and remain authoritative).
|
||||
if dam_frame is not None and any(s in features.DAM_STATIONS for s in stations):
|
||||
version_prefix = "hgb-v4"
|
||||
elif rain_series is not None:
|
||||
version_prefix = "hgb-v3"
|
||||
else:
|
||||
version_prefix = "hgb-v2"
|
||||
model_version = f"{version_prefix}+{_git_short_sha()}"
|
||||
|
||||
station_results: Dict[str, dict] = {}
|
||||
@@ -480,6 +523,7 @@ def train_all(
|
||||
skip_eval=skip_eval,
|
||||
hgb_overrides=hgb_overrides,
|
||||
rain=rain_series,
|
||||
dam=dam_frame,
|
||||
)
|
||||
if bundle is None:
|
||||
logger.warning(f"{station}: failed ({station_metrics.get('reason')})")
|
||||
@@ -542,6 +586,12 @@ def main(argv: Optional[List[str]] = None) -> None:
|
||||
action="store_true",
|
||||
help="train without the Open-Meteo rain features (v2-style bundles)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dam",
|
||||
action="store_true",
|
||||
help="EXPERIMENTAL: include Mae Ngat reservoir features (v4 bundles); "
|
||||
"the 2026-08 ablation showed they cost 1-3 h of alert lead",
|
||||
)
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
if args.stations == "all":
|
||||
@@ -570,6 +620,8 @@ def main(argv: Optional[List[str]] = None) -> None:
|
||||
models_dir=Path(args.models_dir),
|
||||
skip_eval=args.skip_eval,
|
||||
use_rain=not args.no_rain,
|
||||
use_dam=args.dam,
|
||||
db_url=resolve_db_url(args.db_url),
|
||||
)
|
||||
trained = sum(
|
||||
1 for s in metrics_payload["stations"].values() if s["status"] == "trained"
|
||||
|
||||
Reference in New Issue
Block a user