Skip to content

Commit 738550c

Browse files
radar love
1 parent 9682e99 commit 738550c

8 files changed

Lines changed: 220 additions & 70 deletions

cybench/runs/analysis/global_insights_lib.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,17 @@
2828
{
2929
"id": "spatial",
3030
"label": "Spatial",
31-
"note": "Pearson r on regional means (aggregate across years).",
31+
"note": "Median per-year Pearson r across regions (typical-year slice).",
3232
"metrics": (
33-
{"id": "r", "column": "r_spatial_agg", "label": "r", "higher_better": True},
33+
{"id": "r", "column": "r_spatial", "label": "r", "higher_better": True},
3434
),
3535
},
3636
{
3737
"id": "temporal",
3838
"label": "Temporal",
39-
"note": "Pearson r on yearly national means (aggregate across regions).",
39+
"note": "Median per-region Pearson r across years (typical-region slice).",
4040
"metrics": (
41-
{"id": "r", "column": "r_temporal_agg", "label": "r", "higher_better": True},
41+
{"id": "r", "column": "r_temporal", "label": "r", "higher_better": True},
4242
),
4343
},
4444
{

cybench/runs/analysis/model_family_radar_lib.py

Lines changed: 80 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,22 @@
7474
}
7575

7676
RADAR_NORMALIZATION_NOTE = (
77-
"Each axis is independently normalized to highlight the relative strengths of "
78-
"each modeling paradigm (NRMSE on Overall, Pearson r on other views). "
79-
"Absolute values are reported in the tables below."
77+
"Relative: each axis is min–max normalized across the five family representatives "
78+
"(best paradigm on that axis reaches the outer ring). "
79+
"Absolute values are in the table below."
80+
)
81+
82+
RADAR_ABSOLUTE_SCALES: dict[str, dict[str, Any]] = {
83+
"nrmse": {"lo": 0.1, "hi": 0.30, "higher_better": False, "display": "NRMSE"},
84+
"r_spatial": {"lo": 0.0, "hi": 1.0, "higher_better": True, "display": "r"},
85+
"r_temporal": {"lo": 0.0, "hi": 1.0, "higher_better": True, "display": "r"},
86+
"r_res": {"lo": 0.0, "hi": 1.0, "higher_better": True, "display": "r"},
87+
}
88+
89+
RADAR_ABSOLUTE_NOTE = (
90+
"Absolute: fixed scales per axis — NRMSE 0.10 (outer, best) to 0.30 (center, worst); "
91+
"Pearson r axes 0 (center) to 1 (outer). Values outside the range are clamped; "
92+
"negative r is shown at 0."
8093
)
8194

