josephsoo commited on
Commit
bb9fb13
·
1 Parent(s): 3e61b47

Polish dashboard and add latent consistency views

Browse files
Files changed (6) hide show
  1. .gitignore +3 -0
  2. README.md +8 -13
  3. app.py +173 -103
  4. assets/styles.css +85 -78
  5. data/latent_samples.csv +0 -0
  6. requirements.txt +5 -5
.gitignore ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ __pycache__/
2
+ *.pyc
3
+ .cache/
README.md CHANGED
@@ -6,18 +6,13 @@ app_port: 7860
6
 
7
  # BenchDash
8
 
9
- Interactive dashboard for the Tang Lab neural-behavioral benchmark summaries.
 
10
 
11
- This Space is intentionally visualization-first. It loads lightweight CSV
12
- exports from the benchmark repository and lets readers inspect:
 
13
 
14
- - clean prediction scores across models and datasets
15
- - robustness curves under count-noise perturbations
16
- - cross-recording latent consistency
17
- - training time and memory summaries
18
- - neuron-SHAP and trial-Shapley perturbation summaries
19
- - model runtime coverage and container assignments
20
-
21
- The Space does not train the full benchmark model suite. Full reproduction is
22
- container-backed, while a curated Colab companion is the right place for a
23
- small interactive training demo.
 
6
 
7
  # BenchDash
8
 
9
+ Interactive result dashboard for the Tang Lab neural-behavioral model
10
+ benchmark.
11
 
12
+ The dashboard summarizes exported benchmark results across five datasets and
13
+ 23 model variants, including clean prediction, count-noise robustness,
14
+ cross-session latent consistency, attribution analyses, and compute cost.
15
 
16
+ The bundled files in `data/` are lightweight summary exports for interactive
17
+ inspection. The full benchmark artifacts and reproduction workflow live in the
18
+ main benchmark repository.
 
 
 
 
 
 
 
app.py CHANGED
@@ -8,6 +8,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
 
12
 
13
  DATA_DIR = Path(__file__).resolve().parent / "data"
