Suitability tables
#4
by odunbar - opened
- CLAUDE.md +29 -2
- src/common/leaderboard.py +216 -213
- src/data_store.py +9 -0
- src/pages/OptimizationLeaderboard.py +4 -2
- src/pages/UQLeaderboard.py +4 -2
CLAUDE.md
CHANGED
|
@@ -47,8 +47,10 @@ calibration_benchmark/
|
|
| 47 |
│ ├── streamlit_app.py Entrypoint + Home/"Optimization Leaderboard"
|
| 48 |
│ ├── data_store.py load_metric_store() / load_uq_store() / load_uq_budget_store()
|
| 49 |
│ │ → @st.cache_data DataFrames; DATASET_FILES, UQ_BUDGET_FILES
|
|
|
|
| 50 |
│ ├── common/
|
| 51 |
│ │ ├── leaderboard.py render_leaderboard() — shared scored-table + chart pipeline
|
|
|
|
| 52 |
│ │ │ optional budget_store param adds budget/iters chart section
|
| 53 |
│ │ └── method_registry.py KNOWN_METHODS registry + name canonicalization
|
| 54 |
│ └── pages/
|
|
@@ -122,6 +124,21 @@ all-quantiles-satisfied condition.
|
|
| 122 |
|
| 123 |
All three stores are `@st.cache_data` — clear Streamlit cache to pick up new data files.
|
| 124 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
## Method registry (`src/common/method_registry.py`)
|
| 126 |
|
| 127 |
| Canonical | Abbreviation | Family | Aliases |
|
|
@@ -144,8 +161,10 @@ All three stores are `@st.cache_data` — clear Streamlit cache to pick up new d
|
|
| 144 |
3. Place result NetCDF(s) in `data/` and add paths to `DATASET_FILES` in `src/data_store.py`.
|
| 145 |
|
| 146 |
### Add an optimization benchmark dataset
|
| 147 |
-
Add a `"BENCHMARK_NAME": [list_of_nc_paths]` entry to `DATASET_FILES` in `src/data_store.py`.
|
| 148 |
-
Paths are relative to the `data/` directory.
|
|
|
|
|
|
|
| 149 |
|
| 150 |
### Add UQ data
|
| 151 |
Add an `(algorithm_type, nc_path)` tuple to the appropriate benchmark key in `UQ_BUDGET_FILES`
|
|
@@ -183,3 +202,11 @@ the `budget_store` parameter of `render_leaderboard()` in `src/common/leaderboar
|
|
| 183 |
never reached (no `metric == -1` sentinel); `failure_rate` is computed differently.
|
| 184 |
- **`render_leaderboard` budget_store param** — must be passed explicitly from
|
| 185 |
`UQLeaderboard.py`; `leaderboard.py` is not aware of UQ-specific data paths.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
│ ├── streamlit_app.py Entrypoint + Home/"Optimization Leaderboard"
|
| 48 |
│ ├── data_store.py load_metric_store() / load_uq_store() / load_uq_budget_store()
|
| 49 |
│ │ → @st.cache_data DataFrames; DATASET_FILES, UQ_BUDGET_FILES
|
| 50 |
+
│ │ → BENCHMARK_DIMS — physical (param, state, output) dims per benchmark
|
| 51 |
│ ├── common/
|
| 52 |
│ │ ├── leaderboard.py render_leaderboard() — shared scored-table + chart pipeline
|
| 53 |
+
│ │ │ _render_suitability_table() — method × benchmark suitability grid
|
| 54 |
│ │ │ optional budget_store param adds budget/iters chart section
|
| 55 |
│ │ └── method_registry.py KNOWN_METHODS registry + name canonicalization
|
| 56 |
│ └── pages/
|
|
|
|
| 124 |
|
| 125 |
All three stores are `@st.cache_data` — clear Streamlit cache to pick up new data files.
|
| 126 |
|
| 127 |
+
### Benchmark dimensions (`BENCHMARK_DIMS`)
|
| 128 |
+
|
| 129 |
+
Physical dimensions of each benchmark exported from `src/data_store.py` as
|
| 130 |
+
`BENCHMARK_DIMS: dict[str, tuple[int, int, int]]` — `(param_dim, state_dim, output_dim)`:
|
| 131 |
+
|
| 132 |
+
| Benchmark | param | state | output |
|
| 133 |
+
|---|---|---|---|
|
| 134 |
+
| `L63` | 2 | 3 | 9 |
|
| 135 |
+
| `L96` | 1 | 40 | 80 |
|
| 136 |
+
| `L96_NN_FORCING` | 61 | 100 | 200 |
|
| 137 |
+
| `L96_SPATIAL_FORCING` | 40 | 40 | 80 |
|
| 138 |
+
|
| 139 |
+
Passed as `benchmark_dims=BENCHMARK_DIMS` to `render_leaderboard()` for column-header
|
| 140 |
+
annotations in the suitability table. Update if a new benchmark is added.
|
| 141 |
+
|
| 142 |
## Method registry (`src/common/method_registry.py`)
|
| 143 |
|
| 144 |
| Canonical | Abbreviation | Family | Aliases |
|
|
|
|
| 161 |
3. Place result NetCDF(s) in `data/` and add paths to `DATASET_FILES` in `src/data_store.py`.
|
| 162 |
|
| 163 |
### Add an optimization benchmark dataset
|
| 164 |
+
1. Add a `"BENCHMARK_NAME": [list_of_nc_paths]` entry to `DATASET_FILES` in `src/data_store.py`.
|
| 165 |
+
Paths are relative to the `data/` directory.
|
| 166 |
+
2. Add a `"BENCHMARK_NAME": (param_dim, state_dim, output_dim)` entry to `BENCHMARK_DIMS`
|
| 167 |
+
in `src/data_store.py` for the suitability table column header.
|
| 168 |
|
| 169 |
### Add UQ data
|
| 170 |
Add an `(algorithm_type, nc_path)` tuple to the appropriate benchmark key in `UQ_BUDGET_FILES`
|
|
|
|
| 202 |
never reached (no `metric == -1` sentinel); `failure_rate` is computed differently.
|
| 203 |
- **`render_leaderboard` budget_store param** — must be passed explicitly from
|
| 204 |
`UQLeaderboard.py`; `leaderboard.py` is not aware of UQ-specific data paths.
|
| 205 |
+
- **`render_leaderboard` requires `default_target`** — callers must pass the fixed target level
|
| 206 |
+
shown on first load (`1.1` for optimization, `1.5` for UQ). There is no "All targets" option.
|
| 207 |
+
- **Suitability table is fixed at `default_target`** — `_render_suitability_table()` always
|
| 208 |
+
evaluates suitability at the `default_target` passed to `render_leaderboard()`, independent
|
| 209 |
+
of what the user selects in the target radio control below it.
|
| 210 |
+
- **Suitability criteria** — a method is green for a benchmark if there exists an ensemble size
|
| 211 |
+
with `failure_rate < 20%` and `mean_budget ≤ 3× global_best` at `default_target`. Gray means
|
| 212 |
+
no data for that benchmark; the method may be suitable on other benchmarks.
|
src/common/leaderboard.py
CHANGED
|
@@ -14,6 +14,7 @@ Usage::
|
|
| 14 |
target_label="RMSE Target Level",
|
| 15 |
title="Optimization Leaderboard",
|
| 16 |
state_prefix="opt",
|
|
|
|
| 17 |
raw_page="pages/RawData.py",
|
| 18 |
)
|
| 19 |
"""
|
|
@@ -31,6 +32,98 @@ _METHOD_PALETTE = [
|
|
| 31 |
"#eeca3b", "#b279a2", "#ff9da6", "#9d755d", "#bab0ac",
|
| 32 |
]
|
| 33 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
|
| 35 |
def render_leaderboard(
|
| 36 |
metric_store: pd.DataFrame,
|
|
@@ -39,11 +132,13 @@ def render_leaderboard(
|
|
| 39 |
target_label: str,
|
| 40 |
title: str,
|
| 41 |
state_prefix: str,
|
|
|
|
| 42 |
raw_page: str | None = None,
|
| 43 |
show_failure_panel: bool = False,
|
| 44 |
show_scoring_modes: bool = True,
|
| 45 |
canonical_target_levels: list[float] | None = None,
|
| 46 |
budget_store: pd.DataFrame | None = None,
|
|
|
|
| 47 |
) -> None:
|
| 48 |
"""Render a scored leaderboard backed by *metric_store*.
|
| 49 |
|
|
@@ -66,12 +161,17 @@ def render_leaderboard(
|
|
| 66 |
Short string used to namespace ``st.session_state`` keys so multiple
|
| 67 |
leaderboard pages keep independent control state. Use ``"opt"`` for
|
| 68 |
the Optimization leaderboard and ``"uq"`` for the UQ leaderboard.
|
|
|
|
|
|
|
|
|
|
| 69 |
raw_page:
|
| 70 |
Optional Streamlit page path for an "Open Raw Data" link shown at the
|
| 71 |
bottom. Pass ``None`` to suppress the link.
|
| 72 |
show_failure_panel:
|
| 73 |
If ``True``, render a grouped-bar failure-rate chart below the main
|
| 74 |
performance chart.
|
|
|
|
|
|
|
| 75 |
canonical_target_levels:
|
| 76 |
If provided, the target-level selector always offers exactly these
|
| 77 |
values (as strings) regardless of what is present in the data. Use
|
|
@@ -79,10 +179,11 @@ def render_leaderboard(
|
|
| 79 |
levels even when some have 100 % failure.
|
| 80 |
budget_store:
|
| 81 |
Optional DataFrame produced by ``load_uq_budget_store()``. When
|
| 82 |
-
provided, an additional "
|
| 83 |
-
rendered below the main performance chart
|
| 84 |
-
|
| 85 |
-
|
|
|
|
| 86 |
"""
|
| 87 |
st.header(title)
|
| 88 |
|
|
@@ -90,24 +191,25 @@ def render_leaderboard(
|
|
| 90 |
st.warning("No metric data found. Expected NetCDF files in `data/` with a `metric` variable.")
|
| 91 |
return
|
| 92 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
# Derived column name for the string version of the target coordinate
|
| 94 |
target_str_col = f"{target_col}_str"
|
| 95 |
|
| 96 |
-
benchmark_values
|
| 97 |
-
|
| 98 |
-
selected_benchmark = st.selectbox("Benchmark", options=benchmark_options, index=0)
|
| 99 |
|
| 100 |
-
filtered = (
|
| 101 |
-
metric_store.copy()
|
| 102 |
-
if selected_benchmark == "All"
|
| 103 |
-
else metric_store[metric_store["benchmark"] == selected_benchmark].copy()
|
| 104 |
-
)
|
| 105 |
filtered[target_str_col] = filtered[target_col].astype(str)
|
| 106 |
|
| 107 |
if canonical_target_levels is not None:
|
| 108 |
-
target_options = [
|
| 109 |
else:
|
| 110 |
-
target_options =
|
| 111 |
metric_store[target_col].astype(str).unique().tolist()
|
| 112 |
)
|
| 113 |
|
|
@@ -120,14 +222,19 @@ def render_leaderboard(
|
|
| 120 |
|
| 121 |
# Session-state keys namespaced by state_prefix so two leaderboard pages
|
| 122 |
# don't share control state.
|
| 123 |
-
k_target
|
| 124 |
k_scoring = f"{state_prefix}_scoring_mode"
|
| 125 |
-
k_weight
|
| 126 |
k_methods = f"{state_prefix}_selected_methods"
|
| 127 |
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
|
| 132 |
current_scoring_mode = st.session_state.get(k_scoring, "Mean Forward Model Runs")
|
| 133 |
if current_scoring_mode not in scoring_options:
|
|
@@ -136,22 +243,21 @@ def render_leaderboard(
|
|
| 136 |
current_fwdruns_weight_percent = int(st.session_state.get(k_weight, 80))
|
| 137 |
current_fwdruns_weight_percent = max(0, min(100, current_fwdruns_weight_percent))
|
| 138 |
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
fwdruns_weight = current_fwdruns_weight_percent / 100.0
|
| 142 |
ensemble_weight = 1.0 - fwdruns_weight
|
| 143 |
|
| 144 |
# Available methods for the current benchmark selection; used to populate the
|
| 145 |
# multiselect and to prune any stale saved selections when the benchmark changes.
|
| 146 |
available_methods = sorted(filtered["abbreviation"].dropna().unique().tolist())
|
| 147 |
-
saved_methods
|
| 148 |
-
valid_saved
|
| 149 |
st.session_state[k_methods] = valid_saved if valid_saved else available_methods
|
| 150 |
|
| 151 |
# Stable color scale: domain covers ALL methods so colors don't shift when a
|
| 152 |
# subset is displayed.
|
| 153 |
color_domain = available_methods
|
| 154 |
-
color_range
|
| 155 |
method_color = alt.Color(
|
| 156 |
"abbreviation:N",
|
| 157 |
title="Method",
|
|
@@ -159,11 +265,7 @@ def render_leaderboard(
|
|
| 159 |
)
|
| 160 |
|
| 161 |
def build_scored_table(input_df: pd.DataFrame, add_rank: bool = True) -> pd.DataFrame:
|
| 162 |
-
ranking_source =
|
| 163 |
-
input_df
|
| 164 |
-
if selected_target == "All targets"
|
| 165 |
-
else input_df[input_df[target_str_col] == selected_target]
|
| 166 |
-
)
|
| 167 |
if ranking_source.empty:
|
| 168 |
return ranking_source
|
| 169 |
|
|
@@ -182,7 +284,6 @@ def render_leaderboard(
|
|
| 182 |
).agg(
|
| 183 |
**{"Mean Forward Model Runs": ("metric", "mean")},
|
| 184 |
**{"Minimum Forward Model Runs": ("metric", "min")},
|
| 185 |
-
**{"Targets Used": (target_str_col, "nunique")},
|
| 186 |
**{"Ensemble Sizes Used": ("ensemble_size", "nunique")},
|
| 187 |
)
|
| 188 |
|
|
@@ -216,10 +317,10 @@ def render_leaderboard(
|
|
| 216 |
how="left",
|
| 217 |
)
|
| 218 |
|
| 219 |
-
scored_df["Optimal Ensemble Size"]
|
| 220 |
-
scored_df["Mean Forward Model Runs"]
|
| 221 |
scored_df["Minimum Forward Model Runs"] = scored_df["Minimum Forward Model Runs"].round(4)
|
| 222 |
-
scored_df["Mean Failure Rate (%)"]
|
| 223 |
|
| 224 |
mean_runs_min = scored_df["Mean Forward Model Runs"].min()
|
| 225 |
mean_runs_max = scored_df["Mean Forward Model Runs"].max()
|
|
@@ -278,85 +379,20 @@ def render_leaderboard(
|
|
| 278 |
|
| 279 |
return scored_df
|
| 280 |
|
| 281 |
-
|
| 282 |
-
benchmark_scores = []
|
| 283 |
-
for benchmark_name in benchmark_values:
|
| 284 |
-
benchmark_df = metric_store[metric_store["benchmark"] == benchmark_name].copy()
|
| 285 |
-
benchmark_df[target_str_col] = benchmark_df[target_col].astype(str)
|
| 286 |
-
scored = build_scored_table(benchmark_df, add_rank=False)
|
| 287 |
-
if scored.empty:
|
| 288 |
-
continue
|
| 289 |
-
scored["benchmark"] = benchmark_name
|
| 290 |
-
benchmark_scores.append(scored)
|
| 291 |
-
|
| 292 |
-
if benchmark_scores:
|
| 293 |
-
combined_scores = (
|
| 294 |
-
benchmark_scores[0].copy()
|
| 295 |
-
if len(benchmark_scores) == 1
|
| 296 |
-
else pd.concat(benchmark_scores, ignore_index=True)
|
| 297 |
-
)
|
| 298 |
-
leaderboard_df = combined_scores.groupby(
|
| 299 |
-
["algorithm_type", "abbreviation", "Method", "family"], as_index=False
|
| 300 |
-
).agg(
|
| 301 |
-
Score=("Score", "mean"),
|
| 302 |
-
**{"Mean Forward Model Runs": ("Mean Forward Model Runs", "mean")},
|
| 303 |
-
**{"Minimum Forward Model Runs": ("Minimum Forward Model Runs", "mean")},
|
| 304 |
-
**{"Optimal Ensemble Size": ("Optimal Ensemble Size", "mean")},
|
| 305 |
-
**{"Targets Used": ("Targets Used", "mean")},
|
| 306 |
-
**{"Ensemble Sizes Used": ("Ensemble Sizes Used", "mean")},
|
| 307 |
-
**{"Benchmarks Used": ("benchmark", "nunique")},
|
| 308 |
-
**{"Mean Failure Rate (%)": ("Mean Failure Rate (%)", "mean")},
|
| 309 |
-
)
|
| 310 |
-
|
| 311 |
-
leaderboard_df["Mean Forward Model Runs"] = leaderboard_df["Mean Forward Model Runs"].round(4)
|
| 312 |
-
leaderboard_df["Minimum Forward Model Runs"] = leaderboard_df["Minimum Forward Model Runs"].round(4)
|
| 313 |
-
leaderboard_df["Optimal Ensemble Size"] = leaderboard_df["Optimal Ensemble Size"].round(2)
|
| 314 |
-
leaderboard_df["Targets Used"] = leaderboard_df["Targets Used"].round().astype(int)
|
| 315 |
-
leaderboard_df["Ensemble Sizes Used"] = leaderboard_df["Ensemble Sizes Used"].round().astype(int)
|
| 316 |
-
leaderboard_df["Mean Failure Rate (%)"] = leaderboard_df["Mean Failure Rate (%)"].round(1)
|
| 317 |
-
leaderboard_df = leaderboard_df.sort_values(
|
| 318 |
-
["Score", "Mean Forward Model Runs", "abbreviation"], ascending=[False, True, True]
|
| 319 |
-
).reset_index(drop=True)
|
| 320 |
-
leaderboard_df["Rank"] = leaderboard_df.index + 1
|
| 321 |
-
leaderboard_df["Placement"] = leaderboard_df["Rank"].apply(
|
| 322 |
-
lambda rank: f"{ {1: '🥇', 2: '🥈', 3: '🥉'}.get(rank, '')} #{rank}".strip()
|
| 323 |
-
)
|
| 324 |
-
else:
|
| 325 |
-
leaderboard_df = pd.DataFrame(
|
| 326 |
-
columns=[
|
| 327 |
-
"Placement",
|
| 328 |
-
"abbreviation",
|
| 329 |
-
"Method",
|
| 330 |
-
"family",
|
| 331 |
-
"Mean Forward Model Runs",
|
| 332 |
-
"Minimum Forward Model Runs",
|
| 333 |
-
"Score",
|
| 334 |
-
"Optimal Ensemble Size",
|
| 335 |
-
"Targets Used",
|
| 336 |
-
"Ensemble Sizes Used",
|
| 337 |
-
"Benchmarks Used",
|
| 338 |
-
"Mean Failure Rate (%)",
|
| 339 |
-
]
|
| 340 |
-
)
|
| 341 |
-
else:
|
| 342 |
-
leaderboard_df = build_scored_table(filtered, add_rank=True)
|
| 343 |
-
leaderboard_df["Benchmarks Used"] = 1
|
| 344 |
|
| 345 |
if scoring_mode == "Mean Forward Model Runs":
|
| 346 |
-
score_basis = "
|
| 347 |
elif scoring_mode == "Minimum Forward Model Runs":
|
| 348 |
-
score_basis = "
|
| 349 |
elif scoring_mode == "Smallest Optimal Ensemble Size":
|
| 350 |
-
score_basis = "
|
| 351 |
else:
|
| 352 |
score_basis = (
|
| 353 |
-
"weighted blend of normalized forward-model-runs score
|
| 354 |
-
f"
|
| 355 |
)
|
| 356 |
|
| 357 |
-
if selected_benchmark == "All":
|
| 358 |
-
score_basis = f"{score_basis}; in All mode, each method's final score is the mean of its per-benchmark scores"
|
| 359 |
-
|
| 360 |
# Controls expander — always shown so users can change target even when the
|
| 361 |
# current selection yields all failures.
|
| 362 |
with st.expander("Scoring & Target Controls", expanded=False):
|
|
@@ -398,6 +434,19 @@ def render_leaderboard(
|
|
| 398 |
if not selected_methods:
|
| 399 |
selected_methods = available_methods
|
| 400 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 401 |
if leaderboard_df.empty:
|
| 402 |
st.warning(
|
| 403 |
"All runs failed to reach the target at this selection. "
|
|
@@ -406,34 +455,6 @@ def render_leaderboard(
|
|
| 406 |
else "No rows available for the current benchmark/target selection."
|
| 407 |
)
|
| 408 |
else:
|
| 409 |
-
if selected_benchmark == "All":
|
| 410 |
-
table_column_order = [
|
| 411 |
-
"Placement",
|
| 412 |
-
"abbreviation",
|
| 413 |
-
"Method",
|
| 414 |
-
"family",
|
| 415 |
-
"Score",
|
| 416 |
-
"Mean Failure Rate (%)",
|
| 417 |
-
"Targets Used",
|
| 418 |
-
"Ensemble Sizes Used",
|
| 419 |
-
"Benchmarks Used",
|
| 420 |
-
]
|
| 421 |
-
else:
|
| 422 |
-
table_column_order = [
|
| 423 |
-
"Placement",
|
| 424 |
-
"abbreviation",
|
| 425 |
-
"Method",
|
| 426 |
-
"family",
|
| 427 |
-
"Score",
|
| 428 |
-
"Mean Forward Model Runs",
|
| 429 |
-
"Minimum Forward Model Runs",
|
| 430 |
-
"Mean Failure Rate (%)",
|
| 431 |
-
"Optimal Ensemble Size",
|
| 432 |
-
"Targets Used",
|
| 433 |
-
"Ensemble Sizes Used",
|
| 434 |
-
"Benchmarks Used",
|
| 435 |
-
]
|
| 436 |
-
|
| 437 |
st.subheader(f"Ranked Leaderboard — {selected_benchmark}")
|
| 438 |
st.dataframe(
|
| 439 |
leaderboard_df,
|
|
@@ -449,91 +470,82 @@ def render_leaderboard(
|
|
| 449 |
"Score": st.column_config.ProgressColumn("Score (0-100)", min_value=0.0, max_value=100.0, format="%.1f"),
|
| 450 |
"Optimal Ensemble Size": st.column_config.NumberColumn("Mean Optimal Ensemble Size", format="%.2f"),
|
| 451 |
"Mean Failure Rate (%)": st.column_config.NumberColumn("Mean Failure Rate (%)", format="%.1f"),
|
| 452 |
-
"Targets Used": st.column_config.NumberColumn("Targets Used", format="%d"),
|
| 453 |
"Ensemble Sizes Used": st.column_config.NumberColumn("Ensemble Sizes Used", format="%d"),
|
| 454 |
-
"Benchmarks Used": st.column_config.NumberColumn("Benchmarks Used", format="%d"),
|
| 455 |
},
|
| 456 |
column_order=table_column_order,
|
| 457 |
)
|
| 458 |
|
| 459 |
st.info(
|
| 460 |
f"Score is a normalized 0–100 ranking based on **{score_basis}**. "
|
| 461 |
-
"
|
| 462 |
-
"and all ensemble sizes after averaging over random seeds."
|
| 463 |
)
|
| 464 |
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
)
|
| 472 |
-
chart_source = chart_source[chart_source["abbreviation"].isin(selected_methods)]
|
| 473 |
-
chart_df = chart_source.dropna(subset=["metric"]).groupby(
|
| 474 |
-
["abbreviation", "ensemble_size"], as_index=False
|
| 475 |
-
).agg(mean_forward_runs=("metric", "mean"))
|
| 476 |
-
|
| 477 |
-
all_ens_combos = chart_source[["abbreviation", "ensemble_size"]].drop_duplicates()
|
| 478 |
-
ens_ticks = sorted(all_ens_combos["ensemble_size"].unique().tolist()) if not all_ens_combos.empty else []
|
| 479 |
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
fail_df = all_ens_combos.merge(_ok, on=["abbreviation", "ensemble_size"], how="left")
|
| 483 |
-
fail_df = fail_df[fail_df["_ok"].isna()].drop(columns="_ok").assign(mean_forward_runs=0.0)
|
| 484 |
-
else:
|
| 485 |
-
fail_df = all_ens_combos.assign(mean_forward_runs=0.0)
|
| 486 |
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
alt.Y("mean_forward_runs:Q", title="Mean Forward Model Runs"
|
| 507 |
-
|
| 508 |
-
|
| 509 |
)
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 527 |
)
|
| 528 |
-
|
| 529 |
-
|
|
|
|
| 530 |
|
| 531 |
# Mean-iterations-for-coverage section (UQ only, when budget_store provided)
|
| 532 |
-
if budget_store is not None and not budget_store.empty
|
| 533 |
bf = budget_store[budget_store["benchmark"] == selected_benchmark].copy()
|
| 534 |
bf[target_str_col] = bf[target_col].astype(str)
|
| 535 |
-
|
| 536 |
-
bf = bf[bf[target_str_col] == selected_target]
|
| 537 |
bf = bf[bf["abbreviation"].isin(selected_methods)]
|
| 538 |
|
| 539 |
iters_df = (
|
|
@@ -603,12 +615,8 @@ def render_leaderboard(
|
|
| 603 |
st.altair_chart(alt.layer(*iters_layers), use_container_width=True)
|
| 604 |
|
| 605 |
# Failure panel — rendered regardless of whether the scored table has rows
|
| 606 |
-
if show_failure_panel
|
| 607 |
-
failure_source =
|
| 608 |
-
filtered
|
| 609 |
-
if selected_target == "All targets"
|
| 610 |
-
else filtered[filtered[target_str_col] == selected_target]
|
| 611 |
-
)
|
| 612 |
failure_source = failure_source[failure_source["abbreviation"].isin(selected_methods)]
|
| 613 |
if not failure_source.empty:
|
| 614 |
failure_df = failure_source.groupby(
|
|
@@ -616,12 +624,7 @@ def render_leaderboard(
|
|
| 616 |
).agg(mean_failure_rate=("failure_rate", "mean"))
|
| 617 |
failure_df = failure_df.sort_values("ensemble_size")
|
| 618 |
|
| 619 |
-
|
| 620 |
-
"All Targets"
|
| 621 |
-
if selected_target == "All targets"
|
| 622 |
-
else f"Target {selected_target}"
|
| 623 |
-
)
|
| 624 |
-
st.subheader(f"Failure Rate of Hitting {target_str}")
|
| 625 |
ens_ticks_fail = sorted(failure_df["ensemble_size"].unique().tolist())
|
| 626 |
failure_chart = (
|
| 627 |
alt.Chart(failure_df)
|
|
|
|
| 14 |
target_label="RMSE Target Level",
|
| 15 |
title="Optimization Leaderboard",
|
| 16 |
state_prefix="opt",
|
| 17 |
+
default_target=1.1,
|
| 18 |
raw_page="pages/RawData.py",
|
| 19 |
)
|
| 20 |
"""
|
|
|
|
| 32 |
"#eeca3b", "#b279a2", "#ff9da6", "#9d755d", "#bab0ac",
|
| 33 |
]
|
| 34 |
|
| 35 |
+
_SUITABLE = "#009E73" # Okabe-Ito teal-green (colorblind-safe)
|
| 36 |
+
_UNSUITABLE = "#C0392B" # dark red
|
| 37 |
+
_UNTESTED = "#BDBDBD" # gray
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def _render_suitability_table(
|
| 41 |
+
store: pd.DataFrame,
|
| 42 |
+
target_col: str,
|
| 43 |
+
suitability_target: float,
|
| 44 |
+
benchmark_dims: dict[str, tuple[int, int, int]] | None = None,
|
| 45 |
+
failure_threshold: float = 20.0,
|
| 46 |
+
ratio_threshold: float = 3.0,
|
| 47 |
+
) -> None:
|
| 48 |
+
"""Render the method × benchmark suitability grid above the leaderboard controls."""
|
| 49 |
+
benchmarks = sorted(
|
| 50 |
+
store["benchmark"].unique().tolist(),
|
| 51 |
+
key=lambda bm: benchmark_dims[bm][0] if (benchmark_dims and bm in benchmark_dims) else bm,
|
| 52 |
+
)
|
| 53 |
+
methods = sorted(store["abbreviation"].dropna().unique().tolist())
|
| 54 |
+
|
| 55 |
+
target_str = str(float(suitability_target))
|
| 56 |
+
target_str_col = f"{target_col}_str"
|
| 57 |
+
target_df = store.copy()
|
| 58 |
+
target_df[target_str_col] = target_df[target_col].astype(str)
|
| 59 |
+
target_df = target_df[target_df[target_str_col] == target_str]
|
| 60 |
+
|
| 61 |
+
# Global best per benchmark: min metric across all methods with failure_rate < threshold
|
| 62 |
+
global_best: dict[str, float | None] = {}
|
| 63 |
+
for bm in benchmarks:
|
| 64 |
+
bm_df = target_df[target_df["benchmark"] == bm]
|
| 65 |
+
qualifying = bm_df[bm_df["failure_rate"] < failure_threshold].dropna(subset=["metric"])
|
| 66 |
+
global_best[bm] = float(qualifying["metric"].min()) if not qualifying.empty else None
|
| 67 |
+
|
| 68 |
+
cell_text: dict[str, dict[str, str]] = {}
|
| 69 |
+
cell_color: dict[str, dict[str, str]] = {}
|
| 70 |
+
|
| 71 |
+
for method in methods:
|
| 72 |
+
cell_text[method] = {}
|
| 73 |
+
cell_color[method] = {}
|
| 74 |
+
for bm in benchmarks:
|
| 75 |
+
sub = target_df[
|
| 76 |
+
(target_df["abbreviation"] == method) & (target_df["benchmark"] == bm)
|
| 77 |
+
]
|
| 78 |
+
if sub.empty:
|
| 79 |
+
cell_text[method][bm] = "—"
|
| 80 |
+
cell_color[method][bm] = _UNTESTED
|
| 81 |
+
continue
|
| 82 |
+
qualifying = sub[sub["failure_rate"] < failure_threshold].dropna(subset=["metric"])
|
| 83 |
+
if qualifying.empty:
|
| 84 |
+
cell_text[method][bm] = "failed"
|
| 85 |
+
cell_color[method][bm] = _UNSUITABLE
|
| 86 |
+
continue
|
| 87 |
+
method_best = float(qualifying["metric"].min())
|
| 88 |
+
gb = global_best.get(bm)
|
| 89 |
+
ratio = (method_best / gb) if (gb is not None and gb > 0) else 1.0
|
| 90 |
+
cell_text[method][bm] = f"{ratio:.1f}×"
|
| 91 |
+
cell_color[method][bm] = _SUITABLE if ratio <= ratio_threshold else _UNSUITABLE
|
| 92 |
+
|
| 93 |
+
col_labels: dict[str, str] = {}
|
| 94 |
+
for bm in benchmarks:
|
| 95 |
+
if benchmark_dims and bm in benchmark_dims:
|
| 96 |
+
p, s, o = benchmark_dims[bm]
|
| 97 |
+
col_labels[bm] = f"{bm} (p={p}, s={s}, o={o})"
|
| 98 |
+
else:
|
| 99 |
+
col_labels[bm] = bm
|
| 100 |
+
|
| 101 |
+
display_df = pd.DataFrame(cell_text).T.rename(columns=col_labels)
|
| 102 |
+
color_df = pd.DataFrame(cell_color).T.rename(columns=col_labels)
|
| 103 |
+
display_df.index.name = "Method"
|
| 104 |
+
|
| 105 |
+
def _style(df: pd.DataFrame) -> pd.DataFrame:
|
| 106 |
+
result = pd.DataFrame("", index=df.index, columns=df.columns)
|
| 107 |
+
for row in df.index:
|
| 108 |
+
for col in df.columns:
|
| 109 |
+
bg = color_df.loc[row, col]
|
| 110 |
+
fg = "#212529" if bg == _UNTESTED else "white"
|
| 111 |
+
result.loc[row, col] = (
|
| 112 |
+
f"background-color: {bg}; color: {fg}; "
|
| 113 |
+
"text-align: center; font-weight: bold"
|
| 114 |
+
)
|
| 115 |
+
return result
|
| 116 |
+
|
| 117 |
+
st.subheader("Method Suitability Overview")
|
| 118 |
+
st.caption(
|
| 119 |
+
f"Evaluated at target = {suitability_target}. "
|
| 120 |
+
f"**Green**: at some ensemble size, failure rate < {failure_threshold:.0f}% "
|
| 121 |
+
f"and mean budget ≤ {ratio_threshold:.0f}× the best method (ratio shown). "
|
| 122 |
+
"**Red**: does not meet criteria. "
|
| 123 |
+
"**Gray**: no data for this experiment."
|
| 124 |
+
)
|
| 125 |
+
st.dataframe(display_df.style.apply(_style, axis=None), use_container_width=True)
|
| 126 |
+
|
| 127 |
|
| 128 |
def render_leaderboard(
|
| 129 |
metric_store: pd.DataFrame,
|
|
|
|
| 132 |
target_label: str,
|
| 133 |
title: str,
|
| 134 |
state_prefix: str,
|
| 135 |
+
default_target: float,
|
| 136 |
raw_page: str | None = None,
|
| 137 |
show_failure_panel: bool = False,
|
| 138 |
show_scoring_modes: bool = True,
|
| 139 |
canonical_target_levels: list[float] | None = None,
|
| 140 |
budget_store: pd.DataFrame | None = None,
|
| 141 |
+
benchmark_dims: dict[str, tuple[int, int, int]] | None = None,
|
| 142 |
) -> None:
|
| 143 |
"""Render a scored leaderboard backed by *metric_store*.
|
| 144 |
|
|
|
|
| 161 |
Short string used to namespace ``st.session_state`` keys so multiple
|
| 162 |
leaderboard pages keep independent control state. Use ``"opt"`` for
|
| 163 |
the Optimization leaderboard and ``"uq"`` for the UQ leaderboard.
|
| 164 |
+
default_target:
|
| 165 |
+
Target level shown on first load (e.g. ``1.1`` for optimization,
|
| 166 |
+
``1.5`` for UQ).
|
| 167 |
raw_page:
|
| 168 |
Optional Streamlit page path for an "Open Raw Data" link shown at the
|
| 169 |
bottom. Pass ``None`` to suppress the link.
|
| 170 |
show_failure_panel:
|
| 171 |
If ``True``, render a grouped-bar failure-rate chart below the main
|
| 172 |
performance chart.
|
| 173 |
+
show_scoring_modes:
|
| 174 |
+
If ``True``, show scoring-mode radio controls in the expander.
|
| 175 |
canonical_target_levels:
|
| 176 |
If provided, the target-level selector always offers exactly these
|
| 177 |
values (as strings) regardless of what is present in the data. Use
|
|
|
|
| 179 |
levels even when some have 100 % failure.
|
| 180 |
budget_store:
|
| 181 |
Optional DataFrame produced by ``load_uq_budget_store()``. When
|
| 182 |
+
provided, an additional "Mean Iterations for Coverage" section is
|
| 183 |
+
rendered below the main performance chart.
|
| 184 |
+
benchmark_dims:
|
| 185 |
+
Optional mapping of benchmark name → (param_dim, state_dim, output_dim)
|
| 186 |
+
used to annotate column headers in the suitability table.
|
| 187 |
"""
|
| 188 |
st.header(title)
|
| 189 |
|
|
|
|
| 191 |
st.warning("No metric data found. Expected NetCDF files in `data/` with a `metric` variable.")
|
| 192 |
return
|
| 193 |
|
| 194 |
+
_render_suitability_table(
|
| 195 |
+
metric_store, target_col, default_target, benchmark_dims=benchmark_dims
|
| 196 |
+
)
|
| 197 |
+
|
| 198 |
+
st.divider()
|
| 199 |
+
|
| 200 |
# Derived column name for the string version of the target coordinate
|
| 201 |
target_str_col = f"{target_col}_str"
|
| 202 |
|
| 203 |
+
benchmark_values = sorted(metric_store["benchmark"].unique().tolist())
|
| 204 |
+
selected_benchmark = st.selectbox("Benchmark", options=benchmark_values, index=0)
|
|
|
|
| 205 |
|
| 206 |
+
filtered = metric_store[metric_store["benchmark"] == selected_benchmark].copy()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 207 |
filtered[target_str_col] = filtered[target_col].astype(str)
|
| 208 |
|
| 209 |
if canonical_target_levels is not None:
|
| 210 |
+
target_options = [str(float(t)) for t in canonical_target_levels]
|
| 211 |
else:
|
| 212 |
+
target_options = sorted(
|
| 213 |
metric_store[target_col].astype(str).unique().tolist()
|
| 214 |
)
|
| 215 |
|
|
|
|
| 222 |
|
| 223 |
# Session-state keys namespaced by state_prefix so two leaderboard pages
|
| 224 |
# don't share control state.
|
| 225 |
+
k_target = f"{state_prefix}_selected_target"
|
| 226 |
k_scoring = f"{state_prefix}_scoring_mode"
|
| 227 |
+
k_weight = f"{state_prefix}_fwdruns_weight_percent"
|
| 228 |
k_methods = f"{state_prefix}_selected_methods"
|
| 229 |
|
| 230 |
+
default_target_str = str(float(default_target))
|
| 231 |
+
|
| 232 |
+
# Pre-populate session state so the radio widget and the filter agree on first load.
|
| 233 |
+
if k_target not in st.session_state or st.session_state[k_target] not in target_options:
|
| 234 |
+
st.session_state[k_target] = (
|
| 235 |
+
default_target_str if default_target_str in target_options else target_options[0]
|
| 236 |
+
)
|
| 237 |
+
selected_target = st.session_state[k_target]
|
| 238 |
|
| 239 |
current_scoring_mode = st.session_state.get(k_scoring, "Mean Forward Model Runs")
|
| 240 |
if current_scoring_mode not in scoring_options:
|
|
|
|
| 243 |
current_fwdruns_weight_percent = int(st.session_state.get(k_weight, 80))
|
| 244 |
current_fwdruns_weight_percent = max(0, min(100, current_fwdruns_weight_percent))
|
| 245 |
|
| 246 |
+
scoring_mode = current_scoring_mode
|
| 247 |
+
fwdruns_weight = current_fwdruns_weight_percent / 100.0
|
|
|
|
| 248 |
ensemble_weight = 1.0 - fwdruns_weight
|
| 249 |
|
| 250 |
# Available methods for the current benchmark selection; used to populate the
|
| 251 |
# multiselect and to prune any stale saved selections when the benchmark changes.
|
| 252 |
available_methods = sorted(filtered["abbreviation"].dropna().unique().tolist())
|
| 253 |
+
saved_methods = st.session_state.get(k_methods, available_methods)
|
| 254 |
+
valid_saved = [m for m in saved_methods if m in available_methods]
|
| 255 |
st.session_state[k_methods] = valid_saved if valid_saved else available_methods
|
| 256 |
|
| 257 |
# Stable color scale: domain covers ALL methods so colors don't shift when a
|
| 258 |
# subset is displayed.
|
| 259 |
color_domain = available_methods
|
| 260 |
+
color_range = [_METHOD_PALETTE[i % len(_METHOD_PALETTE)] for i in range(len(available_methods))]
|
| 261 |
method_color = alt.Color(
|
| 262 |
"abbreviation:N",
|
| 263 |
title="Method",
|
|
|
|
| 265 |
)
|
| 266 |
|
| 267 |
def build_scored_table(input_df: pd.DataFrame, add_rank: bool = True) -> pd.DataFrame:
|
| 268 |
+
ranking_source = input_df[input_df[target_str_col] == selected_target]
|
|
|
|
|
|
|
|
|
|
|
|
|
| 269 |
if ranking_source.empty:
|
| 270 |
return ranking_source
|
| 271 |
|
|
|
|
| 284 |
).agg(
|
| 285 |
**{"Mean Forward Model Runs": ("metric", "mean")},
|
| 286 |
**{"Minimum Forward Model Runs": ("metric", "min")},
|
|
|
|
| 287 |
**{"Ensemble Sizes Used": ("ensemble_size", "nunique")},
|
| 288 |
)
|
| 289 |
|
|
|
|
| 317 |
how="left",
|
| 318 |
)
|
| 319 |
|
| 320 |
+
scored_df["Optimal Ensemble Size"] = scored_df["Optimal Ensemble Size"].round(2)
|
| 321 |
+
scored_df["Mean Forward Model Runs"] = scored_df["Mean Forward Model Runs"].round(4)
|
| 322 |
scored_df["Minimum Forward Model Runs"] = scored_df["Minimum Forward Model Runs"].round(4)
|
| 323 |
+
scored_df["Mean Failure Rate (%)"] = scored_df["Mean Failure Rate (%)"].round(1)
|
| 324 |
|
| 325 |
mean_runs_min = scored_df["Mean Forward Model Runs"].min()
|
| 326 |
mean_runs_max = scored_df["Mean Forward Model Runs"].max()
|
|
|
|
| 379 |
|
| 380 |
return scored_df
|
| 381 |
|
| 382 |
+
leaderboard_df = build_scored_table(filtered, add_rank=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 383 |
|
| 384 |
if scoring_mode == "Mean Forward Model Runs":
|
| 385 |
+
score_basis = "mean forward-model runs at the selected target level (lower is better)"
|
| 386 |
elif scoring_mode == "Minimum Forward Model Runs":
|
| 387 |
+
score_basis = "minimum forward-model runs at the selected target level (lower is better)"
|
| 388 |
elif scoring_mode == "Smallest Optimal Ensemble Size":
|
| 389 |
+
score_basis = "mean optimal ensemble size at the selected target level (lower is better)"
|
| 390 |
else:
|
| 391 |
score_basis = (
|
| 392 |
+
f"weighted blend of normalized forward-model-runs score ({fwdruns_weight:.0%}) "
|
| 393 |
+
f"and normalized ensemble-size score ({ensemble_weight:.0%})"
|
| 394 |
)
|
| 395 |
|
|
|
|
|
|
|
|
|
|
| 396 |
# Controls expander — always shown so users can change target even when the
|
| 397 |
# current selection yields all failures.
|
| 398 |
with st.expander("Scoring & Target Controls", expanded=False):
|
|
|
|
| 434 |
if not selected_methods:
|
| 435 |
selected_methods = available_methods
|
| 436 |
|
| 437 |
+
table_column_order = [
|
| 438 |
+
"Placement",
|
| 439 |
+
"abbreviation",
|
| 440 |
+
"Method",
|
| 441 |
+
"family",
|
| 442 |
+
"Score",
|
| 443 |
+
"Mean Forward Model Runs",
|
| 444 |
+
"Minimum Forward Model Runs",
|
| 445 |
+
"Mean Failure Rate (%)",
|
| 446 |
+
"Optimal Ensemble Size",
|
| 447 |
+
"Ensemble Sizes Used",
|
| 448 |
+
]
|
| 449 |
+
|
| 450 |
if leaderboard_df.empty:
|
| 451 |
st.warning(
|
| 452 |
"All runs failed to reach the target at this selection. "
|
|
|
|
| 455 |
else "No rows available for the current benchmark/target selection."
|
| 456 |
)
|
| 457 |
else:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 458 |
st.subheader(f"Ranked Leaderboard — {selected_benchmark}")
|
| 459 |
st.dataframe(
|
| 460 |
leaderboard_df,
|
|
|
|
| 470 |
"Score": st.column_config.ProgressColumn("Score (0-100)", min_value=0.0, max_value=100.0, format="%.1f"),
|
| 471 |
"Optimal Ensemble Size": st.column_config.NumberColumn("Mean Optimal Ensemble Size", format="%.2f"),
|
| 472 |
"Mean Failure Rate (%)": st.column_config.NumberColumn("Mean Failure Rate (%)", format="%.1f"),
|
|
|
|
| 473 |
"Ensemble Sizes Used": st.column_config.NumberColumn("Ensemble Sizes Used", format="%d"),
|
|
|
|
| 474 |
},
|
| 475 |
column_order=table_column_order,
|
| 476 |
)
|
| 477 |
|
| 478 |
st.info(
|
| 479 |
f"Score is a normalized 0–100 ranking based on **{score_basis}**. "
|
| 480 |
+
"Values are computed from all ensemble sizes after averaging over random seeds."
|
|
|
|
| 481 |
)
|
| 482 |
|
| 483 |
+
st.subheader("Mean Forward Model Runs vs Ensemble Size")
|
| 484 |
+
chart_source = filtered[filtered[target_str_col] == selected_target]
|
| 485 |
+
chart_source = chart_source[chart_source["abbreviation"].isin(selected_methods)]
|
| 486 |
+
chart_df = chart_source.dropna(subset=["metric"]).groupby(
|
| 487 |
+
["abbreviation", "ensemble_size"], as_index=False
|
| 488 |
+
).agg(mean_forward_runs=("metric", "mean"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 489 |
|
| 490 |
+
all_ens_combos = chart_source[["abbreviation", "ensemble_size"]].drop_duplicates()
|
| 491 |
+
ens_ticks = sorted(all_ens_combos["ensemble_size"].unique().tolist()) if not all_ens_combos.empty else []
|
|
|
|
|
|
|
|
|
|
|
|
|
| 492 |
|
| 493 |
+
if not chart_df.empty:
|
| 494 |
+
_ok = chart_df[["abbreviation", "ensemble_size"]].assign(_ok=True)
|
| 495 |
+
fail_df = all_ens_combos.merge(_ok, on=["abbreviation", "ensemble_size"], how="left")
|
| 496 |
+
fail_df = fail_df[fail_df["_ok"].isna()].drop(columns="_ok").assign(mean_forward_runs=0.0)
|
| 497 |
+
else:
|
| 498 |
+
fail_df = all_ens_combos.assign(mean_forward_runs=0.0)
|
| 499 |
+
|
| 500 |
+
all_failed = chart_df.empty
|
| 501 |
+
chart_layers = []
|
| 502 |
+
if not chart_df.empty:
|
| 503 |
+
chart_layers.append(
|
| 504 |
+
alt.Chart(chart_df)
|
| 505 |
+
.mark_line(point=True)
|
| 506 |
+
.encode(
|
| 507 |
+
x=alt.X(
|
| 508 |
+
"ensemble_size:Q",
|
| 509 |
+
title="Ensemble Size",
|
| 510 |
+
axis=alt.Axis(values=ens_ticks, format="d"),
|
| 511 |
+
),
|
| 512 |
+
y=alt.Y("mean_forward_runs:Q", title="Mean Forward Model Runs"),
|
| 513 |
+
color=method_color,
|
| 514 |
+
tooltip=["abbreviation", "ensemble_size", alt.Tooltip("mean_forward_runs:Q", format=".4f")],
|
| 515 |
)
|
| 516 |
+
)
|
| 517 |
+
if not fail_df.empty:
|
| 518 |
+
y_fwd = (
|
| 519 |
+
alt.Y("mean_forward_runs:Q", title="Mean Forward Model Runs", scale=alt.Scale(domain=[0, 1]))
|
| 520 |
+
if all_failed
|
| 521 |
+
else alt.Y("mean_forward_runs:Q", title="Mean Forward Model Runs")
|
| 522 |
+
)
|
| 523 |
+
chart_layers.append(
|
| 524 |
+
alt.Chart(fail_df)
|
| 525 |
+
.mark_point(shape="cross", angle=45, size=200, filled=True, opacity=1.0)
|
| 526 |
+
.encode(
|
| 527 |
+
x=alt.X(
|
| 528 |
+
"ensemble_size:Q",
|
| 529 |
+
title="Ensemble Size",
|
| 530 |
+
axis=alt.Axis(values=ens_ticks, format="d"),
|
| 531 |
+
),
|
| 532 |
+
y=y_fwd,
|
| 533 |
+
color=method_color,
|
| 534 |
+
tooltip=[
|
| 535 |
+
alt.Tooltip("abbreviation:N", title="Method"),
|
| 536 |
+
alt.Tooltip("ensemble_size:Q", title="Ensemble Size"),
|
| 537 |
+
alt.Tooltip("mean_forward_runs:Q", title="Value (all failed)"),
|
| 538 |
+
],
|
| 539 |
)
|
| 540 |
+
)
|
| 541 |
+
if chart_layers:
|
| 542 |
+
st.altair_chart(alt.layer(*chart_layers), use_container_width=True)
|
| 543 |
|
| 544 |
# Mean-iterations-for-coverage section (UQ only, when budget_store provided)
|
| 545 |
+
if budget_store is not None and not budget_store.empty:
|
| 546 |
bf = budget_store[budget_store["benchmark"] == selected_benchmark].copy()
|
| 547 |
bf[target_str_col] = bf[target_col].astype(str)
|
| 548 |
+
bf = bf[bf[target_str_col] == selected_target]
|
|
|
|
| 549 |
bf = bf[bf["abbreviation"].isin(selected_methods)]
|
| 550 |
|
| 551 |
iters_df = (
|
|
|
|
| 615 |
st.altair_chart(alt.layer(*iters_layers), use_container_width=True)
|
| 616 |
|
| 617 |
# Failure panel — rendered regardless of whether the scored table has rows
|
| 618 |
+
if show_failure_panel:
|
| 619 |
+
failure_source = filtered[filtered[target_str_col] == selected_target]
|
|
|
|
|
|
|
|
|
|
|
|
|
| 620 |
failure_source = failure_source[failure_source["abbreviation"].isin(selected_methods)]
|
| 621 |
if not failure_source.empty:
|
| 622 |
failure_df = failure_source.groupby(
|
|
|
|
| 624 |
).agg(mean_failure_rate=("failure_rate", "mean"))
|
| 625 |
failure_df = failure_df.sort_values("ensemble_size")
|
| 626 |
|
| 627 |
+
st.subheader(f"Failure Rate of Hitting Target {selected_target}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 628 |
ens_ticks_fail = sorted(failure_df["ensemble_size"].unique().tolist())
|
| 629 |
failure_chart = (
|
| 630 |
alt.Chart(failure_df)
|
src/data_store.py
CHANGED
|
@@ -87,6 +87,15 @@ UQ_BUDGET_FILES: dict[str, list[tuple[str, str]]] = {
|
|
| 87 |
# Budget = N_ens · k_iter where first k s.t. |S(q)−q| ≤ c·√(q(1−q)/N_y) for ALL q below.
|
| 88 |
UQ_COVERAGE_QUANTILES: list[float] = [0.15, 0.5, 0.85]
|
| 89 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
|
| 91 |
|
| 92 |
# ---------------------------------------------------------------------------
|
|
|
|
| 87 |
# Budget = N_ens · k_iter where first k s.t. |S(q)−q| ≤ c·√(q(1−q)/N_y) for ALL q below.
|
| 88 |
UQ_COVERAGE_QUANTILES: list[float] = [0.15, 0.5, 0.85]
|
| 89 |
|
| 90 |
+
# Physical dimensions of each benchmark: (param_dim, state_dim, output_dim).
|
| 91 |
+
# Used only for column-header annotations in the suitability table.
|
| 92 |
+
BENCHMARK_DIMS: dict[str, tuple[int, int, int]] = {
|
| 93 |
+
"L63": (2, 3, 9),
|
| 94 |
+
"L96": (1, 40, 80),
|
| 95 |
+
"L96_NN_FORCING": (61, 100, 200),
|
| 96 |
+
"L96_SPATIAL_FORCING": (40, 40, 80),
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
|
| 100 |
|
| 101 |
# ---------------------------------------------------------------------------
|
src/pages/OptimizationLeaderboard.py
CHANGED
|
@@ -4,11 +4,11 @@ import sys
|
|
| 4 |
import streamlit as st
|
| 5 |
|
| 6 |
try:
|
| 7 |
-
from data_store import load_metric_store
|
| 8 |
from common.leaderboard import render_leaderboard
|
| 9 |
except ModuleNotFoundError:
|
| 10 |
sys.path.append(str(Path(__file__).resolve().parents[1]))
|
| 11 |
-
from data_store import load_metric_store
|
| 12 |
from common.leaderboard import render_leaderboard
|
| 13 |
|
| 14 |
st.set_page_config(page_title="Optimization Leaderboard", page_icon="📊", layout="wide")
|
|
@@ -31,5 +31,7 @@ render_leaderboard(
|
|
| 31 |
target_label="RMSE Target Level",
|
| 32 |
title="Optimization Leaderboard",
|
| 33 |
state_prefix="opt",
|
|
|
|
| 34 |
raw_page="pages/RawData.py",
|
|
|
|
| 35 |
)
|
|
|
|
| 4 |
import streamlit as st
|
| 5 |
|
| 6 |
try:
|
| 7 |
+
from data_store import load_metric_store, BENCHMARK_DIMS
|
| 8 |
from common.leaderboard import render_leaderboard
|
| 9 |
except ModuleNotFoundError:
|
| 10 |
sys.path.append(str(Path(__file__).resolve().parents[1]))
|
| 11 |
+
from data_store import load_metric_store, BENCHMARK_DIMS
|
| 12 |
from common.leaderboard import render_leaderboard
|
| 13 |
|
| 14 |
st.set_page_config(page_title="Optimization Leaderboard", page_icon="📊", layout="wide")
|
|
|
|
| 31 |
target_label="RMSE Target Level",
|
| 32 |
title="Optimization Leaderboard",
|
| 33 |
state_prefix="opt",
|
| 34 |
+
default_target=1.1,
|
| 35 |
raw_page="pages/RawData.py",
|
| 36 |
+
benchmark_dims=BENCHMARK_DIMS,
|
| 37 |
)
|
src/pages/UQLeaderboard.py
CHANGED
|
@@ -4,11 +4,11 @@ import sys
|
|
| 4 |
import streamlit as st
|
| 5 |
|
| 6 |
try:
|
| 7 |
-
from data_store import load_uq_store, load_uq_budget_store, UQ_TARGET_LEVELS
|
| 8 |
from common.leaderboard import render_leaderboard
|
| 9 |
except ModuleNotFoundError:
|
| 10 |
sys.path.append(str(Path(__file__).resolve().parents[1]))
|
| 11 |
-
from data_store import load_uq_store, load_uq_budget_store, UQ_TARGET_LEVELS
|
| 12 |
from common.leaderboard import render_leaderboard
|
| 13 |
|
| 14 |
st.set_page_config(page_title="UQ Leaderboard", page_icon="🎯", layout="wide")
|
|
@@ -33,8 +33,10 @@ render_leaderboard(
|
|
| 33 |
target_label="UQ Target (coverage tolerance scaling)",
|
| 34 |
title="Uncertainty Quantification Leaderboard",
|
| 35 |
state_prefix="uq",
|
|
|
|
| 36 |
show_failure_panel=True,
|
| 37 |
show_scoring_modes=False,
|
| 38 |
canonical_target_levels=UQ_TARGET_LEVELS,
|
| 39 |
budget_store=load_uq_budget_store(),
|
|
|
|
| 40 |
)
|
|
|
|
| 4 |
import streamlit as st
|
| 5 |
|
| 6 |
try:
|
| 7 |
+
from data_store import load_uq_store, load_uq_budget_store, UQ_TARGET_LEVELS, BENCHMARK_DIMS
|
| 8 |
from common.leaderboard import render_leaderboard
|
| 9 |
except ModuleNotFoundError:
|
| 10 |
sys.path.append(str(Path(__file__).resolve().parents[1]))
|
| 11 |
+
from data_store import load_uq_store, load_uq_budget_store, UQ_TARGET_LEVELS, BENCHMARK_DIMS
|
| 12 |
from common.leaderboard import render_leaderboard
|
| 13 |
|
| 14 |
st.set_page_config(page_title="UQ Leaderboard", page_icon="🎯", layout="wide")
|
|
|
|
| 33 |
target_label="UQ Target (coverage tolerance scaling)",
|
| 34 |
title="Uncertainty Quantification Leaderboard",
|
| 35 |
state_prefix="uq",
|
| 36 |
+
default_target=1.5,
|
| 37 |
show_failure_panel=True,
|
| 38 |
show_scoring_modes=False,
|
| 39 |
canonical_target_levels=UQ_TARGET_LEVELS,
|
| 40 |
budget_store=load_uq_budget_store(),
|
| 41 |
+
benchmark_dims=BENCHMARK_DIMS,
|
| 42 |
)
|