Skip to content

Commit ebe5377

Browse files
updated metrics
1 parent 2ddc139 commit ebe5377

10 files changed

Lines changed: 291 additions & 226 deletions

cybench/evaluation/aggregated_metrics.py

Lines changed: 94 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,15 @@ def calc_median_yearly_r2(
4848
target_col: str,
4949
model_col: str,
5050
*,
51+
loc_col: str = KEY_LOC,
5152
year_col: str = KEY_YEAR,
53+
min_regions: int = 3,
5254
) -> float:
5355
"""Median of per-year R², where each year's R² is computed across regions."""
5456
yearly_r2: list[float] = []
5557
for _, year_df in df.groupby(year_col):
58+
if year_df[loc_col].nunique() < min_regions:
59+
continue
5660
_, r2 = calc_r_r2(
5761
year_df[target_col].values,
5862
year_df[model_col].values,
@@ -63,6 +67,55 @@ def calc_median_yearly_r2(
6367
return float(np.nanmedian(yearly_r2))
6468

6569

70+
def calc_median_regional_r2(
71+
df: pd.DataFrame,
72+
target_col: str,
73+
model_col: str,
74+
*,
75+
loc_col: str = KEY_LOC,
76+
year_col: str = KEY_YEAR,
77+
min_years: int = 3,
78+
) -> float:
79+
"""Median of per-region R², where each region's R² is computed across years."""
80+
del year_col # API symmetry with yearly helper; years come from row groups.
81+
regional_r2: list[float] = []
82+
for _, loc_df in df.groupby(loc_col):
83+
if len(loc_df) < min_years:
84+
continue
85+
_, r2 = calc_r_r2(
86+
loc_df[target_col].values,
87+
loc_df[model_col].values,
88+
)
89+
regional_r2.append(r2)
90+
if not regional_r2:
91+
return float("nan")
92+
return float(np.nanmedian(regional_r2))
93+
94+
95+
def calc_median_regional_r2_res(
96+
df: pd.DataFrame,
97+
target_col: str,
98+
model_col: str,
99+
*,
100+
loc_col: str = KEY_LOC,
101+
min_years: int = 3,
102+
) -> float:
103+
"""Median of per-region R² on location-de-meaned yields (anomaly view)."""
104+
loc_means = df.groupby(loc_col)[target_col].mean()
105+
work = df.copy()
106+
work["_true_res"] = work[target_col] - work[loc_col].map(loc_means)
107+
work["_pred_res"] = work[model_col] - work[loc_col].map(loc_means)
108+
regional_r2: list[float] = []
109+
for _, loc_df in work.groupby(loc_col):
110+
if len(loc_df) < min_years:
111+
continue
112+
_, r2 = calc_r_r2(loc_df["_true_res"].values, loc_df["_pred_res"].values)
113+
regional_r2.append(r2)
114+
if not regional_r2:
115+
return float("nan")
116+
return float(np.nanmedian(regional_r2))
117+
118+
66119
def get_metrics_dict(
67120
df: pd.DataFrame,
68121
target_col: str,
@@ -102,36 +155,53 @@ def compute_report_metrics(
102155
Metrics used by visualize_results_aggregated / build_results_dashboard.
103156
104157
Views:
105-
- region_year: pooled region-year rows (r, R², NRMSE, med per-year R², anomaly r/R²)
106-
- spatial: per-location means across years
107-
- temporal: per-year means across locations (r, R²)
158+
- region_year: pooled region-year rows (r, R², NRMSE, pooled anomaly r/R²)
159+
- spatial: typical-year cross-region R² (median over years); climatology map r/R²
160+
- temporal: typical-region cross-year R² (median over regions); aggregate series r/R²
161+
- anomaly: typical-region R² on residuals; pooled anomaly r/R²
108162
"""
109163
complete = df[target_col].notna() & df[model_col].notna()
110164
df = df.loc[complete].copy()
111165
region_year = get_metrics_dict(df, target_col, model_col, loc_col=loc_col)
112-
region_year["median_r2"] = calc_median_yearly_r2(
113-
df, target_col, model_col, year_col=year_col
114-
)
115166

116-
spatial = df.groupby(loc_col)[[target_col, model_col]].mean()
117-
r_spatial, r2_spatial = calc_r_r2(
118-
spatial[target_col].values,
119-
spatial[model_col].values,
167+
spatial_clim = df.groupby(loc_col)[[target_col, model_col]].mean()
168+
r_spatial_clim, r2_spatial_clim = calc_r_r2(
169+
spatial_clim[target_col].values,
170+
spatial_clim[model_col].values,
120171
)
121172

122-
temporal = df.groupby(year_col)[[target_col, model_col]].mean().sort_index()
123-
r_time, r2_time = calc_r_r2(
124-
temporal[target_col].values,
125-
temporal[model_col].values,
173+
temporal_agg = df.groupby(year_col)[[target_col, model_col]].mean().sort_index()
174+
r_temporal_agg, r2_temporal_agg = calc_r_r2(
175+
temporal_agg[target_col].values,
176+
temporal_agg[model_col].values,
126177
)
127178

128179
return {
129180
"n_regions": int(df[loc_col].nunique()),
130181
"n_years": int(df[year_col].nunique()),
131182
"n_samples": int(len(df)),
132183
"region_year": region_year,
133-
"spatial": {"r": r_spatial, "r2": r2_spatial},
134-
"temporal": {"r": r_time, "r2": r2_time},
184+
"spatial": {
185+
"r2_typical_year": calc_median_yearly_r2(
186+
df, target_col, model_col, loc_col=loc_col, year_col=year_col
187+
),
188+
"r_climatology": r_spatial_clim,
189+
"r2_climatology": r2_spatial_clim,
190+
},
191+
"temporal": {
192+
"r2_typical_region": calc_median_regional_r2(
193+
df, target_col, model_col, loc_col=loc_col, year_col=year_col
194+
),
195+
"r_aggregate": r_temporal_agg,
196+
"r2_aggregate": r2_temporal_agg,
197+
},
198+
"anomaly": {
199+
"r2_typical_region": calc_median_regional_r2_res(
200+
df, target_col, model_col, loc_col=loc_col
201+
),
202+
"r_pooled": region_year["r_res"],
203+
"r2_pooled": region_year["r2_res"],
204+
},
135205
}
136206

137207

@@ -140,10 +210,13 @@ def format_report_metrics(metrics: dict[str, Any]) -> str:
140210
ry = metrics["region_year"]
141211
sp = metrics["spatial"]
142212
tm = metrics["temporal"]
213+
an = metrics["anomaly"]
143214
return (
144-
f"region-year r={ry['r']:.2f} R²={ry['r2']:.2f} NRMSE={ry['nrmse']:.2f} "
145-
f"med-R²/yr={ry['median_r2']:.2f} | "
146-
f"spatial r={sp['r']:.2f} R²={sp['r2']:.2f} | "
147-
f"temporal r={tm['r']:.2f} R²={tm['r2']:.2f} | "
148-
f"anomaly r={ry['r_res']:.2f} R²={ry['r2_res']:.2f}"
215+
f"region-year r={ry['r']:.2f} R²={ry['r2']:.2f} NRMSE={ry['nrmse']:.2f} | "
216+
f"spatial R²(med/yr)={sp['r2_typical_year']:.2f} "
217+
f"clim R²={sp['r2_climatology']:.2f} | "
218+
f"temporal R²(med/reg)={tm['r2_typical_region']:.2f} "
219+
f"agg R²={tm['r2_aggregate']:.2f} | "
220+
f"anomaly R²(med/reg)={an['r2_typical_region']:.2f} "
221+
f"pooled R²={an['r2_pooled']:.2f}"
149222
)

cybench/runs/analysis/benchmark_run_catalog.py

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,13 @@
2020
"r",
2121
"r2",
2222
"nrmse",
23-
"r2_yearly_median",
24-
"r_spatial",
2523
"r2_spatial",
26-
"r_temporal",
24+
"r_spatial_clim",
25+
"r2_spatial_clim",
2726
"r2_temporal",
27+
"r_temporal_agg",
28+
"r2_temporal_agg",
29+
"r2_anomaly",
2830
"r_res",
2931
"r2_res",
3032
)
@@ -34,11 +36,13 @@
3436
{
3537
"r",
3638
"r2",
37-
"r_spatial",
3839
"r2_spatial",
39-
"r_temporal",
40+
"r_spatial_clim",
41+
"r2_spatial_clim",
4042
"r2_temporal",
41-
"r2_yearly_median",
43+
"r_temporal_agg",
44+
"r2_temporal_agg",
45+
"r2_anomaly",
4246
"r_res",
4347
"r2_res",
4448
}
@@ -50,20 +54,30 @@ def flatten_report_metrics(metrics: dict[str, Any]) -> dict[str, Any]:
5054
ry = metrics.get("region_year", {})
5155
sp = metrics.get("spatial", {})
5256
tm = metrics.get("temporal", {})
57+
an = metrics.get("anomaly", {})
58+
r2_spatial = sp.get("r2_typical_year")
59+
if r2_spatial is None:
60+
r2_spatial = ry.get("median_r2")
61+
r2_temporal = tm.get("r2_typical_region")
62+
r2_anomaly = an.get("r2_typical_region")
63+
if r2_anomaly is None:
64+
r2_anomaly = ry.get("r2_res")
5365
return {
5466
"n_regions": metrics.get("n_regions"),
5567
"n_years": metrics.get("n_years"),
5668
"n_samples": metrics.get("n_samples"),
5769
"r": ry.get("r"),
5870
"r2": ry.get("r2"),
5971
"nrmse": ry.get("nrmse"),
60-
"r2_yearly_median": ry.get("median_r2"),
6172
"r_res": ry.get("r_res"),
6273
"r2_res": ry.get("r2_res"),
63-
"r_spatial": sp.get("r"),
64-
"r2_spatial": sp.get("r2"),
65-
"r_temporal": tm.get("r"),
66-
"r2_temporal": tm.get("r2"),
74+
"r2_spatial": r2_spatial,
75+
"r_spatial_clim": sp.get("r_climatology", sp.get("r")),
76+
"r2_spatial_clim": sp.get("r2_climatology", sp.get("r2")),
77+
"r2_temporal": r2_temporal,
78+
"r_temporal_agg": tm.get("r_aggregate", tm.get("r")),
79+
"r2_temporal_agg": tm.get("r2_aggregate", tm.get("r2")),
80+
"r2_anomaly": r2_anomaly,
6781
}
6882

6983

@@ -190,6 +204,7 @@ def _metrics_from_report_yaml(path: Path) -> dict[str, Any]:
190204
"region_year": raw.get("region_year") or {},
191205
"spatial": raw.get("spatial") or {},
192206
"temporal": raw.get("temporal") or {},
207+
"anomaly": raw.get("anomaly") or {},
193208
}
194209
)
195210

cybench/runs/analysis/collect_walk_forward_results.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -344,13 +344,9 @@ def summary_rows_to_dashboard_records(
344344
("region_year", "r", "r"),
345345
("region_year", "r2", "r2"),
346346
("region_year", "nrmse", "nrmse"),
347-
("region_year", "median_r2", "r2_yearly_median"),
348-
("spatial", "r", "r_spatial"),
349347
("spatial", "r2", "r2_spatial"),
350-
("temporal", "r", "r_temporal"),
351348
("temporal", "r2", "r2_temporal"),
352-
("anomaly", "r", "r_res"),
353-
("anomaly", "r2", "r2_res"),
349+
("anomaly", "r2", "r2_anomaly"),
354350
]
355351
records: list[dict[str, Any]] = []
356352
for row in summary_rows:

cybench/runs/analysis/model_family_radar_lib.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
},
3030
{
3131
"label": "Anomaly",
32-
"metric": "r2_res",
32+
"metric": "r2_anomaly",
3333
"question": "Can it predict deviations from a region's expected yield?",
3434
},
3535
)

0 commit comments

Comments
 (0)