Spaces:
Running on Zero
Running on Zero
Download scripts/graph_building/diagnose_persistence_baseline.py from ageraustine/River_Network: direct link, hf CLI and curl.
- Browser
- Download file 5.48 kB
-
https://huggingface.co/spaces/ageraustine/River_Network/resolve/main/scripts/graph_building/diagnose_persistence_baseline.py
- Command line
-
hf download hf://spaces/ageraustine/River_Network/scripts/graph_building/diagnose_persistence_baseline.py
-
curl -L -o diagnose_persistence_baseline.py https://huggingface.co/spaces/ageraustine/River_Network/resolve/main/scripts/graph_building/diagnose_persistence_baseline.py
5.48 kB
| """ | |
| Diagnostic: adds a naive persistence baseline (tomorrow's anomaly = | |
| today's real anomaly, no model at all) to the climatology comparison, | |
| computed directly from real anomaly data -- no trained model needed. | |
| This exists specifically to distinguish two very different explanations | |
| for the anomaly model losing to climatology at short horizons: | |
| (a) there's genuinely no learnable short-term signal beyond | |
| climatology for that variable, or | |
| (b) the model/training setup has a real problem, and even the | |
| simplest possible non-climatology guess (yesterday's value) | |
| would have beaten climatology if given the chance. | |
| If persistence beats climatology at h=1 but the trained model doesn't, | |
| that's a training/architecture issue, not a genuine predictability | |
| limit -- a very different, much more actionable finding. | |
| Usage: | |
| python -m scripts.diagnose_persistence_baseline --data-root datasets \ | |
| --train-end 2023-12-31 --test-end 2026-12-31 | |
| """ | |
| import argparse | |
| from pathlib import Path | |
| import numpy as np | |
| import pandas as pd | |
| try: | |
| from src.graph.dynamic_features import build_climate_grid_timeseries | |
| from src.graph.climate_climatology import fit_harmonic_climatology, compute_anomalies | |
| except ImportError: | |
| import sys | |
| sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) | |
| from src.graph.dynamic_features import build_climate_grid_timeseries | |
| from src.graph.climate_climatology import fit_harmonic_climatology, compute_anomalies | |
| DEFAULT_HORIZONS = [1, 5, 10, 30, 60, 90, 180] | |
| def evaluate_persistence(anomalies: pd.DataFrame, horizons: list) -> pd.DataFrame: | |
| """ | |
| For every real (date, cell) anomaly value, compares: | |
| - persistence_rmse: predicting that SAME day's anomaly repeated | |
| forward h days (i.e. "nothing has changed since today") | |
| - climatology_only_rmse: predicting zero anomaly (pure climatology) | |
| against the REAL anomaly that actually occurred h days later. | |
| Both computed only where the real target h days ahead exists. | |
| """ | |
| values = anomalies.values | |
| n_dates, n_cells = values.shape | |
| rows = [] | |
| for h in horizons: | |
| if h >= n_dates: | |
| continue | |
| today = values[:n_dates - h, :] | |
| future = values[h:, :] | |
| valid = ~np.isnan(today) & ~np.isnan(future) | |
| if not valid.any(): | |
| continue | |
| persistence_pred = today[valid] # "tomorrow = today" | |
| actual = future[valid] | |
| persistence_rmse = np.sqrt(np.mean((persistence_pred - actual) ** 2)) | |
| climatology_rmse = np.sqrt(np.mean(actual ** 2)) # predicting anomaly = 0 | |
| rows.append({ | |
| "horizon_days": h, "n_pairs": int(valid.sum()), | |
| "persistence_rmse": persistence_rmse, "climatology_only_rmse": climatology_rmse, | |
| "persistence_beats_climatology": persistence_rmse < climatology_rmse, | |
| }) | |
| return pd.DataFrame(rows) | |
| def main() -> None: | |
| parser = argparse.ArgumentParser(description="Persistence-vs-climatology diagnostic") | |
| parser.add_argument("--data-root", type=Path, default=Path("datasets")) | |
| parser.add_argument("--stations-path", type=Path, default=None) | |
| parser.add_argument("--bbox-margin-deg", type=float, default=0.3) | |
| parser.add_argument("--train-start", type=str, default="2013-01-01") | |
| parser.add_argument("--train-end", type=str, default="2023-12-31") | |
| parser.add_argument("--test-end", type=str, default="2026-12-31") | |
| parser.add_argument("--horizons", type=int, nargs="+", default=DEFAULT_HORIZONS) | |
| parser.add_argument("--n-harmonics", type=int, default=2) | |
| parser.add_argument("--min-months-covered", type=int, default=8) | |
| args = parser.parse_args() | |
| stations_path = args.stations_path or (args.data_root / "station_elevations.csv") | |
| nodes_df = pd.read_csv(stations_path) | |
| m = args.bbox_margin_deg | |
| bbox = (nodes_df["longitude"].min() - m, nodes_df["latitude"].min() - m, | |
| nodes_df["longitude"].max() + m, nodes_df["latitude"].max() + m) | |
| print("Loading real ERA5 grid data...") | |
| climate_dict, grid_coords = build_climate_grid_timeseries( | |
| args.data_root / "safran", bbox, (args.train_start, args.test_end) | |
| ) | |
| print(f"Loaded {len(climate_dict)} real variable(s) across {len(grid_coords)} real grid cell(s)\n") | |
| train_start, train_end, test_end = ( | |
| pd.Timestamp(args.train_start), pd.Timestamp(args.train_end), pd.Timestamp(args.test_end) | |
| ) | |
| print("=" * 80) | |
| print(f"{'Variable':<15}{'Horizon':>8} {'Persistence RMSE':>17} {'Climatology RMSE':>17} {'Persistence wins?':>18}") | |
| print("=" * 80) | |
| for var, wide in climate_dict.items(): | |
| train_wide = wide[(wide.index >= train_start) & (wide.index <= train_end)] | |
| test_wide = wide[(wide.index > train_end) & (wide.index <= test_end)] | |
| coeffs = fit_harmonic_climatology(train_wide, n_harmonics=args.n_harmonics, | |
| min_months_covered=args.min_months_covered) | |
| if not coeffs: | |
| continue | |
| test_anomalies = compute_anomalies(test_wide, coeffs, args.n_harmonics) | |
| result = evaluate_persistence(test_anomalies, args.horizons) | |
| for _, row in result.iterrows(): | |
| print(f"{var:<15}{int(row['horizon_days']):>8} {row['persistence_rmse']:>17.4f} " | |
| f"{row['climatology_only_rmse']:>17.4f} {str(row['persistence_beats_climatology']):>18}") | |
| print() | |
| if __name__ == "__main__": | |
| main() |