File size: 4,467 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
"""
Computes the real empirical quantile distribution of discharge for
every real gauged station -- built to answer a real, specific question
before deciding how to re-weight or restructure the model's predicted
quantiles: where do real near-zero/low-flow, normal-flow, and peak
events actually fall for each real station, rather than assuming the
current quantile set [0.5, 0.9, 0.95, 0.99] is well-matched to the
real data's actual shape.

Reuses build_discharge_timeseries directly (the same real, established
loading path train_spatiotemporal_gnn.py itself uses) and the same
real per-basin filtering fix check_dry_valleys.py needed -- 
build_discharge_timeseries returns every real gauged station across
BOTH basins regardless of which nodes_df is passed in, confirmed
directly in that earlier diagnostic.

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

import numpy as np
import pandas as pd


QUANTILE_LEVELS = [0.01, 0.05, 0.10, 0.25, 0.50, 0.75, 0.90, 0.95, 0.99]


def main() -> None:
    parser = argparse.ArgumentParser(description="Check the real empirical discharge quantile distribution per station")
    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.")
    parser.add_argument("--end-date", type=str, default="2026-12-31")
    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)
        # Real per-basin filtering, same fix check_dry_valleys.py needed:
        # build_discharge_timeseries returns every real gauged station
        # across BOTH basins, not filtered by the nodes_df passed in.
        this_basin_codes = set(nodes_df["station_code"]) & set(wide.columns)
        wide = wide[sorted(this_basin_codes)]

        for station_code in wide.columns:
            series = wide[station_code].dropna()
            n_real = len(series)
            if n_real == 0:
                continue
            real_quantiles = series.quantile(QUANTILE_LEVELS)
            row = {"basin": basin, "station_code": station_code, "n_real": n_real}
            for q_level, q_value in zip(QUANTILE_LEVELS, real_quantiles):
                row[f"Q{int(q_level*100):02d}"] = float(q_value)
            rows.append(row)

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

    result_df = pd.DataFrame(rows)
    print("=" * 100)
    print("Real empirical discharge quantile distribution, per real gauged station (m3/s)")
    print("=" * 100)
    print(result_df.to_string(index=False))

    print()
    print("=" * 100)
    print("Real skew/spread ratios -- how far the tails sit from the real median, per station")
    print("=" * 100)
    for _, row in result_df.iterrows():
        q01, q50, q99 = row["Q01"], row["Q50"], row["Q99"]
        low_ratio = q50 / q01 if q01 > 0 else float("inf")
        high_ratio = q99 / q50 if q50 > 0 else float("inf")
        print(f"{row['station_code']} ({row['basin']}): Q50/Q01={low_ratio:>8.1f}x, "
              f"Q99/Q50={high_ratio:>8.1f}x  (real Q01={q01:.2f}, Q50={q50:.2f}, Q99={q99:.2f})")

    print()
    print("=" * 100)
    print("Where does this project's CURRENT model quantile set [0.5, 0.9, 0.95, 0.99] actually land?")
    print("=" * 100)
    print("No real LOW-tail quantile exists in the current model output at all -- Q50 is the lowest "
          "quantile predicted, meaning there is no dedicated, explicit low-flow signal in the model's "
          "own output structure, only whatever Q50 happens to learn implicitly.")


if __name__ == "__main__":
    main()