River_Network / scripts /analysis /check_forecast_lead5_anomaly.py
ageraustine's picture
Upload folder using huggingface_hub (part 2)
f2046b4 verified
Raw History Blame Contribute Delete
5.66 kB
"""
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()