Skip to content

Commit 3ad9273

Browse files
add median yearly_r2
1 parent f3e3132 commit 3ad9273

8 files changed

Lines changed: 89 additions & 14 deletions

cybench/evaluation/aggregated_metrics.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,26 @@ def calc_nrmse(y_true: npt.ArrayLike, y_pred: npt.ArrayLike) -> float:
4040
return float("nan") if denom == 0 else float(rmse / denom)
4141

4242

43+
def calc_median_yearly_r2(
44+
df: pd.DataFrame,
45+
target_col: str,
46+
model_col: str,
47+
*,
48+
year_col: str = KEY_YEAR,
49+
) -> float:
50+
"""Median of per-year R², where each year's R² is computed across regions."""
51+
yearly_r2: list[float] = []
52+
for _, year_df in df.groupby(year_col):
53+
_, r2 = calc_r_r2(
54+
year_df[target_col].values,
55+
year_df[model_col].values,
56+
)
57+
yearly_r2.append(r2)
58+
if not yearly_r2:
59+
return float("nan")
60+
return float(np.nanmedian(yearly_r2))
61+
62+
4363
def get_metrics_dict(
4464
df: pd.DataFrame,
4565
target_col: str,
@@ -79,13 +99,16 @@ def compute_report_metrics(
7999
Metrics used by visualize_results_aggregated / build_results_dashboard.
80100
81101
Views:
82-
- region_year: pooled region-year rows (r, R², NRMSE, anomaly r/R²)
102+
- region_year: pooled region-year rows (r, R², NRMSE, med per-year R², anomaly r/R²)
83103
- spatial: per-location means across years
84-
- temporal: per-year means across locations
104+
- temporal: per-year means across locations (r, R²)
85105
"""
86106
complete = df[target_col].notna() & df[model_col].notna()
87107
df = df.loc[complete].copy()
88108
region_year = get_metrics_dict(df, target_col, model_col, loc_col=loc_col)
109+
region_year["median_r2"] = calc_median_yearly_r2(
110+
df, target_col, model_col, year_col=year_col
111+
)
89112

90113
spatial = df.groupby(loc_col)[[target_col, model_col]].mean()
91114
r_spatial, r2_spatial = calc_r_r2(
@@ -115,7 +138,8 @@ def format_report_metrics(metrics: dict[str, Any]) -> str:
115138
sp = metrics["spatial"]
116139
tm = metrics["temporal"]
117140
return (
118-
f"region-year r={ry['r']:.2f} R²={ry['r2']:.2f} NRMSE={ry['nrmse']:.2f} | "
141+
f"region-year r={ry['r']:.2f} R²={ry['r2']:.2f} NRMSE={ry['nrmse']:.2f} "
142+
f"med-R²/yr={ry['median_r2']:.2f} | "
119143
f"spatial r={sp['r']:.2f} R²={sp['r2']:.2f} | "
120144
f"temporal r={tm['r']:.2f} R²={tm['r2']:.2f} | "
121145
f"anomaly r={ry['r_res']:.2f} R²={ry['r2_res']:.2f}"

cybench/runs/analysis/benchmark_run_catalog.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"r",
2121
"r2",
2222
"nrmse",
23+
"r2_yearly_median",
2324
"r_spatial",
2425
"r2_spatial",
2526
"r_temporal",
@@ -29,7 +30,19 @@
2930
)
3031

3132
# For deltas: higher is better vs lower is better.
32-
HIGHER_IS_BETTER = frozenset({"r", "r2", "r_spatial", "r2_spatial", "r_temporal", "r2_temporal", "r_res", "r2_res"})
33+
HIGHER_IS_BETTER = frozenset(
34+
{
35+
"r",
36+
"r2",
37+
"r_spatial",
38+
"r2_spatial",
39+
"r_temporal",
40+
"r2_temporal",
41+
"r2_yearly_median",
42+
"r_res",
43+
"r2_res",
44+
}
45+
)
3346
LOWER_IS_BETTER = frozenset({"nrmse"})
3447

3548

@@ -44,6 +57,7 @@ def flatten_report_metrics(metrics: dict[str, Any]) -> dict[str, Any]:
4457
"r": ry.get("r"),
4558
"r2": ry.get("r2"),
4659
"nrmse": ry.get("nrmse"),
60+
"r2_yearly_median": ry.get("median_r2"),
4761
"r_res": ry.get("r_res"),
4862
"r2_res": ry.get("r2_res"),
4963
"r_spatial": sp.get("r"),

cybench/runs/analysis/collect_walk_forward_results.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ def summary_rows_to_dashboard_records(
162162
("region_year", "r", "r"),
163163
("region_year", "r2", "r2"),
164164
("region_year", "nrmse", "nrmse"),
165+
("region_year", "median_r2", "r2_yearly_median"),
165166
("spatial", "r", "r_spatial"),
166167
("spatial", "r2", "r2_spatial"),
167168
("temporal", "r", "r_temporal"),

cybench/runs/viz/build_results_dashboard.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
import numpy as np
1414
import pandas as pd
1515

16+
from cybench.evaluation.aggregated_metrics import calc_median_yearly_r2
17+
1618

1719
@dataclass
1820
class SourceConfig:
@@ -152,6 +154,7 @@ def load_records_from_runs_root(
152154
("region_year", "r", safe_float(m.get("r"))),
153155
("region_year", "r2", safe_float(m.get("r2"))),
154156
("region_year", "nrmse", safe_float(m.get("nrmse"))),
157+
("region_year", "median_r2", safe_float(s.get("r2_yearly_median"))),
155158
("spatial", "r", safe_float(sp.get("r"))),
156159
("spatial", "r2", safe_float(sp.get("r2"))),
157160
("temporal", "r", safe_float(s.get("r_time_model"))),
@@ -198,6 +201,7 @@ def load_records_from_runs_root(
198201

199202
temporal = clean.groupby("year")[["yield", "pred"]].mean().sort_index()
200203
r_tm, r2_tm = calc_r_r2(temporal["yield"].values, temporal["pred"].values)
204+
r2_yearly_median = calc_median_yearly_r2(clean, "yield", "pred", year_col="year")
201205

202206
loc_means = clean.groupby("adm_id")["yield"].mean()
203207
y_res = clean["yield"] - clean["adm_id"].map(loc_means)
@@ -223,6 +227,7 @@ def load_records_from_runs_root(
223227
("region_year", "r", safe_float(r)),
224228
("region_year", "r2", safe_float(r2)),
225229
("region_year", "nrmse", safe_float(nrmse)),
230+
("region_year", "median_r2", safe_float(r2_yearly_median)),
226231
("spatial", "r", safe_float(r_sp)),
227232
("spatial", "r2", safe_float(r2_sp)),
228233
("temporal", "r", safe_float(r_tm)),
@@ -339,6 +344,7 @@ def load_records(sources: List[SourceConfig], output_dir: str) -> List[Dict]:
339344
("region_year", "r", safe_float(m.get("r"))),
340345
("region_year", "r2", safe_float(m.get("r2"))),
341346
("region_year", "nrmse", safe_float(m.get("nrmse"))),
347+
("region_year", "median_r2", safe_float(s.get("r2_yearly_median"))),
342348
("spatial", "r", safe_float(sp.get("r"))),
343349
("spatial", "r2", safe_float(sp.get("r2"))),
344350
("temporal", "r", safe_float(s.get("r_time_model"))),

cybench/runs/viz/dashboard_template.html

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,7 @@ <h3>Drill-down plots</h3>
250250
{ key: "region_year_r", label: "r", group: "Region-Year", higherBetter: true },
251251
{ key: "region_year_r2", label: "R²", group: "Region-Year", higherBetter: true },
252252
{ key: "region_year_nrmse", label: "NRMSE", group: "Region-Year", higherBetter: false },
253+
{ key: "region_year_median_r2", label: "med R²/yr", group: "Region-Year", higherBetter: true },
253254
{ key: "spatial_r", label: "r", group: "Spatial", higherBetter: true },
254255
{ key: "spatial_r2", label: "R²", group: "Spatial", higherBetter: true },
255256
{ key: "temporal_r", label: "r", group: "Temporal", higherBetter: true },
@@ -263,6 +264,7 @@ <h3>Drill-down plots</h3>
263264
region_year_r: { min: 0, max: 1, higherBetter: true },
264265
region_year_r2: { min: 0, max: 1, higherBetter: true },
265266
region_year_nrmse: { min: 0, max: 0.5, higherBetter: false },
267+
region_year_median_r2: { min: 0, max: 1, higherBetter: true },
266268
spatial_r: { min: 0, max: 1, higherBetter: true },
267269
spatial_r2: { min: 0, max: 1, higherBetter: true },
268270
temporal_r: { min: 0, max: 1, higherBetter: true },
@@ -425,7 +427,7 @@ <h3>Drill-down plots</h3>
425427
<th rowspan="2" class="sortable" data-sort="n_regions">Regions ${sortMark("n_regions")}</th>
426428
<th rowspan="2" class="sortable" data-sort="n_years">Years ${sortMark("n_years")}</th>
427429
<th rowspan="2" class="sortable" data-sort="n_samples">Samples ${sortMark("n_samples")}</th>
428-
<th colspan="3" class="group-head">Region-Year</th>
430+
<th colspan="4" class="group-head">Region-Year</th>
429431
<th colspan="2" class="group-head">Spatial</th>
430432
<th colspan="2" class="group-head">Temporal</th>
431433
<th colspan="2" class="group-head">Anomaly</th>

cybench/runs/viz/visualize_results_aggregated.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,11 @@
2020

2121
from cybench.util.geo import get_shapes_from_polygons, world_shape_path
2222
from cybench.config import KEY_LOC, KEY_TARGET
23-
from cybench.evaluation.aggregated_metrics import calc_r_r2, get_metrics_dict
23+
from cybench.evaluation.aggregated_metrics import (
24+
calc_median_yearly_r2,
25+
calc_r_r2,
26+
get_metrics_dict,
27+
)
2428

2529
# -----------------------------
2630
# Configuration
@@ -208,21 +212,21 @@ def generate_markdown_table(stats_list: List[dict]) -> str:
208212

209213
md = (
210214
"| Dataset | N_regions | N_years | "
211-
"Region-Year | Region-Year | Region-Year | "
215+
"Region-Year | Region-Year | Region-Year | Region-Year | "
212216
"Spatial | Spatial | "
213217
"Temporal | Temporal | "
214218
"Anomaly | Anomaly |\n"
215219
)
216220
md += (
217221
"| | | | "
218-
"r | R² | NRMSE | "
222+
"r | R² | NRMSE | med R²/yr | "
219223
"r | R² | "
220224
"r | R² | "
221225
"r | R² |\n"
222226
)
223227
md += (
224228
"| :--- | ---: | ---: | "
225-
"---: | ---: | ---: | "
229+
"---: | ---: | ---: | ---: | "
226230
"---: | ---: | "
227231
"---: | ---: | "
228232
"---: | ---: |\n"
@@ -255,6 +259,7 @@ def style(text):
255259
f"| {style(fmt(mod['r']))} "
256260
f"| {style(fmt(mod['r2']))} "
257261
f"| {style(fmt(mod['nrmse']))} "
262+
f"| {style(fmt(s['r2_yearly_median']))} "
258263
f"| {style(fmt(sp['r']))} "
259264
f"| {style(fmt(sp['r2']))} "
260265
f"| {style(fmt(s['r_time_model']))} "
@@ -293,13 +298,13 @@ def fmt(val):
293298
<th class="left" rowspan="2">Dataset</th>
294299
<th rowspan="2">N_regions</th>
295300
<th rowspan="2">N_years</th>
296-
<th colspan="3">Region-Year</th>
301+
<th colspan="4">Region-Year</th>
297302
<th colspan="2">Spatial</th>
298303
<th colspan="2">Temporal</th>
299304
<th colspan="2">Anomaly</th>
300305
</tr>
301306
<tr>
302-
<th>r</th><th>R²</th><th>NRMSE</th>
307+
<th>r</th><th>R²</th><th>NRMSE</th><th>med R²/yr</th>
303308
<th>r</th><th>R²</th>
304309
<th>r</th><th>R²</th>
305310
<th>r</th><th>R²</th>
@@ -324,6 +329,7 @@ def fmt(val):
324329
f"<td>{fmt(mod['r'])}</td>"
325330
f"<td>{fmt(mod['r2'])}</td>"
326331
f"<td>{fmt(mod['nrmse'])}</td>"
332+
f"<td>{fmt(s['r2_yearly_median'])}</td>"
327333
f"<td>{fmt(sp['r'])}</td>"
328334
f"<td>{fmt(sp['r2'])}</td>"
329335
f"<td>{fmt(s['r_time_model'])}</td>"
@@ -405,6 +411,7 @@ def generate_local_report_html(stats_list: List[dict], pdf_filename: str) -> str
405411
<td>{m['r']:.2f}</td>
406412
<td>{m['r2']:.2f}</td>
407413
<td>{m['nrmse']:.2f}</td>
414+
<td>{s['r2_yearly_median']:.2f}</td>
408415
<td>{sp['r']:.2f}</td>
409416
<td>{sp['r2']:.2f}</td>
410417
<td>{s['r_time_model']:.2f}</td>
@@ -445,13 +452,13 @@ def generate_local_report_html(stats_list: List[dict], pdf_filename: str) -> str
445452
<th rowspan="2">Dataset</th>
446453
<th rowspan="2">N_regions</th>
447454
<th rowspan="2">N_years</th>
448-
<th colspan="3">Region-Year</th>
455+
<th colspan="4">Region-Year</th>
449456
<th colspan="2">Spatial</th>
450457
<th colspan="2">Temporal</th>
451458
<th colspan="2">Anomaly</th>
452459
</tr>
453460
<tr>
454-
<th>r</th><th>R²</th><th>NRMSE</th>
461+
<th>r</th><th>R²</th><th>NRMSE</th><th>med R²/yr</th>
455462
<th>r</th><th>R²</th>
456463
<th>r</th><th>R²</th>
457464
<th>r</th><th>R²</th>
@@ -681,6 +688,7 @@ def process_dataset(
681688
_as_float_array(ts[KEY_TARGET]),
682689
_as_float_array(ts[model]),
683690
)
691+
r2_yearly_median = calc_median_yearly_r2(df_filtered, KEY_TARGET, model)
684692

685693
# Spatial stats (mean over years per location)
686694
spatial = cast(pd.DataFrame, df_filtered.groupby(KEY_LOC)[[KEY_TARGET, model]].mean())
@@ -713,6 +721,7 @@ def process_dataset(
713721
"metrics_baseline": metrics_base,
714722
"r_time_model": r_time_model,
715723
"r2_time_model": r2_time_model,
724+
"r2_yearly_median": r2_yearly_median,
716725
"r_time_base": r_time_base,
717726
"metrics_spatial": {"r": r_spatial_model, "r2": r2_spatial_model},
718727
}

tests/evaluation/test_aggregated_metrics.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from cybench.config import KEY_LOC, KEY_TARGET, KEY_YEAR
55
from cybench.evaluation.aggregated_metrics import (
6+
calc_median_yearly_r2,
67
calc_nrmse,
78
calc_r_r2,
89
compute_report_metrics,
@@ -37,6 +38,21 @@ def test_get_metrics_dict_has_anomaly_columns():
3738
assert set(out) == {"r", "r2", "nrmse", "r_res", "r2_res"}
3839

3940

41+
def test_calc_median_yearly_r2():
42+
rows = []
43+
for loc in ("A", "B"):
44+
rows.append({KEY_LOC: loc, KEY_YEAR: 2019, KEY_TARGET: 5.0, "model": 5.0})
45+
rows.extend(
46+
[
47+
{KEY_LOC: "A", KEY_YEAR: 2020, KEY_TARGET: 5.0, "model": 5.0},
48+
{KEY_LOC: "B", KEY_YEAR: 2020, KEY_TARGET: 7.0, "model": 6.0},
49+
]
50+
)
51+
df = pd.DataFrame(rows)
52+
_, r2_2020 = calc_r_r2([5.0, 7.0], [5.0, 6.0])
53+
assert calc_median_yearly_r2(df, KEY_TARGET, "model") == np.nanmedian([1.0, r2_2020])
54+
55+
4056
def test_compute_report_metrics_views():
4157
df = _make_df()
4258
out = compute_report_metrics(df, KEY_TARGET, "model")
@@ -46,6 +62,8 @@ def test_compute_report_metrics_views():
4662
assert "region_year" in out
4763
assert "spatial" in out
4864
assert "temporal" in out
65+
assert "median_r2" in out["region_year"]
66+
assert np.isfinite(out["region_year"]["median_r2"])
4967

5068

5169
def test_compute_report_metrics_ignores_nan_predictions():

tests/runs/test_collect_walk_forward_results.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ def test_summary_rows_to_dashboard_records(tmp_path: Path):
121121
"r2_spatial": -0.93,
122122
"r_temporal": 0.18,
123123
"r2_temporal": -5.10,
124+
"r2_yearly_median": 0.42,
124125
"r_res": 0.05,
125126
"r2_res": -1.76,
126127
}
@@ -132,7 +133,7 @@ def test_summary_rows_to_dashboard_records(tmp_path: Path):
132133
(assets / "maize_NL_scatter.png").write_bytes(b"png")
133134

134135
records = summary_rows_to_dashboard_records(rows, tmp_path)
135-
assert len(records) == 9
136+
assert len(records) == 10
136137
assert records[0]["view"] == "region_year"
137138
scatter_recs = [r for r in records if r.get("images", {}).get("scatter")]
138139
assert scatter_recs

0 commit comments

Comments
 (0)