@@ -55,7 +56,7 @@ DEFAULT_MODELS = [
55
  def load_csv(name: str) -> pd.DataFrame:
56
  path = DATA_DIR / name
57
  if not path.exists():
58
- return pd.DataFrame()
59
  return pd.read_csv(path)
60
 
61
 
@@ -65,6 +66,7 @@ consistency = load_csv("consistency_summary.csv")
65
  scalability = load_csv("scalability_summary.csv")
66
  neuron_shap = load_csv("neuron_shap_summary.csv")
67
  trial_shapley = load_csv("trial_shapley_summary.csv")
 
68
 
69
 
70
  def present_rows(df: pd.DataFrame) -> pd.DataFrame:
@@ -202,6 +204,127 @@ def parse_float_list(value: object) -> list[float]:
202
  return out
203
 
204
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
205
  def dataframe_table(
206
  table_id: str,
207
  *,
@@ -254,10 +377,6 @@ def panel(title: str, *children, subtitle: str | None = None) -> html.Div:
254
  return html.Div([html.Div(heading, className="panel-heading"), *children], className="panel")
255
 
256
 
257
- def code_block(text: str) -> html.Pre:
258
- return html.Pre(html.Code(text), className="code-block")
259
-
260
-
261
  present_prediction = present_rows(prediction)
262
  present_cells = len(present_prediction)
263
  total_cells = len(prediction)
@@ -276,10 +395,10 @@ app.layout = html.Div(
276
  html.Div(
277
  [
278
  html.Div("Tang Lab", className="eyebrow"),
279
- html.H1("BenchDash"),
280
  html.P(
281
- "Interactive summaries for the neural-behavioral model benchmark. "
282
- "The Space visualizes tracked result summaries; full training runs remain container-backed.",
283
  className="lede",
284
  ),
285
  ],
@@ -288,12 +407,12 @@ app.layout = html.Div(
288
  html.Div(
289
  [
290
  stat_card("Models", str(len(MODELS)), "registered benchmark methods"),
291
- stat_card("Datasets", str(len(DATASETS)), "default paper grid"),
292
- stat_card("Clean Coverage", coverage_text, f"{coverage_pct:.1f}% present"),
293
  stat_card(
294
- "Runtime Mix",
295
  f"{runtime_counts.get('cpu', 0)} CPU / {runtime_counts.get('gpu', 0)} GPU",
296
- "from model capability metadata",
297
  ),
298
  ],
299
  className="stat-grid",
@@ -340,15 +459,15 @@ app.layout = html.Div(
340
  className="tabs",
341
  children=[
342
  dcc.Tab(
343
- label="Prediction",
344
  value="prediction",
345
  children=[
346
  panel(
347
- "Clean Prediction",
348
  dcc.Graph(id="prediction-heatmap", config={"displayModeBar": False}),
349
  dcc.Graph(id="prediction-ranking", config={"displayModeBar": False}),
350
  dataframe_table("prediction-table"),
351
- subtitle="Task score uses R2 for regression datasets and accuracy for classification datasets.",
352
  )
353
  ],
354
  ),
@@ -369,11 +488,12 @@ app.layout = html.Div(
369
  value="consistency",
370
  children=[
371
  panel(
372
- "Cross-Recording Consistency",
 
373
  dcc.Graph(id="consistency-bars", config={"displayModeBar": False}),
374
  dcc.Graph(id="consistency-heatmap", config={"displayModeBar": False}),
375
  dataframe_table("consistency-table"),
376
- subtitle="Scores summarize latent alignment across sessions for datasets with multiple recordings.",
377
  )
378
  ],
379
  ),
@@ -386,7 +506,7 @@ app.layout = html.Div(
386
  dcc.Graph(id="runtime-scatter", config={"displayModeBar": False}),
387
  dcc.Graph(id="memory-bars", config={"displayModeBar": False}),
388
  dataframe_table("runtime-table"),
389
- subtitle="Training time and memory are exported from the local benchmark artifacts.",
390
  )
391
  ],
392
  ),
@@ -404,11 +524,11 @@ app.layout = html.Div(
404
  ],
405
  ),
406
  dcc.Tab(
407
- label="Coverage",
408
  value="coverage",
409
  children=[
410
  panel(
411
- "Model Coverage",
412
  dcc.Graph(id="coverage-bars", config={"displayModeBar": False}),
413
  dataframe_table(
414
  "coverage-table",
@@ -416,67 +536,7 @@ app.layout = html.Div(
416
  data=model_inventory.to_dict("records"),
417
  page_size=25,
418
  ),
419
- subtitle="Container labels indicate the reproducible full-suite runtime, not what this Space executes.",
420
- )
421
- ],
422
- ),
423
- dcc.Tab(
424
- label="Run It",
425
- value="run-it",
426
- children=[
427
- panel(
428
- "How This Is Meant To Be Used",
429
- html.Div(
430
- [
431
- html.Div(
432
- [
433
- html.H3("This Space"),
434
- html.P(
435
- "Use BenchDash to inspect paper-result summaries, compare models, "
436
- "check missingness, and understand runtime tradeoffs. It does not "
437
- "train the benchmark models on Hugging Face CPU hardware."
438
- ),
439
- ],
440
- className="route-card",
441
- ),
442
- html.Div(
443
- [
444
- html.H3("Colab Companion"),
445
- html.P(
446
- "The right notebook target is a curated subset with clean Python installs: "
447
- "PCA, GPFA, SVC, XGBoost, CEBRA, and selected simple neural baselines."
448
- ),
449
- ],
450
- className="route-card",
451
- ),
452
- html.Div(
453
- [
454
- html.H3("Full Benchmark"),
455
- html.P(
456
- "The full model suite should stay container-backed because several "
457
- "upstream methods have stale pins, MATLAB Runtime requirements, or "
458
- "CUDA/package coupling."
459
- ),
460
- ],
461
- className="route-card",
462
- ),
463
- ],
464
- className="route-grid",
465
- ),
466
- html.H3("Example container commands"),
467
- code_block(
468
- "bin/submit_local --model pca --stage comprehensive -- --dataset local_regression\n"
469
- "bin/submit_gpu --model cebra --stage comprehensive -- --dataset monkey --session sub-C_ses-CO-20151104_behavior+ecephys\n"
470
- "bin/submit_cpu --model gpfa --stage stage1 -- --dataset monkey --session sub-C_ses-CO-20151104_behavior+ecephys"
471
- ),
472
- html.H3("Canonical uploaded-data shape"),
473
- code_block(
474
- "neural: float array, shape (N_trials, T, N_neurons)\n"
475
- "trial_ids: array, shape (N_trials,)\n"
476
- "targets: task-dependent behavior labels or trajectories\n"
477
- "optional: condition_ids, consistency_ids"
478
- ),
479
- subtitle="The upload/data-contract story belongs in the Colab and container path, not as the main dashboard affordance.",
480
  )
481
  ],
482
  ),
@@ -506,8 +566,9 @@ def update_prediction(dataset: str, models: list[str] | None):
506
  heat_df["dataset_label"] = heat_df["dataset"].map(DATASET_LABELS).fillna(heat_df["dataset"])
507
  pivot = heat_df.pivot_table(index="model", columns="dataset_label", values="score", aggfunc="first")
508
  if not pivot.empty:
509
- order = pivot.mean(axis=1, skipna=True).sort_values(ascending=False).index
510
- pivot = pivot.loc[order]
 
511
  dataset_order = [DATASET_LABELS.get(ds, ds) for ds in DATASETS if DATASET_LABELS.get(ds, ds) in pivot.columns]
512
  pivot = pivot.reindex(columns=dataset_order)
513
  text = pivot.map(lambda x: "" if pd.isna(x) else f"{x:.3f}") if not pivot.empty else pivot
@@ -527,7 +588,7 @@ def update_prediction(dataset: str, models: list[str] | None):
527
  hovertemplate="model=%{y}<br>dataset=%{x}<br>score=%{z:.4f}<extra></extra>",
528
  )
529
  )
530
- heatmap.update_layout(title="Clean-score matrix")
531
  fig_layout(heatmap, height=max(430, 28 * len(pivot.index) + 170))
532
 
533
  rank_df = present_rows(df_all)
@@ -563,7 +624,6 @@ def update_prediction(dataset: str, models: list[str] | None):
563
  "n_train_trials",
564
  "n_test_trials",
565
  "n_neurons",
566
- "source_path",
567
  ]
568
  table_df = df_all[df_all["dataset"] == dataset].copy()
569
  for col in ["score", "latent_dim", "n_train_trials", "n_test_trials", "n_neurons"]:
@@ -611,7 +671,7 @@ def update_robustness(dataset: str, models: list[str] | None):
611
  fig.update_yaxes(title=ordered_unique(df["metric"])[0] if "metric" in df else "score")
612
  fig_layout(fig, height=520)
613
 
614
- table_cols = ["model", "metric", "score_at_noise0", "score_at_max_noise", "raw_auc", "mean_score", "source_path"]
615
  table_df = df[table_cols].copy()
616
  for col in ["score_at_noise0", "score_at_max_noise", "raw_auc", "mean_score"]:
617
  table_df[col] = table_df[col].map(lambda v: compact_number(v))
@@ -619,6 +679,7 @@ def update_robustness(dataset: str, models: list[str] | None):
619
 
620
 
621
  @app.callback(
 
622
  Output("consistency-bars", "figure"),
623
  Output("consistency-heatmap", "figure"),
624
  Output("consistency-table", "columns"),
@@ -627,33 +688,40 @@ def update_robustness(dataset: str, models: list[str] | None):
627
  Input("model-filter", "value"),
628
  )
629
  def update_consistency(dataset: str, models: list[str] | None):
 
630
  df_all = filter_models(consistency, models)
631
  if "is_active_model" in df_all.columns:
632
  df_all = df_all[df_all["is_active_model"].astype(str).str.lower() == "true"]
633
  df = df_all[df_all["dataset"] == dataset].copy() if not df_all.empty else df_all
634
  if df.empty:
635
- cols = [{"name": c, "id": c} for c in ["model", "dataset", "mean_r2_procrustes"]]
636
- return empty_figure("No consistency rows for this dataset."), empty_figure("No consistency rows."), cols, []
 
 
 
 
 
 
637
 
638
- for col in ["mean_r2", "mean_r2_procrustes"]:
639
- df[col] = pd.to_numeric(df[col], errors="coerce")
640
- bar_df = df.sort_values("mean_r2_procrustes", ascending=True)
641
  bars = go.Figure(
642
  go.Bar(
643
- x=bar_df["mean_r2_procrustes"],
644
  y=bar_df["model"],
645
  orientation="h",
646
- marker=dict(color=bar_df["mean_r2_procrustes"], colorscale="Bluyl"),
647
- hovertemplate="model=%{y}<br>Procrustes R2=%{x:.4f}<extra></extra>",
648
  )
649
  )
650
  bars.update_layout(title=f"{DATASET_LABELS.get(dataset, dataset)} cross-session alignment")
651
- bars.update_xaxes(title="mean Procrustes R2")
652
  fig_layout(bars, height=max(420, 27 * len(bar_df) + 150))
653
 
654
  heat = df_all.copy()
 
655
  heat["dataset_label"] = heat["dataset"].map(DATASET_LABELS).fillna(heat["dataset"])
656
- pivot = heat.pivot_table(index="model", columns="dataset_label", values="mean_r2_procrustes", aggfunc="first")
657
  if not pivot.empty:
658
  pivot = pivot.loc[pivot.mean(axis=1, skipna=True).sort_values(ascending=False).index]
659
  text = pivot.map(lambda x: "" if pd.isna(x) else f"{x:.2f}") if not pivot.empty else pivot
@@ -678,16 +746,20 @@ def update_consistency(dataset: str, models: list[str] | None):
678
  "n_sessions",
679
  "latent_dim",
680
  "mean_r2",
681
- "mean_r2_procrustes",
682
  "n_pairwise",
683
  "scoring_modes",
684
- "source_path",
685
  ]
686
  table_df = df[[c for c in table_cols if c in df.columns]].copy()
687
- for col in ["mean_r2", "mean_r2_procrustes", "latent_dim", "n_sessions", "n_pairwise"]:
688
  if col in table_df:
689
  table_df[col] = table_df[col].map(lambda v: compact_number(v))
690
- return bars, heatmap, [{"name": c, "id": c} for c in table_df.columns], table_df.to_dict("records")
 
 
 
 
 
 
691
 
692
 
693
  @app.callback(
@@ -750,7 +822,6 @@ def update_runtime(dataset: str, models: list[str] | None):
750
  "inference_time_sec",
751
  "peak_ram_gb",
752
  "peak_vram_gb",
753
- "source_path",
754
  ]
755
  table_df = df[table_cols].copy()
756
  for col in ["score", "training_time_sec", "inference_time_sec", "peak_ram_gb", "peak_vram_gb"]:
@@ -821,11 +892,10 @@ def update_attribution(dataset: str, models: list[str] | None):
821
  "spearman_corr",
822
  "shap_mean_value",
823
  "shap_fraction_positive",
824
- "source_path",
825
  ]
826
  table_df = nshap[[c for c in table_cols if c in nshap.columns]].copy()
827
  for col in table_df.columns:
828
- if col not in {"model", "dataset", "metric", "source_path"}:
829
  table_df[col] = table_df[col].map(lambda v: compact_number(v))
830
  return nshap_fig, tshap_fig, [{"name": c, "id": c} for c in table_df.columns], table_df.to_dict("records")
831
 
 
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
12
 
13
 
14
  DATA_DIR = Path(__file__).resolve().parent / "data"
 
56
  def load_csv(name: str) -> pd.DataFrame:
57
  path = DATA_DIR / name
58
  if not path.exists():
59
+ raise FileNotFoundError(f"Missing bundled dashboard data: {path}")
60
  return pd.read_csv(path)
61
 
62
 
 
66
  scalability = load_csv("scalability_summary.csv")
67
  neuron_shap = load_csv("neuron_shap_summary.csv")
68
  trial_shapley = load_csv("trial_shapley_summary.csv")
69
+ latent_samples = load_csv("latent_samples.csv")
70
 
71
 
72
  def present_rows(df: pd.DataFrame) -> pd.DataFrame:
 
204
  return out
205
 
206
 
207
+ def latent_space_figure(dataset: str, models: list[str] | None) -> go.Figure:
208
+ if latent_samples.empty:
209
+ return empty_figure("Latent-space samples are not packaged for this build.")
210
+
211
+ chosen = selected_models(models)
212
+ df = latent_samples[
213
+ (latent_samples["dataset"].astype(str) == str(dataset))
214
+ & (latent_samples["model"].astype(str).isin(chosen))
215
+ ].copy()
216
+ if df.empty:
217
+ label = DATASET_LABELS.get(dataset, dataset)
218
+ return empty_figure(f"Latent-space samples are not available for {label}.")
219
+
220
+ score_df = consistency.copy()
221
+ if "is_active_model" in score_df.columns:
222
+ score_df = score_df[score_df["is_active_model"].astype(str).str.lower() == "true"]
223
+ score_df = score_df[
224
+ (score_df["dataset"].astype(str) == str(dataset))
225
+ & (score_df["model"].astype(str).isin(df["model"].astype(str).unique()))
226
+ ].copy()
227
+ if not score_df.empty:
228
+ score_df["mean_r2"] = pd.to_numeric(score_df["mean_r2"], errors="coerce")
229
+ plot_models = (
230
+ score_df.sort_values("mean_r2", ascending=False)["model"]
231
+ .astype(str)
232
+ .head(4)
233
+ .tolist()
234
+ )
235
+ else:
236
+ plot_models = sorted(df["model"].astype(str).unique())[:4]
237
+
238
+ plot_df = df[df["model"].astype(str).isin(plot_models)].copy()
239
+ for col in ["x", "y", "z"]:
240
+ plot_df[col] = pd.to_numeric(plot_df[col], errors="coerce")
241
+ plot_df = plot_df.dropna(subset=["x", "y", "z"])
242
+ if plot_df.empty:
243
+ return empty_figure("Latent-space samples are empty after filtering.")
244
+
245
+ score_lookup = (
246
+ score_df.set_index("model")["mean_r2"].to_dict() if not score_df.empty else {}
247
+ )
248
+ titles = []
249
+ for model in plot_models:
250
+ score = score_lookup.get(model)
251
+ suffix = "" if pd.isna(score) else f" R2={score:.2f}"
252
+ titles.append(f"{model}{suffix}")
253
+
254
+ n_cols = 2
255
+ n_rows = int(np.ceil(len(plot_models) / n_cols))
256
+ fig = make_subplots(
257
+ rows=n_rows,
258
+ cols=n_cols,
259
+ specs=[[{"type": "scene"} for _ in range(n_cols)] for _ in range(n_rows)],
260
+ subplot_titles=titles,
261
+ horizontal_spacing=0.03,
262
+ vertical_spacing=0.08,
263
+ )
264
+ palette = px.colors.qualitative.Set2 + px.colors.qualitative.Dark24
265
+ sessions = ordered_unique(plot_df["session_label"])
266
+ session_colors = {session: palette[idx % len(palette)] for idx, session in enumerate(sessions)}
267
+
268
+ for model_idx, model in enumerate(plot_models):
269
+ model_df = plot_df[plot_df["model"].astype(str) == model]
270
+ row = model_idx // n_cols + 1
271
+ col = model_idx % n_cols + 1
272
+ for session in sessions:
273
+ session_df = model_df[model_df["session_label"].astype(str) == session]
274
+ if session_df.empty:
275
+ continue
276
+ fig.add_trace(
277
+ go.Scatter3d(
278
+ x=session_df["x"],
279
+ y=session_df["y"],
280
+ z=session_df["z"],
281
+ mode="markers",
282
+ name=session,
283
+ legendgroup=session,
284
+ showlegend=model_idx == 0,
285
+ marker=dict(
286
+ size=2.4,
287
+ opacity=0.58,
288
+ color=session_colors[session],
289
+ ),
290
+ customdata=np.stack(
291
+ [
292
+ session_df["model"].astype(str),
293
+ session_df["condition"].astype(str),
294
+ session_df["trial_index"].astype(str),
295
+ session_df["time_index"].astype(str),
296
+ ],
297
+ axis=-1,
298
+ ),
299
+ hovertemplate=(
300
+ "model=%{customdata[0]}<br>"
301
+ "session=%{fullData.name}<br>"
302
+ "condition=%{customdata[1]}<br>"
303
+ "trial=%{customdata[2]} time=%{customdata[3]}"
304
+ "<extra></extra>"
305
+ ),
306
+ ),
307
+ row=row,
308
+ col=col,
309
+ )
310
+
311
+ for idx in range(len(plot_models)):
312
+ scene_id = "scene" if idx == 0 else f"scene{idx + 1}"
313
+ fig.update_layout(
314
+ **{
315
+ scene_id: dict(
316
+ xaxis=dict(title="", showgrid=False, zeroline=False, showticklabels=False),
317
+ yaxis=dict(title="", showgrid=False, zeroline=False, showticklabels=False),
318
+ zaxis=dict(title="", showgrid=False, zeroline=False, showticklabels=False),
319
+ bgcolor="#ffffff",
320
+ camera=dict(eye=dict(x=1.55, y=1.55, z=1.1)),
321
+ )
322
+ }
323
+ )
324
+ fig.update_layout(title=f"{DATASET_LABELS.get(dataset, dataset)} latent-space samples")
325
+ return fig_layout(fig, height=720 if n_rows > 1 else 420)
326
+
327
+
328
  def dataframe_table(
329
  table_id: str,
330
  *,
 
377
  return html.Div([html.Div(heading, className="panel-heading"), *children], className="panel")
378
 
379
 
 
 
 
 
380
  present_prediction = present_rows(prediction)
381
  present_cells = len(present_prediction)
382
  total_cells = len(prediction)
 
395
  html.Div(
396
  [
397
  html.Div("Tang Lab", className="eyebrow"),
398
+ html.H1("Benchmark results"),
399
  html.P(
400
+ "Compare neural-behavioral models across prediction, robustness, "
401
+ "cross-session latent geometry, attribution, and compute cost.",
402
  className="lede",
403
  ),
404
  ],
 
407
  html.Div(
408
  [
409
  stat_card("Models", str(len(MODELS)), "registered benchmark methods"),
410
+ stat_card("Datasets", str(len(DATASETS)), "neural-behavioral tasks"),
411
+ stat_card("Result Coverage", coverage_text, f"{coverage_pct:.1f}% complete"),
412
  stat_card(
413
+ "Compute Mix",
414
  f"{runtime_counts.get('cpu', 0)} CPU / {runtime_counts.get('gpu', 0)} GPU",
415
+ "registered model runtimes",
416
  ),
417
  ],
418
  className="stat-grid",
 
459
  className="tabs",
460
  children=[
461
  dcc.Tab(
462
+ label="Overview",
463
  value="prediction",
464
  children=[
465
  panel(
466
+ "Clean Prediction Scores",
467
  dcc.Graph(id="prediction-heatmap", config={"displayModeBar": False}),
468
  dcc.Graph(id="prediction-ranking", config={"displayModeBar": False}),
469
  dataframe_table("prediction-table"),
470
+ subtitle="Scores are shown within dataset. Regression tasks use R2; classification tasks use accuracy.",
471
  )
472
  ],
473
  ),
 
488
  value="consistency",
489
  children=[
490
  panel(
491
+ "Cross-Session Latent Consistency",
492
+ dcc.Graph(id="latent-space", config={"displayModeBar": False}),
493
  dcc.Graph(id="consistency-bars", config={"displayModeBar": False}),
494
  dcc.Graph(id="consistency-heatmap", config={"displayModeBar": False}),
495
  dataframe_table("consistency-table"),
496
+ subtitle="Latent clouds show sampled 3D embeddings from real consistency artifacts. Scores summarize cross-session alignment.",
497
  )
498
  ],
499
  ),
 
506
  dcc.Graph(id="runtime-scatter", config={"displayModeBar": False}),
507
  dcc.Graph(id="memory-bars", config={"displayModeBar": False}),
508
  dataframe_table("runtime-table"),
509
+ subtitle="Training time and memory are summarized from benchmark run metadata.",
510
  )
511
  ],
512
  ),
 
524
  ],
