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:
2026-08-10 15:35:00 +07:00
parent 29f4b5818d
commit e4d5d274f0
6 changed files with 162 additions and 32 deletions
+30 -16
View File
@@ -154,22 +154,36 @@ def _model_forecast(
p_warning = _clip_probability(p_warning)
p_danger = min(_clip_probability(p_danger), p_warning)
results.append(
{
"station_code": station_code,
"horizon_hours": horizon_h,
"p_warning": p_warning,
"p_danger": p_danger,
"predicted_max_level": predicted_max,
"current_level": current_level,
"as_of": as_of.isoformat(),
"model_version": bundle["model_version"],
"trained_at": bundle["trained_at"],
"source": "model",
"threshold_warning": warn_thr,
"threshold_danger": danger_thr,
}
)
row = {
"station_code": station_code,
"horizon_hours": horizon_h,
"p_warning": p_warning,
"p_danger": p_danger,
"predicted_max_level": predicted_max,
"current_level": current_level,
"as_of": as_of.isoformat(),
"model_version": bundle["model_version"],
"trained_at": bundle["trained_at"],
"source": "model",
"threshold_warning": warn_thr,
"threshold_danger": danger_thr,
}
stages = features.FLOOD_STAGES.get(station_code)
if stages:
# Exceedance probability per official inundation stage, from the
# regression head and its validation-residual sigma. These are
# threshold-agnostic, so no retraining is needed to serve them.
row["stages"] = [
{
"stage": s["stage"],
"level": s["level"],
"p_exceed": _clip_probability(
_sigmoid_probability(predicted_max, s["level"], sigma_h)
),
}
for s in stages
]
results.append(row)
return results