Redesign dashboard around leaderboard
Browse files- README.md +7 -8
- app.py +956 -689
- assets/styles.css +174 -87
README.md
CHANGED
|
@@ -1,18 +1,17 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
sdk: docker
|
| 4 |
app_port: 7860
|
| 5 |
---
|
| 6 |
|
| 7 |
-
#
|
| 8 |
|
| 9 |
-
Interactive
|
| 10 |
-
benchmark.
|
| 11 |
|
| 12 |
-
The dashboard
|
| 13 |
-
|
| 14 |
-
|
| 15 |
|
| 16 |
-
The bundled files in `data/` are lightweight summary
|
| 17 |
inspection. The full benchmark artifacts and reproduction workflow live in the
|
| 18 |
main benchmark repository.
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Neural Model Benchmark
|
| 3 |
sdk: docker
|
| 4 |
app_port: 7860
|
| 5 |
---
|
| 6 |
|
| 7 |
+
# Neural Model Benchmark
|
| 8 |
|
| 9 |
+
Interactive dashboard for the Tang Lab neural model benchmark.
|
|
|
|
| 10 |
|
| 11 |
+
The dashboard compares 23 methods across five datasets, with sortable task
|
| 12 |
+
performance, robustness, cross-session alignment, neuron and trial influence,
|
| 13 |
+
compute cost, and 3D latent-space views.
|
| 14 |
|
| 15 |
+
The bundled files in `data/` are lightweight summary tables for interactive
|
| 16 |
inspection. The full benchmark artifacts and reproduction workflow live in the
|
| 17 |
main benchmark repository.
|
app.py
CHANGED
|
@@ -1,11 +1,11 @@
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
|
|
|
| 3 |
from pathlib import Path
|
| 4 |
from typing import Iterable
|
| 5 |
|
| 6 |
import numpy as np
|
| 7 |
import pandas as pd
|
| 8 |
-
import plotly.express as px
|
| 9 |
import plotly.graph_objects as go
|
| 10 |
from dash import Dash, Input, Output, dash_table, dcc, html
|
| 11 |
from plotly.subplots import make_subplots
|
|
@@ -13,50 +13,172 @@ from plotly.subplots import make_subplots
|
|
| 13 |
|
| 14 |
DATA_DIR = Path(__file__).resolve().parent / "data"
|
| 15 |
|
| 16 |
-
|
| 17 |
-
"blend"
|
| 18 |
-
"blend_ndt"
|
| 19 |
-
"cebra": ("gpu", "full_pipeline", "pytorch_gpu.sif", "stage1, stage2, comprehensive, consistency, neuron_shap"),
|
| 20 |
-
"dnn": ("gpu", "comprehensive_only", "tensorflow_gpu.sif", "stage1, stage2, comprehensive, neuron_shap"),
|
| 21 |
-
"dpad": ("gpu", "full_pipeline", "tensorflow_gpu.sif", "comprehensive, consistency, neuron_shap"),
|
| 22 |
-
"gpfa": ("cpu", "full_pipeline", "sklearn_cpu.sif", "stage1, stage2, comprehensive, consistency, neuron_shap"),
|
| 23 |
-
"gru": ("gpu", "comprehensive_only", "tensorflow_gpu.sif", "stage1, stage2, comprehensive, neuron_shap"),
|
| 24 |
-
"langevinflow_ccn": ("gpu", "full_pipeline", "pytorch_gpu.sif", "stage1, stage2, comprehensive, neuron_shap"),
|
| 25 |
-
"ldns": ("gpu", "full_pipeline", "pytorch_gpu.sif", "stage1, stage2, comprehensive, consistency, neuron_shap"),
|
| 26 |
-
"lfads_torch": ("gpu", "full_pipeline", "lfads_torch.sif", "stage1, stage2, comprehensive, consistency, neuron_shap"),
|
| 27 |
-
"lstm": ("gpu", "comprehensive_only", "tensorflow_gpu.sif", "stage1, stage2, comprehensive, neuron_shap"),
|
| 28 |
-
"marble": ("gpu", "full_pipeline", "marble_h100.sif", "stage1, stage2, comprehensive, consistency, neuron_shap"),
|
| 29 |
-
"mint": ("cpu", "comprehensive_only", "mint.sif", "comprehensive, neuron_shap, robustness"),
|
| 30 |
-
"neds": ("gpu", "full_pipeline", "neds.sif", "stage1, stage2, comprehensive, neuron_shap"),
|
| 31 |
-
"neds_pretrained": ("gpu", "full_pipeline", "neds.sif", "stage1, stage2, comprehensive, neuron_shap"),
|
| 32 |
-
"neuro_behavior_conditioning": ("gpu", "full_pipeline", "pytorch_gpu.sif", "comprehensive, consistency, neuron_shap"),
|
| 33 |
-
"pca": ("cpu", "full_pipeline", "sklearn_cpu.sif", "stage1, stage2, comprehensive, consistency, neuron_shap"),
|
| 34 |
-
"rnn": ("gpu", "comprehensive_only", "tensorflow_gpu.sif", "stage1, stage2, comprehensive, neuron_shap"),
|
| 35 |
-
"smc_rnns": ("gpu", "full_pipeline", "pytorch_gpu.sif", "stage1, stage2, comprehensive, consistency, neuron_shap"),
|
| 36 |
-
"svc": ("cpu", "comprehensive_only", "sklearn_cpu.sif", "comprehensive, neuron_shap"),
|
| 37 |
-
"tndm": ("gpu", "full_pipeline", "tensorflow_gpu.sif", "stage1, stage2, comprehensive, consistency, neuron_shap"),
|
| 38 |
-
"torchdfine": ("gpu", "full_pipeline", "pytorch_gpu.sif", "comprehensive, consistency, neuron_shap"),
|
| 39 |
-
"xg": ("cpu", "comprehensive_only", "sklearn_cpu.sif", "comprehensive, neuron_shap"),
|
| 40 |
-
}
|
| 41 |
-
|
| 42 |
-
DEFAULT_MODELS = [
|
| 43 |
-
"pca",
|
| 44 |
-
"gpfa",
|
| 45 |
"cebra",
|
|
|
|
| 46 |
"dpad",
|
|
|
|
|
|
|
|
|
|
| 47 |
"ldns",
|
| 48 |
"lfads_torch",
|
|
|
|
| 49 |
"marble",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
"rnn",
|
|
|
|
| 51 |
"svc",
|
|
|
|
|
|
|
| 52 |
"xg",
|
| 53 |
]
|
| 54 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
|
| 56 |
def load_csv(name: str) -> pd.DataFrame:
|
| 57 |
path = DATA_DIR / name
|
| 58 |
if not path.exists():
|
| 59 |
-
raise FileNotFoundError(f"Missing
|
| 60 |
return pd.read_csv(path)
|
| 61 |
|
| 62 |
|
|
@@ -76,6 +198,12 @@ def present_rows(df: pd.DataFrame) -> pd.DataFrame:
|
|
| 76 |
return df[df["status"].fillna("") == "present"].copy()
|
| 77 |
|
| 78 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
def ordered_unique(values: Iterable[object]) -> list[str]:
|
| 80 |
seen: set[str] = set()
|
| 81 |
out: list[str] = []
|
|
@@ -90,8 +218,6 @@ def ordered_unique(values: Iterable[object]) -> list[str]:
|
|
| 90 |
|
| 91 |
|
| 92 |
def build_dataset_labels() -> dict[str, str]:
|
| 93 |
-
if prediction.empty:
|
| 94 |
-
return {}
|
| 95 |
pairs = (
|
| 96 |
prediction[["dataset", "dataset_display"]]
|
| 97 |
.dropna(subset=["dataset"])
|
|
@@ -102,8 +228,10 @@ def build_dataset_labels() -> dict[str, str]:
|
|
| 102 |
|
| 103 |
DATASET_LABELS = build_dataset_labels()
|
| 104 |
DATASETS = ordered_unique(prediction.get("dataset", pd.Series(dtype=str)))
|
| 105 |
-
|
| 106 |
-
|
|
|
|
|
|
|
| 107 |
|
| 108 |
CONDITION_LABELS = {
|
| 109 |
"monkey": {
|
|
@@ -133,6 +261,33 @@ CONDITION_LABELS = {
|
|
| 133 |
}
|
| 134 |
|
| 135 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
def condition_sort_key(value: object) -> tuple[int, float | str]:
|
| 137 |
try:
|
| 138 |
return (0, float(value))
|
|
@@ -156,71 +311,91 @@ def condition_label(dataset: str, condition: object) -> str:
|
|
| 156 |
return text
|
| 157 |
|
| 158 |
|
| 159 |
-
def
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
.to_dict()
|
| 167 |
-
if not present.empty
|
| 168 |
-
else {}
|
| 169 |
-
)
|
| 170 |
-
for model in MODELS:
|
| 171 |
-
accelerator, pipeline, container, stages = MODEL_INFO.get(
|
| 172 |
-
model, ("unknown", "unknown", "unknown", "")
|
| 173 |
-
)
|
| 174 |
-
rows.append(
|
| 175 |
-
{
|
| 176 |
-
"model": model,
|
| 177 |
-
"accelerator": accelerator,
|
| 178 |
-
"pipeline": pipeline,
|
| 179 |
-
"container": container,
|
| 180 |
-
"present_datasets": int(coverage.get(model, 0)),
|
| 181 |
-
"expected_datasets": len(DATASETS),
|
| 182 |
-
"stages": stages,
|
| 183 |
-
}
|
| 184 |
-
)
|
| 185 |
-
return pd.DataFrame(rows).sort_values(["accelerator", "pipeline", "model"])
|
| 186 |
|
| 187 |
|
| 188 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
|
| 190 |
|
| 191 |
-
def
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
return f"{value:.{digits}g}"
|
| 198 |
|
| 199 |
|
| 200 |
-
def
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
return [m for m in models if m in MODELS]
|
| 204 |
|
| 205 |
|
| 206 |
-
def
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 211 |
|
| 212 |
|
| 213 |
def fig_layout(fig: go.Figure, *, height: int = 420) -> go.Figure:
|
| 214 |
fig.update_layout(
|
| 215 |
height=height,
|
| 216 |
-
paper_bgcolor="
|
| 217 |
-
plot_bgcolor="
|
| 218 |
-
margin=dict(l=
|
| 219 |
-
font=dict(family="Inter, Arial, sans-serif", size=13, color="#
|
|
|
|
| 220 |
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="left", x=0),
|
| 221 |
)
|
| 222 |
-
fig.update_xaxes(showgrid=True, gridcolor="#
|
| 223 |
-
fig.update_yaxes(showgrid=True, gridcolor="#
|
| 224 |
return fig
|
| 225 |
|
| 226 |
|
|
@@ -233,29 +408,11 @@ def empty_figure(message: str) -> go.Figure:
|
|
| 233 |
xref="paper",
|
| 234 |
yref="paper",
|
| 235 |
showarrow=False,
|
| 236 |
-
font=dict(size=15, color="#
|
| 237 |
)
|
| 238 |
fig.update_xaxes(visible=False)
|
| 239 |
fig.update_yaxes(visible=False)
|
| 240 |
-
return fig_layout(fig)
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
def latent_fig_layout(fig: go.Figure, *, height: int) -> go.Figure:
|
| 244 |
-
fig.update_layout(
|
| 245 |
-
height=height,
|
| 246 |
-
paper_bgcolor="white",
|
| 247 |
-
plot_bgcolor="white",
|
| 248 |
-
margin=dict(l=8, r=8, t=76, b=84),
|
| 249 |
-
font=dict(family="Inter, Arial, sans-serif", size=13, color="#1f2933"),
|
| 250 |
-
legend=dict(
|
| 251 |
-
orientation="h",
|
| 252 |
-
yanchor="top",
|
| 253 |
-
y=-0.08,
|
| 254 |
-
xanchor="left",
|
| 255 |
-
x=0,
|
| 256 |
-
),
|
| 257 |
-
)
|
| 258 |
-
return fig
|
| 259 |
|
| 260 |
|
| 261 |
def parse_float_list(value: object) -> list[float]:
|
|
@@ -273,46 +430,304 @@ def parse_float_list(value: object) -> list[float]:
|
|
| 273 |
return out
|
| 274 |
|
| 275 |
|
| 276 |
-
def
|
| 277 |
-
|
| 278 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 279 |
|
|
|
|
| 280 |
chosen = selected_models(models)
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 285 |
if df.empty:
|
| 286 |
-
|
| 287 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 288 |
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 302 |
)
|
| 303 |
-
|
| 304 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 305 |
|
| 306 |
-
plot_df = df[df["model"].astype(str) == model].copy()
|
| 307 |
for col in ["x", "y", "z"]:
|
| 308 |
plot_df[col] = pd.to_numeric(plot_df[col], errors="coerce")
|
| 309 |
plot_df["condition_num"] = pd.to_numeric(plot_df["condition"], errors="coerce")
|
| 310 |
-
plot_df["condition_label"] = plot_df["condition"].map(
|
| 311 |
-
|
| 312 |
-
)
|
| 313 |
plot_df = plot_df.dropna(subset=["x", "y", "z"])
|
| 314 |
if plot_df.empty:
|
| 315 |
-
return empty_figure("
|
| 316 |
|
| 317 |
trajectory_df = latent_trajectories[
|
| 318 |
(latent_trajectories["dataset"].astype(str) == str(dataset))
|
|
@@ -322,72 +737,48 @@ def latent_space_figure(dataset: str, models: list[str] | None) -> go.Figure:
|
|
| 322 |
if col in trajectory_df:
|
| 323 |
trajectory_df[col] = pd.to_numeric(trajectory_df[col], errors="coerce")
|
| 324 |
if not trajectory_df.empty:
|
| 325 |
-
trajectory_df["condition_label"] = trajectory_df["condition"].map(
|
| 326 |
-
|
| 327 |
-
)
|
| 328 |
trajectory_df = trajectory_df.dropna(subset=["x", "y", "z"])
|
| 329 |
|
| 330 |
sessions = ordered_unique(plot_df["session_label"])
|
| 331 |
-
|
|
|
|
| 332 |
n_rows = int(np.ceil(len(sessions) / n_cols))
|
|
|
|
| 333 |
fig = make_subplots(
|
| 334 |
rows=n_rows,
|
| 335 |
cols=n_cols,
|
| 336 |
-
specs=
|
| 337 |
-
subplot_titles=
|
| 338 |
-
horizontal_spacing=0.
|
| 339 |
-
vertical_spacing=0.
|
| 340 |
)
|
| 341 |
|
| 342 |
-
condition_values = sorted(
|
| 343 |
-
|
| 344 |
-
key=condition_sort_key,
|
| 345 |
-
)
|
| 346 |
-
use_categorical_conditions = len(condition_values) <= 12
|
| 347 |
-
direction_palette = [
|
| 348 |
-
"#B23AEE",
|
| 349 |
-
"#3B1C54",
|
| 350 |
-
"#2DD4F6",
|
| 351 |
-
"#289285",
|
| 352 |
-
"#E3D724",
|
| 353 |
-
"#00A65A",
|
| 354 |
-
"#5B8FF9",
|
| 355 |
-
"#F97316",
|
| 356 |
-
"#E45756",
|
| 357 |
-
"#72B7B2",
|
| 358 |
-
"#54A24B",
|
| 359 |
-
"#B279A2",
|
| 360 |
-
]
|
| 361 |
condition_colors = {
|
| 362 |
-
condition:
|
| 363 |
for idx, condition in enumerate(condition_values)
|
| 364 |
}
|
| 365 |
-
|
| 366 |
-
condition_prefix = "Reach direction"
|
| 367 |
-
elif dataset == "ratinabox":
|
| 368 |
-
condition_prefix = "Position bin"
|
| 369 |
-
elif dataset == "allen_neuropixels":
|
| 370 |
-
condition_prefix = "Orientation"
|
| 371 |
-
elif dataset == "speech":
|
| 372 |
-
condition_prefix = "Cue"
|
| 373 |
-
else:
|
| 374 |
-
condition_prefix = "Condition"
|
| 375 |
|
| 376 |
for session_idx, session in enumerate(sessions):
|
| 377 |
-
session_df = plot_df[plot_df["session_label"].astype(str) == session]
|
| 378 |
if session_df.empty:
|
| 379 |
continue
|
| 380 |
row = session_idx // n_cols + 1
|
| 381 |
col = session_idx % n_cols + 1
|
|
|
|
| 382 |
|
| 383 |
-
if
|
| 384 |
for condition in condition_values:
|
| 385 |
cond_df = session_df[session_df["condition"].astype(str) == condition]
|
| 386 |
if cond_df.empty:
|
| 387 |
continue
|
| 388 |
trace_name = condition_label(dataset, condition)
|
| 389 |
session_traj = trajectory_df[
|
| 390 |
-
(trajectory_df["session_label"].astype(str) == session)
|
| 391 |
& (trajectory_df["condition"].astype(str) == condition)
|
| 392 |
].sort_values("time_index")
|
| 393 |
fig.add_trace(
|
|
@@ -400,13 +791,13 @@ def latent_space_figure(dataset: str, models: list[str] | None) -> go.Figure:
|
|
| 400 |
legendgroup=condition,
|
| 401 |
showlegend=session_idx == 0,
|
| 402 |
marker=dict(
|
| 403 |
-
size=
|
| 404 |
-
opacity=0.
|
| 405 |
color=condition_colors[condition],
|
| 406 |
),
|
| 407 |
customdata=np.stack(
|
| 408 |
[
|
| 409 |
-
|
| 410 |
cond_df["condition_label"].astype(str),
|
| 411 |
cond_df["trial_index"].astype(str),
|
| 412 |
cond_df["time_index"].astype(str),
|
|
@@ -414,9 +805,9 @@ def latent_space_figure(dataset: str, models: list[str] | None) -> go.Figure:
|
|
| 414 |
axis=-1,
|
| 415 |
),
|
| 416 |
hovertemplate=(
|
| 417 |
-
"
|
| 418 |
-
f"{
|
| 419 |
-
"
|
| 420 |
"<extra></extra>"
|
| 421 |
),
|
| 422 |
),
|
|
@@ -435,8 +826,8 @@ def latent_space_figure(dataset: str, models: list[str] | None) -> go.Figure:
|
|
| 435 |
showlegend=False,
|
| 436 |
line=dict(color=condition_colors[condition], width=5),
|
| 437 |
hovertemplate=(
|
| 438 |
-
f"{
|
| 439 |
-
"
|
| 440 |
),
|
| 441 |
customdata=session_traj["time_index"],
|
| 442 |
),
|
|
@@ -450,19 +841,19 @@ def latent_space_figure(dataset: str, models: list[str] | None) -> go.Figure:
|
|
| 450 |
y=session_df["y"],
|
| 451 |
z=session_df["z"],
|
| 452 |
mode="markers",
|
| 453 |
-
name=
|
| 454 |
showlegend=False,
|
| 455 |
marker=dict(
|
| 456 |
-
size=2.
|
| 457 |
opacity=0.72,
|
| 458 |
color=session_df["condition_num"],
|
| 459 |
colorscale="Viridis",
|
| 460 |
showscale=session_idx == 0,
|
| 461 |
-
colorbar=dict(title=
|
| 462 |
),
|
| 463 |
customdata=np.stack(
|
| 464 |
[
|
| 465 |
-
|
| 466 |
session_df["condition"].map(lambda value: condition_label(dataset, value)).astype(str),
|
| 467 |
session_df["trial_index"].astype(str),
|
| 468 |
session_df["time_index"].astype(str),
|
|
@@ -470,9 +861,9 @@ def latent_space_figure(dataset: str, models: list[str] | None) -> go.Figure:
|
|
| 470 |
axis=-1,
|
| 471 |
),
|
| 472 |
hovertemplate=(
|
| 473 |
-
"
|
| 474 |
-
f"{
|
| 475 |
-
"
|
| 476 |
"<extra></extra>"
|
| 477 |
),
|
| 478 |
),
|
|
@@ -492,108 +883,222 @@ def latent_space_figure(dataset: str, models: list[str] | None) -> go.Figure:
|
|
| 492 |
fig.update_layout(
|
| 493 |
**{
|
| 494 |
scene_id: dict(
|
| 495 |
-
xaxis=dict(
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
showgrid=False,
|
| 499 |
-
zeroline=False,
|
| 500 |
-
showticklabels=False,
|
| 501 |
-
),
|
| 502 |
-
yaxis=dict(
|
| 503 |
-
title="",
|
| 504 |
-
range=[-lim, lim],
|
| 505 |
-
showgrid=False,
|
| 506 |
-
zeroline=False,
|
| 507 |
-
showticklabels=False,
|
| 508 |
-
),
|
| 509 |
-
zaxis=dict(
|
| 510 |
-
title="",
|
| 511 |
-
range=[-lim, lim],
|
| 512 |
-
showgrid=False,
|
| 513 |
-
zeroline=False,
|
| 514 |
-
showticklabels=False,
|
| 515 |
-
),
|
| 516 |
aspectmode="cube",
|
| 517 |
bgcolor="#ffffff",
|
| 518 |
camera=dict(eye=dict(x=1.55, y=1.45, z=1.05)),
|
| 519 |
)
|
| 520 |
}
|
| 521 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 522 |
score = None
|
| 523 |
-
if not score_df.empty
|
| 524 |
-
score =
|
| 525 |
-
|
|
|
|
| 526 |
fig.update_layout(
|
| 527 |
-
title=(
|
| 528 |
-
|
| 529 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 530 |
)
|
| 531 |
-
fig.for_each_annotation(lambda ann: ann.update(font=dict(size=
|
| 532 |
-
fig
|
| 533 |
-
return latent_fig_layout(fig, height=720 if n_rows > 1 else 430)
|
| 534 |
|
| 535 |
|
| 536 |
-
def
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 567 |
)
|
|
|
|
|
|
|
| 568 |
|
| 569 |
|
| 570 |
-
def
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 578 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 579 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 580 |
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
if subtitle:
|
| 584 |
-
heading.append(html.P(subtitle, className="panel-subtitle"))
|
| 585 |
-
return html.Div([html.Div(heading, className="panel-heading"), *children], className="panel")
|
| 586 |
|
| 587 |
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 594 |
|
| 595 |
|
| 596 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 597 |
server = app.server
|
| 598 |
|
| 599 |
app.layout = html.Div(
|
|
@@ -603,10 +1108,9 @@ app.layout = html.Div(
|
|
| 603 |
html.Div(
|
| 604 |
[
|
| 605 |
html.Div("Tang Lab", className="eyebrow"),
|
| 606 |
-
html.H1("
|
| 607 |
html.P(
|
| 608 |
-
"
|
| 609 |
-
"cross-session latent geometry, attribution, and compute cost.",
|
| 610 |
className="lede",
|
| 611 |
),
|
| 612 |
],
|
|
@@ -614,16 +1118,10 @@ app.layout = html.Div(
|
|
| 614 |
),
|
| 615 |
html.Div(
|
| 616 |
[
|
| 617 |
-
|
| 618 |
-
|
| 619 |
-
stat_card("Result Coverage", coverage_text, f"{coverage_pct:.1f}% complete"),
|
| 620 |
-
stat_card(
|
| 621 |
-
"Compute Mix",
|
| 622 |
-
f"{runtime_counts.get('cpu', 0)} CPU / {runtime_counts.get('gpu', 0)} GPU",
|
| 623 |
-
"registered model runtimes",
|
| 624 |
-
),
|
| 625 |
],
|
| 626 |
-
className="
|
| 627 |
),
|
| 628 |
],
|
| 629 |
className="hero",
|
|
@@ -635,10 +1133,7 @@ app.layout = html.Div(
|
|
| 635 |
html.Label("Dataset"),
|
| 636 |
dcc.Dropdown(
|
| 637 |
id="dataset-filter",
|
| 638 |
-
options=[
|
| 639 |
-
{"label": DATASET_LABELS.get(ds, ds), "value": ds}
|
| 640 |
-
for ds in DATASETS
|
| 641 |
-
],
|
| 642 |
value=DATASETS[0] if DATASETS else None,
|
| 643 |
clearable=False,
|
| 644 |
),
|
|
@@ -647,116 +1142,158 @@ app.layout = html.Div(
|
|
| 647 |
),
|
| 648 |
html.Div(
|
| 649 |
[
|
| 650 |
-
html.Label("
|
| 651 |
dcc.Dropdown(
|
| 652 |
id="model-filter",
|
| 653 |
-
options=[{"label":
|
| 654 |
-
value=
|
| 655 |
multi=True,
|
| 656 |
-
placeholder="
|
| 657 |
),
|
| 658 |
],
|
| 659 |
className="control control-wide",
|
| 660 |
),
|
| 661 |
],
|
| 662 |
-
className="
|
| 663 |
),
|
| 664 |
dcc.Tabs(
|
| 665 |
id="tabs",
|
| 666 |
-
value="
|
| 667 |
className="tabs",
|
| 668 |
children=[
|
| 669 |
dcc.Tab(
|
| 670 |
-
label="
|
| 671 |
-
value="
|
|
|
|
|
|
|
| 672 |
children=[
|
| 673 |
panel(
|
| 674 |
-
"
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 679 |
)
|
| 680 |
],
|
| 681 |
),
|
| 682 |
dcc.Tab(
|
| 683 |
-
label="
|
| 684 |
-
value="
|
|
|
|
|
|
|
| 685 |
children=[
|
| 686 |
panel(
|
| 687 |
-
"
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 691 |
)
|
| 692 |
],
|
| 693 |
),
|
| 694 |
dcc.Tab(
|
| 695 |
-
label="
|
| 696 |
-
value="
|
|
|
|
|
|
|
| 697 |
children=[
|
| 698 |
panel(
|
| 699 |
-
"
|
| 700 |
-
dcc.Graph(
|
| 701 |
-
|
| 702 |
-
|
| 703 |
-
"displayModeBar": "hover",
|
| 704 |
-
"toImageButtonOptions": {
|
| 705 |
-
"format": "png",
|
| 706 |
-
"filename": "benchdash_latent_space",
|
| 707 |
-
"height": 900,
|
| 708 |
-
"width": 1200,
|
| 709 |
-
"scale": 2,
|
| 710 |
-
},
|
| 711 |
-
},
|
| 712 |
-
),
|
| 713 |
-
dcc.Graph(id="consistency-bars", config={"displayModeBar": False}),
|
| 714 |
-
dcc.Graph(id="consistency-heatmap", config={"displayModeBar": False}),
|
| 715 |
-
dataframe_table("consistency-table"),
|
| 716 |
-
subtitle="Latent clouds show sampled 3D embeddings from real consistency artifacts. Scores summarize cross-session alignment.",
|
| 717 |
)
|
| 718 |
],
|
| 719 |
),
|
| 720 |
dcc.Tab(
|
| 721 |
-
label="
|
| 722 |
-
value="
|
|
|
|
|
|
|
| 723 |
children=[
|
| 724 |
panel(
|
| 725 |
-
"
|
| 726 |
-
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 730 |
)
|
| 731 |
],
|
| 732 |
),
|
| 733 |
dcc.Tab(
|
| 734 |
-
label="
|
| 735 |
-
value="
|
|
|
|
|
|
|
| 736 |
children=[
|
| 737 |
panel(
|
| 738 |
-
"
|
| 739 |
-
|
| 740 |
-
|
| 741 |
-
|
| 742 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 743 |
)
|
| 744 |
],
|
| 745 |
),
|
| 746 |
dcc.Tab(
|
| 747 |
label="Methods",
|
| 748 |
-
value="
|
|
|
|
|
|
|
| 749 |
children=[
|
| 750 |
panel(
|
| 751 |
-
"
|
| 752 |
-
|
| 753 |
-
|
| 754 |
-
"coverage-table",
|
| 755 |
-
columns=list(model_inventory.columns),
|
| 756 |
-
data=model_inventory.to_dict("records"),
|
| 757 |
-
page_size=25,
|
| 758 |
-
),
|
| 759 |
-
subtitle="Coverage reports which methods have exported benchmark summaries in this dashboard.",
|
| 760 |
)
|
| 761 |
],
|
| 762 |
),
|
|
@@ -768,381 +1305,111 @@ app.layout = html.Div(
|
|
| 768 |
|
| 769 |
|
| 770 |
@app.callback(
|
| 771 |
-
Output("
|
| 772 |
-
Output("
|
| 773 |
-
Output("
|
| 774 |
-
Output("
|
|
|
|
| 775 |
Input("dataset-filter", "value"),
|
| 776 |
Input("model-filter", "value"),
|
| 777 |
)
|
| 778 |
-
def
|
| 779 |
-
|
| 780 |
-
|
| 781 |
-
|
| 782 |
-
|
| 783 |
-
|
| 784 |
-
heat_df = df_all.copy()
|
| 785 |
-
heat_df["score"] = pd.to_numeric(heat_df["score"], errors="coerce")
|
| 786 |
-
heat_df["dataset_label"] = heat_df["dataset"].map(DATASET_LABELS).fillna(heat_df["dataset"])
|
| 787 |
-
pivot = heat_df.pivot_table(index="model", columns="dataset_label", values="score", aggfunc="first")
|
| 788 |
-
if not pivot.empty:
|
| 789 |
-
preferred = [m for m in selected_models(models) if m in pivot.index]
|
| 790 |
-
remaining = [m for m in sorted(pivot.index.astype(str)) if m not in preferred]
|
| 791 |
-
pivot = pivot.loc[preferred + remaining]
|
| 792 |
-
dataset_order = [DATASET_LABELS.get(ds, ds) for ds in DATASETS if DATASET_LABELS.get(ds, ds) in pivot.columns]
|
| 793 |
-
pivot = pivot.reindex(columns=dataset_order)
|
| 794 |
-
text = pivot.map(lambda x: "" if pd.isna(x) else f"{x:.3f}") if not pivot.empty else pivot
|
| 795 |
-
zmin = min(0.0, float(np.nanmin(pivot.values))) if pivot.size and not np.isnan(pivot.values).all() else 0.0
|
| 796 |
-
zmax = max(1.0, float(np.nanmax(pivot.values))) if pivot.size and not np.isnan(pivot.values).all() else 1.0
|
| 797 |
-
heatmap = go.Figure(
|
| 798 |
-
go.Heatmap(
|
| 799 |
-
z=pivot.values if not pivot.empty else [[]],
|
| 800 |
-
x=list(pivot.columns),
|
| 801 |
-
y=list(pivot.index),
|
| 802 |
-
text=text.values if not pivot.empty else [[]],
|
| 803 |
-
texttemplate="%{text}",
|
| 804 |
-
colorscale=[[0.0, "#b85c38"], [0.5, "#f7f2e8"], [1.0, "#176b5a"]],
|
| 805 |
-
zmin=zmin,
|
| 806 |
-
zmax=zmax,
|
| 807 |
-
colorbar=dict(title="score"),
|
| 808 |
-
hovertemplate="model=%{y}<br>dataset=%{x}<br>score=%{z:.4f}<extra></extra>",
|
| 809 |
-
)
|
| 810 |
-
)
|
| 811 |
-
heatmap.update_layout(title="Clean-score matrix by dataset")
|
| 812 |
-
fig_layout(heatmap, height=max(430, 28 * len(pivot.index) + 170))
|
| 813 |
-
|
| 814 |
-
rank_df = present_rows(df_all)
|
| 815 |
-
rank_df = rank_df[rank_df["dataset"] == dataset].copy()
|
| 816 |
-
rank_df["score"] = pd.to_numeric(rank_df["score"], errors="coerce")
|
| 817 |
-
rank_df = rank_df.dropna(subset=["score"]).sort_values("score", ascending=True)
|
| 818 |
-
if rank_df.empty:
|
| 819 |
-
ranking = empty_figure(f"No clean prediction rows for {DATASET_LABELS.get(dataset, dataset)}.")
|
| 820 |
-
else:
|
| 821 |
-
metric = ordered_unique(rank_df["metric"])[0]
|
| 822 |
-
ranking = go.Figure(
|
| 823 |
-
go.Bar(
|
| 824 |
-
x=rank_df["score"],
|
| 825 |
-
y=rank_df["model"],
|
| 826 |
-
orientation="h",
|
| 827 |
-
marker=dict(color=rank_df["score"], colorscale="Teal", colorbar=dict(title=metric)),
|
| 828 |
-
hovertemplate="model=%{y}<br>score=%{x:.4f}<extra></extra>",
|
| 829 |
-
)
|
| 830 |
-
)
|
| 831 |
-
ranking.update_layout(title=f"{DATASET_LABELS.get(dataset, dataset)} ranking ({metric})")
|
| 832 |
-
ranking.update_xaxes(title=metric)
|
| 833 |
-
ranking.update_yaxes(title="")
|
| 834 |
-
fig_layout(ranking, height=max(420, 27 * len(rank_df) + 160))
|
| 835 |
-
|
| 836 |
-
table_cols = [
|
| 837 |
-
"model",
|
| 838 |
-
"dataset_display",
|
| 839 |
-
"status",
|
| 840 |
"metric",
|
| 841 |
-
"
|
| 842 |
-
"
|
| 843 |
-
"
|
| 844 |
-
"
|
| 845 |
-
"
|
| 846 |
-
"n_neurons",
|
| 847 |
]
|
| 848 |
-
table_df =
|
| 849 |
-
for col in ["score", "latent_dim", "n_train_trials", "n_test_trials", "n_neurons"]:
|
| 850 |
-
if col in table_df:
|
| 851 |
-
table_df[col] = table_df[col].map(lambda v: compact_number(v))
|
| 852 |
-
table_df = table_df[[c for c in table_cols if c in table_df.columns]]
|
| 853 |
return (
|
| 854 |
-
|
| 855 |
-
|
| 856 |
-
|
| 857 |
-
|
|
|
|
| 858 |
)
|
| 859 |
|
| 860 |
|
| 861 |
@app.callback(
|
| 862 |
-
Output("
|
| 863 |
-
Output("
|
| 864 |
-
Output("
|
|
|
|
|
|
|
| 865 |
Input("dataset-filter", "value"),
|
| 866 |
Input("model-filter", "value"),
|
|
|
|
| 867 |
)
|
| 868 |
-
def
|
| 869 |
-
df =
|
| 870 |
-
|
| 871 |
-
|
| 872 |
-
|
| 873 |
-
|
| 874 |
-
|
| 875 |
-
|
| 876 |
-
|
| 877 |
-
|
| 878 |
-
|
| 879 |
-
|
| 880 |
-
fig.add_trace(
|
| 881 |
-
go.Scatter(
|
| 882 |
-
x=xs,
|
| 883 |
-
y=ys,
|
| 884 |
-
mode="lines+markers",
|
| 885 |
-
name=row.model,
|
| 886 |
-
hovertemplate="noise=%{x:.2f}<br>score=%{y:.4f}<extra></extra>",
|
| 887 |
-
)
|
| 888 |
-
)
|
| 889 |
-
fig.update_layout(title=f"{DATASET_LABELS.get(dataset, dataset)} robustness curves")
|
| 890 |
-
fig.update_xaxes(title="noise fraction")
|
| 891 |
-
fig.update_yaxes(title=ordered_unique(df["metric"])[0] if "metric" in df else "score")
|
| 892 |
-
fig_layout(fig, height=520)
|
| 893 |
-
|
| 894 |
-
table_cols = ["model", "metric", "score_at_noise0", "score_at_max_noise", "raw_auc", "mean_score"]
|
| 895 |
-
table_df = df[table_cols].copy()
|
| 896 |
-
for col in ["score_at_noise0", "score_at_max_noise", "raw_auc", "mean_score"]:
|
| 897 |
-
table_df[col] = table_df[col].map(lambda v: compact_number(v))
|
| 898 |
-
return fig, [{"name": c, "id": c} for c in table_df.columns], table_df.to_dict("records")
|
| 899 |
|
| 900 |
|
| 901 |
@app.callback(
|
| 902 |
-
Output("
|
| 903 |
-
Output("
|
| 904 |
-
Output("
|
| 905 |
-
Output("consistency-table", "columns"),
|
| 906 |
-
Output("consistency-table", "data"),
|
| 907 |
Input("dataset-filter", "value"),
|
| 908 |
Input("model-filter", "value"),
|
| 909 |
)
|
| 910 |
-
def
|
| 911 |
-
|
| 912 |
-
df_all = filter_models(consistency, models)
|
| 913 |
-
if "is_active_model" in df_all.columns:
|
| 914 |
-
df_all = df_all[df_all["is_active_model"].astype(str).str.lower() == "true"]
|
| 915 |
-
df = df_all[df_all["dataset"] == dataset].copy() if not df_all.empty else df_all
|
| 916 |
-
if df.empty:
|
| 917 |
-
cols = [{"name": c, "id": c} for c in ["model", "dataset", "mean_r2"]]
|
| 918 |
-
return (
|
| 919 |
-
latent_fig,
|
| 920 |
-
empty_figure("Consistency analysis is not available for this dataset."),
|
| 921 |
-
empty_figure("Consistency analysis is not available for this dataset."),
|
| 922 |
-
cols,
|
| 923 |
-
[],
|
| 924 |
-
)
|
| 925 |
-
|
| 926 |
-
df["mean_r2"] = pd.to_numeric(df["mean_r2"], errors="coerce")
|
| 927 |
-
bar_df = df.sort_values("mean_r2", ascending=True)
|
| 928 |
-
bars = go.Figure(
|
| 929 |
-
go.Bar(
|
| 930 |
-
x=bar_df["mean_r2"],
|
| 931 |
-
y=bar_df["model"],
|
| 932 |
-
orientation="h",
|
| 933 |
-
marker=dict(color=bar_df["mean_r2"], colorscale="Bluyl"),
|
| 934 |
-
hovertemplate="model=%{y}<br>R2=%{x:.4f}<extra></extra>",
|
| 935 |
-
)
|
| 936 |
-
)
|
| 937 |
-
bars.update_layout(title=f"{DATASET_LABELS.get(dataset, dataset)} cross-session alignment")
|
| 938 |
-
bars.update_xaxes(title="mean R2")
|
| 939 |
-
fig_layout(bars, height=max(420, 27 * len(bar_df) + 150))
|
| 940 |
-
|
| 941 |
-
heat = df_all.copy()
|
| 942 |
-
heat["mean_r2"] = pd.to_numeric(heat["mean_r2"], errors="coerce")
|
| 943 |
-
heat["dataset_label"] = heat["dataset"].map(DATASET_LABELS).fillna(heat["dataset"])
|
| 944 |
-
pivot = heat.pivot_table(index="model", columns="dataset_label", values="mean_r2", aggfunc="first")
|
| 945 |
-
if not pivot.empty:
|
| 946 |
-
pivot = pivot.loc[pivot.mean(axis=1, skipna=True).sort_values(ascending=False).index]
|
| 947 |
-
text = pivot.map(lambda x: "" if pd.isna(x) else f"{x:.2f}") if not pivot.empty else pivot
|
| 948 |
-
heatmap = go.Figure(
|
| 949 |
-
go.Heatmap(
|
| 950 |
-
z=pivot.values if not pivot.empty else [[]],
|
| 951 |
-
x=list(pivot.columns),
|
| 952 |
-
y=list(pivot.index),
|
| 953 |
-
text=text.values if not pivot.empty else [[]],
|
| 954 |
-
texttemplate="%{text}",
|
| 955 |
-
colorscale=[[0.0, "#b85c38"], [0.5, "#f7f2e8"], [1.0, "#176b5a"]],
|
| 956 |
-
colorbar=dict(title="R2"),
|
| 957 |
-
hovertemplate="model=%{y}<br>dataset=%{x}<br>R2=%{z:.4f}<extra></extra>",
|
| 958 |
-
)
|
| 959 |
-
)
|
| 960 |
-
heatmap.update_layout(title="Consistency matrix")
|
| 961 |
-
fig_layout(heatmap, height=max(410, 28 * len(pivot.index) + 160))
|
| 962 |
-
|
| 963 |
-
table_cols = [
|
| 964 |
-
"model",
|
| 965 |
-
"dataset",
|
| 966 |
-
"n_sessions",
|
| 967 |
-
"latent_dim",
|
| 968 |
-
"mean_r2",
|
| 969 |
-
"n_pairwise",
|
| 970 |
-
"scoring_modes",
|
| 971 |
-
]
|
| 972 |
-
table_df = df[[c for c in table_cols if c in df.columns]].copy()
|
| 973 |
-
for col in ["mean_r2", "latent_dim", "n_sessions", "n_pairwise"]:
|
| 974 |
-
if col in table_df:
|
| 975 |
-
table_df[col] = table_df[col].map(lambda v: compact_number(v))
|
| 976 |
return (
|
| 977 |
-
|
| 978 |
-
|
| 979 |
-
|
| 980 |
-
[{"name": c, "id": c} for c in table_df.columns],
|
| 981 |
-
table_df.to_dict("records"),
|
| 982 |
)
|
| 983 |
|
| 984 |
|
| 985 |
@app.callback(
|
| 986 |
-
Output("
|
| 987 |
Output("memory-bars", "figure"),
|
| 988 |
-
Output("
|
| 989 |
-
Output("
|
| 990 |
Input("dataset-filter", "value"),
|
| 991 |
Input("model-filter", "value"),
|
| 992 |
)
|
| 993 |
-
def
|
| 994 |
-
|
| 995 |
-
|
| 996 |
-
if df.empty:
|
| 997 |
-
cols = [{"name": c, "id": c} for c in ["model", "training_time_sec", "peak_ram_gb"]]
|
| 998 |
-
return empty_figure("No runtime rows for this selection."), empty_figure("No memory rows."), cols, []
|
| 999 |
-
|
| 1000 |
-
pred = present_rows(prediction)[["model", "dataset", "score", "metric"]].copy()
|
| 1001 |
-
df = df.merge(pred, on=["model", "dataset"], how="left")
|
| 1002 |
-
for col in ["training_time_sec", "inference_time_sec", "peak_ram_gb", "peak_vram_gb", "score"]:
|
| 1003 |
-
df[col] = pd.to_numeric(df[col], errors="coerce")
|
| 1004 |
-
df["accelerator"] = df["model"].map(lambda m: MODEL_INFO.get(m, ("unknown", "", "", ""))[0])
|
| 1005 |
-
df["pipeline"] = df["model"].map(lambda m: MODEL_INFO.get(m, ("", "unknown", "", ""))[1])
|
| 1006 |
-
|
| 1007 |
-
scatter = px.scatter(
|
| 1008 |
-
df,
|
| 1009 |
-
x="training_time_sec",
|
| 1010 |
-
y="score",
|
| 1011 |
-
size="peak_ram_gb",
|
| 1012 |
-
color="accelerator",
|
| 1013 |
-
symbol="pipeline",
|
| 1014 |
-
hover_name="model",
|
| 1015 |
-
hover_data={
|
| 1016 |
-
"training_time_sec": ":.3f",
|
| 1017 |
-
"inference_time_sec": ":.3f",
|
| 1018 |
-
"peak_ram_gb": ":.3f",
|
| 1019 |
-
"peak_vram_gb": ":.3f",
|
| 1020 |
-
"score": ":.4f",
|
| 1021 |
-
},
|
| 1022 |
-
log_x=True,
|
| 1023 |
-
title=f"{DATASET_LABELS.get(dataset, dataset)} score vs training time",
|
| 1024 |
-
)
|
| 1025 |
-
scatter.update_xaxes(title="training time (sec, log)")
|
| 1026 |
-
scatter.update_yaxes(title=ordered_unique(df["metric"].dropna())[0] if df["metric"].notna().any() else "score")
|
| 1027 |
-
fig_layout(scatter, height=500)
|
| 1028 |
-
|
| 1029 |
-
mem_df = df.sort_values("peak_ram_gb", ascending=True)
|
| 1030 |
-
memory = go.Figure()
|
| 1031 |
-
memory.add_trace(go.Bar(x=mem_df["peak_ram_gb"], y=mem_df["model"], orientation="h", name="RAM GB"))
|
| 1032 |
-
memory.add_trace(go.Bar(x=mem_df["peak_vram_gb"], y=mem_df["model"], orientation="h", name="VRAM GB"))
|
| 1033 |
-
memory.update_layout(title=f"{DATASET_LABELS.get(dataset, dataset)} peak memory", barmode="group")
|
| 1034 |
-
memory.update_xaxes(title="GB")
|
| 1035 |
-
fig_layout(memory, height=max(420, 29 * len(mem_df) + 160))
|
| 1036 |
-
|
| 1037 |
-
table_cols = [
|
| 1038 |
-
"model",
|
| 1039 |
-
"accelerator",
|
| 1040 |
-
"score",
|
| 1041 |
-
"training_time_sec",
|
| 1042 |
-
"inference_time_sec",
|
| 1043 |
-
"peak_ram_gb",
|
| 1044 |
-
"peak_vram_gb",
|
| 1045 |
-
]
|
| 1046 |
-
table_df = df[table_cols].copy()
|
| 1047 |
-
for col in ["score", "training_time_sec", "inference_time_sec", "peak_ram_gb", "peak_vram_gb"]:
|
| 1048 |
-
table_df[col] = table_df[col].map(lambda v: compact_number(v))
|
| 1049 |
-
return scatter, memory, [{"name": c, "id": c} for c in table_df.columns], table_df.to_dict("records")
|
| 1050 |
|
| 1051 |
|
| 1052 |
@app.callback(
|
| 1053 |
-
Output("neuron-
|
| 1054 |
-
Output("trial-
|
| 1055 |
-
Output("
|
| 1056 |
-
Output("
|
| 1057 |
Input("dataset-filter", "value"),
|
| 1058 |
Input("model-filter", "value"),
|
| 1059 |
)
|
| 1060 |
-
def
|
| 1061 |
-
|
| 1062 |
-
|
| 1063 |
-
nshap = nshap[nshap["is_active_model"].astype(str).str.lower() == "true"]
|
| 1064 |
-
nshap = nshap[nshap["dataset"] == dataset].copy() if not nshap.empty else nshap
|
| 1065 |
-
if nshap.empty:
|
| 1066 |
-
nshap_fig = empty_figure("No neuron-SHAP rows for this dataset.")
|
| 1067 |
-
else:
|
| 1068 |
-
nshap["auc"] = pd.to_numeric(nshap["auc"], errors="coerce")
|
| 1069 |
-
bar_df = nshap.dropna(subset=["auc"]).sort_values("auc", ascending=True)
|
| 1070 |
-
nshap_fig = go.Figure(
|
| 1071 |
-
go.Bar(
|
| 1072 |
-
x=bar_df["auc"],
|
| 1073 |
-
y=bar_df["model"],
|
| 1074 |
-
orientation="h",
|
| 1075 |
-
marker=dict(color=bar_df["auc"], colorscale="Teal"),
|
| 1076 |
-
hovertemplate="model=%{y}<br>AUC=%{x:.4f}<extra></extra>",
|
| 1077 |
-
)
|
| 1078 |
-
)
|
| 1079 |
-
nshap_fig.update_layout(title=f"{DATASET_LABELS.get(dataset, dataset)} neuron perturbation AUC")
|
| 1080 |
-
nshap_fig.update_xaxes(title="AUC")
|
| 1081 |
-
fig_layout(nshap_fig, height=max(420, 27 * len(bar_df) + 150))
|
| 1082 |
-
|
| 1083 |
-
tshap = filter_models(trial_shapley, models)
|
| 1084 |
-
if "is_active_model" in tshap.columns:
|
| 1085 |
-
tshap = tshap[tshap["is_active_model"].astype(str).str.lower() == "true"]
|
| 1086 |
-
tshap = tshap[tshap["dataset"] == dataset].copy() if not tshap.empty else tshap
|
| 1087 |
-
if tshap.empty:
|
| 1088 |
-
tshap_fig = empty_figure("No trial-Shapley rows for this dataset.")
|
| 1089 |
-
else:
|
| 1090 |
-
tshap["perturbation_auc"] = pd.to_numeric(tshap["perturbation_auc"], errors="coerce")
|
| 1091 |
-
trial_df = tshap.dropna(subset=["perturbation_auc"]).sort_values("perturbation_auc", ascending=True)
|
| 1092 |
-
tshap_fig = go.Figure(
|
| 1093 |
-
go.Bar(
|
| 1094 |
-
x=trial_df["perturbation_auc"],
|
| 1095 |
-
y=trial_df["model"],
|
| 1096 |
-
orientation="h",
|
| 1097 |
-
marker=dict(color=trial_df["perturbation_auc"], colorscale="Bluyl"),
|
| 1098 |
-
hovertemplate="model=%{y}<br>perturbation AUC=%{x:.4f}<extra></extra>",
|
| 1099 |
-
)
|
| 1100 |
-
)
|
| 1101 |
-
tshap_fig.update_layout(title=f"{DATASET_LABELS.get(dataset, dataset)} trial-Shapley perturbation AUC")
|
| 1102 |
-
tshap_fig.update_xaxes(title="perturbation AUC")
|
| 1103 |
-
fig_layout(tshap_fig, height=max(420, 27 * len(trial_df) + 150))
|
| 1104 |
-
|
| 1105 |
-
table_cols = [
|
| 1106 |
-
"model",
|
| 1107 |
-
"dataset",
|
| 1108 |
-
"metric",
|
| 1109 |
-
"baseline_score",
|
| 1110 |
-
"full_model_score",
|
| 1111 |
-
"auc",
|
| 1112 |
-
"spearman_corr",
|
| 1113 |
-
"shap_mean_value",
|
| 1114 |
-
"shap_fraction_positive",
|
| 1115 |
-
]
|
| 1116 |
-
table_df = nshap[[c for c in table_cols if c in nshap.columns]].copy()
|
| 1117 |
-
for col in table_df.columns:
|
| 1118 |
-
if col not in {"model", "dataset", "metric"}:
|
| 1119 |
-
table_df[col] = table_df[col].map(lambda v: compact_number(v))
|
| 1120 |
-
return nshap_fig, tshap_fig, [{"name": c, "id": c} for c in table_df.columns], table_df.to_dict("records")
|
| 1121 |
|
| 1122 |
|
| 1123 |
-
@app.callback(
|
| 1124 |
-
|
| 1125 |
-
|
| 1126 |
-
|
| 1127 |
-
|
| 1128 |
-
|
| 1129 |
-
|
| 1130 |
-
|
| 1131 |
-
.reset_index(name="n_models")
|
| 1132 |
-
.sort_values("n_models", ascending=False)
|
| 1133 |
-
)
|
| 1134 |
-
fig = px.bar(
|
| 1135 |
-
counts,
|
| 1136 |
-
x="pipeline",
|
| 1137 |
-
y="n_models",
|
| 1138 |
-
color="accelerator",
|
| 1139 |
-
barmode="group",
|
| 1140 |
-
text="n_models",
|
| 1141 |
-
title="Registered model runtimes",
|
| 1142 |
-
)
|
| 1143 |
-
fig.update_xaxes(title="")
|
| 1144 |
-
fig.update_yaxes(title="models")
|
| 1145 |
-
return fig_layout(fig, height=420)
|
| 1146 |
|
| 1147 |
|
| 1148 |
if __name__ == "__main__":
|
|
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
+
import re
|
| 4 |
from pathlib import Path
|
| 5 |
from typing import Iterable
|
| 6 |
|
| 7 |
import numpy as np
|
| 8 |
import pandas as pd
|
|
|
|
| 9 |
import plotly.graph_objects as go
|
| 10 |
from dash import Dash, Input, Output, dash_table, dcc, html
|
| 11 |
from plotly.subplots import make_subplots
|
|
|
|
| 13 |
|
| 14 |
DATA_DIR = Path(__file__).resolve().parent / "data"
|
| 15 |
|
| 16 |
+
PAPER_MODEL_ORDER = [
|
| 17 |
+
"blend",
|
| 18 |
+
"blend_ndt",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
"cebra",
|
| 20 |
+
"dnn",
|
| 21 |
"dpad",
|
| 22 |
+
"gpfa",
|
| 23 |
+
"gru",
|
| 24 |
+
"langevinflow_ccn",
|
| 25 |
"ldns",
|
| 26 |
"lfads_torch",
|
| 27 |
+
"lstm",
|
| 28 |
"marble",
|
| 29 |
+
"mint",
|
| 30 |
+
"neds",
|
| 31 |
+
"neds_pretrained",
|
| 32 |
+
"neuro_behavior_conditioning",
|
| 33 |
+
"pca",
|
| 34 |
"rnn",
|
| 35 |
+
"smc_rnns",
|
| 36 |
"svc",
|
| 37 |
+
"tndm",
|
| 38 |
+
"torchdfine",
|
| 39 |
"xg",
|
| 40 |
]
|
| 41 |
|
| 42 |
+
DISPLAY_NAMES = {
|
| 43 |
+
"blend": "BLEND-LFADS",
|
| 44 |
+
"blend_ndt": "BLEND-NDT",
|
| 45 |
+
"cebra": "CEBRA",
|
| 46 |
+
"dnn": "DNN",
|
| 47 |
+
"dpad": "DPAD",
|
| 48 |
+
"gpfa": "GPFA",
|
| 49 |
+
"gru": "GRU",
|
| 50 |
+
"langevinflow_ccn": "LangevinFlow",
|
| 51 |
+
"ldns": "LDNS",
|
| 52 |
+
"lfads_torch": "AutoLFADS",
|
| 53 |
+
"lstm": "LSTM",
|
| 54 |
+
"marble": "MARBLE",
|
| 55 |
+
"mint": "MINT",
|
| 56 |
+
"neds": "NEDS",
|
| 57 |
+
"neds_pretrained": "NEDS-pt",
|
| 58 |
+
"neuro_behavior_conditioning": "mVAE",
|
| 59 |
+
"pca": "PCA",
|
| 60 |
+
"rnn": "RNN",
|
| 61 |
+
"smc_rnns": "SMC-RNN",
|
| 62 |
+
"svc": "SVC",
|
| 63 |
+
"tndm": "TNDM",
|
| 64 |
+
"torchdfine": "DFINE",
|
| 65 |
+
"xg": "XGBoost",
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
METHOD_FAMILY = {
|
| 69 |
+
"pca": "Linear latent",
|
| 70 |
+
"gpfa": "Linear latent",
|
| 71 |
+
"dnn": "Supervised decoder",
|
| 72 |
+
"gru": "Supervised decoder",
|
| 73 |
+
"lstm": "Supervised decoder",
|
| 74 |
+
"rnn": "Supervised decoder",
|
| 75 |
+
"svc": "Supervised decoder",
|
| 76 |
+
"xg": "Supervised decoder",
|
| 77 |
+
"lfads_torch": "Sequential latent",
|
| 78 |
+
"dpad": "Sequential latent",
|
| 79 |
+
"torchdfine": "Sequential latent",
|
| 80 |
+
"smc_rnns": "Sequential latent",
|
| 81 |
+
"tndm": "Sequential latent",
|
| 82 |
+
"langevinflow_ccn": "Sequential latent",
|
| 83 |
+
"ldns": "Sequential latent",
|
| 84 |
+
"neuro_behavior_conditioning": "Sequential latent",
|
| 85 |
+
"blend": "Distillation",
|
| 86 |
+
"blend_ndt": "Distillation",
|
| 87 |
+
"neds": "Foundation model",
|
| 88 |
+
"neds_pretrained": "Foundation model",
|
| 89 |
+
"cebra": "Contrastive",
|
| 90 |
+
"marble": "Geometric",
|
| 91 |
+
"mint": "Non-parametric",
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
METHOD_HARDWARE = {
|
| 95 |
+
"gpfa": "CPU",
|
| 96 |
+
"mint": "CPU",
|
| 97 |
+
"pca": "CPU",
|
| 98 |
+
"svc": "CPU",
|
| 99 |
+
"xg": "CPU",
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
TABLE_LABELS = {
|
| 103 |
+
"rank": "Rank",
|
| 104 |
+
"method": "Method",
|
| 105 |
+
"family": "Family",
|
| 106 |
+
"hardware": "Hardware",
|
| 107 |
+
"metric": "Metric",
|
| 108 |
+
"task_score": "Task performance",
|
| 109 |
+
"score": "Task performance",
|
| 110 |
+
"robustness_auc": "Robustness AUC",
|
| 111 |
+
"alignment_score": "Cross-session alignment",
|
| 112 |
+
"training_time_sec": "Training time (s)",
|
| 113 |
+
"inference_time_sec": "Evaluation time (s)",
|
| 114 |
+
"peak_ram_gb": "Peak memory (GB)",
|
| 115 |
+
"peak_vram_gb": "Peak GPU memory (GB)",
|
| 116 |
+
"reference_score": "Reference score",
|
| 117 |
+
"highest_noise_score": "Highest-noise score",
|
| 118 |
+
"average_noisy_score": "Average noisy score",
|
| 119 |
+
"latent_dim": "Latent dimensions",
|
| 120 |
+
"n_sessions": "Sessions",
|
| 121 |
+
"n_pairwise": "Session pairs",
|
| 122 |
+
"baseline_score": "Baseline score",
|
| 123 |
+
"full_model_score": "Full-model score",
|
| 124 |
+
"neuron_influence_auc": "Neuron influence AUC",
|
| 125 |
+
"trial_influence_auc": "Trial influence AUC",
|
| 126 |
+
"shap_mean_value": "Mean signed neuron contribution",
|
| 127 |
+
"shap_fraction_positive": "Fraction positive",
|
| 128 |
+
"n_train_trials": "Training trials",
|
| 129 |
+
"n_test_trials": "Test trials",
|
| 130 |
+
"n_neurons": "Neurons",
|
| 131 |
+
"notes": "Notes",
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
NUMERIC_COLUMNS = {
|
| 135 |
+
"task_score",
|
| 136 |
+
"score",
|
| 137 |
+
"robustness_auc",
|
| 138 |
+
"alignment_score",
|
| 139 |
+
"training_time_sec",
|
| 140 |
+
"inference_time_sec",
|
| 141 |
+
"peak_ram_gb",
|
| 142 |
+
"peak_vram_gb",
|
| 143 |
+
"reference_score",
|
| 144 |
+
"highest_noise_score",
|
| 145 |
+
"average_noisy_score",
|
| 146 |
+
"latent_dim",
|
| 147 |
+
"n_sessions",
|
| 148 |
+
"n_pairwise",
|
| 149 |
+
"baseline_score",
|
| 150 |
+
"full_model_score",
|
| 151 |
+
"neuron_influence_auc",
|
| 152 |
+
"trial_influence_auc",
|
| 153 |
+
"shap_mean_value",
|
| 154 |
+
"shap_fraction_positive",
|
| 155 |
+
"n_train_trials",
|
| 156 |
+
"n_test_trials",
|
| 157 |
+
"n_neurons",
|
| 158 |
+
}
|
| 159 |
+
RIGHT_ALIGNED_COLUMNS = NUMERIC_COLUMNS | {"rank"}
|
| 160 |
+
|
| 161 |
+
SCORE_SCALE = [[0.0, "#9f3a38"], [0.5, "#f3efe4"], [1.0, "#006d77"]]
|
| 162 |
+
CATEGORICAL_PALETTE = [
|
| 163 |
+
"#0072B2",
|
| 164 |
+
"#E69F00",
|
| 165 |
+
"#009E73",
|
| 166 |
+
"#CC79A7",
|
| 167 |
+
"#D55E00",
|
| 168 |
+
"#56B4E9",
|
| 169 |
+
"#F0E442",
|
| 170 |
+
"#6A3D9A",
|
| 171 |
+
"#8C564B",
|
| 172 |
+
"#4E79A7",
|
| 173 |
+
"#59A14F",
|
| 174 |
+
"#AF7AA1",
|
| 175 |
+
]
|
| 176 |
+
|
| 177 |
|
| 178 |
def load_csv(name: str) -> pd.DataFrame:
|
| 179 |
path = DATA_DIR / name
|
| 180 |
if not path.exists():
|
| 181 |
+
raise FileNotFoundError(f"Missing dashboard data: {path}")
|
| 182 |
return pd.read_csv(path)
|
| 183 |
|
| 184 |
|
|
|
|
| 198 |
return df[df["status"].fillna("") == "present"].copy()
|
| 199 |
|
| 200 |
|
| 201 |
+
def active_rows(df: pd.DataFrame) -> pd.DataFrame:
|
| 202 |
+
if df.empty or "is_active_model" not in df.columns:
|
| 203 |
+
return df.copy()
|
| 204 |
+
return df[df["is_active_model"].astype(str).str.lower() == "true"].copy()
|
| 205 |
+
|
| 206 |
+
|
| 207 |
def ordered_unique(values: Iterable[object]) -> list[str]:
|
| 208 |
seen: set[str] = set()
|
| 209 |
out: list[str] = []
|
|
|
|
| 218 |
|
| 219 |
|
| 220 |
def build_dataset_labels() -> dict[str, str]:
|
|
|
|
|
|
|
| 221 |
pairs = (
|
| 222 |
prediction[["dataset", "dataset_display"]]
|
| 223 |
.dropna(subset=["dataset"])
|
|
|
|
| 228 |
|
| 229 |
DATASET_LABELS = build_dataset_labels()
|
| 230 |
DATASETS = ordered_unique(prediction.get("dataset", pd.Series(dtype=str)))
|
| 231 |
+
MODEL_SET = set(prediction.get("model", pd.Series(dtype=str)).dropna().astype(str))
|
| 232 |
+
MODELS = [model for model in PAPER_MODEL_ORDER if model in MODEL_SET]
|
| 233 |
+
MODELS += sorted(model for model in MODEL_SET if model not in set(MODELS))
|
| 234 |
+
MODEL_RANK = {model: idx for idx, model in enumerate(MODELS)}
|
| 235 |
|
| 236 |
CONDITION_LABELS = {
|
| 237 |
"monkey": {
|
|
|
|
| 261 |
}
|
| 262 |
|
| 263 |
|
| 264 |
+
def model_label(model: object) -> str:
|
| 265 |
+
if pd.isna(model):
|
| 266 |
+
return ""
|
| 267 |
+
text = str(model)
|
| 268 |
+
return DISPLAY_NAMES.get(text, text)
|
| 269 |
+
|
| 270 |
+
|
| 271 |
+
def model_sort_value(model: object) -> int:
|
| 272 |
+
return MODEL_RANK.get(str(model), len(MODEL_RANK))
|
| 273 |
+
|
| 274 |
+
|
| 275 |
+
def hardware_label(model: object) -> str:
|
| 276 |
+
return METHOD_HARDWARE.get(str(model), "GPU")
|
| 277 |
+
|
| 278 |
+
|
| 279 |
+
def selected_models(models: list[str] | None) -> list[str]:
|
| 280 |
+
if not models:
|
| 281 |
+
return MODELS.copy()
|
| 282 |
+
return [model for model in MODELS if model in set(models)]
|
| 283 |
+
|
| 284 |
+
|
| 285 |
+
def filter_models(df: pd.DataFrame, models: list[str] | None) -> pd.DataFrame:
|
| 286 |
+
if df.empty or "model" not in df.columns:
|
| 287 |
+
return df.copy()
|
| 288 |
+
return df[df["model"].astype(str).isin(selected_models(models))].copy()
|
| 289 |
+
|
| 290 |
+
|
| 291 |
def condition_sort_key(value: object) -> tuple[int, float | str]:
|
| 292 |
try:
|
| 293 |
return (0, float(value))
|
|
|
|
| 311 |
return text
|
| 312 |
|
| 313 |
|
| 314 |
+
def condition_axis_label(dataset: str) -> str:
|
| 315 |
+
return {
|
| 316 |
+
"monkey": "Reach direction",
|
| 317 |
+
"allen_neuropixels": "Orientation",
|
| 318 |
+
"speech": "Cue",
|
| 319 |
+
"ratinabox": "Position bin",
|
| 320 |
+
}.get(dataset, "Condition")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 321 |
|
| 322 |
|
| 323 |
+
def session_display_label(dataset: str, session: object) -> str:
|
| 324 |
+
text = "" if pd.isna(session) else str(session)
|
| 325 |
+
if dataset == "monkey":
|
| 326 |
+
match = re.search(r"sub-([A-Za-z])_ses-CO-(\d{4})(\d{2})(\d{2})", text)
|
| 327 |
+
if match:
|
| 328 |
+
monkey, year, month, day = match.groups()
|
| 329 |
+
return f"Monkey {monkey}, {year}-{month}-{day}"
|
| 330 |
+
if dataset == "ratinabox":
|
| 331 |
+
match = re.search(r"s(\d+)", text)
|
| 332 |
+
if match:
|
| 333 |
+
return f"Run {match.group(1)}"
|
| 334 |
+
if dataset == "speech":
|
| 335 |
+
return f"Session {text}"
|
| 336 |
+
if dataset == "allen_neuropixels":
|
| 337 |
+
return f"Session {text}"
|
| 338 |
+
return text or "Session"
|
| 339 |
+
|
| 340 |
+
|
| 341 |
+
def add_method_columns(df: pd.DataFrame) -> pd.DataFrame:
|
| 342 |
+
out = df.copy()
|
| 343 |
+
if "model" in out.columns:
|
| 344 |
+
out["method"] = out["model"].map(model_label)
|
| 345 |
+
out["model_order"] = out["model"].map(model_sort_value)
|
| 346 |
+
return out
|
| 347 |
|
| 348 |
|
| 349 |
+
def round_numeric(df: pd.DataFrame, columns: Iterable[str], digits: int = 3) -> pd.DataFrame:
|
| 350 |
+
out = df.copy()
|
| 351 |
+
for col in columns:
|
| 352 |
+
if col in out.columns:
|
| 353 |
+
out[col] = pd.to_numeric(out[col], errors="coerce").round(digits)
|
| 354 |
+
return out
|
|
|
|
| 355 |
|
| 356 |
|
| 357 |
+
def records(df: pd.DataFrame) -> list[dict]:
|
| 358 |
+
clean = df.astype(object).where(pd.notna(df), None)
|
| 359 |
+
return clean.to_dict("records")
|
|
|
|
| 360 |
|
| 361 |
|
| 362 |
+
def column_defs(columns: Iterable[str]) -> list[dict]:
|
| 363 |
+
out = []
|
| 364 |
+
for col in columns:
|
| 365 |
+
item = {"name": TABLE_LABELS.get(col, col), "id": col}
|
| 366 |
+
if col in RIGHT_ALIGNED_COLUMNS:
|
| 367 |
+
item["type"] = "numeric"
|
| 368 |
+
out.append(item)
|
| 369 |
+
return out
|
| 370 |
+
|
| 371 |
+
|
| 372 |
+
def metric_text(value: object) -> str:
|
| 373 |
+
if pd.isna(value):
|
| 374 |
+
return "score"
|
| 375 |
+
return str(value)
|
| 376 |
+
|
| 377 |
+
|
| 378 |
+
def value_text(value: object, digits: int = 3) -> str:
|
| 379 |
+
if value is None or pd.isna(value):
|
| 380 |
+
return "Not available"
|
| 381 |
+
number = float(value)
|
| 382 |
+
if abs(number) >= 1000:
|
| 383 |
+
return f"{number:,.0f}"
|
| 384 |
+
return f"{number:.{digits}f}"
|
| 385 |
|
| 386 |
|
| 387 |
def fig_layout(fig: go.Figure, *, height: int = 420) -> go.Figure:
|
| 388 |
fig.update_layout(
|
| 389 |
height=height,
|
| 390 |
+
paper_bgcolor="#ffffff",
|
| 391 |
+
plot_bgcolor="#ffffff",
|
| 392 |
+
margin=dict(l=22, r=22, t=52, b=38),
|
| 393 |
+
font=dict(family="Inter, Arial, sans-serif", size=13, color="#17202a"),
|
| 394 |
+
title=dict(font=dict(size=15, color="#17202a")),
|
| 395 |
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="left", x=0),
|
| 396 |
)
|
| 397 |
+
fig.update_xaxes(showgrid=True, gridcolor="#edf1f4", zerolinecolor="#d7e0e6")
|
| 398 |
+
fig.update_yaxes(showgrid=True, gridcolor="#edf1f4", zerolinecolor="#d7e0e6")
|
| 399 |
return fig
|
| 400 |
|
| 401 |
|
|
|
|
| 408 |
xref="paper",
|
| 409 |
yref="paper",
|
| 410 |
showarrow=False,
|
| 411 |
+
font=dict(size=15, color="#637381"),
|
| 412 |
)
|
| 413 |
fig.update_xaxes(visible=False)
|
| 414 |
fig.update_yaxes(visible=False)
|
| 415 |
+
return fig_layout(fig, height=360)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 416 |
|
| 417 |
|
| 418 |
def parse_float_list(value: object) -> list[float]:
|
|
|
|
| 430 |
return out
|
| 431 |
|
| 432 |
|
| 433 |
+
def dataframe_table(
|
| 434 |
+
table_id: str,
|
| 435 |
+
*,
|
| 436 |
+
page_size: int = 8,
|
| 437 |
+
max_height: str = "520px",
|
| 438 |
+
) -> dash_table.DataTable:
|
| 439 |
+
return dash_table.DataTable(
|
| 440 |
+
id=table_id,
|
| 441 |
+
columns=[],
|
| 442 |
+
data=[],
|
| 443 |
+
page_size=page_size,
|
| 444 |
+
sort_action="native",
|
| 445 |
+
cell_selectable=True,
|
| 446 |
+
style_as_list_view=True,
|
| 447 |
+
fixed_rows={"headers": True},
|
| 448 |
+
style_table={"overflowX": "auto", "overflowY": "auto", "maxHeight": max_height},
|
| 449 |
+
style_header={
|
| 450 |
+
"backgroundColor": "#f3f6f8",
|
| 451 |
+
"fontWeight": "700",
|
| 452 |
+
"border": "0",
|
| 453 |
+
"borderBottom": "1px solid #cfd8df",
|
| 454 |
+
"color": "#26323f",
|
| 455 |
+
},
|
| 456 |
+
style_cell={
|
| 457 |
+
"fontFamily": "Inter, Arial, sans-serif",
|
| 458 |
+
"fontSize": "13px",
|
| 459 |
+
"padding": "10px 12px",
|
| 460 |
+
"textAlign": "left",
|
| 461 |
+
"minWidth": "90px",
|
| 462 |
+
"maxWidth": "260px",
|
| 463 |
+
"whiteSpace": "normal",
|
| 464 |
+
"height": "auto",
|
| 465 |
+
"border": "0",
|
| 466 |
+
"borderBottom": "1px solid #edf1f4",
|
| 467 |
+
},
|
| 468 |
+
style_cell_conditional=[
|
| 469 |
+
{"if": {"column_id": col}, "textAlign": "right"} for col in RIGHT_ALIGNED_COLUMNS
|
| 470 |
+
],
|
| 471 |
+
style_data_conditional=[
|
| 472 |
+
{"if": {"row_index": "odd"}, "backgroundColor": "#fbfcfd"},
|
| 473 |
+
{"if": {"state": "active"}, "backgroundColor": "#e5f3f2", "border": "1px solid #4c908b"},
|
| 474 |
+
],
|
| 475 |
+
)
|
| 476 |
+
|
| 477 |
+
|
| 478 |
+
def panel(title: str, *children, subtitle: str | None = None, className: str = "") -> html.Div:
|
| 479 |
+
heading = [html.H2(title)]
|
| 480 |
+
if subtitle:
|
| 481 |
+
heading.append(html.P(subtitle, className="panel-subtitle"))
|
| 482 |
+
classes = "panel" if not className else f"panel {className}"
|
| 483 |
+
return html.Div([html.Div(heading, className="panel-heading"), *children], className=classes)
|
| 484 |
+
|
| 485 |
+
|
| 486 |
+
def details_table(summary: str, table: dash_table.DataTable) -> html.Details:
|
| 487 |
+
return html.Details(
|
| 488 |
+
[html.Summary(summary), html.Div(table, className="details-body")],
|
| 489 |
+
className="details-table",
|
| 490 |
+
)
|
| 491 |
+
|
| 492 |
+
|
| 493 |
+
def metric_card(label: str, value: str, detail: str | None = None) -> html.Div:
|
| 494 |
+
children = [
|
| 495 |
+
html.Div(label, className="metric-label"),
|
| 496 |
+
html.Div(value, className="metric-value"),
|
| 497 |
+
]
|
| 498 |
+
if detail:
|
| 499 |
+
children.append(html.Div(detail, className="metric-detail"))
|
| 500 |
+
return html.Div(children, className="metric-card")
|
| 501 |
+
|
| 502 |
|
| 503 |
+
def leaderboard_frame(dataset: str, models: list[str] | None) -> pd.DataFrame:
|
| 504 |
chosen = selected_models(models)
|
| 505 |
+
base = pd.DataFrame({"model": chosen})
|
| 506 |
+
base = add_method_columns(base)
|
| 507 |
+
|
| 508 |
+
pred = filter_models(prediction, chosen)
|
| 509 |
+
pred = pred[pred["dataset"].astype(str) == str(dataset)].copy()
|
| 510 |
+
pred["score"] = pd.to_numeric(pred["score"], errors="coerce")
|
| 511 |
+
pred = pred[["model", "metric", "score", "n_train_trials", "n_test_trials", "n_neurons", "latent_dim"]]
|
| 512 |
+
pred = pred.rename(columns={"score": "task_score"})
|
| 513 |
+
|
| 514 |
+
rob = filter_models(present_rows(robustness), chosen)
|
| 515 |
+
rob = rob[rob["dataset"].astype(str) == str(dataset)].copy()
|
| 516 |
+
rob["raw_auc"] = pd.to_numeric(rob["raw_auc"], errors="coerce")
|
| 517 |
+
rob = rob[["model", "raw_auc"]].rename(columns={"raw_auc": "robustness_auc"})
|
| 518 |
+
|
| 519 |
+
cons = filter_models(active_rows(consistency), chosen)
|
| 520 |
+
cons = cons[cons["dataset"].astype(str) == str(dataset)].copy()
|
| 521 |
+
cons["mean_r2"] = pd.to_numeric(cons["mean_r2"], errors="coerce")
|
| 522 |
+
cons = cons[["model", "mean_r2"]].rename(columns={"mean_r2": "alignment_score"})
|
| 523 |
+
|
| 524 |
+
scale = filter_models(present_rows(scalability), chosen)
|
| 525 |
+
scale = scale[scale["dataset"].astype(str) == str(dataset)].copy()
|
| 526 |
+
scale = scale[["model", "training_time_sec", "inference_time_sec", "peak_ram_gb", "peak_vram_gb"]]
|
| 527 |
+
for col in ["training_time_sec", "inference_time_sec", "peak_ram_gb", "peak_vram_gb"]:
|
| 528 |
+
scale[col] = pd.to_numeric(scale[col], errors="coerce")
|
| 529 |
+
|
| 530 |
+
df = base.merge(pred, on="model", how="left")
|
| 531 |
+
df = df.merge(rob, on="model", how="left")
|
| 532 |
+
df = df.merge(cons, on="model", how="left")
|
| 533 |
+
df = df.merge(scale, on="model", how="left")
|
| 534 |
+
df["notes"] = np.where(df["task_score"].isna(), "Not available", "")
|
| 535 |
+
|
| 536 |
+
available = df["task_score"].notna()
|
| 537 |
+
order = df.loc[available].sort_values(
|
| 538 |
+
["task_score", "model_order"], ascending=[False, True]
|
| 539 |
+
).index
|
| 540 |
+
df["rank"] = None
|
| 541 |
+
for rank, idx in enumerate(order, start=1):
|
| 542 |
+
df.at[idx, "rank"] = rank
|
| 543 |
+
|
| 544 |
+
df = df.sort_values(
|
| 545 |
+
["task_score", "model_order"], ascending=[False, True], na_position="last"
|
| 546 |
+
)
|
| 547 |
+
df["id"] = df["model"]
|
| 548 |
+
return round_numeric(df, NUMERIC_COLUMNS)
|
| 549 |
+
|
| 550 |
+
|
| 551 |
+
def leaderboard_summary(dataset: str, table_df: pd.DataFrame) -> list[html.Div]:
|
| 552 |
+
label = DATASET_LABELS.get(dataset, dataset)
|
| 553 |
+
metric = metric_text(table_df["metric"].dropna().iloc[0]) if table_df["metric"].notna().any() else "score"
|
| 554 |
+
available = table_df.dropna(subset=["task_score"]).head(3)
|
| 555 |
+
cards = [metric_card("Dataset", label, f"Primary metric: {metric}")]
|
| 556 |
+
for _, row in available.iterrows():
|
| 557 |
+
cards.append(
|
| 558 |
+
metric_card(
|
| 559 |
+
f"Rank {int(row['rank'])}",
|
| 560 |
+
str(row["method"]),
|
| 561 |
+
f"{value_text(row['task_score'])} {metric}",
|
| 562 |
+
)
|
| 563 |
+
)
|
| 564 |
+
if len(cards) == 1:
|
| 565 |
+
cards.append(metric_card("Top method", "Not available"))
|
| 566 |
+
return cards
|
| 567 |
+
|
| 568 |
+
|
| 569 |
+
def performance_heatmap(dataset: str, models: list[str] | None) -> go.Figure:
|
| 570 |
+
df = filter_models(prediction, models)
|
| 571 |
if df.empty:
|
| 572 |
+
return empty_figure("No task performance results are available.")
|
| 573 |
+
df = add_method_columns(df)
|
| 574 |
+
df["score"] = pd.to_numeric(df["score"], errors="coerce")
|
| 575 |
+
df["dataset_label"] = df["dataset"].map(DATASET_LABELS).fillna(df["dataset"])
|
| 576 |
+
pivot = df.pivot_table(
|
| 577 |
+
index="method", columns="dataset_label", values="score", aggfunc="first"
|
| 578 |
+
)
|
| 579 |
|
| 580 |
+
selected_label = DATASET_LABELS.get(dataset, dataset)
|
| 581 |
+
order_df = leaderboard_frame(dataset, models)
|
| 582 |
+
method_order = [m for m in order_df["method"] if m in set(pivot.index)]
|
| 583 |
+
pivot = pivot.reindex(method_order)
|
| 584 |
+
dataset_order = [DATASET_LABELS.get(ds, ds) for ds in DATASETS]
|
| 585 |
+
pivot = pivot.reindex(columns=[label for label in dataset_order if label in pivot.columns])
|
| 586 |
+
text = pivot.map(lambda x: "" if pd.isna(x) else f"{x:.3f}") if not pivot.empty else pivot
|
| 587 |
+
|
| 588 |
+
zmin = min(0.0, float(np.nanmin(pivot.values))) if pivot.size and not np.isnan(pivot.values).all() else 0.0
|
| 589 |
+
zmax = max(1.0, float(np.nanmax(pivot.values))) if pivot.size and not np.isnan(pivot.values).all() else 1.0
|
| 590 |
+
fig = go.Figure(
|
| 591 |
+
go.Heatmap(
|
| 592 |
+
z=pivot.values if not pivot.empty else [[]],
|
| 593 |
+
x=list(pivot.columns),
|
| 594 |
+
y=list(pivot.index),
|
| 595 |
+
text=text.values if not pivot.empty else [[]],
|
| 596 |
+
texttemplate="%{text}",
|
| 597 |
+
colorscale=SCORE_SCALE,
|
| 598 |
+
zmin=zmin,
|
| 599 |
+
zmax=zmax,
|
| 600 |
+
colorbar=dict(title="Score", thickness=12),
|
| 601 |
+
hovertemplate="Method=%{y}<br>Dataset=%{x}<br>Score=%{z:.4f}<extra></extra>",
|
| 602 |
)
|
| 603 |
+
)
|
| 604 |
+
fig.update_layout(title=f"Task performance matrix, sorted by {selected_label}")
|
| 605 |
+
return fig_layout(fig, height=max(430, 26 * len(pivot.index) + 150))
|
| 606 |
+
|
| 607 |
+
|
| 608 |
+
def ranking_figure(dataset: str, table_df: pd.DataFrame) -> go.Figure:
|
| 609 |
+
rank_df = table_df.dropna(subset=["task_score"]).sort_values("task_score", ascending=True)
|
| 610 |
+
if rank_df.empty:
|
| 611 |
+
return empty_figure(f"No task performance results are available for {DATASET_LABELS.get(dataset, dataset)}.")
|
| 612 |
+
metric = metric_text(rank_df["metric"].dropna().iloc[0]) if rank_df["metric"].notna().any() else "score"
|
| 613 |
+
fig = go.Figure(
|
| 614 |
+
go.Bar(
|
| 615 |
+
x=rank_df["task_score"],
|
| 616 |
+
y=rank_df["method"],
|
| 617 |
+
orientation="h",
|
| 618 |
+
marker=dict(color="#006d77"),
|
| 619 |
+
hovertemplate="Method=%{y}<br>Score=%{x:.4f}<extra></extra>",
|
| 620 |
+
)
|
| 621 |
+
)
|
| 622 |
+
fig.update_layout(title=f"{DATASET_LABELS.get(dataset, dataset)} ranking")
|
| 623 |
+
fig.update_xaxes(title=metric)
|
| 624 |
+
fig.update_yaxes(title="")
|
| 625 |
+
return fig_layout(fig, height=max(420, 25 * len(rank_df) + 150))
|
| 626 |
+
|
| 627 |
+
|
| 628 |
+
def consistency_frame(dataset: str, models: list[str] | None) -> pd.DataFrame:
|
| 629 |
+
chosen = selected_models(models)
|
| 630 |
+
base = add_method_columns(pd.DataFrame({"model": chosen}))
|
| 631 |
+
|
| 632 |
+
cons = filter_models(active_rows(consistency), chosen)
|
| 633 |
+
cons = cons[cons["dataset"].astype(str) == str(dataset)].copy()
|
| 634 |
+
for col in ["mean_r2", "latent_dim", "n_sessions", "n_pairwise"]:
|
| 635 |
+
cons[col] = pd.to_numeric(cons[col], errors="coerce")
|
| 636 |
+
cons = cons[["model", "mean_r2", "latent_dim", "n_sessions", "n_pairwise"]]
|
| 637 |
+
cons = cons.rename(columns={"mean_r2": "alignment_score"})
|
| 638 |
+
|
| 639 |
+
df = base.merge(cons, on="model", how="left")
|
| 640 |
+
df["notes"] = np.where(df["alignment_score"].isna(), "Not available", "")
|
| 641 |
+
available = df["alignment_score"].notna()
|
| 642 |
+
order = df.loc[available].sort_values(
|
| 643 |
+
["alignment_score", "model_order"], ascending=[False, True]
|
| 644 |
+
).index
|
| 645 |
+
df["rank"] = None
|
| 646 |
+
for rank, idx in enumerate(order, start=1):
|
| 647 |
+
df.at[idx, "rank"] = rank
|
| 648 |
+
df = df.sort_values(
|
| 649 |
+
["alignment_score", "model_order"], ascending=[False, True], na_position="last"
|
| 650 |
+
)
|
| 651 |
+
df["id"] = df["model"]
|
| 652 |
+
return round_numeric(df, NUMERIC_COLUMNS)
|
| 653 |
+
|
| 654 |
+
|
| 655 |
+
def selected_consistency_model(df: pd.DataFrame, active_cell: dict | None) -> str | None:
|
| 656 |
+
if active_cell and active_cell.get("row_id") in set(df["model"]):
|
| 657 |
+
return str(active_cell["row_id"])
|
| 658 |
+
available = df.dropna(subset=["alignment_score"])
|
| 659 |
+
if available.empty:
|
| 660 |
+
return None
|
| 661 |
+
return str(available.iloc[0]["model"])
|
| 662 |
+
|
| 663 |
+
|
| 664 |
+
def consistency_bar_figure(dataset: str, df: pd.DataFrame) -> go.Figure:
|
| 665 |
+
bar_df = df.dropna(subset=["alignment_score"]).sort_values("alignment_score", ascending=True)
|
| 666 |
+
if bar_df.empty:
|
| 667 |
+
return empty_figure(f"No cross-session alignment results are available for {DATASET_LABELS.get(dataset, dataset)}.")
|
| 668 |
+
fig = go.Figure(
|
| 669 |
+
go.Bar(
|
| 670 |
+
x=bar_df["alignment_score"],
|
| 671 |
+
y=bar_df["method"],
|
| 672 |
+
orientation="h",
|
| 673 |
+
marker=dict(color="#4c908b"),
|
| 674 |
+
hovertemplate="Method=%{y}<br>Alignment=%{x:.4f}<extra></extra>",
|
| 675 |
+
)
|
| 676 |
+
)
|
| 677 |
+
fig.update_layout(title=f"{DATASET_LABELS.get(dataset, dataset)} cross-session alignment")
|
| 678 |
+
fig.update_xaxes(title="Alignment score")
|
| 679 |
+
fig.update_yaxes(title="")
|
| 680 |
+
return fig_layout(fig, height=max(360, 25 * len(bar_df) + 130))
|
| 681 |
+
|
| 682 |
+
|
| 683 |
+
def consistency_heatmap(models: list[str] | None) -> go.Figure:
|
| 684 |
+
df = filter_models(active_rows(consistency), models)
|
| 685 |
+
if df.empty:
|
| 686 |
+
return empty_figure("No cross-session alignment results are available.")
|
| 687 |
+
df = add_method_columns(df)
|
| 688 |
+
df["mean_r2"] = pd.to_numeric(df["mean_r2"], errors="coerce")
|
| 689 |
+
df["dataset_label"] = df["dataset"].map(DATASET_LABELS).fillna(df["dataset"])
|
| 690 |
+
pivot = df.pivot_table(index="method", columns="dataset_label", values="mean_r2", aggfunc="first")
|
| 691 |
+
if not pivot.empty:
|
| 692 |
+
pivot = pivot.loc[pivot.mean(axis=1, skipna=True).sort_values(ascending=False).index]
|
| 693 |
+
text = pivot.map(lambda x: "" if pd.isna(x) else f"{x:.2f}") if not pivot.empty else pivot
|
| 694 |
+
fig = go.Figure(
|
| 695 |
+
go.Heatmap(
|
| 696 |
+
z=pivot.values if not pivot.empty else [[]],
|
| 697 |
+
x=list(pivot.columns),
|
| 698 |
+
y=list(pivot.index),
|
| 699 |
+
text=text.values if not pivot.empty else [[]],
|
| 700 |
+
texttemplate="%{text}",
|
| 701 |
+
colorscale=SCORE_SCALE,
|
| 702 |
+
colorbar=dict(title="Score", thickness=12),
|
| 703 |
+
hovertemplate="Method=%{y}<br>Dataset=%{x}<br>Alignment=%{z:.4f}<extra></extra>",
|
| 704 |
+
)
|
| 705 |
+
)
|
| 706 |
+
fig.update_layout(title="Alignment across datasets")
|
| 707 |
+
return fig_layout(fig, height=max(360, 25 * len(pivot.index) + 135))
|
| 708 |
+
|
| 709 |
+
|
| 710 |
+
def latent_space_figure(dataset: str, model: str | None) -> go.Figure:
|
| 711 |
+
if not model:
|
| 712 |
+
return empty_figure("No latent-space view is available for this selection.")
|
| 713 |
+
if latent_samples.empty:
|
| 714 |
+
return empty_figure("Latent-space samples are unavailable in this view.")
|
| 715 |
+
|
| 716 |
+
plot_df = latent_samples[
|
| 717 |
+
(latent_samples["dataset"].astype(str) == str(dataset))
|
| 718 |
+
& (latent_samples["model"].astype(str) == str(model))
|
| 719 |
+
].copy()
|
| 720 |
+
if plot_df.empty:
|
| 721 |
+
return empty_figure(f"No latent-space samples are available for {model_label(model)} on {DATASET_LABELS.get(dataset, dataset)}.")
|
| 722 |
|
|
|
|
| 723 |
for col in ["x", "y", "z"]:
|
| 724 |
plot_df[col] = pd.to_numeric(plot_df[col], errors="coerce")
|
| 725 |
plot_df["condition_num"] = pd.to_numeric(plot_df["condition"], errors="coerce")
|
| 726 |
+
plot_df["condition_label"] = plot_df["condition"].map(lambda value: condition_label(dataset, value))
|
| 727 |
+
plot_df["session_display"] = plot_df["session_label"].map(lambda value: session_display_label(dataset, value))
|
|
|
|
| 728 |
plot_df = plot_df.dropna(subset=["x", "y", "z"])
|
| 729 |
if plot_df.empty:
|
| 730 |
+
return empty_figure("No latent-space samples match this selection.")
|
| 731 |
|
| 732 |
trajectory_df = latent_trajectories[
|
| 733 |
(latent_trajectories["dataset"].astype(str) == str(dataset))
|
|
|
|
| 737 |
if col in trajectory_df:
|
| 738 |
trajectory_df[col] = pd.to_numeric(trajectory_df[col], errors="coerce")
|
| 739 |
if not trajectory_df.empty:
|
| 740 |
+
trajectory_df["condition_label"] = trajectory_df["condition"].map(lambda value: condition_label(dataset, value))
|
| 741 |
+
trajectory_df["session_display"] = trajectory_df["session_label"].map(lambda value: session_display_label(dataset, value))
|
|
|
|
| 742 |
trajectory_df = trajectory_df.dropna(subset=["x", "y", "z"])
|
| 743 |
|
| 744 |
sessions = ordered_unique(plot_df["session_label"])
|
| 745 |
+
session_titles = [session_display_label(dataset, session) for session in sessions]
|
| 746 |
+
n_cols = 2 if len(sessions) > 1 else 1
|
| 747 |
n_rows = int(np.ceil(len(sessions) / n_cols))
|
| 748 |
+
specs = [[{"type": "scene"} for _ in range(n_cols)] for _ in range(n_rows)]
|
| 749 |
fig = make_subplots(
|
| 750 |
rows=n_rows,
|
| 751 |
cols=n_cols,
|
| 752 |
+
specs=specs,
|
| 753 |
+
subplot_titles=session_titles,
|
| 754 |
+
horizontal_spacing=0.045,
|
| 755 |
+
vertical_spacing=0.12,
|
| 756 |
)
|
| 757 |
|
| 758 |
+
condition_values = sorted(plot_df["condition"].astype(str).unique(), key=condition_sort_key)
|
| 759 |
+
use_categorical = len(condition_values) <= 12
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 760 |
condition_colors = {
|
| 761 |
+
condition: CATEGORICAL_PALETTE[idx % len(CATEGORICAL_PALETTE)]
|
| 762 |
for idx, condition in enumerate(condition_values)
|
| 763 |
}
|
| 764 |
+
condition_name = condition_axis_label(dataset)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 765 |
|
| 766 |
for session_idx, session in enumerate(sessions):
|
| 767 |
+
session_df = plot_df[plot_df["session_label"].astype(str) == str(session)]
|
| 768 |
if session_df.empty:
|
| 769 |
continue
|
| 770 |
row = session_idx // n_cols + 1
|
| 771 |
col = session_idx % n_cols + 1
|
| 772 |
+
display_session = session_display_label(dataset, session)
|
| 773 |
|
| 774 |
+
if use_categorical:
|
| 775 |
for condition in condition_values:
|
| 776 |
cond_df = session_df[session_df["condition"].astype(str) == condition]
|
| 777 |
if cond_df.empty:
|
| 778 |
continue
|
| 779 |
trace_name = condition_label(dataset, condition)
|
| 780 |
session_traj = trajectory_df[
|
| 781 |
+
(trajectory_df["session_label"].astype(str) == str(session))
|
| 782 |
& (trajectory_df["condition"].astype(str) == condition)
|
| 783 |
].sort_values("time_index")
|
| 784 |
fig.add_trace(
|
|
|
|
| 791 |
legendgroup=condition,
|
| 792 |
showlegend=session_idx == 0,
|
| 793 |
marker=dict(
|
| 794 |
+
size=2.4 if not session_traj.empty else 3.0,
|
| 795 |
+
opacity=0.32 if not session_traj.empty else 0.78,
|
| 796 |
color=condition_colors[condition],
|
| 797 |
),
|
| 798 |
customdata=np.stack(
|
| 799 |
[
|
| 800 |
+
np.repeat(display_session, len(cond_df)),
|
| 801 |
cond_df["condition_label"].astype(str),
|
| 802 |
cond_df["trial_index"].astype(str),
|
| 803 |
cond_df["time_index"].astype(str),
|
|
|
|
| 805 |
axis=-1,
|
| 806 |
),
|
| 807 |
hovertemplate=(
|
| 808 |
+
"Session=%{customdata[0]}<br>"
|
| 809 |
+
f"{condition_name}=%{{customdata[1]}}<br>"
|
| 810 |
+
"Trial=%{customdata[2]} time=%{customdata[3]}"
|
| 811 |
"<extra></extra>"
|
| 812 |
),
|
| 813 |
),
|
|
|
|
| 826 |
showlegend=False,
|
| 827 |
line=dict(color=condition_colors[condition], width=5),
|
| 828 |
hovertemplate=(
|
| 829 |
+
f"{condition_name}={trace_name}<br>"
|
| 830 |
+
"Time=%{customdata}<extra></extra>"
|
| 831 |
),
|
| 832 |
customdata=session_traj["time_index"],
|
| 833 |
),
|
|
|
|
| 841 |
y=session_df["y"],
|
| 842 |
z=session_df["z"],
|
| 843 |
mode="markers",
|
| 844 |
+
name=display_session,
|
| 845 |
showlegend=False,
|
| 846 |
marker=dict(
|
| 847 |
+
size=2.8,
|
| 848 |
opacity=0.72,
|
| 849 |
color=session_df["condition_num"],
|
| 850 |
colorscale="Viridis",
|
| 851 |
showscale=session_idx == 0,
|
| 852 |
+
colorbar=dict(title=condition_name, thickness=12),
|
| 853 |
),
|
| 854 |
customdata=np.stack(
|
| 855 |
[
|
| 856 |
+
np.repeat(display_session, len(session_df)),
|
| 857 |
session_df["condition"].map(lambda value: condition_label(dataset, value)).astype(str),
|
| 858 |
session_df["trial_index"].astype(str),
|
| 859 |
session_df["time_index"].astype(str),
|
|
|
|
| 861 |
axis=-1,
|
| 862 |
),
|
| 863 |
hovertemplate=(
|
| 864 |
+
"Session=%{customdata[0]}<br>"
|
| 865 |
+
f"{condition_name}=%{{customdata[1]}}<br>"
|
| 866 |
+
"Trial=%{customdata[2]} time=%{customdata[3]}"
|
| 867 |
"<extra></extra>"
|
| 868 |
),
|
| 869 |
),
|
|
|
|
| 883 |
fig.update_layout(
|
| 884 |
**{
|
| 885 |
scene_id: dict(
|
| 886 |
+
xaxis=dict(title="", range=[-lim, lim], showgrid=False, zeroline=False, showticklabels=False),
|
| 887 |
+
yaxis=dict(title="", range=[-lim, lim], showgrid=False, zeroline=False, showticklabels=False),
|
| 888 |
+
zaxis=dict(title="", range=[-lim, lim], showgrid=False, zeroline=False, showticklabels=False),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 889 |
aspectmode="cube",
|
| 890 |
bgcolor="#ffffff",
|
| 891 |
camera=dict(eye=dict(x=1.55, y=1.45, z=1.05)),
|
| 892 |
)
|
| 893 |
}
|
| 894 |
)
|
| 895 |
+
|
| 896 |
+
score_df = active_rows(consistency)
|
| 897 |
+
score_df = score_df[
|
| 898 |
+
(score_df["dataset"].astype(str) == str(dataset))
|
| 899 |
+
& (score_df["model"].astype(str) == str(model))
|
| 900 |
+
].copy()
|
| 901 |
score = None
|
| 902 |
+
if not score_df.empty:
|
| 903 |
+
score = pd.to_numeric(score_df["mean_r2"], errors="coerce").dropna()
|
| 904 |
+
score = float(score.iloc[0]) if not score.empty else None
|
| 905 |
+
score_suffix = "" if score is None else f" | alignment {score:.2f}"
|
| 906 |
fig.update_layout(
|
| 907 |
+
title=f"{model_label(model)} latent space on {DATASET_LABELS.get(dataset, dataset)}{score_suffix}",
|
| 908 |
+
height=760 if n_rows > 1 else 520,
|
| 909 |
+
paper_bgcolor="#ffffff",
|
| 910 |
+
plot_bgcolor="#ffffff",
|
| 911 |
+
margin=dict(l=8, r=8, t=78, b=92),
|
| 912 |
+
font=dict(family="Inter, Arial, sans-serif", size=13, color="#17202a"),
|
| 913 |
+
legend=dict(orientation="h", yanchor="top", y=-0.08, xanchor="left", x=0, title=condition_name),
|
| 914 |
)
|
| 915 |
+
fig.for_each_annotation(lambda ann: ann.update(font=dict(size=12, color="#526171")))
|
| 916 |
+
return fig
|
|
|
|
| 917 |
|
| 918 |
|
| 919 |
+
def robustness_figure(dataset: str, models: list[str] | None) -> go.Figure:
|
| 920 |
+
df = filter_models(present_rows(robustness), models)
|
| 921 |
+
df = df[df["dataset"].astype(str) == str(dataset)].copy() if not df.empty else df
|
| 922 |
+
if df.empty:
|
| 923 |
+
return empty_figure("No robustness results are available for this selection.")
|
| 924 |
+
df = add_method_columns(df).sort_values("model_order")
|
| 925 |
+
fig = go.Figure()
|
| 926 |
+
for idx, row in enumerate(df.itertuples()):
|
| 927 |
+
xs = parse_float_list(row.noise_levels)
|
| 928 |
+
ys = parse_float_list(row.scores)
|
| 929 |
+
if xs and len(xs) == len(ys):
|
| 930 |
+
fig.add_trace(
|
| 931 |
+
go.Scatter(
|
| 932 |
+
x=xs,
|
| 933 |
+
y=ys,
|
| 934 |
+
mode="lines+markers",
|
| 935 |
+
name=row.method,
|
| 936 |
+
line=dict(color=CATEGORICAL_PALETTE[idx % len(CATEGORICAL_PALETTE)], width=2),
|
| 937 |
+
marker=dict(size=5),
|
| 938 |
+
hovertemplate="Noise=%{x:.2f}<br>Score=%{y:.4f}<extra></extra>",
|
| 939 |
+
)
|
| 940 |
+
)
|
| 941 |
+
fig.update_layout(title=f"{DATASET_LABELS.get(dataset, dataset)} robustness curves")
|
| 942 |
+
fig.update_xaxes(title="Noise fraction")
|
| 943 |
+
fig.update_yaxes(title=metric_text(df["metric"].dropna().iloc[0]) if df["metric"].notna().any() else "score")
|
| 944 |
+
return fig_layout(fig, height=520)
|
| 945 |
+
|
| 946 |
+
|
| 947 |
+
def robustness_table_frame(dataset: str, models: list[str] | None) -> pd.DataFrame:
|
| 948 |
+
df = filter_models(present_rows(robustness), models)
|
| 949 |
+
df = df[df["dataset"].astype(str) == str(dataset)].copy() if not df.empty else df
|
| 950 |
+
if df.empty:
|
| 951 |
+
return pd.DataFrame(columns=["method", "metric", "reference_score", "highest_noise_score", "robustness_auc", "average_noisy_score"])
|
| 952 |
+
df = add_method_columns(df)
|
| 953 |
+
df = df.rename(
|
| 954 |
+
columns={
|
| 955 |
+
"score_at_noise0": "reference_score",
|
| 956 |
+
"score_at_max_noise": "highest_noise_score",
|
| 957 |
+
"raw_auc": "robustness_auc",
|
| 958 |
+
"mean_score": "average_noisy_score",
|
| 959 |
+
}
|
| 960 |
)
|
| 961 |
+
cols = ["method", "metric", "reference_score", "highest_noise_score", "robustness_auc", "average_noisy_score"]
|
| 962 |
+
return round_numeric(df[cols].sort_values("robustness_auc", ascending=False), NUMERIC_COLUMNS)
|
| 963 |
|
| 964 |
|
| 965 |
+
def compute_figures(dataset: str, models: list[str] | None) -> tuple[go.Figure, go.Figure, pd.DataFrame]:
|
| 966 |
+
df = filter_models(present_rows(scalability), models)
|
| 967 |
+
df = df[df["dataset"].astype(str) == str(dataset)].copy() if not df.empty else df
|
| 968 |
+
if df.empty:
|
| 969 |
+
empty = pd.DataFrame(columns=["method", "hardware", "training_time_sec", "inference_time_sec", "peak_ram_gb", "peak_vram_gb"])
|
| 970 |
+
return (
|
| 971 |
+
empty_figure("No compute results are available for this selection."),
|
| 972 |
+
empty_figure("No memory results are available for this selection."),
|
| 973 |
+
empty,
|
| 974 |
+
)
|
| 975 |
+
|
| 976 |
+
pred = present_rows(prediction)[["model", "dataset", "score", "metric"]].copy()
|
| 977 |
+
df = df.merge(pred, on=["model", "dataset"], how="left")
|
| 978 |
+
df = add_method_columns(df)
|
| 979 |
+
df["hardware"] = df["model"].map(hardware_label)
|
| 980 |
+
for col in ["training_time_sec", "inference_time_sec", "peak_ram_gb", "peak_vram_gb", "score"]:
|
| 981 |
+
df[col] = pd.to_numeric(df[col], errors="coerce")
|
| 982 |
+
|
| 983 |
+
scatter = go.Figure(
|
| 984 |
+
go.Scatter(
|
| 985 |
+
x=df["training_time_sec"],
|
| 986 |
+
y=df["score"],
|
| 987 |
+
mode="markers",
|
| 988 |
+
text=df["method"],
|
| 989 |
+
marker=dict(
|
| 990 |
+
size=np.clip(df["peak_ram_gb"].fillna(1.0) * 4, 8, 26),
|
| 991 |
+
color=df["hardware"].map({"CPU": "#8a6f2a", "GPU": "#006d77"}).fillna("#637381"),
|
| 992 |
+
opacity=0.82,
|
| 993 |
+
line=dict(color="#ffffff", width=1),
|
| 994 |
+
),
|
| 995 |
+
customdata=np.stack(
|
| 996 |
+
[
|
| 997 |
+
df["method"].astype(str),
|
| 998 |
+
df["hardware"].astype(str),
|
| 999 |
+
df["peak_ram_gb"].round(3).astype(str),
|
| 1000 |
+
df["peak_vram_gb"].round(3).astype(str),
|
| 1001 |
+
],
|
| 1002 |
+
axis=-1,
|
| 1003 |
+
),
|
| 1004 |
+
hovertemplate=(
|
| 1005 |
+
"Method=%{customdata[0]}<br>"
|
| 1006 |
+
"Hardware=%{customdata[1]}<br>"
|
| 1007 |
+
"Training time=%{x:.3f} s<br>"
|
| 1008 |
+
"Score=%{y:.4f}<br>"
|
| 1009 |
+
"Peak memory=%{customdata[2]} GB<br>"
|
| 1010 |
+
"Peak GPU memory=%{customdata[3]} GB"
|
| 1011 |
+
"<extra></extra>"
|
| 1012 |
+
),
|
| 1013 |
+
)
|
| 1014 |
)
|
| 1015 |
+
scatter.update_layout(title=f"{DATASET_LABELS.get(dataset, dataset)} performance and training time")
|
| 1016 |
+
scatter.update_xaxes(title="Training time (s, log scale)", type="log")
|
| 1017 |
+
scatter.update_yaxes(title=metric_text(df["metric"].dropna().iloc[0]) if df["metric"].notna().any() else "score")
|
| 1018 |
+
fig_layout(scatter, height=500)
|
| 1019 |
|
| 1020 |
+
mem_df = df.sort_values("peak_ram_gb", ascending=True)
|
| 1021 |
+
memory = go.Figure()
|
| 1022 |
+
memory.add_trace(go.Bar(x=mem_df["peak_ram_gb"], y=mem_df["method"], orientation="h", name="RAM"))
|
| 1023 |
+
memory.add_trace(go.Bar(x=mem_df["peak_vram_gb"], y=mem_df["method"], orientation="h", name="GPU memory"))
|
| 1024 |
+
memory.update_layout(title=f"{DATASET_LABELS.get(dataset, dataset)} peak memory", barmode="group")
|
| 1025 |
+
memory.update_xaxes(title="GB")
|
| 1026 |
+
memory.update_yaxes(title="")
|
| 1027 |
+
fig_layout(memory, height=max(420, 26 * len(mem_df) + 150))
|
| 1028 |
|
| 1029 |
+
table = df[["method", "hardware", "score", "training_time_sec", "inference_time_sec", "peak_ram_gb", "peak_vram_gb"]]
|
| 1030 |
+
return scatter, memory, round_numeric(table.sort_values("training_time_sec"), NUMERIC_COLUMNS)
|
|
|
|
|
|
|
|
|
|
| 1031 |
|
| 1032 |
|
| 1033 |
+
def influence_figures(dataset: str, models: list[str] | None) -> tuple[go.Figure, go.Figure, pd.DataFrame]:
|
| 1034 |
+
nshap = filter_models(active_rows(neuron_shap), models)
|
| 1035 |
+
nshap = nshap[nshap["dataset"].astype(str) == str(dataset)].copy() if not nshap.empty else nshap
|
| 1036 |
+
if nshap.empty:
|
| 1037 |
+
neuron_fig = empty_figure("No neuron-influence results are available for this dataset.")
|
| 1038 |
+
table = pd.DataFrame(columns=["method", "metric", "baseline_score", "full_model_score", "neuron_influence_auc", "shap_mean_value", "shap_fraction_positive"])
|
| 1039 |
+
else:
|
| 1040 |
+
nshap = add_method_columns(nshap)
|
| 1041 |
+
nshap["auc"] = pd.to_numeric(nshap["auc"], errors="coerce")
|
| 1042 |
+
bar_df = nshap.dropna(subset=["auc"]).sort_values("auc", ascending=True)
|
| 1043 |
+
neuron_fig = go.Figure(
|
| 1044 |
+
go.Bar(
|
| 1045 |
+
x=bar_df["auc"],
|
| 1046 |
+
y=bar_df["method"],
|
| 1047 |
+
orientation="h",
|
| 1048 |
+
marker=dict(color="#006d77"),
|
| 1049 |
+
hovertemplate="Method=%{y}<br>AUC=%{x:.4f}<extra></extra>",
|
| 1050 |
+
)
|
| 1051 |
+
)
|
| 1052 |
+
neuron_fig.update_layout(title=f"{DATASET_LABELS.get(dataset, dataset)} neuron influence")
|
| 1053 |
+
neuron_fig.update_xaxes(title="Neuron influence AUC")
|
| 1054 |
+
neuron_fig.update_yaxes(title="")
|
| 1055 |
+
fig_layout(neuron_fig, height=max(420, 25 * len(bar_df) + 150))
|
| 1056 |
+
table = nshap.rename(columns={"auc": "neuron_influence_auc"})
|
| 1057 |
+
table = table[
|
| 1058 |
+
["method", "metric", "baseline_score", "full_model_score", "neuron_influence_auc", "shap_mean_value", "shap_fraction_positive"]
|
| 1059 |
+
]
|
| 1060 |
+
|
| 1061 |
+
tshap = filter_models(active_rows(trial_shapley), models)
|
| 1062 |
+
tshap = tshap[tshap["dataset"].astype(str) == str(dataset)].copy() if not tshap.empty else tshap
|
| 1063 |
+
if tshap.empty:
|
| 1064 |
+
trial_fig = empty_figure("No trial-influence results are available for this dataset.")
|
| 1065 |
+
else:
|
| 1066 |
+
tshap = add_method_columns(tshap)
|
| 1067 |
+
tshap["perturbation_auc"] = pd.to_numeric(tshap["perturbation_auc"], errors="coerce")
|
| 1068 |
+
trial_df = tshap.dropna(subset=["perturbation_auc"]).sort_values("perturbation_auc", ascending=True)
|
| 1069 |
+
trial_fig = go.Figure(
|
| 1070 |
+
go.Bar(
|
| 1071 |
+
x=trial_df["perturbation_auc"],
|
| 1072 |
+
y=trial_df["method"],
|
| 1073 |
+
orientation="h",
|
| 1074 |
+
marker=dict(color="#8a6f2a"),
|
| 1075 |
+
hovertemplate="Method=%{y}<br>AUC=%{x:.4f}<extra></extra>",
|
| 1076 |
+
)
|
| 1077 |
+
)
|
| 1078 |
+
trial_fig.update_layout(title=f"{DATASET_LABELS.get(dataset, dataset)} trial influence")
|
| 1079 |
+
trial_fig.update_xaxes(title="Trial influence AUC")
|
| 1080 |
+
trial_fig.update_yaxes(title="")
|
| 1081 |
+
fig_layout(trial_fig, height=max(420, 25 * len(trial_df) + 150))
|
| 1082 |
+
|
| 1083 |
+
return neuron_fig, trial_fig, round_numeric(table, NUMERIC_COLUMNS)
|
| 1084 |
|
| 1085 |
|
| 1086 |
+
def methods_frame(models: list[str] | None) -> pd.DataFrame:
|
| 1087 |
+
chosen = selected_models(models)
|
| 1088 |
+
rows = []
|
| 1089 |
+
for model in chosen:
|
| 1090 |
+
rows.append(
|
| 1091 |
+
{
|
| 1092 |
+
"id": model,
|
| 1093 |
+
"method": model_label(model),
|
| 1094 |
+
"family": METHOD_FAMILY.get(model, "Model"),
|
| 1095 |
+
"hardware": hardware_label(model),
|
| 1096 |
+
}
|
| 1097 |
+
)
|
| 1098 |
+
return pd.DataFrame(rows).sort_values("method")
|
| 1099 |
+
|
| 1100 |
+
|
| 1101 |
+
app = Dash(__name__, title="Neural Model Benchmark")
|
| 1102 |
server = app.server
|
| 1103 |
|
| 1104 |
app.layout = html.Div(
|
|
|
|
| 1108 |
html.Div(
|
| 1109 |
[
|
| 1110 |
html.Div("Tang Lab", className="eyebrow"),
|
| 1111 |
+
html.H1("Neural Model Benchmark"),
|
| 1112 |
html.P(
|
| 1113 |
+
"Explore task performance, robustness, latent alignment, neuron and trial influence, and compute cost across 23 benchmarked methods.",
|
|
|
|
| 1114 |
className="lede",
|
| 1115 |
),
|
| 1116 |
],
|
|
|
|
| 1118 |
),
|
| 1119 |
html.Div(
|
| 1120 |
[
|
| 1121 |
+
metric_card("Methods", str(len(MODELS)), "Benchmarked in the paper"),
|
| 1122 |
+
metric_card("Datasets", str(len(DATASETS)), "Selectable below"),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1123 |
],
|
| 1124 |
+
className="hero-metrics",
|
| 1125 |
),
|
| 1126 |
],
|
| 1127 |
className="hero",
|
|
|
|
| 1133 |
html.Label("Dataset"),
|
| 1134 |
dcc.Dropdown(
|
| 1135 |
id="dataset-filter",
|
| 1136 |
+
options=[{"label": DATASET_LABELS.get(ds, ds), "value": ds} for ds in DATASETS],
|
|
|
|
|
|
|
|
|
|
| 1137 |
value=DATASETS[0] if DATASETS else None,
|
| 1138 |
clearable=False,
|
| 1139 |
),
|
|
|
|
| 1142 |
),
|
| 1143 |
html.Div(
|
| 1144 |
[
|
| 1145 |
+
html.Label("Method filter"),
|
| 1146 |
dcc.Dropdown(
|
| 1147 |
id="model-filter",
|
| 1148 |
+
options=[{"label": model_label(model), "value": model} for model in MODELS],
|
| 1149 |
+
value=[],
|
| 1150 |
multi=True,
|
| 1151 |
+
placeholder="All methods",
|
| 1152 |
),
|
| 1153 |
],
|
| 1154 |
className="control control-wide",
|
| 1155 |
),
|
| 1156 |
],
|
| 1157 |
+
className="toolbar",
|
| 1158 |
),
|
| 1159 |
dcc.Tabs(
|
| 1160 |
id="tabs",
|
| 1161 |
+
value="leaderboard",
|
| 1162 |
className="tabs",
|
| 1163 |
children=[
|
| 1164 |
dcc.Tab(
|
| 1165 |
+
label="Leaderboard",
|
| 1166 |
+
value="leaderboard",
|
| 1167 |
+
className="tab",
|
| 1168 |
+
selected_className="tab tab-selected",
|
| 1169 |
children=[
|
| 1170 |
panel(
|
| 1171 |
+
"Leaderboard",
|
| 1172 |
+
html.Div(id="leaderboard-top", className="top-strip"),
|
| 1173 |
+
html.Div(
|
| 1174 |
+
[
|
| 1175 |
+
html.Div(
|
| 1176 |
+
dataframe_table("leaderboard-table", page_size=23, max_height="680px"),
|
| 1177 |
+
className="leaderboard-table-wrap",
|
| 1178 |
+
),
|
| 1179 |
+
dcc.Graph(id="dataset-ranking", config={"displayModeBar": False}),
|
| 1180 |
+
],
|
| 1181 |
+
className="leaderboard-grid",
|
| 1182 |
+
),
|
| 1183 |
+
dcc.Graph(id="performance-heatmap", config={"displayModeBar": False}),
|
| 1184 |
+
subtitle="Sorted by the selected dataset. Regression datasets use R2; classification datasets use accuracy. Higher is better.",
|
| 1185 |
+
className="leaderboard-panel",
|
| 1186 |
)
|
| 1187 |
],
|
| 1188 |
),
|
| 1189 |
dcc.Tab(
|
| 1190 |
+
label="Consistency",
|
| 1191 |
+
value="consistency",
|
| 1192 |
+
className="tab",
|
| 1193 |
+
selected_className="tab tab-selected",
|
| 1194 |
children=[
|
| 1195 |
panel(
|
| 1196 |
+
"Cross-session alignment",
|
| 1197 |
+
html.Div(
|
| 1198 |
+
[
|
| 1199 |
+
html.Div(
|
| 1200 |
+
[
|
| 1201 |
+
dataframe_table("consistency-table", page_size=23, max_height="520px"),
|
| 1202 |
+
html.P("Click a method to update the 3D latent view.", className="table-hint"),
|
| 1203 |
+
],
|
| 1204 |
+
className="consistency-table-wrap",
|
| 1205 |
+
),
|
| 1206 |
+
dcc.Graph(
|
| 1207 |
+
id="latent-space",
|
| 1208 |
+
config={
|
| 1209 |
+
"displayModeBar": "hover",
|
| 1210 |
+
"toImageButtonOptions": {
|
| 1211 |
+
"format": "png",
|
| 1212 |
+
"filename": "benchdash_latent_space",
|
| 1213 |
+
"height": 900,
|
| 1214 |
+
"width": 1200,
|
| 1215 |
+
"scale": 2,
|
| 1216 |
+
},
|
| 1217 |
+
},
|
| 1218 |
+
),
|
| 1219 |
+
],
|
| 1220 |
+
className="latent-grid",
|
| 1221 |
+
),
|
| 1222 |
+
html.Div(
|
| 1223 |
+
[
|
| 1224 |
+
dcc.Graph(id="consistency-bars", config={"displayModeBar": False}),
|
| 1225 |
+
dcc.Graph(id="consistency-heatmap", config={"displayModeBar": False}),
|
| 1226 |
+
],
|
| 1227 |
+
className="chart-grid two",
|
| 1228 |
+
),
|
| 1229 |
+
subtitle="Each latent panel shows one recording session. Colors indicate task condition, stimulus, cue, or spatial bin.",
|
| 1230 |
)
|
| 1231 |
],
|
| 1232 |
),
|
| 1233 |
dcc.Tab(
|
| 1234 |
+
label="Robustness",
|
| 1235 |
+
value="robustness",
|
| 1236 |
+
className="tab",
|
| 1237 |
+
selected_className="tab tab-selected",
|
| 1238 |
children=[
|
| 1239 |
panel(
|
| 1240 |
+
"Robustness",
|
| 1241 |
+
dcc.Graph(id="robustness-curve", config={"displayModeBar": False}),
|
| 1242 |
+
details_table("View robustness rows", dataframe_table("robustness-table")),
|
| 1243 |
+
subtitle="Curves show how task performance changes as neural count noise increases.",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1244 |
)
|
| 1245 |
],
|
| 1246 |
),
|
| 1247 |
dcc.Tab(
|
| 1248 |
+
label="Influence",
|
| 1249 |
+
value="influence",
|
| 1250 |
+
className="tab",
|
| 1251 |
+
selected_className="tab tab-selected",
|
| 1252 |
children=[
|
| 1253 |
panel(
|
| 1254 |
+
"Neuron and trial influence",
|
| 1255 |
+
html.Div(
|
| 1256 |
+
[
|
| 1257 |
+
dcc.Graph(id="neuron-influence-bars", config={"displayModeBar": False}),
|
| 1258 |
+
dcc.Graph(id="trial-influence-bars", config={"displayModeBar": False}),
|
| 1259 |
+
],
|
| 1260 |
+
className="chart-grid two",
|
| 1261 |
+
),
|
| 1262 |
+
details_table("View neuron-influence rows", dataframe_table("influence-table")),
|
| 1263 |
+
subtitle="Signed contribution summaries preserve whether neurons or trials helped or hurt prediction.",
|
| 1264 |
)
|
| 1265 |
],
|
| 1266 |
),
|
| 1267 |
dcc.Tab(
|
| 1268 |
+
label="Compute",
|
| 1269 |
+
value="compute",
|
| 1270 |
+
className="tab",
|
| 1271 |
+
selected_className="tab tab-selected",
|
| 1272 |
children=[
|
| 1273 |
panel(
|
| 1274 |
+
"Performance and compute cost",
|
| 1275 |
+
html.Div(
|
| 1276 |
+
[
|
| 1277 |
+
dcc.Graph(id="compute-scatter", config={"displayModeBar": False}),
|
| 1278 |
+
dcc.Graph(id="memory-bars", config={"displayModeBar": False}),
|
| 1279 |
+
],
|
| 1280 |
+
className="chart-grid two",
|
| 1281 |
+
),
|
| 1282 |
+
details_table("View compute rows", dataframe_table("compute-table")),
|
| 1283 |
+
subtitle="Shows training time and peak memory for each method on the selected dataset.",
|
| 1284 |
)
|
| 1285 |
],
|
| 1286 |
),
|
| 1287 |
dcc.Tab(
|
| 1288 |
label="Methods",
|
| 1289 |
+
value="methods",
|
| 1290 |
+
className="tab",
|
| 1291 |
+
selected_className="tab tab-selected",
|
| 1292 |
children=[
|
| 1293 |
panel(
|
| 1294 |
+
"Benchmarked methods",
|
| 1295 |
+
dataframe_table("methods-table", page_size=23, max_height="600px"),
|
| 1296 |
+
subtitle="Paper-facing method names and broad method families.",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1297 |
)
|
| 1298 |
],
|
| 1299 |
),
|
|
|
|
| 1305 |
|
| 1306 |
|
| 1307 |
@app.callback(
|
| 1308 |
+
Output("leaderboard-top", "children"),
|
| 1309 |
+
Output("leaderboard-table", "columns"),
|
| 1310 |
+
Output("leaderboard-table", "data"),
|
| 1311 |
+
Output("dataset-ranking", "figure"),
|
| 1312 |
+
Output("performance-heatmap", "figure"),
|
| 1313 |
Input("dataset-filter", "value"),
|
| 1314 |
Input("model-filter", "value"),
|
| 1315 |
)
|
| 1316 |
+
def update_leaderboard(dataset: str, models: list[str] | None):
|
| 1317 |
+
df = leaderboard_frame(dataset, models)
|
| 1318 |
+
visible_cols = [
|
| 1319 |
+
"rank",
|
| 1320 |
+
"method",
|
| 1321 |
+
"task_score",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1322 |
"metric",
|
| 1323 |
+
"robustness_auc",
|
| 1324 |
+
"alignment_score",
|
| 1325 |
+
"training_time_sec",
|
| 1326 |
+
"peak_ram_gb",
|
| 1327 |
+
"notes",
|
|
|
|
| 1328 |
]
|
| 1329 |
+
table_df = df[[c for c in visible_cols + ["id", "model"] if c in df.columns]]
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1330 |
return (
|
| 1331 |
+
leaderboard_summary(dataset, df),
|
| 1332 |
+
column_defs([c for c in visible_cols if c in table_df.columns]),
|
| 1333 |
+
records(table_df),
|
| 1334 |
+
ranking_figure(dataset, df),
|
| 1335 |
+
performance_heatmap(dataset, models),
|
| 1336 |
)
|
| 1337 |
|
| 1338 |
|
| 1339 |
@app.callback(
|
| 1340 |
+
Output("consistency-table", "columns"),
|
| 1341 |
+
Output("consistency-table", "data"),
|
| 1342 |
+
Output("latent-space", "figure"),
|
| 1343 |
+
Output("consistency-bars", "figure"),
|
| 1344 |
+
Output("consistency-heatmap", "figure"),
|
| 1345 |
Input("dataset-filter", "value"),
|
| 1346 |
Input("model-filter", "value"),
|
| 1347 |
+
Input("consistency-table", "active_cell"),
|
| 1348 |
)
|
| 1349 |
+
def update_consistency(dataset: str, models: list[str] | None, active_cell: dict | None):
|
| 1350 |
+
df = consistency_frame(dataset, models)
|
| 1351 |
+
model = selected_consistency_model(df, active_cell)
|
| 1352 |
+
visible_cols = ["rank", "method", "alignment_score", "n_sessions", "latent_dim", "n_pairwise", "notes"]
|
| 1353 |
+
table_df = df[[c for c in visible_cols + ["id", "model"] if c in df.columns]]
|
| 1354 |
+
return (
|
| 1355 |
+
column_defs([c for c in visible_cols if c in table_df.columns]),
|
| 1356 |
+
records(table_df),
|
| 1357 |
+
latent_space_figure(dataset, model),
|
| 1358 |
+
consistency_bar_figure(dataset, df),
|
| 1359 |
+
consistency_heatmap(models),
|
| 1360 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1361 |
|
| 1362 |
|
| 1363 |
@app.callback(
|
| 1364 |
+
Output("robustness-curve", "figure"),
|
| 1365 |
+
Output("robustness-table", "columns"),
|
| 1366 |
+
Output("robustness-table", "data"),
|
|
|
|
|
|
|
| 1367 |
Input("dataset-filter", "value"),
|
| 1368 |
Input("model-filter", "value"),
|
| 1369 |
)
|
| 1370 |
+
def update_robustness(dataset: str, models: list[str] | None):
|
| 1371 |
+
table = robustness_table_frame(dataset, models)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1372 |
return (
|
| 1373 |
+
robustness_figure(dataset, models),
|
| 1374 |
+
column_defs(table.columns),
|
| 1375 |
+
records(table),
|
|
|
|
|
|
|
| 1376 |
)
|
| 1377 |
|
| 1378 |
|
| 1379 |
@app.callback(
|
| 1380 |
+
Output("compute-scatter", "figure"),
|
| 1381 |
Output("memory-bars", "figure"),
|
| 1382 |
+
Output("compute-table", "columns"),
|
| 1383 |
+
Output("compute-table", "data"),
|
| 1384 |
Input("dataset-filter", "value"),
|
| 1385 |
Input("model-filter", "value"),
|
| 1386 |
)
|
| 1387 |
+
def update_compute(dataset: str, models: list[str] | None):
|
| 1388 |
+
scatter, memory, table = compute_figures(dataset, models)
|
| 1389 |
+
return scatter, memory, column_defs(table.columns), records(table)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1390 |
|
| 1391 |
|
| 1392 |
@app.callback(
|
| 1393 |
+
Output("neuron-influence-bars", "figure"),
|
| 1394 |
+
Output("trial-influence-bars", "figure"),
|
| 1395 |
+
Output("influence-table", "columns"),
|
| 1396 |
+
Output("influence-table", "data"),
|
| 1397 |
Input("dataset-filter", "value"),
|
| 1398 |
Input("model-filter", "value"),
|
| 1399 |
)
|
| 1400 |
+
def update_influence(dataset: str, models: list[str] | None):
|
| 1401 |
+
neuron_fig, trial_fig, table = influence_figures(dataset, models)
|
| 1402 |
+
return neuron_fig, trial_fig, column_defs(table.columns), records(table)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1403 |
|
| 1404 |
|
| 1405 |
+
@app.callback(
|
| 1406 |
+
Output("methods-table", "columns"),
|
| 1407 |
+
Output("methods-table", "data"),
|
| 1408 |
+
Input("model-filter", "value"),
|
| 1409 |
+
)
|
| 1410 |
+
def update_methods(models: list[str] | None):
|
| 1411 |
+
table = methods_frame(models)
|
| 1412 |
+
return column_defs(table.columns), records(table)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1413 |
|
| 1414 |
|
| 1415 |
if __name__ == "__main__":
|
assets/styles.css
CHANGED
|
@@ -4,25 +4,31 @@
|
|
| 4 |
|
| 5 |
body {
|
| 6 |
margin: 0;
|
| 7 |
-
background: #
|
| 8 |
-
color: #
|
| 9 |
font-family: Inter, Arial, sans-serif;
|
| 10 |
}
|
| 11 |
|
| 12 |
.app-shell {
|
| 13 |
min-height: 100vh;
|
| 14 |
-
padding: 22px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
}
|
| 16 |
|
| 17 |
.hero {
|
| 18 |
display: grid;
|
| 19 |
-
grid-template-columns: minmax(
|
| 20 |
-
gap:
|
| 21 |
align-items: end;
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
padding: 14px 4px 18px;
|
| 25 |
-
border-bottom: 1px solid #dfe6eb;
|
| 26 |
}
|
| 27 |
|
| 28 |
.hero-copy {
|
|
@@ -30,8 +36,8 @@ body {
|
|
| 30 |
}
|
| 31 |
|
| 32 |
.eyebrow {
|
| 33 |
-
margin-bottom:
|
| 34 |
-
color: #
|
| 35 |
font-size: 12px;
|
| 36 |
font-weight: 800;
|
| 37 |
letter-spacing: 0;
|
|
@@ -46,137 +52,222 @@ p {
|
|
| 46 |
}
|
| 47 |
|
| 48 |
h1 {
|
| 49 |
-
margin-bottom:
|
| 50 |
-
font-size:
|
| 51 |
line-height: 1.05;
|
| 52 |
letter-spacing: 0;
|
| 53 |
}
|
| 54 |
|
| 55 |
h2 {
|
| 56 |
-
margin-bottom:
|
| 57 |
font-size: 20px;
|
| 58 |
letter-spacing: 0;
|
| 59 |
}
|
| 60 |
|
| 61 |
-
|
| 62 |
-
|
|
|
|
|
|
|
| 63 |
font-size: 15px;
|
| 64 |
-
|
| 65 |
}
|
| 66 |
|
| 67 |
-
.
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
}
|
| 74 |
|
| 75 |
-
.
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
align-items: end;
|
| 80 |
-
border: 1px solid #dfe6eb;
|
| 81 |
border-radius: 8px;
|
| 82 |
background: #ffffff;
|
| 83 |
}
|
| 84 |
|
| 85 |
-
.
|
| 86 |
-
|
| 87 |
-
padding: 18px;
|
| 88 |
-
border-right: 1px solid #e6ecef;
|
| 89 |
-
}
|
| 90 |
-
|
| 91 |
-
.stat-card:last-child {
|
| 92 |
-
border-right: 0;
|
| 93 |
-
}
|
| 94 |
-
|
| 95 |
-
.stat-label {
|
| 96 |
-
color: #617282;
|
| 97 |
font-size: 11px;
|
| 98 |
font-weight: 800;
|
| 99 |
letter-spacing: 0;
|
| 100 |
text-transform: uppercase;
|
| 101 |
}
|
| 102 |
|
| 103 |
-
.
|
| 104 |
-
margin-top:
|
| 105 |
-
color: #
|
| 106 |
-
font-size:
|
| 107 |
font-weight: 800;
|
| 108 |
-
line-height: 1;
|
| 109 |
}
|
| 110 |
|
| 111 |
-
.
|
| 112 |
-
margin-top:
|
| 113 |
-
color: #
|
| 114 |
font-size: 12px;
|
| 115 |
line-height: 1.35;
|
| 116 |
}
|
| 117 |
|
| 118 |
-
.
|
|
|
|
|
|
|
|
|
|
| 119 |
display: grid;
|
| 120 |
-
grid-template-columns: minmax(
|
| 121 |
-
gap:
|
| 122 |
-
margin:
|
| 123 |
-
|
| 124 |
-
padding:
|
| 125 |
-
border: 1px solid #
|
| 126 |
border-radius: 8px;
|
| 127 |
-
background:
|
|
|
|
|
|
|
| 128 |
}
|
| 129 |
|
| 130 |
.control label {
|
| 131 |
display: block;
|
| 132 |
-
margin-bottom:
|
| 133 |
-
color: #
|
| 134 |
font-size: 12px;
|
| 135 |
font-weight: 800;
|
| 136 |
letter-spacing: 0;
|
| 137 |
text-transform: uppercase;
|
| 138 |
}
|
| 139 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
.tabs {
|
| 141 |
-
|
| 142 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
}
|
| 144 |
|
| 145 |
.panel {
|
| 146 |
margin-top: 14px;
|
| 147 |
padding: 18px;
|
| 148 |
-
border: 1px solid #
|
| 149 |
border-radius: 8px;
|
| 150 |
background: #ffffff;
|
| 151 |
}
|
| 152 |
|
| 153 |
.panel-heading {
|
| 154 |
-
margin-bottom:
|
| 155 |
}
|
| 156 |
|
| 157 |
.panel-subtitle {
|
|
|
|
| 158 |
margin-bottom: 0;
|
| 159 |
-
color: #
|
| 160 |
font-size: 13px;
|
| 161 |
line-height: 1.45;
|
| 162 |
}
|
| 163 |
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
grid-template-columns: 1fr;
|
| 168 |
-
}
|
| 169 |
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 173 |
|
| 174 |
-
|
| 175 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
}
|
| 177 |
|
| 178 |
-
.
|
| 179 |
-
|
| 180 |
}
|
| 181 |
}
|
| 182 |
|
|
@@ -186,20 +277,16 @@ h3 {
|
|
| 186 |
}
|
| 187 |
|
| 188 |
h1 {
|
| 189 |
-
font-size:
|
| 190 |
-
}
|
| 191 |
-
|
| 192 |
-
.stat-grid {
|
| 193 |
-
grid-template-columns: 1fr;
|
| 194 |
}
|
| 195 |
|
| 196 |
-
.
|
| 197 |
-
|
| 198 |
-
border-right: 0;
|
| 199 |
-
border-bottom: 1px solid #e6ecef;
|
| 200 |
}
|
| 201 |
|
| 202 |
-
.
|
| 203 |
-
|
|
|
|
|
|
|
| 204 |
}
|
| 205 |
}
|
|
|
|
| 4 |
|
| 5 |
body {
|
| 6 |
margin: 0;
|
| 7 |
+
background: #f4f6f7;
|
| 8 |
+
color: #17202a;
|
| 9 |
font-family: Inter, Arial, sans-serif;
|
| 10 |
}
|
| 11 |
|
| 12 |
.app-shell {
|
| 13 |
min-height: 100vh;
|
| 14 |
+
padding: 18px 22px 28px;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
.hero,
|
| 18 |
+
.toolbar,
|
| 19 |
+
.tabs {
|
| 20 |
+
max-width: 1560px;
|
| 21 |
+
margin-left: auto;
|
| 22 |
+
margin-right: auto;
|
| 23 |
}
|
| 24 |
|
| 25 |
.hero {
|
| 26 |
display: grid;
|
| 27 |
+
grid-template-columns: minmax(360px, 1fr) auto;
|
| 28 |
+
gap: 26px;
|
| 29 |
align-items: end;
|
| 30 |
+
padding: 8px 2px 14px;
|
| 31 |
+
border-bottom: 1px solid #d8e1e7;
|
|
|
|
|
|
|
| 32 |
}
|
| 33 |
|
| 34 |
.hero-copy {
|
|
|
|
| 36 |
}
|
| 37 |
|
| 38 |
.eyebrow {
|
| 39 |
+
margin-bottom: 7px;
|
| 40 |
+
color: #526171;
|
| 41 |
font-size: 12px;
|
| 42 |
font-weight: 800;
|
| 43 |
letter-spacing: 0;
|
|
|
|
| 52 |
}
|
| 53 |
|
| 54 |
h1 {
|
| 55 |
+
margin-bottom: 8px;
|
| 56 |
+
font-size: 40px;
|
| 57 |
line-height: 1.05;
|
| 58 |
letter-spacing: 0;
|
| 59 |
}
|
| 60 |
|
| 61 |
h2 {
|
| 62 |
+
margin-bottom: 5px;
|
| 63 |
font-size: 20px;
|
| 64 |
letter-spacing: 0;
|
| 65 |
}
|
| 66 |
|
| 67 |
+
.lede {
|
| 68 |
+
max-width: 840px;
|
| 69 |
+
margin-bottom: 0;
|
| 70 |
+
color: #526171;
|
| 71 |
font-size: 15px;
|
| 72 |
+
line-height: 1.45;
|
| 73 |
}
|
| 74 |
|
| 75 |
+
.hero-metrics,
|
| 76 |
+
.top-strip {
|
| 77 |
+
display: flex;
|
| 78 |
+
gap: 10px;
|
| 79 |
+
align-items: stretch;
|
| 80 |
+
flex-wrap: wrap;
|
| 81 |
}
|
| 82 |
|
| 83 |
+
.metric-card {
|
| 84 |
+
min-width: 145px;
|
| 85 |
+
padding: 12px 14px;
|
| 86 |
+
border: 1px solid #d8e1e7;
|
|
|
|
|
|
|
| 87 |
border-radius: 8px;
|
| 88 |
background: #ffffff;
|
| 89 |
}
|
| 90 |
|
| 91 |
+
.metric-label {
|
| 92 |
+
color: #607080;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
font-size: 11px;
|
| 94 |
font-weight: 800;
|
| 95 |
letter-spacing: 0;
|
| 96 |
text-transform: uppercase;
|
| 97 |
}
|
| 98 |
|
| 99 |
+
.metric-value {
|
| 100 |
+
margin-top: 7px;
|
| 101 |
+
color: #101820;
|
| 102 |
+
font-size: 22px;
|
| 103 |
font-weight: 800;
|
| 104 |
+
line-height: 1.1;
|
| 105 |
}
|
| 106 |
|
| 107 |
+
.metric-detail {
|
| 108 |
+
margin-top: 6px;
|
| 109 |
+
color: #647383;
|
| 110 |
font-size: 12px;
|
| 111 |
line-height: 1.35;
|
| 112 |
}
|
| 113 |
|
| 114 |
+
.toolbar {
|
| 115 |
+
position: sticky;
|
| 116 |
+
top: 0;
|
| 117 |
+
z-index: 10;
|
| 118 |
display: grid;
|
| 119 |
+
grid-template-columns: minmax(230px, 310px) minmax(360px, 1fr);
|
| 120 |
+
gap: 16px;
|
| 121 |
+
margin-top: 14px;
|
| 122 |
+
margin-bottom: 14px;
|
| 123 |
+
padding: 12px;
|
| 124 |
+
border: 1px solid #d8e1e7;
|
| 125 |
border-radius: 8px;
|
| 126 |
+
background: rgba(255, 255, 255, 0.96);
|
| 127 |
+
box-shadow: 0 8px 24px rgba(18, 28, 38, 0.06);
|
| 128 |
+
backdrop-filter: blur(6px);
|
| 129 |
}
|
| 130 |
|
| 131 |
.control label {
|
| 132 |
display: block;
|
| 133 |
+
margin-bottom: 6px;
|
| 134 |
+
color: #344452;
|
| 135 |
font-size: 12px;
|
| 136 |
font-weight: 800;
|
| 137 |
letter-spacing: 0;
|
| 138 |
text-transform: uppercase;
|
| 139 |
}
|
| 140 |
|
| 141 |
+
.Select--multi .Select-value {
|
| 142 |
+
margin-top: 3px;
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
.Select-multi-value-wrapper {
|
| 146 |
+
max-height: 86px;
|
| 147 |
+
overflow-y: auto;
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
.tabs {
|
| 151 |
+
border: 0;
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
.tab {
|
| 155 |
+
padding: 11px 18px !important;
|
| 156 |
+
border: 0 !important;
|
| 157 |
+
border-bottom: 2px solid transparent !important;
|
| 158 |
+
background: transparent !important;
|
| 159 |
+
color: #637381 !important;
|
| 160 |
+
font-size: 14px;
|
| 161 |
+
font-weight: 700;
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
.tab-selected {
|
| 165 |
+
color: #12302f !important;
|
| 166 |
+
border-bottom-color: #006d77 !important;
|
| 167 |
+
background: #ffffff !important;
|
| 168 |
}
|
| 169 |
|
| 170 |
.panel {
|
| 171 |
margin-top: 14px;
|
| 172 |
padding: 18px;
|
| 173 |
+
border: 1px solid #d8e1e7;
|
| 174 |
border-radius: 8px;
|
| 175 |
background: #ffffff;
|
| 176 |
}
|
| 177 |
|
| 178 |
.panel-heading {
|
| 179 |
+
margin-bottom: 14px;
|
| 180 |
}
|
| 181 |
|
| 182 |
.panel-subtitle {
|
| 183 |
+
max-width: 980px;
|
| 184 |
margin-bottom: 0;
|
| 185 |
+
color: #526171;
|
| 186 |
font-size: 13px;
|
| 187 |
line-height: 1.45;
|
| 188 |
}
|
| 189 |
|
| 190 |
+
.leaderboard-panel {
|
| 191 |
+
padding-top: 16px;
|
| 192 |
+
}
|
|
|
|
|
|
|
| 193 |
|
| 194 |
+
.top-strip {
|
| 195 |
+
margin-bottom: 14px;
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
.leaderboard-grid {
|
| 199 |
+
display: grid;
|
| 200 |
+
grid-template-columns: minmax(560px, 1.08fr) minmax(360px, 0.92fr);
|
| 201 |
+
gap: 16px;
|
| 202 |
+
align-items: start;
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
.leaderboard-table-wrap,
|
| 206 |
+
.consistency-table-wrap {
|
| 207 |
+
min-width: 0;
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
.latent-grid {
|
| 211 |
+
display: grid;
|
| 212 |
+
grid-template-columns: minmax(360px, 0.42fr) minmax(620px, 1fr);
|
| 213 |
+
gap: 16px;
|
| 214 |
+
align-items: start;
|
| 215 |
+
}
|
| 216 |
|
| 217 |
+
.chart-grid {
|
| 218 |
+
display: grid;
|
| 219 |
+
gap: 16px;
|
| 220 |
+
align-items: start;
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
.chart-grid.two {
|
| 224 |
+
grid-template-columns: repeat(2, minmax(320px, 1fr));
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
.table-hint {
|
| 228 |
+
margin: 10px 2px 0;
|
| 229 |
+
color: #647383;
|
| 230 |
+
font-size: 12px;
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
.details-table {
|
| 234 |
+
margin-top: 12px;
|
| 235 |
+
border-top: 1px solid #e5ebef;
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
.details-table summary {
|
| 239 |
+
cursor: pointer;
|
| 240 |
+
padding: 12px 0 8px;
|
| 241 |
+
color: #344452;
|
| 242 |
+
font-size: 13px;
|
| 243 |
+
font-weight: 800;
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
.details-body {
|
| 247 |
+
padding-top: 4px;
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
.dash-table-container .dash-spreadsheet-container .dash-spreadsheet-inner th {
|
| 251 |
+
background: #f3f6f8 !important;
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
.dash-table-container .previous-next-container {
|
| 255 |
+
margin-top: 8px;
|
| 256 |
+
color: #526171;
|
| 257 |
+
font-size: 12px;
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
@media (max-width: 1180px) {
|
| 261 |
+
.hero,
|
| 262 |
+
.toolbar,
|
| 263 |
+
.leaderboard-grid,
|
| 264 |
+
.latent-grid,
|
| 265 |
+
.chart-grid.two {
|
| 266 |
+
grid-template-columns: 1fr;
|
| 267 |
}
|
| 268 |
|
| 269 |
+
.hero-metrics {
|
| 270 |
+
justify-content: flex-start;
|
| 271 |
}
|
| 272 |
}
|
| 273 |
|
|
|
|
| 277 |
}
|
| 278 |
|
| 279 |
h1 {
|
| 280 |
+
font-size: 32px;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 281 |
}
|
| 282 |
|
| 283 |
+
.panel {
|
| 284 |
+
padding: 14px;
|
|
|
|
|
|
|
| 285 |
}
|
| 286 |
|
| 287 |
+
.tab {
|
| 288 |
+
padding-left: 10px !important;
|
| 289 |
+
padding-right: 10px !important;
|
| 290 |
+
font-size: 13px;
|
| 291 |
}
|
| 292 |
}
|