525
  ),
526
  dcc.Tab(
527
+ label="Methods",
528
  value="coverage",
529
  children=[
530
  panel(
531
+ "Model and Analysis Coverage",
532
  dcc.Graph(id="coverage-bars", config={"displayModeBar": False}),
533
  dataframe_table(
534
  "coverage-table",
 
536
  data=model_inventory.to_dict("records"),
537
  page_size=25,
538
  ),
539
+ subtitle="Coverage reports which methods have exported benchmark summaries in this dashboard.",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
540
  )
541
  ],
542
  ),
 
566
  heat_df["dataset_label"] = heat_df["dataset"].map(DATASET_LABELS).fillna(heat_df["dataset"])
567
  pivot = heat_df.pivot_table(index="model", columns="dataset_label", values="score", aggfunc="first")
568
  if not pivot.empty:
569
+ preferred = [m for m in selected_models(models) if m in pivot.index]
570
+ remaining = [m for m in sorted(pivot.index.astype(str)) if m not in preferred]
571
+ pivot = pivot.loc[preferred + remaining]
572
  dataset_order = [DATASET_LABELS.get(ds, ds) for ds in DATASETS if DATASET_LABELS.get(ds, ds) in pivot.columns]
573
  pivot = pivot.reindex(columns=dataset_order)
