File size: 6,211 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
122
123
124
125
126
127
128
"""
Directly traces a handful of real false positives (from evaluate_flood_
detection.py's --dump-fp-details output) through build_forecast_tensor's
actual real lookup logic, by hand, to check for a real date-convention
mismatch -- built after noticing something concrete while investigating
the day-5 false-positive pattern:

Every other real place in this project that maps a horizon h to a real
calendar date uses target_date = anchor + (h - 1) -- confirmed directly
in both prepare_graph_training_windows (the real training-target
construction) and evaluate_flood_detection.py's own confusion-matrix
loop. But build_forecast_tensor uses target_date = anchor + h (no "-1").

This means: for horizon h, the real target this project is actually
predicting is at anchor + (h-1), but the forecast value FED INTO the
model for that same horizon slot is looked up at anchor + h -- one
real calendar day later than the thing actually being predicted. This
script shows exactly what real forecast row/column gets retrieved for
real (anchor, h=5) pairs pulled from an actual false-positive dump, so
the real size and direction of this mismatch is visible directly,
not just asserted.

Usage:
    python -m scripts.analysis.trace_forecast_lookup --data-root datasets \
        --fp-details datasets/fp_details_horizon_5.csv --horizon 5
"""
import argparse
from pathlib import Path

import pandas as pd


def trace_one(anchor: pd.Timestamp, station_code: str, horizon: int, df_indexed: pd.DataFrame) -> dict:
    """
    Mirrors build_forecast_tensor's real lookup exactly -- same
    target_date/column formula, same real row lookup -- so this shows
    precisely what that function actually retrieves for this real
    (anchor, horizon) pair, not an approximation of it.
    """
    real_target_date = anchor + pd.Timedelta(days=horizon - 1)  # what's ACTUALLY being predicted
    forecast_lookup_date = anchor + pd.Timedelta(days=horizon)  # what build_forecast_tensor ACTUALLY queries
    col = f"precipitation_previous_day{horizon}"

    result = {
        "anchor": anchor.date().isoformat(),
        "real_target_date": real_target_date.date().isoformat(),
        "forecast_lookup_date": forecast_lookup_date.date().isoformat(),
        "date_mismatch_days": (forecast_lookup_date - real_target_date).days,
        "station_code": station_code,
        "column": col,
    }
    try:
        value = df_indexed.loc[(forecast_lookup_date, station_code), col]
        result["forecast_value_actually_used"] = float(value) if pd.notna(value) else None
    except KeyError:
        result["forecast_value_actually_used"] = None

    # What the forecast value WOULD have been if looked up at the real
    # target date instead, for direct comparison -- not necessarily a
    # "correct" value (that date's own previous_day{h} column reflects
    # a different, (h) days in advance forecast, not h-1), just useful
    # to see whether it's meaningfully different from what's actually used.
    try:
        value_at_real_target = df_indexed.loc[(real_target_date, station_code), col]
        result["value_at_real_target_date_same_column"] = float(value_at_real_target) if pd.notna(value_at_real_target) else None
    except KeyError:
        result["value_at_real_target_date_same_column"] = None

    return result


def main() -> None:
    parser = argparse.ArgumentParser(description="Trace real forecast lookups for a handful of real false positives")
    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")
    parser.add_argument("--fp-details", type=Path, required=True,
                         help="Path to a real fp_details_horizon_N.csv from evaluate_flood_detection.py's --dump-fp-details")
    parser.add_argument("--horizon", type=int, required=True)
    parser.add_argument("--n-samples", type=int, default=10)
    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
    if not args.fp_details.exists():
        print(f"No real false-positive detail file found at {args.fp_details}.")
        return

    forecast_df = pd.read_csv(forecast_path)
    forecast_df["date"] = pd.to_datetime(forecast_df["date"])
    df_indexed = forecast_df.set_index(["date", "station_code"])

    fp_df = pd.read_csv(args.fp_details)
    fp_df["target_date"] = pd.to_datetime(fp_df["target_date"])
    # fp_details' own target_date IS the real target date (h-1
    # convention, matching evaluate_flood_detection.py's own confusion-
    # matrix loop) -- recover the real anchor from it directly.
    fp_df["anchor"] = fp_df["target_date"] - pd.Timedelta(days=args.horizon - 1)

    sample = fp_df.head(args.n_samples)
    print(f"Tracing {len(sample)} real false positive(s) at horizon={args.horizon}d through the real "
          f"forecast lookup logic\n")

    rows = []
    for _, row in sample.iterrows():
        traced = trace_one(row["anchor"], row["station_code"], args.horizon, df_indexed)
        rows.append(traced)

    result_df = pd.DataFrame(rows)
    print(result_df.to_string(index=False))

    n_mismatched_dates = int((result_df["date_mismatch_days"] != 0).sum())
    print(f"\n{n_mismatched_dates}/{len(result_df)} real traced examples show a real date mismatch "
          f"between what's being predicted (real_target_date) and what the forecast is actually "
          f"looked up for (forecast_lookup_date).")
    if n_mismatched_dates > 0:
        print(f"This is a real, systematic {result_df['date_mismatch_days'].iloc[0]}-day offset (from "
              f"build_forecast_tensor's target_date = anchor + h, vs. every other real place in this "
              f"project using anchor + h - 1) -- the model is being conditioned on the forecast for the "
              f"day AFTER what it's actually predicting at this horizon, not the forecast for the real "
              f"target date itself.")


if __name__ == "__main__":
    main()