8295
MODEL_DISPLAY_NAMES: dict[str, str] = {
@@ -180,10 +193,54 @@ def relative_scores(raw: pd.DataFrame) -> pd.DataFrame:
180193
return out
181194

182195

196+
def absolute_scores(raw: pd.DataFrame) -> pd.DataFrame:
197+
"""Map raw metrics to 0–1 radar radius using fixed CY-Bench scales (higher radius = better)."""
198+
out = raw.copy()
199+
for view in EVALUATION_VIEWS:
200+
label = view["label"]
201+
metric = view["metric"]
202+
spec = RADAR_ABSOLUTE_SCALES.get(metric)
203+
if spec is None or metric not in out.columns:
204+
out[label] = float("nan")
205+
continue
206+
lo = float(spec["lo"])
207+
hi = float(spec["hi"])
208+
higher = bool(spec.get("higher_better", True))
209+
vals = out[metric].astype(float).clip(lower=lo, upper=hi)
210+
if hi == lo:
211+
out[label] = 0.5
212+
else:
213+
norm = (vals - lo) / (hi - lo)
214+
out[label] = norm if higher else 1.0 - norm
215+
return out
216+
217+
218+
def radar_scales_payload() -> dict[str, Any]:
219+
"""JSON-serializable absolute radar scale definitions per view label."""
220+
by_label: dict[str, Any] = {}
221+
for view in EVALUATION_VIEWS:
222+
spec = RADAR_ABSOLUTE_SCALES.get(view["metric"], {})
223+
lo = float(spec.get("lo", 0.0))
224+
hi = float(spec.get("hi", 1.0))
225+
higher = bool(spec.get("higher_better", True))
226+
by_label[view["label"]] = {
227+
"metric": view["metric"],
228+
"display": spec.get("display", view["display"]),
229+
"lo": lo,
230+
"hi": hi,
231+
"higher_better": higher,
232+
"outer_label": f"{lo:.2f}" if view["metric"] == "nrmse" else f"{lo:g}",
233+
"center_label": f"{hi:.2f}" if view["metric"] == "nrmse" else f"{hi:g}",
234+
}
235+
return {"absolute": by_label}
236+
237+
183238
def _records_from_medians(
184239
medians: pd.DataFrame,
185240
rel_all: pd.DataFrame,
186241
entries: list[tuple[str, str, str, str, bool]],
242+
*,
243+
abs_all: pd.DataFrame | None = None,
187244
) -> list[dict[str, Any]]:
188245
"""Build radar rows from median table. *entries*: (index, family, display, color, is_naive)."""
189246
view_labels = [v["label"] for v in EVALUATION_VIEWS]
@@ -206,6 +263,16 @@ def _records_from_medians(
206263
)
207264
for label in view_labels
208265
}
266+
absolute: dict[str, float | None] = {}
267+
if abs_all is not None:
268+
absolute = {
269+
label: (
270+
round(float(abs_all[label].loc[model_key]), 4)
271+
if label in abs_all.columns and model_key in abs_all.index
272+
else None
273+
)
274+
for label in view_labels
275+
}
209276
rows.append(
210277
{
211278
"family": family,
@@ -215,6 +282,7 @@ def _records_from_medians(
215282
"is_naive": is_naive,
216283
"raw": raw,
217284
"relative": relative,
285+
"absolute": absolute,
218286
}
219287
)
220288
return rows
@@ -225,9 +293,12 @@ def _family_records(
225293
representatives: dict[str, str],
226294
*,
227295
rel_all: pd.DataFrame | None = None,
296+
abs_all: pd.DataFrame | None = None,
228297
) -> list[dict[str, Any]]:
229298
if rel_all is None:
230299
rel_all = relative_scores(medians.copy()) if not medians.empty else pd.DataFrame()
300+
if abs_all is None:
301+
abs_all = absolute_scores(medians.copy()) if not medians.empty else pd.DataFrame()
231302
entries = [
232303
(
233304
representatives[family],
@@ -239,7 +310,7 @@ def _family_records(
239310
for family in FAMILY_ORDER
240311
if family in representatives
241312
]
242-
return _records_from_medians(medians, rel_all, entries)
313+
return _records_from_medians(medians, rel_all, entries, abs_all=abs_all)
243314

244315

245316
def family_for_model(model: str) -> str | None:
@@ -460,7 +531,8 @@ def build_radar_slice(
460531
rep_models = list(reps.values())
461532
medians = _median_for_models(work, rep_models, VIEW_METRICS)
462533
rel_all = relative_scores(medians.copy()) if not medians.empty else pd.DataFrame()
463-
families = _family_records(medians, reps, rel_all=rel_all)
534+
abs_all = absolute_scores(medians.copy()) if not medians.empty else pd.DataFrame()
535+
families = _family_records(medians, reps, rel_all=rel_all, abs_all=abs_all)
464536
return {
465537
"batch_horizon": batch_horizon,
466538
"crop": crop or "all",
@@ -514,6 +586,9 @@ def build_radar_payload(
514586
"by_horizon": by_horizon,
515587
"sample_scatter_metric": SAMPLE_SCATTER_METRIC,
516588
"sample_scatter": build_sample_scatter_payload(df, representatives=representatives),
589+
"radar_scales": radar_scales_payload(),
590+
"relative_note": RADAR_NORMALIZATION_NOTE,
591+
"absolute_note": RADAR_ABSOLUTE_NOTE,
517592
"normalization_note": RADAR_NORMALIZATION_NOTE,
518593
"representative_selection": (
519594
"One model per family: lowest median NRMSE across datasets in the selection; "

cybench/runs/analysis/publish_dashboard_bundle.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -257,19 +257,19 @@ def render_cards(items: list[IndexEntry]) -> str:
257257
if publish_root and (publish_root / "insights.html").is_file():
258258
insights_card = """
259259
<section class="block">
260-
<h2>Global insights</h2>
260+
<h2>Global benchmarks</h2>
261261
<div class="grid">
262262
<a class="link-card" href="insights.html">
263263
<div class="link-card-top"><span class="badge badge-neutral">ALL</span><span class="arrow" aria-hidden="true">→</span></div>
264-
<h3>Cross-country model ranking</h3>
265-
<p class="muted">NRMSE leaderboard and end-of-season vs mid-season comparison</p>
264+
<h3>Global insights</h3>
265+
<p class="muted">All models — leaderboard, model×country heatmap, end-of-season vs mid-season</p>
266266
</a>"""
267267
if (publish_root / "model_families.html").is_file():
268268
insights_card += """
269269
<a class="link-card" href="model_families.html">
270270
<div class="link-card-top"><span class="badge badge-neutral">ALL</span><span class="arrow" aria-hidden="true">→</span></div>
271-
<h3>Model family radar</h3>
272-
<p class="muted">Relative performance across overall, spatial, temporal, and anomaly views</p>
271+
<h3>Model families (paper summary)</h3>
272+
<p class="muted">Five paradigms — radar chart, median metrics, performance vs training size</p>
273273
</a>"""
274274
insights_card += """
275275
</div>

cybench/runs/viz/global_insights_template.html

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@
163163
<div class="page">
164164
<h1>CY-Bench global insights</h1>
165165
<p class="lead">Cross-country model comparison and end-of-season vs mid-season walk-forward results.</p>
166+
<p class="muted">Every model in the benchmark. For a five-paradigm summary (radar chart), see <a href="model_families.html">Model families (paper summary)</a>.</p>
166167
<p class="muted" id="meta"></p>
167168

168169
<div class="card">
@@ -245,7 +246,10 @@ <h3 style="font-size:1rem;margin:1rem 0 0.5rem;">Detail per crop × country × m
245246
</p>
246247
</div>
247248

248-
<p class="muted"><a href="index.html">← Back to country dashboards</a></p>
249+
<p class="muted">
250+
<a href="model_families.html">Model families (paper summary)</a> ·
251+
<a href="index.html">← Back to country dashboards</a>
252+
</p>
249253
</div>
250254

251255
<script>
@@ -372,8 +376,8 @@ <h3 style="font-size:1rem;margin:1rem 0 0.5rem;">Detail per crop × country × m
372376
{ id: "nrmse", label: "NRMSE", higher_better: false },
373377
],
374378
},
375-
{ id: "spatial", label: "Spatial", note: "Pearson r on regional means (aggregate).", metrics: [{ id: "r", label: "r", higher_better: true }] },
376-
{ id: "temporal", label: "Temporal", note: "Pearson r on yearly national means (aggregate).", metrics: [{ id: "r", label: "r", higher_better: true }] },
379+
{ id: "spatial", label: "Spatial", note: "Median per-year r across regions (typical-year slice).", metrics: [{ id: "r", label: "r", higher_better: true }] },
380+
{ id: "temporal", label: "Temporal", note: "Median per-region r across years (typical-region slice).", metrics: [{ id: "r", label: "r", higher_better: true }] },
377381
{ id: "anomaly", label: "Anomaly", note: "Pooled Pearson r on de-meaned yields.", metrics: [{ id: "r", label: "r", higher_better: true }] },
378382
];
379383

cybench/runs/viz/index_map_template.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,10 +159,10 @@ <h3>Select a country</h3>
159159

160160
const topLinks = document.getElementById("top-links");
161161
if (DATA.has_insights) {
162-
topLinks.innerHTML += `<a class="pill" href="insights.html">Global insights</a>`;
162+
topLinks.innerHTML += `<a class="pill" href="insights.html">Global insights (all models)</a>`;
163163
}
164164
if (DATA.has_model_families) {
165-
topLinks.innerHTML += `<a class="pill" href="model_families.html">Model families</a>`;
165+
topLinks.innerHTML += `<a class="pill" href="model_families.html">Model families (paper)</a>`;
166166
}
167167
if (DATA.has_screening && DATA.screening_href) {
168168
topLinks.innerHTML += `<a class="pill" href="${DATA.screening_href}">Screening (all)</a>`;

0 commit comments

Comments
 (0)