574
  text = pivot.map(lambda x: "" if pd.isna(x) else f"{x:.3f}") if not pivot.empty else pivot
 
588
  hovertemplate="model=%{y}<br>dataset=%{x}<br>score=%{z:.4f}<extra></extra>",
589
  )
590
  )
591
+ heatmap.update_layout(title="Clean-score matrix by dataset")
592
  fig_layout(heatmap, height=max(430, 28 * len(pivot.index) + 170))
593
 
594
  rank_df = present_rows(df_all)
 
624
  "n_train_trials",
625
  "n_test_trials",
626
  "n_neurons",
 
627
  ]
628
  table_df = df_all[df_all["dataset"] == dataset].copy()
629
  for col in ["score", "latent_dim", "n_train_trials", "n_test_trials", "n_neurons"]:
 
671
  fig.update_yaxes(title=ordered_unique(df["metric"])[0] if "metric" in df else "score")
672
  fig_layout(fig, height=520)
673
 
674
+ table_cols = ["model", "metric", "score_at_noise0", "score_at_max_noise", "raw_auc", "mean_score"]
675
  table_df = df[table_cols].copy()
676
  for col in ["score_at_noise0", "score_at_max_noise", "raw_auc", "mean_score"]:
677
  table_df[col] = table_df[col].map(lambda v: compact_number(v))
 
