File size: 5,655 Bytes
f2046b4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
116
117
118
119
120
121
"""
Direct check of the raw previous_runs_forecast.csv archive, independent
of the model/evaluation pipeline -- built specifically because
evaluate_flood_detection.py's --dump-fp-details showed something
systematic and graph-wide at the 5-day horizon (185/185 real test
examples produced at least one real false positive there, often
several stations simultaneously, including stations on different
rivers with no surface connection). --check-forecast-fp-correlation
already ruled out forecast AVAILABILITY as the driver; this checks
whether the real forecast VALUES themselves are anomalous specifically
at lead time 5, compared to the other real lead times in the same file.

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

import numpy as np
import pandas as pd

FORECAST_LEAD_TIMES = [1, 2, 3, 5, 7]  # matches dynamic_features.py's real, confirmed coverage


def summarize_lead_time_column(df: pd.DataFrame, lead_time: int) -> dict:
    """
    Real summary statistics for one real precipitation_previous_dayN
    column -- real coverage, real central tendency, real spread, and
    explicit counts of two real, concrete anomaly types (negative
    precipitation, which is physically impossible, and implausibly
    extreme values) rather than just eyeballing a mean/max pair.
    """
    col = f"precipitation_previous_day{lead_time}"
    if col not in df.columns:
        return {"lead_time": lead_time, "column_present": False}

    values = df[col].dropna()
    n_negative = int((values < 0).sum())
    # A real, extreme daily precipitation threshold -- the wettest days
    # ever recorded anywhere are on the order of a few hundred mm; used
    # here only to flag values worth a second look, not as a hard
    # physical cutoff.
    n_extreme = int((values > 300).sum())

    return {
        "lead_time": lead_time,
        "column_present": True,
        "n_real": len(values),
        "mean": float(values.mean()) if len(values) else float("nan"),
        "median": float(values.median()) if len(values) else float("nan"),
        "std": float(values.std()) if len(values) else float("nan"),
        "min": float(values.min()) if len(values) else float("nan"),
        "max": float(values.max()) if len(values) else float("nan"),
        "n_negative": n_negative,
        "n_extreme_gt_300mm": n_extreme,
    }


def main() -> None:
    parser = argparse.ArgumentParser(description="Check the real ECMWF forecast archive for a lead-time-5-specific anomaly")
    parser.add_argument("--data-root", type=Path, default=Path("datasets"))
    parser.add_argument("--forecast-path", type=Path, default=None,
                         help="Defaults to <data-root>/previous_runs_forecast.csv")
    args = parser.parse_args()

    forecast_path = args.forecast_path or (args.data_root / "previous_runs_forecast.csv")
    if not forecast_path.exists():
        print(f"No real forecast archive found at {forecast_path}.")
        return

    df = pd.read_csv(forecast_path)
    print(f"Loaded {len(df)} real row(s) from {forecast_path}")
    print()

    rows = []
    for lead_time in FORECAST_LEAD_TIMES:
        summary = summarize_lead_time_column(df, lead_time)
        rows.append(summary)

    result_df = pd.DataFrame(rows)
    print("=" * 70)
    print("Real forecast precipitation, by lead time (mm)")
    print("=" * 70)
    print(result_df.to_string(index=False))

    present = result_df[result_df["column_present"]]
    if len(present) < 2:
        print("\nFewer than 2 real lead-time columns present -- nothing to compare.")
        return

    if 5 in present["lead_time"].values:
        lead5 = present[present["lead_time"] == 5].iloc[0]
        others = present[present["lead_time"] != 5]
        others_mean_of_means = others["mean"].mean()
        others_mean_of_maxes = others["max"].mean()
        print(f"\nLead time 5 mean ({lead5['mean']:.2f}mm) vs. real average of other lead times' means "
              f"({others_mean_of_means:.2f}mm)")
        print(f"Lead time 5 max ({lead5['max']:.2f}mm) vs. real average of other lead times' maxes "
              f"({others_mean_of_maxes:.2f}mm)")
        print(f"Lead time 5 negative values: {int(lead5['n_negative'])}; extreme (>300mm) values: "
              f"{int(lead5['n_extreme_gt_300mm'])}")

        mean_ratio = lead5["mean"] / others_mean_of_means if others_mean_of_means > 0 else float("inf")
        max_ratio = lead5["max"] / others_mean_of_maxes if others_mean_of_maxes > 0 else float("inf")
        if mean_ratio > 2.0 or max_ratio > 2.0 or lead5["n_negative"] > 0:
            print(f"\nLead time 5 shows a real, meaningful anomaly relative to the other real lead times "
                  f"(mean ratio {mean_ratio:.2f}x, max ratio {max_ratio:.2f}x) -- real, direct evidence "
                  f"worth investigating further as the actual cause of the day-5 false-positive pattern.")
        else:
            print(f"\nLead time 5 does NOT show a meaningful anomaly relative to the other real lead "
                  f"times (mean ratio {mean_ratio:.2f}x, max ratio {max_ratio:.2f}x) -- the raw forecast "
                  f"values themselves aren't the explanation; the real cause likely lies in how this "
                  f"lead time's data is being consumed downstream (e.g. the anchor/target-date mapping, "
                  f"or the model's own horizon-conditioning for h=5 specifically), not the raw archive.")
    else:
        print("\nLead time 5 column not present in this real file -- can't run the real comparison.")


if __name__ == "__main__":
    main()