Skip to content

Commit 71adaed

Browse files
dashboard
1 parent c1cabf7 commit 71adaed

3 files changed

Lines changed: 332 additions & 192 deletions

File tree

cybench/runs/analysis/global_insights_lib.py

Lines changed: 105 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -920,6 +920,72 @@ def _eos_only_family_points(
920920
return [{"horizon": "eos", "metrics": metrics, "n_countries": n_countries}]
921921

922922

923+
def _build_model_horizon_entry(
924+
*,
925+
model: str,
926+
wide: pd.DataFrame,
927+
work: pd.DataFrame,
928+
trend_model: str,
929+
horizons: tuple[str, ...],
930+
value_columns: tuple[str, ...],
931+
) -> dict[str, Any] | None:
932+
eos_only = model in EOS_ONLY_HORIZON_CURVE_MODELS
933+
if eos_only:
934+
points = _eos_only_family_points(
935+
work,
936+
model=model,
937+
trend_model=trend_model,
938+
value_columns=value_columns,
939+
)
940+
else:
941+
points = _family_curve_points(
942+
wide,
943+
model=model,
944+
trend_model=trend_model,
945+
horizons=horizons,
946+
value_columns=value_columns,
947+
)
948+
if not points:
949+
return None
950+
n_horizons_with_data = sum(
951+
1 for p in points if (p.get("metrics") or {}).get("nrmse", {}).get("median") is not None
952+
)
953+
return {
954+
"model": model,
955+
"points": points,
956+
"eos_only": eos_only,
957+
"plot": (not eos_only) and n_horizons_with_data >= 2,
958+
}
959+
960+
961+
def _build_all_model_horizon_entries(
962+
work: pd.DataFrame,
963+
wide: pd.DataFrame,
964+
*,
965+
trend_model: str,
966+
horizons: tuple[str, ...],
967+
value_columns: tuple[str, ...],
968+
model_display_names: dict[str, str],
969+
) -> list[dict[str, Any]]:
970+
if work.empty or "model" not in work.columns:
971+
return []
972+
entries: list[dict[str, Any]] = []
973+
for model in sorted(work["model"].astype(str).unique()):
974+
entry = _build_model_horizon_entry(
975+
model=model,
976+
wide=wide,
977+
work=work,
978+
trend_model=trend_model,
979+
horizons=horizons,
980+
value_columns=value_columns,
981+
)
982+
if entry is None:
983+
continue
984+
entry["display"] = model_display_names.get(model, model)
985+
entries.append(entry)
986+
return entries
987+
988+
923989
def build_horizon_skill_curves_payload(
924990
df: pd.DataFrame,
925991
*,
@@ -953,64 +1019,56 @@ def build_horizon_skill_curves_payload(
9531019
by_crop: dict[str, Any] = {}
9541020
for crop_key in crop_keys:
9551021
crop_filter = None if crop_key == "all" else crop_key
956-
work = _filter_summary_work(df, crop=crop_filter)
957-
work = work[work["model"].isin(rep_models)]
958-
wide = _wide_country_model_horizon_metrics(
959-
work, horizons, crop=crop_filter, value_columns=value_columns
960-
)
961-
if wide.empty:
962-
by_crop[crop_key] = {
963-
"n_countries": 0,
964-
"countries": [],
965-
"excluded_countries": [],
966-
"families": [],
967-
}
968-
continue
969-
970-
countries = sorted(wide["country"].astype(str).unique())
9711022
crop_work = _filter_summary_work(df, crop=crop_filter)
1023+
wide_all = _wide_country_model_horizon_metrics(
1024+
crop_work, horizons, crop=crop_filter, value_columns=value_columns
1025+
)
1026+
countries = (
1027+
sorted(wide_all["country"].astype(str).unique()) if not wide_all.empty else []
1028+
)
9721029
any_countries = (
9731030
set(crop_work["country"].astype(str).unique()) if "country" in crop_work.columns else set()
9741031
)
9751032
excluded = sorted(any_countries - set(countries))
9761033

1034+
models = _build_all_model_horizon_entries(
1035+
crop_work,
1036+
wide_all,
1037+
trend_model=trend_model,
1038+
horizons=horizons,
1039+
value_columns=value_columns,
1040+
model_display_names=MODEL_DISPLAY_NAMES,
1041+
)
1042+
1043+
work = crop_work[crop_work["model"].isin(rep_models)]
1044+
wide = (
1045+
wide_all[wide_all["model"].isin(rep_models)].copy()
1046+
if not wide_all.empty
1047+
else wide_all
1048+
)
1049+
9771050
families: list[dict[str, Any]] = []
9781051
for family in FAMILY_ORDER:
9791052
model = reps.get(family)
9801053
if not model or model == trend_model:
9811054
continue
982-
eos_only = model in EOS_ONLY_HORIZON_CURVE_MODELS
983-
if eos_only:
984-
points = _eos_only_family_points(
985-
work,
986-
model=model,
987-
trend_model=trend_model,
988-
value_columns=value_columns,
989-
)
990-
else:
991-
points = _family_curve_points(
992-
wide,
993-
model=model,
994-
trend_model=trend_model,
995-
horizons=horizons,
996-
value_columns=value_columns,
997-
)
998-
if not points:
999-
continue
1000-
n_horizons_with_data = sum(
1001-
1
1002-
for p in points
1003-
if (p.get("metrics") or {}).get("nrmse", {}).get("median") is not None
1055+
entry = _build_model_horizon_entry(
1056+
model=model,
1057+
wide=wide,
1058+
work=work,
1059+
trend_model=trend_model,
1060+
horizons=horizons,
1061+
value_columns=value_columns,
10041062
)
1063+
if entry is None:
1064+
continue
10051065
families.append(
10061066
{
10071067
"family": family,
10081068
"model": model,
10091069
"display": MODEL_DISPLAY_NAMES.get(model, model),
10101070
"color": FAMILY_COLORS.get(family, "#666"),
1011-
"points": points,
1012-
"eos_only": eos_only,
1013-
"plot": (not eos_only) and n_horizons_with_data >= 2,
1071+
**entry,
10141072
}
10151073
)
10161074

@@ -1019,6 +1077,7 @@ def build_horizon_skill_curves_payload(
10191077
"countries": countries,
10201078
"excluded_countries": excluded,
10211079
"families": families,
1080+
"models": models,
10221081
}
10231082

10241083
rep_labels = ", ".join(f"{f}: {m}" for f, m in reps.items())
@@ -1043,9 +1102,14 @@ def build_horizon_skill_curves_payload(
10431102
"Hyperparameters are tuned per horizon (screening at each lead time)."
10441103
),
10451104
"plot_excluded_note": (
1046-
"EOS-only baselines (LPJmL) are omitted from the curve plot; see the table for "
1105+
"EOS-only baselines (LPJmL) are omitted from the curve plot; see the tables for "
10471106
"their end-of-season median."
10481107
),
1108+
"models_table_note": (
1109+
"Median per metric across countries. Multi-horizon models use only countries with "
1110+
"data at every collected horizon (inner join). EOS-only baselines show end-of-season "
1111+
"values only."
1112+
),
10491113
"axes": _horizon_skill_axes(),
10501114
"metric_notes": {
10511115
"nrmse": "Median pooled NRMSE across countries (lower is better).",
@@ -1213,7 +1277,6 @@ def build_insights_payload(output_root: Path, *, version: int = 1) -> dict[str,
12131277
"""Build JSON-serializable payload for the global insights dashboard."""
12141278
paths = discover_summary_tables(output_root, version=version)
12151279
df = load_summary_frame(paths)
1216-
horizon_detail, horizon_summary = compare_horizons(df)
12171280

12181281
available_horizons = horizons_in_data(df)
12191282
leaderboards: dict[str, dict[str, list[dict[str, Any]]]] = {}
@@ -1250,30 +1313,11 @@ def build_insights_payload(output_root: Path, *, version: int = 1) -> dict[str,
12501313
"leaderboards_skilled": leaderboards_skilled,
12511314
"model_country": model_country,
12521315
"model_country_skilled": model_country_skilled,
1253-
"horizon_summary": _df_records(horizon_summary),
1254-
"horizon_detail": _df_records(horizon_detail),
1255-
"overall_horizon": _overall_horizon_stats(horizon_detail),
12561316
"horizon_skill_curves": build_horizon_skill_curves_payload(df),
12571317
"crop_comparison": build_crop_comparison_payload(df),
12581318
}
12591319

12601320

1261-
def _overall_horizon_stats(detail: pd.DataFrame) -> dict[str, Any]:
1262-
if detail.empty:
1263-
return {}
1264-
weights = detail["pair_weight"]
1265-
return {
1266-
"n_pairs": int(len(detail)),
1267-
"eos_win_rate": float(detail["eos_better"].mean()),
1268-
"mean_delta_nrmse": float(detail["delta_nrmse"].mean()),
1269-
"weighted_delta_nrmse": float(_weighted_mean(detail["delta_nrmse"], weights)),
1270-
"interpretation": (
1271-
"delta_nrmse = mid − eos; positive values mean end-of-season (nowcast) "
1272-
"has lower NRMSE than mid-season."
1273-
),
1274-
}
1275-
1276-
12771321
def _overall_crop_pair_stats(
12781322
detail: pd.DataFrame, crop_a: str, crop_b: str
12791323
) -> dict[str, Any]:

0 commit comments

Comments
 (0)