679
 
680
 
681
  @app.callback(
682
+ Output("latent-space", "figure"),
683
  Output("consistency-bars", "figure"),
684
  Output("consistency-heatmap", "figure"),
685
  Output("consistency-table", "columns"),
 
688
  Input("model-filter", "value"),
689
  )
690
  def update_consistency(dataset: str, models: list[str] | None):
691
+ latent_fig = latent_space_figure(dataset, models)
692
  df_all = filter_models(consistency, models)
693
  if "is_active_model" in df_all.columns:
694
  df_all = df_all[df_all["is_active_model"].astype(str).str.lower() == "true"]
695
  df = df_all[df_all["dataset"] == dataset].copy() if not df_all.empty else df_all
696
  if df.empty:
697
+ cols = [{"name": c, "id": c} for c in ["model", "dataset", "mean_r2"]]
698
+ return (
699
+ latent_fig,
700
+ empty_figure("Consistency analysis is not available for this dataset."),
701
+ empty_figure("Consistency analysis is not available for this dataset."),
702
+ cols,
703
+ [],
704
+ )
705
 
706
+ df["mean_r2"] = pd.to_numeric(df["mean_r2"], errors="coerce")
707
+ bar_df = df.sort_values("mean_r2", ascending=True)
 
708
  bars = go.Figure(
709
  go.Bar(
710
+ x=bar_df["mean_r2"],
711
  y=bar_df["model"],
712
  orientation="h",
713
+ marker=dict(color=bar_df["mean_r2"], colorscale="Bluyl"),
714
+ hovertemplate="model=%{y}<br>R2=%{x:.4f}<extra></extra>",
715
  )
716
  )
