josephsoo commited on
Commit
1c16d59
·
1 Parent(s): 3c15358

Add interactive dataset and feature exploration

Browse files
app.py CHANGED
@@ -67,20 +67,22 @@ DISPLAY_NAMES = {
67
  "xg": "XGBoost",
68
  }
69
 
 
 
70
  DATASET_LABELS = {
71
  "monkey": "Macaque center-out reaching",
72
- "allen_neuropixels": "Allen visual coding",
73
  "speech": "Attempted speech",
74
- "mc_pacman": "MC PacMan force decoding",
75
- "ratinabox": "RatInABox navigation",
76
  }
77
 
78
  DATASET_TICK_LABELS = {
79
  "monkey": "Macaque<br>center-out reaching",
80
- "allen_neuropixels": "Allen<br>visual coding",
81
  "speech": "Attempted<br>speech",
82
- "mc_pacman": "MC PacMan<br>force decoding",
83
- "ratinabox": "RatInABox<br>navigation",
84
  }
85
 
86
  DATASET_DESCRIPTIONS = {
@@ -113,6 +115,28 @@ CONSISTENCY_SCALE = [[0.0, "#F1FAF8"], [1.0, CONSISTENCY_COLOR]]
113
  FEATURE_SCALE = [[0.0, "#F7F2FA"], [1.0, FEATURE_COLOR]]
114
  TRIAL_SCALE = [[0.0, "#F8F2FA"], [1.0, TRIAL_COLOR]]
115
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  MODEL_COLORS = {
117
  model: color
118
  for model, color in zip(
@@ -295,11 +319,15 @@ NUMERIC_COLUMNS = {
295
  }
296
 
297
  DOWNLOADABLE_FILES = {
 
 
 
298
  "clean_prediction_summary.csv",
299
  "robustness_summary.csv",
300
  "consistency_summary.csv",
301
  "scalability_summary.csv",
302
  "neuron_shap_summary.csv",
 
303
  "trial_shapley_summary.csv",
304
  "trial_shapley_retrain_summary.csv",
305
  "trial_historical_trajectories.csv",
@@ -391,11 +419,15 @@ def load_historical_trajectories() -> pd.DataFrame:
391
  return frame.sort_values(["direction_index", "trial_index", "time_index"]).reset_index(drop=True)
392
 
393
 
 
 
 
394
  prediction = load_csv("clean_prediction_summary.csv")
395
  robustness = load_csv("robustness_summary.csv")
396
  consistency = load_csv("consistency_summary.csv")
397
  scalability = load_csv("scalability_summary.csv")
398
  neuron_shap = load_csv("neuron_shap_summary.csv")
 
399
  trial_shapley = load_csv("trial_shapley_summary.csv")
400
  trial_retrain = load_csv("trial_shapley_retrain_summary.csv")
401
  trial_historical_trajectories = load_historical_trajectories()
@@ -792,6 +824,226 @@ def overview_frame(dataset: str, models: Sequence[str] | None) -> pd.DataFrame:
792
  return round_numeric(frame)
793
 
794
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
795
  def overview_cards(dataset: str, models: Sequence[str] | None) -> list[html.Div]:
796
  frame = overview_frame(dataset, models)
797
  available = frame.dropna(subset=["task_score"]).sort_values(
@@ -1151,7 +1403,7 @@ def feature_spec(dataset: str) -> tuple[str, str, str, float | None]:
1151
  return (
1152
  "spearman_corr",
1153
  "Drifting-gratings orientation selectivity",
1154
- "Spearman’s ρ",
1155
  0.0,
1156
  )
1157
  if dataset == "ratinabox":
@@ -1253,6 +1505,180 @@ def feature_heatmap(models: Sequence[str] | None) -> go.Figure:
1253
  )
1254
 
1255
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1256
  def trial_frame(dataset: str, models: Sequence[str] | None) -> pd.DataFrame:
1257
  frame = filter_models(active_rows(trial_shapley), models)
1258
  frame = frame[frame["dataset"].astype(str) == str(dataset)].copy()
@@ -2126,6 +2552,47 @@ app.layout = html.Div(
2126
  value="overview",
2127
  className="tabs",
2128
  children=[
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2129
  dcc.Tab(
2130
  label="Overview",
2131
  value="overview",
@@ -2228,6 +2695,54 @@ app.layout = html.Div(
2228
  className="tab",
2229
  selected_className="tab tab-selected",
2230
  children=[
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2231
  panel(
2232
  "Feature-attribution validation",
2233
  html.Div(
@@ -2331,6 +2846,19 @@ app.layout = html.Div(
2331
  )
2332
 
2333
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2334
  @app.callback(
2335
  Output("overview-cards", "children"),
2336
  Output("overview-table", "columns"),
@@ -2468,6 +2996,82 @@ def update_consistency(
2468
  )
2469
 
2470
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2471
  @app.callback(
2472
  Output("feature-definition", "children"),
2473
  Output("feature-validation-bars", "figure"),
@@ -2483,7 +3087,7 @@ def update_feature(dataset: str, models: list[str] | None):
2483
  _column, _target, metric, _reference = feature_spec(dataset)
2484
  if dataset == "allen_neuropixels":
2485
  definition = (
2486
- "Spearman’s ρ measures association between feature-attribution values "
2487
  "and each unit’s drifting-gratings orientation selectivity."
2488
  )
2489
  elif dataset == "ratinabox":
 
67
  "xg": "XGBoost",
68
  }
69
 
70
+ # Canonical dataset names from manuscript v7, Supplementary Table 1. Task names
71
+ # belong in descriptions, not in alternate dataset labels.
72
  DATASET_LABELS = {
73
  "monkey": "Macaque center-out reaching",
74
+ "allen_neuropixels": "Allen Neuropixels",
75
  "speech": "Attempted speech",
76
+ "mc_pacman": "MC PacMan",
77
+ "ratinabox": "RatInABox",
78
  }
79
 
80
  DATASET_TICK_LABELS = {
81
  "monkey": "Macaque<br>center-out reaching",
82
+ "allen_neuropixels": "Allen<br>Neuropixels",
83
  "speech": "Attempted<br>speech",
84
+ "mc_pacman": "MC<br>PacMan",
85
+ "ratinabox": "RatInABox",
86
  }
87
 
88
  DATASET_DESCRIPTIONS = {
 
115
  FEATURE_SCALE = [[0.0, "#F7F2FA"], [1.0, FEATURE_COLOR]]
116
  TRIAL_SCALE = [[0.0, "#F8F2FA"], [1.0, TRIAL_COLOR]]
117
 
118
+ ATTRIBUTION_BIN_ORDER = ["Top", "Middle", "Bottom"]
119
+ ATTRIBUTION_BIN_COLORS = {
120
+ "Top": "#238B45",
121
+ "Middle": "#74C476",
122
+ "Bottom": "#D9F0A3",
123
+ }
124
+ FEATURE_GROUP_COLORS = {
125
+ "monkey": {"Recorded": "#0072B2", "Synthetic control": "#BDBDBD"},
126
+ "speech": {"Recorded": "#009E73", "Synthetic control": "#BDBDBD"},
127
+ "mc_pacman": {"Recorded": "#D55E00", "Synthetic control": "#BDBDBD"},
128
+ "allen_neuropixels": {
129
+ "High orientation selectivity": "#E69F00",
130
+ "Intermediate orientation selectivity": "#F0E442",
131
+ "Low orientation selectivity": "#BDBDBD",
132
+ },
133
+ "ratinabox": {
134
+ "Place": "#CC79A7",
135
+ "Head direction": "#56B4E9",
136
+ "Speed": "#E69F00",
137
+ },
138
+ }
139
+
140
  MODEL_COLORS = {
141
  model: color
142
  for model, color in zip(
 
319
  }
320
 
321
  DOWNLOADABLE_FILES = {
322
+ "dataset_overview.csv",
323
+ "dataset_example_neural.csv",
324
+ "dataset_example_targets.csv",
325
  "clean_prediction_summary.csv",
326
  "robustness_summary.csv",
327
  "consistency_summary.csv",
328
  "scalability_summary.csv",
329
  "neuron_shap_summary.csv",
330
+ "neuron_attributions.csv",
331
  "trial_shapley_summary.csv",
332
  "trial_shapley_retrain_summary.csv",
333
  "trial_historical_trajectories.csv",
 
419
  return frame.sort_values(["direction_index", "trial_index", "time_index"]).reset_index(drop=True)
420
 
421
 
422
+ dataset_overview = load_csv("dataset_overview.csv")
423
+ dataset_example_neural = load_csv("dataset_example_neural.csv")
424
+ dataset_example_targets = load_csv("dataset_example_targets.csv")
425
  prediction = load_csv("clean_prediction_summary.csv")
426
  robustness = load_csv("robustness_summary.csv")
427
  consistency = load_csv("consistency_summary.csv")
428
  scalability = load_csv("scalability_summary.csv")
429
  neuron_shap = load_csv("neuron_shap_summary.csv")
430
+ neuron_attributions = load_csv("neuron_attributions.csv")
431
  trial_shapley = load_csv("trial_shapley_summary.csv")
432
  trial_retrain = load_csv("trial_shapley_retrain_summary.csv")
433
  trial_historical_trajectories = load_historical_trajectories()
 
824
  return round_numeric(frame)
825
 
826
 
827
+ def dataset_cards(selected_dataset: str) -> list[html.Article]:
828
+ cards: list[html.Article] = []
829
+ ordered = dataset_overview.set_index("dataset").reindex(DATASETS).reset_index()
830
+ for row in ordered.itertuples(index=False):
831
+ classes = "dataset-card"
832
+ if str(row.dataset) == str(selected_dataset):
833
+ classes += " dataset-card-selected"
834
+ cards.append(
835
+ html.Article(
836
+ [
837
+ html.Div(str(row.dataset_name), className="dataset-card-title"),
838
+ html.Div(
839
+ f"{row.species} · {row.task}",
840
+ className="dataset-card-context",
841
+ ),
842
+ html.Div(
843
+ [
844
+ html.Div(
845
+ [
846
+ html.Span(
847
+ "Primary array",
848
+ className="dataset-fact-label",
849
+ title="Trials × time bins × features",
850
+ ),
851
+ html.Strong(str(row.array_shape)),
852
+ ]
853
+ ),
854
+ html.Div(
855
+ [
856
+ html.Span("Target", className="dataset-fact-label"),
857
+ html.Strong(f"{row.target} · {row.score}"),
858
+ ]
859
+ ),
860
+ html.Div(
861
+ [
862
+ html.Span("Sampling", className="dataset-fact-label"),
863
+ html.Strong(f"{float(row.bin_ms):g} ms bins"),
864
+ ]
865
+ ),
866
+ html.Div(
867
+ [
868
+ html.Span("Consistency cohort", className="dataset-fact-label"),
869
+ html.Strong(str(row.recordings)),
870
+ ]
871
+ ),
872
+ ],
873
+ className="dataset-facts",
874
+ ),
875
+ html.A(
876
+ str(row.source_label),
877
+ href=str(row.source_url),
878
+ target="_blank",
879
+ rel="noopener noreferrer",
880
+ className="dataset-source",
881
+ ),
882
+ ],
883
+ className=classes,
884
+ )
885
+ )
886
+ return cards
887
+
888
+
889
+ def dataset_example_figures(dataset: str) -> tuple[go.Figure, go.Figure, str]:
890
+ metadata = dataset_overview[dataset_overview["dataset"].astype(str).eq(dataset)].iloc[0]
891
+ neural = dataset_example_neural[
892
+ dataset_example_neural["dataset"].astype(str).eq(dataset)
893
+ ].copy()
894
+ target = dataset_example_targets[
895
+ dataset_example_targets["dataset"].astype(str).eq(dataset)
896
+ ].copy()
897
+ for column in (
898
+ "time_index",
899
+ "time_ms",
900
+ "feature_display_index",
901
+ "feature_index",
902
+ "neural_value",
903
+ ):
904
+ neural[column] = pd.to_numeric(neural[column], errors="coerce")
905
+
906
+ values = neural.pivot(
907
+ index="feature_display_index", columns="time_index", values="neural_value"
908
+ ).sort_index()
909
+ time_values = (
910
+ neural[["time_index", "time_ms"]]
911
+ .drop_duplicates()
912
+ .sort_values("time_index")["time_ms"]
913
+ .to_numpy(dtype=float)
914
+ )
915
+ feature_ids = (
916
+ neural[["feature_display_index", "feature_index"]]
917
+ .drop_duplicates()
918
+ .sort_values("feature_display_index")["feature_index"]
919
+ .to_numpy(dtype=int)
920
+ )
921
+ customdata = np.repeat(feature_ids[:, None], values.shape[1], axis=1)
922
+ upper = max(float(np.nanpercentile(values.to_numpy(dtype=float), 99.5)), 1.0)
923
+ neural_figure = go.Figure(
924
+ go.Heatmap(
925
+ z=values.to_numpy(dtype=float),
926
+ x=time_values,
927
+ y=np.arange(len(feature_ids)),
928
+ customdata=customdata,
929
+ colorscale=[[0.0, "#F7FAFC"], [1.0, "#164E63"]],
930
+ zmin=0,
931
+ zmax=upper,
932
+ colorbar=dict(title="Count", thickness=13),
933
+ hovertemplate=(
934
+ "Feature=%{customdata}<br>Time=%{x:.0f} ms<br>"
935
+ "Neural value=%{z:.3f}<extra></extra>"
936
+ ),
937
+ )
938
+ )
939
+ neural_figure.add_vline(x=0, line_color="#D55E00", line_dash="dash")
940
+ neural_figure.update_layout(title="Neural activity")
941
+ neural_figure.update_xaxes(title="Time from scoring onset (ms)")
942
+ neural_figure.update_yaxes(title="Neural features", showticklabels=False)
943
+ figure_layout(neural_figure, height=470)
944
+
945
+ if dataset in {"allen_neuropixels", "speech"}:
946
+ label = str(target["target_label"].iloc[0])
947
+ target_figure = go.Figure()
948
+ if dataset == "allen_neuropixels":
949
+ angle = np.deg2rad(float(label.split("°")[0]))
950
+ x = np.asarray([-np.cos(angle), np.cos(angle)])
951
+ y = np.asarray([-np.sin(angle), np.sin(angle)])
952
+ target_figure.add_trace(
953
+ go.Scatter(
954
+ x=x,
955
+ y=y,
956
+ mode="lines",
957
+ line=dict(color="#E69F00", width=12),
958
+ hoverinfo="skip",
959
+ )
960
+ )
961
+ target_figure.update_xaxes(visible=False, range=[-1.25, 1.25])
962
+ target_figure.update_yaxes(
963
+ visible=False,
964
+ range=[-1.25, 1.25],
965
+ scaleanchor="x",
966
+ scaleratio=1,
967
+ )
968
+ target_figure.add_annotation(
969
+ text=label,
970
+ x=0.5,
971
+ y=0.08,
972
+ xref="paper",
973
+ yref="paper",
974
+ showarrow=False,
975
+ font=dict(size=18),
976
+ )
977
+ target_figure.update_layout(title="Target orientation")
978
+ else:
979
+ target_figure.add_annotation(
980
+ text=label,
981
+ x=0.5,
982
+ y=0.52,
983
+ xref="paper",
984
+ yref="paper",
985
+ showarrow=False,
986
+ font=dict(size=36, color=FEATURE_COLOR),
987
+ )
988
+ target_figure.update_xaxes(visible=False)
989
+ target_figure.update_yaxes(visible=False)
990
+ target_figure.update_layout(title="Attempted-word target")
991
+ figure_layout(target_figure, height=470)
992
+ else:
993
+ for column in ("time_ms", "target_0", "target_1"):
994
+ target[column] = pd.to_numeric(target[column], errors="coerce")
995
+ if dataset == "mc_pacman":
996
+ target_figure = go.Figure(
997
+ go.Scatter(
998
+ x=target["time_ms"],
999
+ y=target["target_0"],
1000
+ mode="lines",
1001
+ line=dict(color="#D55E00", width=3),
1002
+ hovertemplate="Time=%{x:.0f} ms<br>Force=%{y:.4f}<extra></extra>",
1003
+ )
1004
+ )
1005
+ target_figure.add_vline(x=0, line_color="#71808D", line_dash="dash")
1006
+ target_figure.update_layout(title="Target force")
1007
+ target_figure.update_xaxes(title="Time from scoring onset (ms)")
1008
+ target_figure.update_yaxes(title="Force")
1009
+ else:
1010
+ target_figure = go.Figure(
1011
+ go.Scatter(
1012
+ x=target["target_0"],
1013
+ y=target["target_1"],
1014
+ mode="lines+markers",
1015
+ line=dict(color=PREDICTION_COLOR, width=3),
1016
+ marker=dict(
1017
+ size=5,
1018
+ color=target["time_ms"],
1019
+ colorscale="Viridis",
1020
+ showscale=True,
1021
+ colorbar=dict(title="Time (ms)", thickness=13),
1022
+ ),
1023
+ customdata=target["time_ms"],
1024
+ hovertemplate=(
1025
+ "x=%{x:.3f}<br>y=%{y:.3f}<br>"
1026
+ "Time=%{customdata:.0f} ms<extra></extra>"
1027
+ ),
1028
+ )
1029
+ )
1030
+ title = "Target hand position" if dataset == "monkey" else "Target position"
1031
+ target_figure.update_layout(title=title)
1032
+ target_figure.update_xaxes(title="x", scaleanchor="y", scaleratio=1)
1033
+ target_figure.update_yaxes(title="y")
1034
+ figure_layout(target_figure, height=470)
1035
+
1036
+ shown = int(metadata.example_features_shown)
1037
+ total = int(metadata.array_shape.split("×")[-1].strip())
1038
+ feature_text = (
1039
+ f"all {total} neural features are shown"
1040
+ if shown == total
1041
+ else f"{shown} of {total} neural features are shown for legibility"
1042
+ )
1043
+ description = f"One example trial; {feature_text}."
1044
+ return neural_figure, target_figure, description
1045
+
1046
+
1047
  def overview_cards(dataset: str, models: Sequence[str] | None) -> list[html.Div]:
1048
  frame = overview_frame(dataset, models)
1049
  available = frame.dropna(subset=["task_score"]).sort_values(
 
1403
  return (
1404
  "spearman_corr",
1405
  "Drifting-gratings orientation selectivity",
1406
+ "Spearman’s r",
1407
  0.0,
1408
  )
1409
  if dataset == "ratinabox":
 
1505
  )
1506
 
1507
 
1508
+ def feature_attribution_frame(dataset: str, model: str | None) -> pd.DataFrame:
1509
+ if not model:
1510
+ return pd.DataFrame(columns=neuron_attributions.columns)
1511
+ frame = neuron_attributions[
1512
+ neuron_attributions["dataset"].astype(str).eq(str(dataset))
1513
+ & neuron_attributions["model"].astype(str).eq(str(model))
1514
+ ].copy()
1515
+ for column in (
1516
+ "feature_index",
1517
+ "signed_attribution",
1518
+ "attribution_rank",
1519
+ "validation_value",
1520
+ ):
1521
+ frame[column] = pd.to_numeric(frame[column], errors="coerce")
1522
+ return frame.sort_values("attribution_rank", kind="stable")
1523
+
1524
+
1525
+ def rgba(hex_color: str, alpha: float) -> str:
1526
+ value = hex_color.lstrip("#")
1527
+ red, green, blue = (int(value[index : index + 2], 16) for index in (0, 2, 4))
1528
+ return f"rgba({red},{green},{blue},{alpha})"
1529
+
1530
+
1531
+ def feature_attribution_figures(
1532
+ dataset: str,
1533
+ model: str | None,
1534
+ selected_rank: int | None = None,
1535
+ ) -> tuple[go.Figure, go.Figure, str]:
1536
+ frame = feature_attribution_frame(dataset, model)
1537
+ if frame.empty:
1538
+ message = "Select an available method to inspect feature-level attributions."
1539
+ return empty_figure(message, height=460), empty_figure(message, height=460), ""
1540
+ if frame["signed_attribution"].nunique(dropna=True) <= 1:
1541
+ message = "Signed feature-attribution values are tied for this method and dataset."
1542
+ return empty_figure(message, height=480), empty_figure(message, height=480), message
1543
+
1544
+ rank_values = frame["attribution_rank"].astype(int)
1545
+ if selected_rank is None or int(selected_rank) not in set(rank_values):
1546
+ selected_rank = int(rank_values.min())
1547
+ selected = frame[rank_values.eq(int(selected_rank))].iloc[0]
1548
+ selected_group = str(selected["feature_group"])
1549
+ selected_bin = str(selected["attribution_bin"])
1550
+
1551
+ group_colors = FEATURE_GROUP_COLORS[dataset]
1552
+ rank_figure = go.Figure()
1553
+ for group, color in group_colors.items():
1554
+ subset = frame[frame["feature_group"].astype(str).eq(group)]
1555
+ if subset.empty:
1556
+ continue
1557
+ customdata = np.stack(
1558
+ [subset["feature_index"], subset["attribution_rank"], subset["validation_value"]],
1559
+ axis=-1,
1560
+ )
1561
+ validation_hover = (
1562
+ "<br>Orientation selectivity=%{customdata[2]:.4f}"
1563
+ if dataset == "allen_neuropixels"
1564
+ else ""
1565
+ )
1566
+ rank_figure.add_trace(
1567
+ go.Scatter(
1568
+ x=subset["attribution_rank"],
1569
+ y=subset["signed_attribution"],
1570
+ mode="markers",
1571
+ name=group,
1572
+ marker=dict(
1573
+ color=color,
1574
+ size=5 if len(frame) > 300 else 7,
1575
+ opacity=0.78,
1576
+ line=dict(color="#FFFFFF", width=0.4),
1577
+ ),
1578
+ customdata=customdata,
1579
+ hovertemplate=(
1580
+ "Feature=%{customdata[0]:.0f}<br>Group="
1581
+ + group
1582
+ + "<br>Rank=%{customdata[1]:.0f}<br>Signed attribution=%{y:.5f}"
1583
+ + validation_hover
1584
+ + "<extra></extra>"
1585
+ ),
1586
+ )
1587
+ )
1588
+ rank_figure.add_trace(
1589
+ go.Scatter(
1590
+ x=[int(selected["attribution_rank"])],
1591
+ y=[float(selected["signed_attribution"])],
1592
+ mode="markers",
1593
+ name="Selected feature",
1594
+ showlegend=False,
1595
+ marker=dict(
1596
+ color=group_colors[selected_group],
1597
+ size=14,
1598
+ line=dict(color="#172938", width=2.2),
1599
+ ),
1600
+ hovertemplate=(
1601
+ f"Feature index={int(selected['feature_index'])}<br>"
1602
+ f"Group={selected_group}<br>Rank={int(selected['attribution_rank'])}<br>"
1603
+ f"Signed attribution={float(selected['signed_attribution']):.5f}"
1604
+ "<extra></extra>"
1605
+ ),
1606
+ )
1607
+ )
1608
+ rank_figure.add_hline(y=0, line_color="#71808D", line_dash="dash")
1609
+ rank_figure.update_layout(title="Signed feature ranking")
1610
+ rank_figure.update_xaxes(title="Attribution rank")
1611
+ rank_figure.update_yaxes(title="Signed Kernel SHAP value")
1612
+ figure_layout(rank_figure, height=480, legend_below=True)
1613
+
1614
+ groups = [group for group in group_colors if group in set(frame["feature_group"])]
1615
+ nodes = groups + ATTRIBUTION_BIN_ORDER
1616
+ node_index = {label: index for index, label in enumerate(nodes)}
1617
+ counts = (
1618
+ frame.groupby(["feature_group", "attribution_bin"], observed=True)
1619
+ .size()
1620
+ .to_dict()
1621
+ )
1622
+ sources: list[int] = []
1623
+ targets: list[int] = []
1624
+ values: list[int] = []
1625
+ link_colors: list[str] = []
1626
+ for group in groups:
1627
+ for rank_bin in ATTRIBUTION_BIN_ORDER:
1628
+ count = int(counts.get((group, rank_bin), 0))
1629
+ if count == 0:
1630
+ continue
1631
+ sources.append(node_index[group])
1632
+ targets.append(node_index[rank_bin])
1633
+ values.append(count)
1634
+ is_selected_route = group == selected_group and rank_bin == selected_bin
1635
+ link_colors.append(
1636
+ rgba(group_colors[group], 0.78 if is_selected_route else 0.18)
1637
+ )
1638
+
1639
+ source_y = np.linspace(0.08, 0.92, len(groups)).tolist()
1640
+ target_y = [0.12, 0.5, 0.88]
1641
+ sankey = go.Figure(
1642
+ go.Sankey(
1643
+ arrangement="fixed",
1644
+ node=dict(
1645
+ label=groups + [f"{label} third" for label in ATTRIBUTION_BIN_ORDER],
1646
+ color=[group_colors[group] for group in groups]
1647
+ + [ATTRIBUTION_BIN_COLORS[label] for label in ATTRIBUTION_BIN_ORDER],
1648
+ line=dict(color="#FFFFFF", width=0.8),
1649
+ pad=18,
1650
+ thickness=18,
1651
+ x=[0.02] * len(groups) + [0.98] * len(ATTRIBUTION_BIN_ORDER),
1652
+ y=source_y + target_y,
1653
+ hovertemplate="%{label}<br>%{value:.0f} features<extra></extra>",
1654
+ ),
1655
+ link=dict(
1656
+ source=sources,
1657
+ target=targets,
1658
+ value=values,
1659
+ color=link_colors,
1660
+ hovertemplate=(
1661
+ "%{source.label} → %{target.label}<br>"
1662
+ "%{value:.0f} features<extra></extra>"
1663
+ ),
1664
+ ),
1665
+ )
1666
+ )
1667
+ sankey.update_layout(title="Feature groups by attribution rank")
1668
+ figure_layout(sankey, height=480)
1669
+ sankey.update_layout(margin=dict(l=28, r=28, t=62, b=30))
1670
+
1671
+ detail = (
1672
+ f"Feature index {int(selected['feature_index'])} · "
1673
+ f"rank {int(selected['attribution_rank'])} of {len(frame)} · "
1674
+ f"{selected_group} → {selected_bin.lower()} third · "
1675
+ f"signed Kernel SHAP {float(selected['signed_attribution']):+.5f}"
1676
+ )
1677
+ if dataset == "allen_neuropixels" and pd.notna(selected["validation_value"]):
1678
+ detail += f" · orientation selectivity {float(selected['validation_value']):.4f}"
1679
+ return rank_figure, sankey, detail
1680
+
1681
+
1682
  def trial_frame(dataset: str, models: Sequence[str] | None) -> pd.DataFrame:
1683
  frame = filter_models(active_rows(trial_shapley), models)
1684
  frame = frame[frame["dataset"].astype(str) == str(dataset)].copy()
 
2552
  value="overview",
2553
  className="tabs",
2554
  children=[
2555
+ dcc.Tab(
2556
+ label="Datasets",
2557
+ value="datasets",
2558
+ className="tab",
2559
+ selected_className="tab tab-selected",
2560
+ children=[
2561
+ panel(
2562
+ "Benchmark datasets",
2563
+ html.Div(id="dataset-cards", className="dataset-card-grid"),
2564
+ subtitle="Motor, visual, speech and spatial decoding across recordings, participants and simulations. Array dimensions are trials × time bins × features.",
2565
+ class_name="axis-datasets",
2566
+ ),
2567
+ panel(
2568
+ "Example benchmark input and target",
2569
+ html.Div(
2570
+ [
2571
+ graph_box(
2572
+ "dataset-neural-example",
2573
+ "Example trial neural activity for the selected dataset.",
2574
+ ),
2575
+ graph_box(
2576
+ "dataset-target-example",
2577
+ "Paired task target for the selected example trial.",
2578
+ ),
2579
+ ],
2580
+ className="chart-grid two",
2581
+ ),
2582
+ html.Div(id="dataset-example-description", className="dataset-example-note"),
2583
+ html.Div(
2584
+ [
2585
+ source_link("dataset_overview.csv", "Dataset manifest"),
2586
+ source_link("dataset_example_neural.csv", "Example neural data"),
2587
+ source_link("dataset_example_targets.csv", "Example targets"),
2588
+ ],
2589
+ className="download-grid panel-downloads",
2590
+ ),
2591
+ subtitle="Examples are drawn from the preprocessed arrays used in the benchmark.",
2592
+ class_name="axis-datasets",
2593
+ ),
2594
+ ],
2595
+ ),
2596
  dcc.Tab(
2597
  label="Overview",
2598
  value="overview",
 
2695
  className="tab",
2696
  selected_className="tab tab-selected",
2697
  children=[
2698
+ panel(
2699
+ "Feature-level attribution",
2700
+ html.Div(
2701
+ [
2702
+ html.Div(
2703
+ [
2704
+ html.Label("Method", htmlFor="feature-method"),
2705
+ dcc.Dropdown(id="feature-method", clearable=False),
2706
+ ],
2707
+ className="control",
2708
+ ),
2709
+ html.Div(
2710
+ [
2711
+ html.Label("Feature rank", htmlFor="feature-rank"),
2712
+ dcc.Slider(
2713
+ id="feature-rank",
2714
+ min=1,
2715
+ max=2,
2716
+ step=1,
2717
+ value=1,
2718
+ marks={1: "Highest", 2: "Lowest"},
2719
+ disabled=True,
2720
+ tooltip={"placement": "bottom"},
2721
+ ),
2722
+ ],
2723
+ className="control feature-rank-control",
2724
+ ),
2725
+ ],
2726
+ className="inline-controls feature-controls",
2727
+ ),
2728
+ html.Div(id="feature-selection-detail", className="feature-selection-detail"),
2729
+ html.Div(
2730
+ [
2731
+ graph_box(
2732
+ "feature-rank-plot",
2733
+ "Signed neural-feature attributions ranked from highest to lowest.",
2734
+ ),
2735
+ graph_box(
2736
+ "feature-sankey",
2737
+ "Feature groups mapped to top, middle and bottom attribution-rank thirds.",
2738
+ ),
2739
+ ],
2740
+ className="chart-grid two",
2741
+ ),
2742
+ source_link("neuron_attributions.csv", "Feature-level CSV"),
2743
+ subtitle="Move through the signed Kernel SHAP ranking to highlight where each input feature falls. Ranks are within the selected method and dataset.",
2744
+ class_name="axis-feature",
2745
+ ),
2746
  panel(
2747
  "Feature-attribution validation",
2748
  html.Div(
 
2846
  )
2847
 
2848
 
2849
+ @app.callback(
2850
+ Output("dataset-cards", "children"),
2851
+ Output("dataset-neural-example", "figure"),
2852
+ Output("dataset-target-example", "figure"),
2853
+ Output("dataset-example-description", "children"),
2854
+ Input("dataset-filter", "value"),
2855
+ )
2856
+ def update_dataset_examples(dataset: str):
2857
+ dataset = dataset or DATASETS[0]
2858
+ neural, target, description = dataset_example_figures(dataset)
2859
+ return dataset_cards(dataset), neural, target, description
2860
+
2861
+
2862
  @app.callback(
2863
  Output("overview-cards", "children"),
2864
  Output("overview-table", "columns"),
 
2996
  )
2997
 
2998
 
2999
+ @app.callback(
3000
+ Output("feature-method", "options"),
3001
+ Output("feature-method", "value"),
3002
+ Output("feature-method", "disabled"),
3003
+ Input("dataset-filter", "value"),
3004
+ Input("method-filter", "value"),
3005
+ State("feature-method", "value"),
3006
+ )
3007
+ def update_feature_selector(
3008
+ dataset: str,
3009
+ models: list[str] | None,
3010
+ current: str | None,
3011
+ ):
3012
+ dataset = dataset or DATASETS[0]
3013
+ frame = feature_frame(dataset, models).dropna(subset=["validation_score"])
3014
+ available_pairs = set(
3015
+ zip(
3016
+ neuron_attributions["model"].astype(str),
3017
+ neuron_attributions["dataset"].astype(str),
3018
+ )
3019
+ )
3020
+ available = [
3021
+ model
3022
+ for model in frame.sort_values(
3023
+ ["validation_score", "model_order"], ascending=[False, True]
3024
+ )["model"].astype(str)
3025
+ if (model, dataset) in available_pairs
3026
+ ]
3027
+ options = [
3028
+ {"label": dataset_model_label(model, dataset), "value": model}
3029
+ for model in available
3030
+ ]
3031
+ value = current if current in available else (available[0] if available else None)
3032
+ return options, value, not bool(options)
3033
+
3034
+
3035
+ @app.callback(
3036
+ Output("feature-rank", "max"),
3037
+ Output("feature-rank", "value"),
3038
+ Output("feature-rank", "marks"),
3039
+ Output("feature-rank", "disabled"),
3040
+ Input("dataset-filter", "value"),
3041
+ Input("feature-method", "value"),
3042
+ State("feature-rank", "value"),
3043
+ )
3044
+ def update_feature_rank_slider(
3045
+ dataset: str,
3046
+ method: str | None,
3047
+ current_rank: int | None,
3048
+ ):
3049
+ frame = feature_attribution_frame(dataset or DATASETS[0], method)
3050
+ if frame.empty or frame["signed_attribution"].nunique(dropna=True) <= 1:
3051
+ return 2, 1, {1: "Highest", 2: "Lowest"}, True
3052
+ maximum = int(frame["attribution_rank"].max())
3053
+ value = int(current_rank) if current_rank and int(current_rank) <= maximum else 1
3054
+ return maximum, value, {1: "Highest", maximum: "Lowest"}, False
3055
+
3056
+
3057
+ @app.callback(
3058
+ Output("feature-rank-plot", "figure"),
3059
+ Output("feature-sankey", "figure"),
3060
+ Output("feature-selection-detail", "children"),
3061
+ Input("dataset-filter", "value"),
3062
+ Input("feature-method", "value"),
3063
+ Input("feature-rank", "value"),
3064
+ )
3065
+ def update_feature_attributions(
3066
+ dataset: str,
3067
+ method: str | None,
3068
+ selected_rank: int | None,
3069
+ ):
3070
+ return feature_attribution_figures(
3071
+ dataset or DATASETS[0], method, selected_rank
3072
+ )
3073
+
3074
+
3075
  @app.callback(
3076
  Output("feature-definition", "children"),
3077
  Output("feature-validation-bars", "figure"),
 
3087
  _column, _target, metric, _reference = feature_spec(dataset)
3088
  if dataset == "allen_neuropixels":
3089
  definition = (
3090
+ "Spearman’s r measures association between feature-attribution values "
3091
  "and each unit’s drifting-gratings orientation selectivity."
3092
  )
3093
  elif dataset == "ratinabox":
assets/styles.css CHANGED
@@ -6,6 +6,7 @@
6
  --paper: #ffffff;
7
  --canvas: #f7f8f9;
8
  --prediction: #1565c0;
 
9
  --robustness: #2e7d32;
10
  --compute: #e65100;
11
  --consistency: #007c91;
@@ -156,8 +157,8 @@ h1 {
156
  border: 1px solid #d5dde3;
157
  border-radius: 999px;
158
  background: #ffffff;
159
- color: #71808b;
160
- font-size: 9px;
161
  font-weight: 750;
162
  letter-spacing: 0.035em;
163
  line-height: 1.4;
@@ -282,6 +283,10 @@ h1 {
282
  border-top-color: var(--prediction);
283
  }
284
 
 
 
 
 
285
  .panel.axis-robustness {
286
  border-top-color: var(--robustness);
287
  }
@@ -320,6 +325,10 @@ h1 {
320
  color: var(--prediction);
321
  }
322
 
 
 
 
 
323
  .axis-robustness .section-eyebrow {
324
  color: var(--robustness);
325
  }
@@ -363,6 +372,105 @@ h2 {
363
  margin-bottom: 14px;
364
  }
365
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
366
  .metric-card {
367
  position: relative;
368
  min-width: 0;
@@ -511,6 +619,30 @@ h2 {
511
  background: #fafbfc;
512
  }
513
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
514
  .details-table {
515
  margin-top: 15px;
516
  border: 1px solid #e1e7eb;
@@ -711,10 +843,6 @@ h2 {
711
  gap: 14px;
712
  }
713
 
714
- .nav-placeholder small {
715
- display: none;
716
- }
717
-
718
  .toolbar {
719
  position: relative;
720
  grid-template-columns: 1fr;
@@ -739,6 +867,10 @@ h2 {
739
  grid-template-columns: 1fr;
740
  }
741
 
 
 
 
 
742
  .heatmap-graph,
743
  .latent-graph {
744
  overflow-x: auto;
 
6
  --paper: #ffffff;
7
  --canvas: #f7f8f9;
8
  --prediction: #1565c0;
9
+ --datasets: #455a64;
10
  --robustness: #2e7d32;
11
  --compute: #e65100;
12
  --consistency: #007c91;
 
157
  border: 1px solid #d5dde3;
158
  border-radius: 999px;
159
  background: #ffffff;
160
+ color: #5f707d;
161
+ font-size: 10px;
162
  font-weight: 750;
163
  letter-spacing: 0.035em;
164
  line-height: 1.4;
 
283
  border-top-color: var(--prediction);
284
  }
285
 
286
+ .panel.axis-datasets {
287
+ border-top-color: var(--datasets);
288
+ }
289
+
290
  .panel.axis-robustness {
291
  border-top-color: var(--robustness);
292
  }
 
325
  color: var(--prediction);
326
  }
327
 
328
+ .axis-datasets .section-eyebrow {
329
+ color: var(--datasets);
330
+ }
331
+
332
  .axis-robustness .section-eyebrow {
333
  color: var(--robustness);
334
  }
 
372
  margin-bottom: 14px;
373
  }
374
 
375
+ .dataset-card-grid {
376
+ display: grid;
377
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
378
+ gap: 12px;
379
+ }
380
+
381
+ .dataset-card {
382
+ position: relative;
383
+ overflow: hidden;
384
+ padding: 17px 16px 15px;
385
+ border: 1px solid #dce4e9;
386
+ border-radius: 9px;
387
+ background: #ffffff;
388
+ }
389
+
390
+ .dataset-card::before {
391
+ position: absolute;
392
+ top: 0;
393
+ right: 0;
394
+ left: 0;
395
+ height: 3px;
396
+ background: #aeb9c1;
397
+ content: "";
398
+ }
399
+
400
+ .dataset-card-selected {
401
+ border-color: #98aab6;
402
+ box-shadow: 0 4px 14px rgba(39, 58, 72, 0.08);
403
+ }
404
+
405
+ .dataset-card-selected::before {
406
+ background: var(--datasets);
407
+ }
408
+
409
+ .dataset-card-title {
410
+ color: #172938;
411
+ font-size: 15px;
412
+ font-weight: 780;
413
+ line-height: 1.25;
414
+ }
415
+
416
+ .dataset-card-context {
417
+ min-height: 36px;
418
+ margin-top: 5px;
419
+ color: #657684;
420
+ font-size: 11px;
421
+ line-height: 1.45;
422
+ }
423
+
424
+ .dataset-facts {
425
+ display: grid;
426
+ gap: 7px;
427
+ margin-top: 12px;
428
+ padding-top: 11px;
429
+ border-top: 1px solid var(--soft-line);
430
+ }
431
+
432
+ .dataset-facts > div {
433
+ display: flex;
434
+ align-items: baseline;
435
+ justify-content: space-between;
436
+ gap: 10px;
437
+ }
438
+
439
+ .dataset-fact-label {
440
+ color: #758590;
441
+ font-size: 10px;
442
+ font-weight: 800;
443
+ letter-spacing: 0.055em;
444
+ text-transform: uppercase;
445
+ }
446
+
447
+ .dataset-facts strong {
448
+ color: #324654;
449
+ font-size: 11px;
450
+ font-weight: 700;
451
+ text-align: right;
452
+ }
453
+
454
+ .dataset-source {
455
+ display: inline-block;
456
+ margin-top: 12px;
457
+ color: #355b77;
458
+ font-size: 11px;
459
+ font-weight: 750;
460
+ text-decoration: none;
461
+ }
462
+
463
+ .dataset-source::after {
464
+ margin-left: 4px;
465
+ content: "↗";
466
+ }
467
+
468
+ .dataset-example-note {
469
+ margin-top: 12px;
470
+ color: #637581;
471
+ font-size: 11px;
472
+ }
473
+
474
  .metric-card {
475
  position: relative;
476
  min-width: 0;
 
619
  background: #fafbfc;
620
  }
621
 
622
+ .inline-controls.single {
623
+ grid-template-columns: minmax(240px, 420px);
624
+ }
625
+
626
+ .feature-controls {
627
+ grid-template-columns: minmax(240px, 360px) minmax(320px, 1fr);
628
+ }
629
+
630
+ .feature-rank-control {
631
+ padding: 0 7px 7px;
632
+ }
633
+
634
+ .feature-selection-detail {
635
+ min-height: 36px;
636
+ margin: 0 0 12px;
637
+ padding: 9px 11px;
638
+ border-left: 3px solid var(--feature);
639
+ border-radius: 6px;
640
+ background: #f8f6fb;
641
+ color: #445664;
642
+ font-size: 12px;
643
+ line-height: 1.45;
644
+ }
645
+
646
  .details-table {
647
  margin-top: 15px;
648
  border: 1px solid #e1e7eb;
 
843
  gap: 14px;
844
  }
845
 
 
 
 
 
846
  .toolbar {
847
  position: relative;
848
  grid-template-columns: 1fr;
 
867
  grid-template-columns: 1fr;
868
  }
869
 
870
+ .modebar-container {
871
+ display: none !important;
872
+ }
873
+
874
  .heatmap-graph,
875
  .latent-graph {
876
  overflow-x: auto;
data/dataset_example_neural.csv ADDED
The diff for this file is too large to render. See raw diff
 
data/dataset_example_targets.csv ADDED
@@ -0,0 +1,215 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ dataset,trial_index,time_index,time_ms,target_0,target_1,target_label
2
+ monkey,19,0,-300.0,0.32637837529182434,0.9757941961288452,
3
+ monkey,19,1,-280.0,0.3266376554965973,0.9760781526565552,
4
+ monkey,19,2,-260.0,0.32639679312705994,0.9759191870689392,
5
+ monkey,19,3,-240.0,0.3267260789871216,0.9760299921035767,
6
+ monkey,19,4,-220.0,0.32616183161735535,0.9759241938591003,
7
+ monkey,19,5,-200.0,0.3303698003292084,0.9805266261100769,
8
+ monkey,19,6,-180.0,0.3312961757183075,0.9897047281265259,
9
+ monkey,19,7,-160.0,0.33124345541000366,0.9998650550842285,
10
+ monkey,19,8,-140.0,0.3350425660610199,1.0021278858184814,
11
+ monkey,19,9,-120.0,0.33221206068992615,1.003627061843872,
12
+ monkey,19,10,-100.0,0.3279237151145935,1.0078579187393188,
13
+ monkey,19,11,-80.0,0.32607147097587585,1.0092488527297974,
14
+ monkey,19,12,-60.0,0.3212782144546509,1.0135948657989502,
15
+ monkey,19,13,-40.0,0.31931623816490173,1.015254259109497,
16
+ monkey,19,14,-20.0,0.31412604451179504,1.0198619365692139,
17
+ monkey,19,15,0.0,0.314896821975708,1.0191636085510254,
18
+ monkey,19,16,20.0,0.3141116797924042,1.019865870475769,
19
+ monkey,19,17,40.0,0.3087146580219269,1.0246158838272095,
20
+ monkey,19,18,60.0,0.3078997731208801,1.0253390073776245,
21
+ monkey,19,19,80.0,0.3083854913711548,1.0249062776565552,
22
+ monkey,19,20,100.0,0.30375054478645325,1.0289989709854126,
23
+ monkey,19,21,120.0,0.2999488115310669,1.0323445796966553,
24
+ monkey,19,22,140.0,0.2953139543533325,1.0364488363265991,
25
+ monkey,19,23,160.0,0.29061010479927063,1.040581226348877,
26
+ monkey,19,24,180.0,0.2853701710700989,1.045244812965393,
27
+ monkey,19,25,200.0,0.27945226430892944,1.0504240989685059,
28
+ monkey,19,26,220.0,0.27346527576446533,1.0558046102523804,
29
+ monkey,19,27,240.0,0.26590073108673096,1.062351942062378,
30
+ monkey,19,28,260.0,0.2580970227718353,1.069669246673584,
31
+ monkey,19,29,280.0,0.252856969833374,1.0822207927703857,
32
+ monkey,19,30,300.0,0.24749480187892914,1.0943732261657715,
33
+ monkey,19,31,320.0,0.2417447715997696,1.1059606075286865,
34
+ monkey,19,32,340.0,0.2357359230518341,1.1191096305847168,
35
+ monkey,19,33,360.0,0.2288716584444046,1.1295506954193115,
36
+ monkey,19,34,380.0,0.22376346588134766,1.1424471139907837,
37
+ monkey,19,35,400.0,0.2146560549736023,1.152639389038086,
38
+ monkey,19,36,420.0,0.20424097776412964,1.1610658168792725,
39
+ monkey,19,37,440.0,0.19646596908569336,1.1687617301940918,
40
+ monkey,19,38,460.0,0.1878175586462021,1.1754132509231567,
41
+ monkey,19,39,480.0,0.1806408017873764,1.187225580215454,
42
+ monkey,19,40,500.0,0.177394300699234,1.2083221673965454,
43
+ monkey,19,41,520.0,0.17321859300136566,1.2519882917404175,
44
+ monkey,19,42,540.0,0.16704171895980835,1.3540817499160767,
45
+ monkey,19,43,560.0,0.15717653930187225,1.5343878269195557,
46
+ monkey,19,44,580.0,0.1392321139574051,1.7867273092269897,
47
+ monkey,19,45,600.0,0.11189127713441849,2.11149001121521,
48
+ monkey,19,46,620.0,0.09077581763267517,2.504103422164917,
49
+ monkey,19,47,640.0,0.08320548385381699,2.9608612060546875,
50
+ monkey,19,48,660.0,0.0900033712387085,3.4947991371154785,
51
+ monkey,19,49,680.0,0.09981474280357361,4.108597755432129,
52
+ allen_neuropixels,0,100,0.0,,,315° orientation
53
+ speech,0,25,0.0,,,choice
54
+ mc_pacman,0,0,-500.0,0.4540364742279053,,
55
+ mc_pacman,0,1,-480.0,0.47354891896247864,,
56
+ mc_pacman,0,2,-460.0,0.49371257424354553,,
57
+ mc_pacman,0,3,-440.0,0.5081393122673035,,
58
+ mc_pacman,0,4,-420.0,0.5131196975708008,,
59
+ mc_pacman,0,5,-400.0,0.5209791660308838,,
60
+ mc_pacman,0,6,-380.0,0.5383294820785522,,
61
+ mc_pacman,0,7,-360.0,0.5535526275634766,,
62
+ mc_pacman,0,8,-340.0,0.5599618554115295,,
63
+ mc_pacman,0,9,-320.0,0.5621652007102966,,
64
+ mc_pacman,0,10,-300.0,0.5593769550323486,,
65
+ mc_pacman,0,11,-280.0,0.5489000678062439,,
66
+ mc_pacman,0,12,-260.0,0.535784900188446,,
67
+ mc_pacman,0,13,-240.0,0.5225427150726318,,
68
+ mc_pacman,0,14,-220.0,0.5026987195014954,,
69
+ mc_pacman,0,15,-200.0,0.4764695167541504,,
70
+ mc_pacman,0,16,-180.0,0.46249258518218994,,
71
+ mc_pacman,0,17,-160.0,0.4804723858833313,,
72
+ mc_pacman,0,18,-140.0,0.5240657329559326,,
73
+ mc_pacman,0,19,-120.0,0.5628992319107056,,
74
+ mc_pacman,0,20,-100.0,0.570605993270874,,
75
+ mc_pacman,0,21,-80.0,0.5441458225250244,,
76
+ mc_pacman,0,22,-60.0,0.5028995871543884,,
77
+ mc_pacman,0,23,-40.0,0.47658416628837585,,
78
+ mc_pacman,0,24,-20.0,0.4860197901725769,,
79
+ mc_pacman,0,25,0.0,0.5244206190109253,,
80
+ mc_pacman,0,26,20.0,0.5643056035041809,,
81
+ mc_pacman,0,27,40.0,0.588422417640686,,
82
+ mc_pacman,0,28,60.0,0.5981853008270264,,
83
+ mc_pacman,0,29,80.0,0.5974154472351074,,
84
+ mc_pacman,0,30,100.0,0.592046320438385,,
85
+ mc_pacman,0,31,120.0,0.5933641791343689,,
86
+ mc_pacman,0,32,140.0,0.601443350315094,,
87
+ mc_pacman,0,33,160.0,0.606227457523346,,
88
+ mc_pacman,0,34,180.0,0.6097249388694763,,
89
+ mc_pacman,0,35,200.0,0.6227228045463562,,
90
+ mc_pacman,0,36,220.0,0.6448730826377869,,
91
+ mc_pacman,0,37,240.0,0.6626847982406616,,
92
+ mc_pacman,0,38,260.0,0.6634147763252258,,
93
+ mc_pacman,0,39,280.0,0.6510913372039795,,
94
+ mc_pacman,0,40,300.0,0.6460963487625122,,
95
+ mc_pacman,0,41,320.0,0.6580253839492798,,
96
+ mc_pacman,0,42,340.0,0.6721721887588501,,
97
+ mc_pacman,0,43,360.0,0.6763845682144165,,
98
+ mc_pacman,0,44,380.0,0.6803927421569824,,
99
+ mc_pacman,0,45,400.0,0.6943899393081665,,
100
+ mc_pacman,0,46,420.0,0.705275297164917,,
101
+ mc_pacman,0,47,440.0,0.691665768623352,,
102
+ mc_pacman,0,48,460.0,0.6565446853637695,,
103
+ mc_pacman,0,49,480.0,0.6260090470314026,,
104
+ mc_pacman,0,50,500.0,0.61765056848526,,
105
+ mc_pacman,0,51,520.0,0.62873774766922,,
106
+ mc_pacman,0,52,540.0,0.653647243976593,,
107
+ mc_pacman,0,53,560.0,0.6875089406967163,,
108
+ mc_pacman,0,54,580.0,0.7174137234687805,,
109
+ mc_pacman,0,55,600.0,0.7349939942359924,,
110
+ mc_pacman,0,56,620.0,0.7430776357650757,,
111
+ mc_pacman,0,57,640.0,0.7382182478904724,,
112
+ mc_pacman,0,58,660.0,0.7191638946533203,,
113
+ mc_pacman,0,59,680.0,0.7111793160438538,,
114
+ mc_pacman,0,60,700.0,0.7467876076698303,,
115
+ mc_pacman,0,61,720.0,0.8286340832710266,,
116
+ mc_pacman,0,62,740.0,0.9286566376686096,,
117
+ mc_pacman,0,63,760.0,1.0168689489364624,,
118
+ mc_pacman,0,64,780.0,1.0888168811798096,,
119
+ mc_pacman,0,65,800.0,1.1605585813522339,,
120
+ mc_pacman,0,66,820.0,1.2344834804534912,,
121
+ mc_pacman,0,67,840.0,1.292007327079773,,
122
+ mc_pacman,0,68,860.0,1.329041838645935,,
123
+ mc_pacman,0,69,880.0,1.3740489482879639,,
124
+ mc_pacman,0,70,900.0,1.4568270444869995,,
125
+ mc_pacman,0,71,920.0,1.5872440338134766,,
126
+ mc_pacman,0,72,940.0,1.7747671604156494,,
127
+ mc_pacman,0,73,960.0,2.02529239654541,,
128
+ mc_pacman,0,74,980.0,2.306344509124756,,
129
+ mc_pacman,0,75,1000.0,2.5660526752471924,,
130
+ mc_pacman,0,76,1020.0,2.7944984436035156,,
131
+ mc_pacman,0,77,1040.0,3.01857328414917,,
132
+ mc_pacman,0,78,1060.0,3.2419419288635254,,
133
+ mc_pacman,0,79,1080.0,3.4372360706329346,,
134
+ mc_pacman,0,80,1100.0,3.5911450386047363,,
135
+ mc_pacman,0,81,1120.0,3.7210190296173096,,
136
+ mc_pacman,0,82,1140.0,3.8501505851745605,,
137
+ mc_pacman,0,83,1160.0,3.9841997623443604,,
138
+ mc_pacman,0,84,1180.0,4.111367225646973,,
139
+ mc_pacman,0,85,1200.0,4.223678112030029,,
140
+ mc_pacman,0,86,1220.0,4.3313422203063965,,
141
+ mc_pacman,0,87,1240.0,4.443793296813965,,
142
+ mc_pacman,0,88,1260.0,4.5481343269348145,,
143
+ mc_pacman,0,89,1280.0,4.624568939208984,,
144
+ mc_pacman,0,90,1300.0,4.674068450927734,,
145
+ mc_pacman,0,91,1320.0,4.7209272384643555,,
146
+ mc_pacman,0,92,1340.0,4.778489112854004,,
147
+ mc_pacman,0,93,1360.0,4.846485137939453,,
148
+ mc_pacman,0,94,1380.0,4.9120378494262695,,
149
+ mc_pacman,0,95,1400.0,4.962754249572754,,
150
+ mc_pacman,0,96,1420.0,5.003695964813232,,
151
+ mc_pacman,0,97,1440.0,5.054315567016602,,
152
+ mc_pacman,0,98,1460.0,5.122193813323975,,
153
+ mc_pacman,0,99,1480.0,5.1932783126831055,,
154
+ mc_pacman,0,100,1500.0,5.255855083465576,,
155
+ mc_pacman,0,101,1520.0,5.3188982009887695,,
156
+ mc_pacman,0,102,1540.0,5.396027565002441,,
157
+ mc_pacman,0,103,1560.0,5.482247352600098,,
158
+ mc_pacman,0,104,1580.0,5.557141304016113,,
159
+ mc_pacman,0,105,1600.0,5.605292797088623,,
160
+ mc_pacman,0,106,1620.0,5.631208896636963,,
161
+ mc_pacman,0,107,1640.0,5.653810501098633,,
162
+ mc_pacman,0,108,1660.0,5.6811137199401855,,
163
+ mc_pacman,0,109,1680.0,5.702667713165283,,
164
+ mc_pacman,0,110,1700.0,5.715582370758057,,
165
+ mc_pacman,0,111,1720.0,5.735340595245361,,
166
+ ratinabox,0,0,0.0,0.37413349747657776,0.938173770904541,
167
+ ratinabox,0,1,100.0,0.38142383098602295,0.923097550868988,
168
+ ratinabox,0,2,200.0,0.39421314001083374,0.9171221852302551,
169
+ ratinabox,0,3,300.0,0.40402141213417053,0.9103533029556274,
170
+ ratinabox,0,4,400.0,0.41216611862182617,0.9079484939575195,
171
+ ratinabox,0,5,500.0,0.425046443939209,0.9090948104858398,
172
+ ratinabox,0,6,600.0,0.44039642810821533,0.9029134511947632,
173
+ ratinabox,0,7,700.0,0.45487937331199646,0.8972962498664856,
174
+ ratinabox,0,8,800.0,0.46796661615371704,0.8919219374656677,
175
+ ratinabox,0,9,900.0,0.48204344511032104,0.8924508094787598,
176
+ ratinabox,0,10,1000.0,0.4945521652698517,0.8901085257530212,
177
+ ratinabox,0,11,1100.0,0.5004682540893555,0.884657084941864,
178
+ ratinabox,0,12,1200.0,0.508205235004425,0.8746210932731628,
179
+ ratinabox,0,13,1300.0,0.5115118026733398,0.862112283706665,
180
+ ratinabox,0,14,1400.0,0.5177260637283325,0.8499125242233276,
181
+ ratinabox,0,15,1500.0,0.5179038643836975,0.8336840867996216,
182
+ ratinabox,0,16,1600.0,0.5195140242576599,0.8199301362037659,
183
+ ratinabox,0,17,1700.0,0.5207089185714722,0.807529091835022,
184
+ ratinabox,0,18,1800.0,0.5157619118690491,0.7915142774581909,
185
+ ratinabox,0,19,1900.0,0.5180566906929016,0.7749912142753601,
186
+ ratinabox,0,20,2000.0,0.5197643041610718,0.7607824802398682,
187
+ ratinabox,0,21,2100.0,0.5209691524505615,0.7504138946533203,
188
+ ratinabox,0,22,2200.0,0.5253504514694214,0.7415164709091187,
189
+ ratinabox,0,23,2300.0,0.5286636352539062,0.7361718416213989,
190
+ ratinabox,0,24,2400.0,0.5302071571350098,0.7298526763916016,
191
+ ratinabox,0,25,2500.0,0.5307698249816895,0.7216604948043823,
192
+ ratinabox,0,26,2600.0,0.5259577631950378,0.7130606770515442,
193
+ ratinabox,0,27,2700.0,0.5202022790908813,0.7063953876495361,
194
+ ratinabox,0,28,2800.0,0.5116539001464844,0.6951087713241577,
195
+ ratinabox,0,29,2900.0,0.5097408890724182,0.6869532465934753,
196
+ ratinabox,0,30,3000.0,0.5076330900192261,0.680025041103363,
197
+ ratinabox,0,31,3100.0,0.5051463842391968,0.6751620769500732,
198
+ ratinabox,0,32,3200.0,0.5011398792266846,0.6684351563453674,
199
+ ratinabox,0,33,3300.0,0.4966591000556946,0.6637135744094849,
200
+ ratinabox,0,34,3400.0,0.4893237054347992,0.6558409929275513,
201
+ ratinabox,0,35,3500.0,0.4827667474746704,0.6477018594741821,
202
+ ratinabox,0,36,3600.0,0.4740392565727234,0.6415613889694214,
203
+ ratinabox,0,37,3700.0,0.46337786316871643,0.631086528301239,
204
+ ratinabox,0,38,3800.0,0.4466787278652191,0.6238975524902344,
205
+ ratinabox,0,39,3900.0,0.4327094554901123,0.6180192828178406,
206
+ ratinabox,0,40,4000.0,0.41705596446990967,0.6094726324081421,
207
+ ratinabox,0,41,4100.0,0.4069020450115204,0.6019271016120911,
208
+ ratinabox,0,42,4200.0,0.3972027897834778,0.5955141186714172,
209
+ ratinabox,0,43,4300.0,0.38659921288490295,0.5887935757637024,
210
+ ratinabox,0,44,4400.0,0.37808313965797424,0.5859121680259705,
211
+ ratinabox,0,45,4500.0,0.3705003559589386,0.5837921500205994,
212
+ ratinabox,0,46,4600.0,0.36420997977256775,0.5776630640029907,
213
+ ratinabox,0,47,4700.0,0.3610799014568329,0.5762715339660645,
214
+ ratinabox,0,48,4800.0,0.3595122992992401,0.5739803910255432,
215
+ ratinabox,0,49,4900.0,0.35728511214256287,0.5691278576850891,
data/dataset_overview.csv ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ dataset,dataset_name,species,task,array_shape,target,score,bin_ms,recordings,source_label,source_url,example_trial_index,example_features_shown
2
+ monkey,Macaque center-out reaching,Macaque,Center-out reaching,319 × 50 × 59,2D hand position,R²,20.0,4 recordings,DANDI 000688,https://doi.org/10.48324/dandi.000688/0.250122.1735,19,59
3
+ allen_neuropixels,Allen Neuropixels,Mouse,Drifting-grating visual coding,598 × 300 × 444,Eight-way orientation class,Accuracy,10.0,3 recordings,Siegle et al. 2021,https://doi.org/10.1038/s41586-020-03171-x,0,80
4
+ speech,Attempted speech,Human,Isolated-word attempted speech,168 × 50 × 64,Eight-class attempted-speech label,Accuracy,20.0,4 participants,Kunz et al. dataset,https://doi.org/10.5061/dryad.gf1vhhn1j,0,64
5
+ mc_pacman,MC PacMan,Macaque,Force decoding,362 × 112 × 128,One-dimensional force,R²,20.0,1 recording,MINT study,https://doi.org/10.7554/eLife.89421,0,80
6
+ ratinabox,RatInABox,Synthetic,2D position decoding,300 × 50 × 300,2D position,R²,100.0,4 generated sessions,RatInABox,https://doi.org/10.7554/eLife.85274,0,80
data/neuron_attributions.csv ADDED
The diff for this file is too large to render. See raw diff
 
data/release_manifest.json CHANGED
@@ -5,7 +5,7 @@
5
  ],
6
  "manuscript_working_version": "manuscript_v7",
7
  "schema_version": 1,
8
- "source": "paper/results, active consistency embedding artifacts, and Figure 5 prediction sidecars",
9
  "source_git_revision": "80def770609a52f45a835c6b62500b04cdc2b548",
10
  "source_repository": "https://github.com/TangLab-UBC/behavior_benchmarking",
11
  "source_worktree_dirty": true,
@@ -18,6 +18,18 @@
18
  "rows": 54,
19
  "sha256": "7039c0d38c288576e39ea6a1147a41eb2d395559b1d9b1de1b14eac3b3801b7f"
20
  },
 
 
 
 
 
 
 
 
 
 
 
 
21
  "latent_samples.csv": {
22
  "rows": 31140,
23
  "sha256": "bb484c47b78bd7a7d0a2bfea94296e726aa80034f87368b32302aaba0c5aa622"
@@ -26,6 +38,10 @@
26
  "rows": 75040,
27
  "sha256": "d15a19b50d7dbc517b4bf5de60a550fda6870327e5e59fb4a5ec37f01921abf5"
28
  },
 
 
 
 
29
  "neuron_shap_summary.csv": {
30
  "rows": 105,
31
  "sha256": "a36430bdd193040c48e2ab9fd48570d9c37b9ad58025d8a02d2c84293c768389"
 
5
  ],
6
  "manuscript_working_version": "manuscript_v7",
7
  "schema_version": 1,
8
+ "source": "paper/results, active consistency and feature-attribution artifacts, benchmark dataset arrays, and Figure 5 prediction sidecars",
9
  "source_git_revision": "80def770609a52f45a835c6b62500b04cdc2b548",
10
  "source_repository": "https://github.com/TangLab-UBC/behavior_benchmarking",
11
  "source_worktree_dirty": true,
 
18
  "rows": 54,
19
  "sha256": "7039c0d38c288576e39ea6a1147a41eb2d395559b1d9b1de1b14eac3b3801b7f"
20
  },
21
+ "dataset_example_neural.csv": {
22
+ "rows": 43110,
23
+ "sha256": "7b564705c55ddde7b91dacb56688cb1f740a3bb88cc8e5d59f12a9cf18b19f07"
24
+ },
25
+ "dataset_example_targets.csv": {
26
+ "rows": 214,
27
+ "sha256": "5201302ac40cf05b36b76cb950bb03c8dbb8c5a026fa4ca653b29415413a9f53"
28
+ },
29
+ "dataset_overview.csv": {
30
+ "rows": 5,
31
+ "sha256": "b5fc3f2db54c30d4a7c038e5df869c3f3f3a9e609b84cea72400942f94d7605e"
32
+ },
33
  "latent_samples.csv": {
34
  "rows": 31140,
35
  "sha256": "bb484c47b78bd7a7d0a2bfea94296e726aa80034f87368b32302aaba0c5aa622"
 
38
  "rows": 75040,
39
  "sha256": "d15a19b50d7dbc517b4bf5de60a550fda6870327e5e59fb4a5ec37f01921abf5"
40
  },
41
+ "neuron_attributions.csv": {
42
+ "rows": 21606,
43
+ "sha256": "b61262977aeb153f6afcb710ef0a4b9ba83ba676751a7227a05d0ca81ceba248"
44
+ },
45
  "neuron_shap_summary.csv": {
46
  "rows": 105,
47
  "sha256": "a36430bdd193040c48e2ab9fd48570d9c37b9ad58025d8a02d2c84293c768389"
validate_data.py CHANGED
@@ -41,6 +41,19 @@ EXPECTED_COVERAGE = {
41
  }
42
 
43
  REQUIRED_COLUMNS = {
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  "clean_prediction_summary.csv": {
45
  "model", "dataset", "status", "metric", "score", "decoder",
46
  },
@@ -61,6 +74,11 @@ REQUIRED_COLUMNS = {
61
  "shap_mean_value", "shap_min_value", "shap_max_value",
62
  "shap_fraction_positive", "shap_fraction_negative",
63
  },
 
 
 
 
 
64
  "trial_shapley_summary.csv": {
65
  "model", "dataset", "is_active_model", "analysis", "perturbation_auc",
66
  "rotation_angle_deg", "rotation_subspace_dim_spec",
@@ -93,11 +111,15 @@ REQUIRED_COLUMNS = {
93
  }
94
 
95
  UNIQUE_KEYS = {
 
 
 
96
  "clean_prediction_summary.csv": ["model", "dataset"],
97
  "robustness_summary.csv": ["model", "dataset"],
98
  "scalability_summary.csv": ["model", "dataset"],
99
  "consistency_summary.csv": ["model", "dataset"],
100
  "neuron_shap_summary.csv": ["model", "dataset"],
 
101
  "trial_shapley_summary.csv": ["model", "dataset"],
102
  "trial_shapley_retrain_summary.csv": ["analysis", "model", "condition"],
103
  }
@@ -311,6 +333,79 @@ def validate_local(data_dir: Path) -> dict[str, pd.DataFrame]:
311
  observed = set(frames[name]["dataset"].dropna().astype(str))
312
  _require(observed == DATASETS, f"{name}: dataset set is {sorted(observed)}", errors)
313
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
314
  if "clean_prediction_summary.csv" in frames:
315
  prediction = frames["clean_prediction_summary.csv"]
316
  _require(prediction["model"].nunique() == 23, "prediction: expected 23 methods", errors)
@@ -333,6 +428,45 @@ def validate_local(data_dir: Path) -> dict[str, pd.DataFrame]:
333
  _require(other["auc"].notna().all(), "feature attribution: ROC-AUC values missing", errors)
334
  _require(feature["shap_min_value"].lt(0).any(), "feature attribution: signed negative values absent", errors)
335
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
336
  if "trial_shapley_summary.csv" in frames:
337
  trial = frames["trial_shapley_summary.csv"]
338
  _require(set(trial["analysis"].dropna()) == {"subspace_rotation"}, "trial valuation: noncanonical analysis", errors)
 
41
  }
42
 
43
  REQUIRED_COLUMNS = {
44
+ "dataset_overview.csv": {
45
+ "dataset", "dataset_name", "species", "task", "array_shape",
46
+ "target", "score", "bin_ms", "recordings", "source_label",
47
+ "source_url", "example_trial_index", "example_features_shown",
48
+ },
49
+ "dataset_example_neural.csv": {
50
+ "dataset", "trial_index", "time_index", "time_ms",
51
+ "feature_display_index", "feature_index", "neural_value",
52
+ },
53
+ "dataset_example_targets.csv": {
54
+ "dataset", "trial_index", "time_index", "time_ms", "target_0",
55
+ "target_1", "target_label",
56
+ },
57
  "clean_prediction_summary.csv": {
58
  "model", "dataset", "status", "metric", "score", "decoder",
59
  },
 
74
  "shap_mean_value", "shap_min_value", "shap_max_value",
75
  "shap_fraction_positive", "shap_fraction_negative",
76
  },
77
+ "neuron_attributions.csv": {
78
+ "model", "dataset", "feature_index", "feature_group",
79
+ "signed_attribution", "attribution_rank", "attribution_bin",
80
+ "validation_value",
81
+ },
82
  "trial_shapley_summary.csv": {
83
  "model", "dataset", "is_active_model", "analysis", "perturbation_auc",
84
  "rotation_angle_deg", "rotation_subspace_dim_spec",
 
111
  }
112
 
113
  UNIQUE_KEYS = {
114
+ "dataset_overview.csv": ["dataset"],
115
+ "dataset_example_neural.csv": ["dataset", "time_index", "feature_display_index"],
116
+ "dataset_example_targets.csv": ["dataset", "time_index"],
117
  "clean_prediction_summary.csv": ["model", "dataset"],
118
  "robustness_summary.csv": ["model", "dataset"],
119
  "scalability_summary.csv": ["model", "dataset"],
120
  "consistency_summary.csv": ["model", "dataset"],
121
  "neuron_shap_summary.csv": ["model", "dataset"],
122
+ "neuron_attributions.csv": ["model", "dataset", "feature_index"],
123
  "trial_shapley_summary.csv": ["model", "dataset"],
124
  "trial_shapley_retrain_summary.csv": ["analysis", "model", "condition"],
125
  }
 
333
  observed = set(frames[name]["dataset"].dropna().astype(str))
334
  _require(observed == DATASETS, f"{name}: dataset set is {sorted(observed)}", errors)
335
 
336
+ for name in (
337
+ "dataset_overview.csv",
338
+ "dataset_example_neural.csv",
339
+ "dataset_example_targets.csv",
340
+ ):
341
+ if name in frames:
342
+ observed = set(frames[name]["dataset"].dropna().astype(str))
343
+ _require(observed == DATASETS, f"{name}: dataset set is {sorted(observed)}", errors)
344
+
345
+ if "dataset_overview.csv" in frames:
346
+ overview = frames["dataset_overview.csv"]
347
+ _require(len(overview) == 5, "dataset overview: expected five rows", errors)
348
+ shown = pd.to_numeric(overview["example_features_shown"], errors="coerce")
349
+ _require(
350
+ shown.notna().all() and shown.between(1, 80).all(),
351
+ "dataset overview: invalid example feature counts",
352
+ errors,
353
+ )
354
+
355
+ if "dataset_example_neural.csv" in frames:
356
+ examples = frames["dataset_example_neural.csv"]
357
+ values = pd.to_numeric(examples["neural_value"], errors="coerce")
358
+ _require(
359
+ values.notna().all() and np.isfinite(values.to_numpy()).all(),
360
+ "dataset examples: neural values must be finite",
361
+ errors,
362
+ )
363
+ trials_per_dataset = examples.groupby("dataset")["trial_index"].nunique()
364
+ _require(
365
+ trials_per_dataset.eq(1).all(),
366
+ "dataset examples: expected one trial per dataset",
367
+ errors,
368
+ )
369
+
370
+ if "dataset_example_targets.csv" in frames:
371
+ targets = frames["dataset_example_targets.csv"]
372
+ trials_per_dataset = targets.groupby("dataset")["trial_index"].nunique()
373
+ _require(
374
+ trials_per_dataset.eq(1).all(),
375
+ "dataset targets: expected one trial per dataset",
376
+ errors,
377
+ )
378
+ classification = targets[targets["dataset"].isin({"allen_neuropixels", "speech"})]
379
+ _require(
380
+ len(classification) == 2 and classification["target_label"].notna().all(),
381
+ "dataset targets: classification labels are missing",
382
+ errors,
383
+ )
384
+
385
+ if all(
386
+ name in frames
387
+ for name in (
388
+ "dataset_overview.csv",
389
+ "dataset_example_neural.csv",
390
+ "dataset_example_targets.csv",
391
+ )
392
+ ):
393
+ overview_trials = frames["dataset_overview.csv"].set_index("dataset")[
394
+ "example_trial_index"
395
+ ].astype(int)
396
+ neural_trials = frames["dataset_example_neural.csv"].groupby("dataset")[
397
+ "trial_index"
398
+ ].first().astype(int)
399
+ target_trials = frames["dataset_example_targets.csv"].groupby("dataset")[
400
+ "trial_index"
401
+ ].first().astype(int)
402
+ _require(
403
+ overview_trials.equals(neural_trials.reindex(overview_trials.index))
404
+ and overview_trials.equals(target_trials.reindex(overview_trials.index)),
405
+ "dataset examples: manifest, neural and target trial indices differ",
406
+ errors,
407
+ )
408
+
409
  if "clean_prediction_summary.csv" in frames:
410
  prediction = frames["clean_prediction_summary.csv"]
411
  _require(prediction["model"].nunique() == 23, "prediction: expected 23 methods", errors)
 
428
  _require(other["auc"].notna().all(), "feature attribution: ROC-AUC values missing", errors)
429
  _require(feature["shap_min_value"].lt(0).any(), "feature attribution: signed negative values absent", errors)
430
 
431
+ if "neuron_attributions.csv" in frames:
432
+ features = frames["neuron_attributions.csv"]
433
+ observed = set(features["dataset"].dropna().astype(str))
434
+ _require(observed == DATASETS, f"neuron attributions: dataset set is {sorted(observed)}", errors)
435
+ _require(
436
+ features[["model", "dataset"]].drop_duplicates().shape[0] == 105,
437
+ "neuron attributions: expected 105 method-dataset pairs",
438
+ errors,
439
+ )
440
+ values = pd.to_numeric(features["signed_attribution"], errors="coerce")
441
+ _require(
442
+ values.notna().all() and np.isfinite(values.to_numpy()).all(),
443
+ "neuron attributions: signed values must be finite",
444
+ errors,
445
+ )
446
+ _require(
447
+ values.lt(0).any() and values.gt(0).any(),
448
+ "neuron attributions: expected positive and negative signed values",
449
+ errors,
450
+ )
451
+ _require(
452
+ set(features["attribution_bin"].dropna().astype(str))
453
+ == {"Top", "Middle", "Bottom", "Tied"},
454
+ "neuron attributions: invalid rank bins",
455
+ errors,
456
+ )
457
+ if "neuron_shap_summary.csv" in frames:
458
+ expected_counts = (
459
+ frames["neuron_shap_summary.csv"]
460
+ .set_index(["model", "dataset"])["shap_n_values"]
461
+ .astype(int)
462
+ )
463
+ observed_counts = features.groupby(["model", "dataset"]).size()
464
+ _require(
465
+ observed_counts.equals(expected_counts.reindex(observed_counts.index)),
466
+ "neuron attributions: feature counts differ from summary",
467
+ errors,
468
+ )
469
+
470
  if "trial_shapley_summary.csv" in frames:
471
  trial = frames["trial_shapley_summary.csv"]
472
  _require(set(trial["analysis"].dropna()) == {"subspace_rotation"}, "trial valuation: noncanonical analysis", errors)