File size: 5,652 Bytes
04d6dda
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
"""
Checks whether any real gauged station in this project's actual data
ever shows true dry-valley (zero or near-zero) discharge -- a direct,
factual data check, built to resolve whether zero-inflation-specific
loss functions (Tweedie, dual-head zero-inflated models, etc.) are
actually relevant here, rather than assuming either way from generic
hydrology advice that wasn't written for this specific river system.

Reuses build_discharge_timeseries directly (the same real, established
loading path train_spatiotemporal_gnn.py itself uses), not a
re-derived reading of the raw hydrometric files -- avoids the exact
mistake the first version of check_routing_lag_distribution.py made by
not reusing the real, existing function.

Usage:
    python -m scripts.analysis.check_dry_valleys --data-root datasets
"""
import argparse
import sys
from pathlib import Path

import numpy as np
import pandas as pd


def main() -> None:
    parser = argparse.ArgumentParser(description="Check for real dry-valley (zero/near-zero discharge) conditions")
    parser.add_argument("--data-root", type=Path, default=Path("datasets"))
    parser.add_argument("--start-date", type=str, default="2000-01-01",
                         help="As wide a real historical window as available -- a dry-valley check "
                              "benefits from more real history, not just the training window.")
    parser.add_argument("--end-date", type=str, default="2026-12-31")
    parser.add_argument("--near-zero-threshold-m3s", type=float, default=0.01,
                         help="A real, small discharge (L/s equivalent: 10 L/s) below which flow is "
                              "treated as 'near-zero' for reporting purposes -- exact zero is checked "
                              "separately and explicitly, this threshold is only for the 'near' case.")
    args = parser.parse_args()

    try:
        from src.graph.dynamic_features import build_discharge_timeseries
    except ImportError:
        sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent))
        from src.graph.dynamic_features import build_discharge_timeseries

    date_range = (args.start_date, args.end_date)
    rows = []

    for basin in ["eure", "risle"]:
        nodes_path = args.data_root / "reach_graph" / f"{basin}_nodes_enriched.csv"
        if not nodes_path.exists():
            print(f"{basin}: no real nodes file at {nodes_path}, skipping.")
            continue
        nodes_df = pd.read_csv(nodes_path)

        wide = build_discharge_timeseries(nodes_df, args.data_root / "hydrometric", date_range)
        # build_discharge_timeseries returns EVERY real gauged station
        # across BOTH basins (confirmed directly -- it never filters by
        # the nodes_df passed in; that filtering happens later, inside
        # assemble_dynamic_tensor, which this script doesn't call) --
        # restricting to this basin's own real station_codes here is
        # required, or every station gets reported twice, once per
        # basin, with identical values. A real, caught mistake in an
        # earlier version of this exact script.
        this_basin_codes = set(nodes_df["station_code"]) & set(wide.columns)
        wide = wide[sorted(this_basin_codes)]
        print(f"{basin}: real discharge data for {wide.shape[1]} real gauged station(s), "
              f"{wide.shape[0]} real date(s) in range")

        for station_code in wide.columns:
            series = wide[station_code].dropna()
            n_real = len(series)
            if n_real == 0:
                rows.append({"basin": basin, "station_code": station_code, "n_real": 0,
                             "min_m3s": np.nan, "pct_exact_zero": np.nan,
                             "pct_near_zero": np.nan, "p01_m3s": np.nan, "p05_m3s": np.nan})
                continue
            n_zero = int((series == 0).sum())
            n_near_zero = int((series <= args.near_zero_threshold_m3s).sum())
            rows.append({
                "basin": basin, "station_code": station_code, "n_real": n_real,
                "min_m3s": float(series.min()),
                "pct_exact_zero": 100.0 * n_zero / n_real,
                "pct_near_zero": 100.0 * n_near_zero / n_real,
                "p01_m3s": float(series.quantile(0.01)),
                "p05_m3s": float(series.quantile(0.05)),
            })

    if not rows:
        print("No real stations found in either basin -- nothing to report.")
        return

    result_df = pd.DataFrame(rows).sort_values("min_m3s")
    print()
    print("=" * 70)
    print(f"Real per-station discharge floor (near-zero threshold: {args.near_zero_threshold_m3s} m3/s)")
    print("=" * 70)
    print(result_df.to_string(index=False))

    n_ever_exact_zero = int((result_df["pct_exact_zero"] > 0).sum())
    n_ever_near_zero = int((result_df["pct_near_zero"] > 0).sum())
    print()
    if n_ever_exact_zero == 0:
        print("No real station in this dataset ever recorded an exact-zero discharge day -- "
              "zero-inflation-specific losses (Tweedie, dual-head zero-inflated models) do not "
              "appear to match this project's actual real data, based on real evidence.")
    else:
        print(f"{n_ever_exact_zero} real station(s) recorded at least one exact-zero discharge day -- "
              f"real, direct evidence a zero-inflation-aware approach could be worth investigating "
              f"further for those specific stations.")
    print(f"{n_ever_near_zero} real station(s) recorded at least one real day at or below "
          f"{args.near_zero_threshold_m3s} m3/s.")


if __name__ == "__main__":
    main()