717
  bars.update_layout(title=f"{DATASET_LABELS.get(dataset, dataset)} cross-session alignment")
718
+ bars.update_xaxes(title="mean R2")
719
  fig_layout(bars, height=max(420, 27 * len(bar_df) + 150))
720
 
721
  heat = df_all.copy()
722
+ heat["mean_r2"] = pd.to_numeric(heat["mean_r2"], errors="coerce")
723
  heat["dataset_label"] = heat["dataset"].map(DATASET_LABELS).fillna(heat["dataset"])
724
+ pivot = heat.pivot_table(index="model", columns="dataset_label", values="mean_r2", aggfunc="first")
725
  if not pivot.empty:
726
  pivot = pivot.loc[pivot.mean(axis=1, skipna=True).sort_values(ascending=False).index]
727
  text = pivot.map(lambda x: "" if pd.isna(x) else f"{x:.2f}") if not pivot.empty else pivot
 
746
  "n_sessions",
747
  "latent_dim",
748
  "mean_r2",
 
749
  "n_pairwise",
750
  "scoring_modes",
 
751
  ]
752
  table_df = df[[c for c in table_cols if c in df.columns]].copy()
753
+ for col in ["mean_r2", "latent_dim", "n_sessions", "n_pairwise"]:
754
  if col in table_df:
755
  table_df[col] = table_df[col].map(lambda v: compact_number(v))
756
+ return (
757
+ latent_fig,
758
+ bars,
759
+ heatmap,
760
+ [{"name": c, "id": c} for c in table_df.columns],
761
+ table_df.to_dict("records"),
762
+ )
763
 
764
 
765
  @app.callback(
 
822
  "inference_time_sec",
823
  "peak_ram_gb",
824
  "peak_vram_gb",
 
825
  ]
826
  table_df = df[table_cols].copy()
827
  for col in ["score", "training_time_sec", "inference_time_sec", "peak_ram_gb", "peak_vram_gb"]:
 
892
  "spearman_corr",
893
  "shap_mean_value",
894
  "shap_fraction_positive",
 
895
  ]
896
  table_df = nshap[[c for c in table_cols if c in nshap.columns]].copy()
897
  for col in table_df.columns:
