Ctrl K
ComunidadGuías…

flood_warning/thresholds.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""Per-gauge flood thresholds (Warning / Danger / Extreme), in m³/s.

NWS publishes flood categories mostly as *stage* (gauge height in ft), which a
discharge model can't speak to. So we derive flow thresholds the way the
hydrology does when only a short record exists: take the daily-peak flow
distribution over the multi-year record and read off high empirical quantiles
as return-period stand-ins.

  warning  ~ 0.95 quantile of daily peaks   (roughly a 2-year return flow)
  danger   ~ 0.99 quantile                  (~5-year)
  extreme  ~ 0.999 quantile                 (~10-year)

With only a few years of record these are empirical stand-ins, not fitted
return periods — but they give the UI a stable, per-site Normal/Warning/
Danger/Extreme banding that matches each gauge's own behavior.

Run once (slow — pulls multi-year USGS flow per gauge) and cache to
thresholds.json, which predict.py reads on every inference run:

    python -m flood_warning.thresholds
"""
from __future__ import annotations

import json
from pathlib import Path

import pandas as pd

from .fetch import fetch_usgs_hourly
from .sites import SITES

THRESH_PATH = Path(__file__).resolve().parent / 'thresholds.json'
MIN_DAYS = 365


def compute_thresholds(gauge_id: str, years_back: int = 3) -> dict | None:
    """Daily-peak quantile thresholds for one gauge from `years_back` of hourly
    USGS flow. Returns None if the record is too short."""
    end = pd.Timestamp.utcnow().normalize().tz_localize(None)
    start = end - pd.DateOffset(years=years_back)
    flow = fetch_usgs_hourly(gauge_id, start.strftime('%Y-%m-%d'),
                             (end - pd.Timedelta(days=1)).strftime('%Y-%m-%d'))
    daily_peaks = flow.resample('D').max().dropna()
    if len(daily_peaks) < MIN_DAYS:
        return None
    return {
        'warning':      round(float(daily_peaks.quantile(0.95)), 3),
        'danger':       round(float(daily_peaks.quantile(0.99)), 3),
        'extreme':      round(float(daily_peaks.quantile(0.999)), 3),
        'max_observed': round(float(daily_peaks.max()), 3),
        'record_years': int(len(daily_peaks) / 365),
    }


def load() -> dict:
    """Cached thresholds keyed by gauge id, or {} if not yet computed."""
    if THRESH_PATH.exists():
        return json.loads(THRESH_PATH.read_text())
    return {}


def build_all(years_back: int = 3) -> dict:
    """Compute thresholds for every site and write thresholds.json."""
    out = {}
    print(f'Computing flood thresholds from {years_back}y of daily peaks...')
    for site in SITES:
        try:
            th = compute_thresholds(site['id'], years_back=years_back)
        except Exception as e:
            print(f'  ! {site["id"]} {site["short"]:<16} FAILED: {e}')
            continue
        if th:
            out[site['id']] = th
            print(f'  {site["id"]} {site["short"]:<16} '
                  f'warn={th["warning"]:.2f} danger={th["danger"]:.2f} '
                  f'extreme={th["extreme"]:.2f} m³/s ({th["record_years"]}y)')
        else:
            print(f'  {site["id"]} {site["short"]:<16} record too short, skipped')
    THRESH_PATH.write_text(json.dumps(out, indent=2))
    print(f'\nwrote {THRESH_PATH} ({len(out)} gauges)')
    return out


if __name__ == '__main__':
    build_all()