feat: per-station flood thresholds and Chiang Mai inundation stages for P.1
Replace the network-wide (3.0, 4.5) m thresholds with per-station values calibrated from the DB's discharge_percent (RID % of channel capacity): warning = median level at 75-85% capacity, danger = median at 95-105%. Fixes P.103 over-alerting (bank-full ~6.75 m, not 4.5) and P.67 under-alerting (overflow ~2.9 m). Requires a retrain to take effect in the classifier heads. P.1 uses the official Chiang Mai municipal inundation map instead: warning 3.70 m (stage 1, city flooding begins), danger 4.20 m (stage 5), with the full 7-stage table (3.70-4.60 m + discharge) in features.P1_FLOOD_STAGES. Forecast rows for P.1 now include per-stage exceedance probabilities computed from the regression head + calibration sigma - available immediately without retraining. Dashboard: "Chiang Mai city flood outlook" block above the forecast grid (predicted peak + 7 stage-probability chips) and a toggleable georeferenced overlay of the official flood-zone map (static/flood-zones-p1.jpg, bounds tunable in FLOOD_ZONE_BOUNDS).
This commit is contained in:
@@ -78,7 +78,9 @@ def test_no_future_leakage():
|
||||
|
||||
def test_label_alignment():
|
||||
idx = pd.date_range("2020-01-01", periods=12, freq="h")
|
||||
levels = [1.0, 1.0, 1.0, 1.0, 1.0, 3.5, 3.5, 1.0, 1.0, 1.0, 1.0, 1.0]
|
||||
warn_thr, _danger_thr = features.get_thresholds("P.1")
|
||||
peak = warn_thr + 0.3
|
||||
levels = [1.0, 1.0, 1.0, 1.0, 1.0, peak, peak, 1.0, 1.0, 1.0, 1.0, 1.0]
|
||||
df = pd.DataFrame(
|
||||
{
|
||||
"timestamp": idx,
|
||||
@@ -90,14 +92,14 @@ def test_label_alignment():
|
||||
grid = features.make_hourly_grid(df)
|
||||
labels = features.build_labels(grid, "P.1", horizons=(6,))
|
||||
|
||||
# Level crosses warn (3.0) at t=5. A 6h forward window (t, t+6] first
|
||||
# includes t=5 for t=0 .. t=4 (inclusive), so exceed_warn_6 should be 1
|
||||
# for t=0..4 and not (necessarily) for later rows in this hand-built series.
|
||||
# Level crosses the warning threshold at t=5. A 6h forward window (t, t+6]
|
||||
# first includes t=5 for t=0 .. t=4 (inclusive), so exceed_warn_6 should be
|
||||
# 1 for t=0..4 and not (necessarily) for later rows in this hand-built series.
|
||||
for t in range(5):
|
||||
assert labels["exceed_warn_6"].iloc[t] == 1.0, f"t={t} expected warn exceedance"
|
||||
|
||||
# max_level_6 at t=0 covers hours 1..6 -> includes the 3.5 peak.
|
||||
assert labels["max_level_6"].iloc[0] == pytest.approx(3.5)
|
||||
# max_level_6 at t=0 covers hours 1..6 -> includes the peak.
|
||||
assert labels["max_level_6"].iloc[0] == pytest.approx(peak)
|
||||
|
||||
|
||||
def test_label_coverage_gate():
|
||||
@@ -169,12 +171,18 @@ _FORECAST_KEYS = {
|
||||
|
||||
|
||||
def _assert_valid_forecast_row(row: dict) -> None:
|
||||
assert set(row.keys()) == _FORECAST_KEYS
|
||||
# "stages" is optional: model rows for stations in features.FLOOD_STAGES carry
|
||||
# per-inundation-stage exceedance probabilities (currently P.1 only).
|
||||
assert _FORECAST_KEYS <= set(row.keys())
|
||||
assert set(row.keys()) - _FORECAST_KEYS <= {"stages"}
|
||||
assert 0.0 <= row["p_warning"] <= 1.0
|
||||
assert 0.0 <= row["p_danger"] <= 1.0
|
||||
assert row["p_danger"] <= row["p_warning"]
|
||||
assert row["predicted_max_level"] >= row["current_level"]
|
||||
assert row["source"] in ("model", "heuristic")
|
||||
for stage in row.get("stages", []):
|
||||
assert 0.0 <= stage["p_exceed"] <= 1.0
|
||||
assert stage["level"] > 0
|
||||
|
||||
|
||||
def test_train_smoke_and_roundtrip(tmp_path):
|
||||
|
||||
Reference in New Issue
Block a user