898
+ if col not in {"model", "dataset", "metric"}:
899
  table_df[col] = table_df[col].map(lambda v: compact_number(v))
900
  return nshap_fig, tshap_fig, [{"name": c, "id": c} for c in table_df.columns], table_df.to_dict("records")
901
 
assets/styles.css CHANGED
@@ -4,46 +4,37 @@
4
 
5
  body {
6
  margin: 0;
7
- background: #eef3f5;
8
- color: #1f2933;
9
  font-family: Inter, Arial, sans-serif;
10
  }
11
 
12
  .app-shell {
13
  min-height: 100vh;
14
- padding: 24px;
15
  }
16
 
17
  .hero {
18
  display: grid;
19
- grid-template-columns: minmax(280px, 1fr) minmax(360px, 0.95fr);
20
- gap: 20px;
21
- align-items: stretch;
22
  margin: 0 auto 18px;
23
- max-width: 1480px;
24
- }
25
-
26
- .hero-copy,
27
- .stat-card,
28
- .controls,
29
- .panel,
30
- .route-card {
31
- background: #ffffff;
32
- border: 1px solid #d8e2e8;
33
- border-radius: 8px;
34
- box-shadow: 0 1px 2px rgba(31, 41, 51, 0.04);
35
  }
36
 
37
  .hero-copy {
38
- padding: 28px 30px;
39
  }
40
 
41
  .eyebrow {
42
- color: #176b5a;
 
43
  font-size: 12px;
44
  font-weight: 800;
45
  letter-spacing: 0;
46
- margin-bottom: 8px;
47
  text-transform: uppercase;
48
  }
49
 
