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:
@@ -20,10 +20,48 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
# Per-station (warning, danger) level thresholds in meters. "*" is the default
|
||||
# applied to any station without an explicit override.
|
||||
# Per-station (warning, danger) levels in metres on each gauge's own datum.
|
||||
# Calibrated 2026-08-10 from the DB's discharge_percent (RID % of channel
|
||||
# capacity): warning = median level at 75-85% capacity, danger = median level
|
||||
# at 95-105%. P.1 instead uses the official Chiang Mai inundation map keyed to
|
||||
# the P.1 gauge: city flooding begins at 3.70 m (stage 1) and reaches most
|
||||
# districts by 4.20 m (stage 5) — see P1_FLOOD_STAGES.
|
||||
THRESHOLDS: Dict[str, Tuple[float, float]] = {
|
||||
"*": (3.0, 4.5),
|
||||
"P.1": (3.70, 4.20),
|
||||
"P.103": (5.95, 6.75),
|
||||
"P.20": (2.35, 2.80),
|
||||
"P.21": (3.20, 3.60),
|
||||
"P.4A": (3.40, 3.90),
|
||||
"P.5": (4.55, 4.95),
|
||||
"P.67": (2.45, 2.90),
|
||||
"P.75": (2.75, 3.50),
|
||||
"P.76": (5.35, 5.45),
|
||||
"P.77": (2.85, 3.35),
|
||||
"P.81": (5.15, 6.30),
|
||||
"P.82": (3.40, 3.80),
|
||||
"P.84": (3.45, 3.90),
|
||||
"P.85": (2.90, 3.35),
|
||||
"P.87": (3.75, 4.05),
|
||||
"P.92": (2.95, 3.60),
|
||||
}
|
||||
|
||||
# Official Chiang Mai flood-onset stages at the P.1 gauge (Nawarat Bridge),
|
||||
# from the municipal inundation map (พื้นที่ท่วมตัวเมืองเชียงใหม่, events of
|
||||
# 2548/2554/2565 BE): gauge level in m, RID discharge in m³/s. Each stage
|
||||
# floods progressively more city zones.
|
||||
P1_FLOOD_STAGES: List[Dict[str, float]] = [
|
||||
{"stage": 1, "level": 3.70, "discharge_cms": 405},
|
||||
{"stage": 2, "level": 3.90, "discharge_cms": 438},
|
||||
{"stage": 3, "level": 4.00, "discharge_cms": 458},
|
||||
{"stage": 4, "level": 4.10, "discharge_cms": 478},
|
||||
{"stage": 5, "level": 4.20, "discharge_cms": 493},
|
||||
{"stage": 6, "level": 4.30, "discharge_cms": 508},
|
||||
{"stage": 7, "level": 4.60, "discharge_cms": 558},
|
||||
]
|
||||
|
||||
FLOOD_STAGES: Dict[str, List[Dict[str, float]]] = {"P.1": P1_FLOOD_STAGES}
|
||||
|
||||
MONSOON_MONTHS = {6, 7, 8, 9, 10}
|
||||
FFILL_LIMIT_H = 3
|
||||
MIN_WINDOW_COVERAGE = 0.5
|
||||
|
||||
+30
-16
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user