@@ -56,58 +47,72 @@ p {
56
 
57
  h1 {
58
  margin-bottom: 10px;
59
- font-size: 46px;
60
- line-height: 1.02;
61
  letter-spacing: 0;
62
  }
63
 
64
  h2 {
65
  margin-bottom: 4px;
66
  font-size: 20px;
 
67
  }
68
 
69
  h3 {
70
  margin-bottom: 8px;
71
- font-size: 16px;
 
72
  }
73
 
74
  .lede {
75
  max-width: 760px;
76
  margin-bottom: 0;
77
- color: #536471;
78
- font-size: 17px;
79
- line-height: 1.55;
80
  }
81
 
82
  .stat-grid {
83
  display: grid;
84
- grid-template-columns: repeat(2, minmax(160px, 1fr));
85
- gap: 12px;
 
 
 
 
86
  }
87
 
88
  .stat-card {
89
- padding: 20px;
 
 
 
 
 
 
90
  }
91
 
92
  .stat-label {
93
- color: #536471;
94
- font-size: 12px;
95
  font-weight: 800;
 
96
  text-transform: uppercase;
97
  }
98
 
99
  .stat-value {
100
- margin-top: 8px;
101
  color: #111827;
102
- font-size: 30px;
103
  font-weight: 800;
104
  line-height: 1;
105
  }
106
 
107
  .stat-detail {
108
  margin-top: 8px;
109
- color: #687987;
110
- font-size: 13px;
 
111
  }
112
 
113
  .controls {
@@ -115,30 +120,34 @@ h3 {
115
  grid-template-columns: minmax(240px, 320px) minmax(320px, 1fr);
116
  gap: 18px;
117
  margin: 0 auto 18px;
118
- max-width: 1480px;
119
- padding: 16px;
 
 
 
120
  }
121
 
122
  .control label {
123
  display: block;
124
  margin-bottom: 7px;
125
- color: #374151;
126
- font-size: 13px;
127
  font-weight: 800;
 
 
128
  }
129
 
130
  .tabs {
131
  margin: 0 auto;
132
- max-width: 1480px;
133
- }
134
-
135
- .tab {
136
- border-radius: 8px 8px 0 0;
137
  }
138
 
139
  .panel {
140
- margin-top: 16px;
141
  padding: 18px;
 
 
 
142
  }
143
 
144
  .panel-heading {
@@ -147,52 +156,50 @@ h3 {
147
 
148
  .panel-subtitle {
149
  margin-bottom: 0;
150
- color: #5d6e7d;
151
- font-size: 14px;
152
  line-height: 1.45;
153
  }
154
 
155
- .route-grid {
156
- display: grid;
157
- grid-template-columns: repeat(3, minmax(0, 1fr));
158
- gap: 14px;
159
- margin-bottom: 18px;
160
- }
161
 
162
- .route-card {
163
- padding: 18px;
164
- box-shadow: none;
165
- }
166
 
167
- .route-card p {
168
- margin-bottom: 0;
169
- color: #536471;
170
- line-height: 1.5;
171
- }
172
 
173
- .code-block {
174
- overflow-x: auto;
175
- margin: 10px 0 22px;
176
- padding: 14px 16px;
177
- background: #18202a;
178
- border-radius: 8px;
179
- color: #e6edf2;
180
- font-size: 13px;
181
- line-height: 1.5;
182
  }
183
 
184
- @media (max-width: 900px) {
185
  .app-shell {
186
  padding: 12px;
187
  }
188
 
189
- .hero,
190
- .controls,
191
- .route-grid {
 
 
192
  grid-template-columns: 1fr;
193
  }
194
 
195
- h1 {
196
- font-size: 34px;
 
 
 
 
 
 
197
  }
198
  }
 
4
 
5
  body {
6
  margin: 0;
7
+ background: #f7f8fa;
8
+ color: #18212b;
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(340px, 1fr) minmax(420px, 0.9fr);
20
+ gap: 28px;
21
+ align-items: end;
22
  margin: 0 auto 18px;
23
+ max-width: 1500px;
24
+ padding: 14px 4px 18px;
25
+ border-bottom: 1px solid #dfe6eb;
 
 
 
 
 
 
 
 
 
26
  }
27
 
28
  .hero-copy {
29
+ min-width: 0;
30
  }
31
 
32
  .eyebrow {
33
+ margin-bottom: 8px;
34
+ color: #536271;
35
  font-size: 12px;
36
  font-weight: 800;
37
  letter-spacing: 0;
 
38
  text-transform: uppercase;
39
  }
40
 
 
47
 
48
  h1 {
49
  margin-bottom: 10px;
50
+ font-size: 44px;
51
+ line-height: 1.05;
52
  letter-spacing: 0;
53
  }
54
 
55
  h2 {
56
  margin-bottom: 4px;
57
  font-size: 20px;
58
+ letter-spacing: 0;
59
  }
60
 
61
  h3 {
62
  margin-bottom: 8px;
63
+ font-size: 15px;
64
+ letter-spacing: 0;
65
  }
66
 
67
  .lede {
68
  max-width: 760px;
69
  margin-bottom: 0;
70
+ color: #536271;
71
+ font-size: 16px;
72
+ line-height: 1.5;
73
  }
74
 
75
  .stat-grid {
76
  display: grid;
77
+ grid-template-columns: repeat(4, minmax(110px, 1fr));
78
+ gap: 0;
79
+ align-items: end;
80
+ border: 1px solid #dfe6eb;
81
+ border-radius: 8px;
82
+ background: #ffffff;
83
  }
84
 
85
  .stat-card {
86
+ min-height: 108px;
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
  .stat-value {
104
+ margin-top: 9px;
105
  color: #111827;
106
+ font-size: 28px;
107
  font-weight: 800;
108
  line-height: 1;
109
  }
110
 
111
  .stat-detail {
112
  margin-top: 8px;
113
+ color: #657483;
114
+ font-size: 12px;
115
+ line-height: 1.35;
116
  }
117
 
118
  .controls {
 
120
  grid-template-columns: minmax(240px, 320px) minmax(320px, 1fr);
121
  gap: 18px;
122
  margin: 0 auto 18px;
123
+ max-width: 1500px;
124
+ padding: 14px;
125
+ border: 1px solid #dfe6eb;
126
+ border-radius: 8px;
127
+ background: #ffffff;
128
  }
129
 
130
  .control label {
131
  display: block;
132
  margin-bottom: 7px;
133
+ color: #394756;
134
+ font-size: 12px;
135
  font-weight: 800;
136
+ letter-spacing: 0;
137
+ text-transform: uppercase;
138
  }
139
 
140
  .tabs {
141
  margin: 0 auto;
142
+ max-width: 1500px;
 
 
 
 
143
  }
144
 
145
  .panel {
146
+ margin-top: 14px;
147
  padding: 18px;
148
+ border: 1px solid #dfe6eb;
149
+ border-radius: 8px;
150
+ background: #ffffff;
151
  }
152
 
153
  .panel-heading {
 
156
 
157
  .panel-subtitle {
158
  margin-bottom: 0;
159
+ color: #5b6b7b;
160
+ font-size: 13px;
161
  line-height: 1.45;
162
  }
163
 
164
+ @media (max-width: 1050px) {
165
+ .hero,
166
+ .controls {
167
+ grid-template-columns: 1fr;
168
+ }
 
169
 
170
+ .stat-grid {
171
+ grid-template-columns: repeat(2, minmax(120px, 1fr));
172
+ }
 
173
 
174
+ .stat-card:nth-child(2) {
175
+ border-right: 0;
176
+ }
 
 
177
 
178
+ .stat-card:nth-child(-n + 2) {
179
+ border-bottom: 1px solid #e6ecef;
180
+ }
 
 
 
 
 
 
181
  }
182
 
183
+ @media (max-width: 680px) {
184
  .app-shell {
185
  padding: 12px;
186
  }
187
 
188
+ h1 {
189
+ font-size: 34px;
190
+ }
191
+
192
+ .stat-grid {
193
  grid-template-columns: 1fr;
194
  }
195
 
196
+ .stat-card,
197
+ .stat-card:nth-child(2) {
198
+ border-right: 0;
199
+ border-bottom: 1px solid #e6ecef;
200
+ }
201
+
202
+ .stat-card:last-child {
203
+ border-bottom: 0;
204
  }
205
  }
data/latent_samples.csv ADDED
The diff for this file is too large to render. See raw diff
 
requirements.txt CHANGED
@@ -1,5 +1,5 @@
1
- dash
2
- gunicorn
3
- numpy
4
- pandas
5
- plotly
 
1
+ dash>=2.17,<5
2
+ gunicorn>=22,<24
3
+ numpy>=1.26,<3
4
+ pandas>=2.1,<3
5
+ plotly>=5.22,<7