Add semantic latent trajectory views
Browse files- app.py +114 -9
- data/latent_trajectories.csv +0 -0
app.py
CHANGED
|
@@ -67,6 +67,7 @@ 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:
|
|
@@ -104,6 +105,56 @@ DATASETS = ordered_unique(prediction.get("dataset", pd.Series(dtype=str)))
|
|
| 104 |
MODELS = sorted(set(prediction.get("model", pd.Series(dtype=str)).dropna().astype(str)) | set(MODEL_INFO))
|
| 105 |
DEFAULT_SELECTED_MODELS = [m for m in DEFAULT_MODELS if m in MODELS] or MODELS[:10]
|
| 106 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
|
| 108 |
def model_metadata_frame() -> pd.DataFrame:
|
| 109 |
rows = []
|
|
@@ -256,10 +307,26 @@ def latent_space_figure(dataset: str, models: list[str] | None) -> go.Figure:
|
|
| 256 |
for col in ["x", "y", "z"]:
|
| 257 |
plot_df[col] = pd.to_numeric(plot_df[col], errors="coerce")
|
| 258 |
plot_df["condition_num"] = pd.to_numeric(plot_df["condition"], errors="coerce")
|
|
|
|
|
|
|
|
|
|
| 259 |
plot_df = plot_df.dropna(subset=["x", "y", "z"])
|
| 260 |
if plot_df.empty:
|
| 261 |
return empty_figure("Latent-space samples are empty after filtering.")
|
| 262 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 263 |
sessions = ordered_unique(plot_df["session_label"])
|
| 264 |
n_cols = 2
|
| 265 |
n_rows = int(np.ceil(len(sessions) / n_cols))
|
|
@@ -272,7 +339,10 @@ def latent_space_figure(dataset: str, models: list[str] | None) -> go.Figure:
|
|
| 272 |
vertical_spacing=0.105,
|
| 273 |
)
|
| 274 |
|
| 275 |
-
condition_values = sorted(
|
|
|
|
|
|
|
|
|
|
| 276 |
use_categorical_conditions = len(condition_values) <= 12
|
| 277 |
direction_palette = [
|
| 278 |
"#B23AEE",
|
|
@@ -292,7 +362,16 @@ def latent_space_figure(dataset: str, models: list[str] | None) -> go.Figure:
|
|
| 292 |
condition: direction_palette[idx % len(direction_palette)]
|
| 293 |
for idx, condition in enumerate(condition_values)
|
| 294 |
}
|
| 295 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 296 |
|
| 297 |
for session_idx, session in enumerate(sessions):
|
| 298 |
session_df = plot_df[plot_df["session_label"].astype(str) == session]
|
|
@@ -306,7 +385,11 @@ def latent_space_figure(dataset: str, models: list[str] | None) -> go.Figure:
|
|
| 306 |
cond_df = session_df[session_df["condition"].astype(str) == condition]
|
| 307 |
if cond_df.empty:
|
| 308 |
continue
|
| 309 |
-
trace_name =
|
|
|
|
|
|
|
|
|
|
|
|
|
| 310 |
fig.add_trace(
|
| 311 |
go.Scatter3d(
|
| 312 |
x=cond_df["x"],
|
|
@@ -317,13 +400,14 @@ def latent_space_figure(dataset: str, models: list[str] | None) -> go.Figure:
|
|
| 317 |
legendgroup=condition,
|
| 318 |
showlegend=session_idx == 0,
|
| 319 |
marker=dict(
|
| 320 |
-
size=2.6,
|
| 321 |
-
opacity=0.72,
|
| 322 |
color=condition_colors[condition],
|
| 323 |
),
|
| 324 |
customdata=np.stack(
|
| 325 |
[
|
| 326 |
cond_df["session_label"].astype(str),
|
|
|
|
| 327 |
cond_df["trial_index"].astype(str),
|
| 328 |
cond_df["time_index"].astype(str),
|
| 329 |
],
|
|
@@ -331,14 +415,34 @@ def latent_space_figure(dataset: str, models: list[str] | None) -> go.Figure:
|
|
| 331 |
),
|
| 332 |
hovertemplate=(
|
| 333 |
"session=%{customdata[0]}<br>"
|
| 334 |
-
f"{condition_prefix.lower()}={
|
| 335 |
-
"trial=%{customdata[
|
| 336 |
"<extra></extra>"
|
| 337 |
),
|
| 338 |
),
|
| 339 |
row=row,
|
| 340 |
col=col,
|
| 341 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 342 |
else:
|
| 343 |
fig.add_trace(
|
| 344 |
go.Scatter3d(
|
|
@@ -359,6 +463,7 @@ def latent_space_figure(dataset: str, models: list[str] | None) -> go.Figure:
|
|
| 359 |
customdata=np.stack(
|
| 360 |
[
|
| 361 |
session_df["condition"].astype(str),
|
|
|
|
| 362 |
session_df["trial_index"].astype(str),
|
| 363 |
session_df["time_index"].astype(str),
|
| 364 |
],
|
|
@@ -366,8 +471,8 @@ def latent_space_figure(dataset: str, models: list[str] | None) -> go.Figure:
|
|
| 366 |
),
|
| 367 |
hovertemplate=(
|
| 368 |
"session=%{fullData.name}<br>"
|
| 369 |
-
f"{condition_prefix.lower()}=%{{customdata[
|
| 370 |
-
"trial=%{customdata[
|
| 371 |
"<extra></extra>"
|
| 372 |
),
|
| 373 |
),
|
|
|
|
| 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 |
+
latent_trajectories = load_csv("latent_trajectories.csv")
|
| 71 |
|
| 72 |
|
| 73 |
def present_rows(df: pd.DataFrame) -> pd.DataFrame:
|
|
|
|
| 105 |
MODELS = sorted(set(prediction.get("model", pd.Series(dtype=str)).dropna().astype(str)) | set(MODEL_INFO))
|
| 106 |
DEFAULT_SELECTED_MODELS = [m for m in DEFAULT_MODELS if m in MODELS] or MODELS[:10]
|
| 107 |
|
| 108 |
+
CONDITION_LABELS = {
|
| 109 |
+
"monkey": {
|
| 110 |
+
"0": "Up",
|
| 111 |
+
"1": "Up-right",
|
| 112 |
+
"2": "Right",
|
| 113 |
+
"3": "Down-right",
|
| 114 |
+
"4": "Down",
|
| 115 |
+
"5": "Down-left",
|
| 116 |
+
"6": "Left",
|
| 117 |
+
"7": "Up-left",
|
| 118 |
+
},
|
| 119 |
+
"allen_neuropixels": {
|
| 120 |
+
str(i): f"{angle} deg"
|
| 121 |
+
for i, angle in enumerate([0, 45, 90, 135, 180, 225, 270, 315])
|
| 122 |
+
},
|
| 123 |
+
"speech": {
|
| 124 |
+
"0": "Do nothing",
|
| 125 |
+
"1": "ban",
|
| 126 |
+
"2": "choice",
|
| 127 |
+
"3": "day",
|
| 128 |
+
"4": "feel",
|
| 129 |
+
"5": "kite",
|
| 130 |
+
"6": "though",
|
| 131 |
+
"7": "were",
|
| 132 |
+
},
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def condition_sort_key(value: object) -> tuple[int, float | str]:
|
| 137 |
+
try:
|
| 138 |
+
return (0, float(value))
|
| 139 |
+
except (TypeError, ValueError):
|
| 140 |
+
return (1, str(value))
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
def condition_label(dataset: str, condition: object) -> str:
|
| 144 |
+
if pd.isna(condition):
|
| 145 |
+
return "Unknown"
|
| 146 |
+
text = str(condition)
|
| 147 |
+
mapped = CONDITION_LABELS.get(dataset, {}).get(text)
|
| 148 |
+
if mapped is not None:
|
| 149 |
+
return mapped
|
| 150 |
+
if dataset == "ratinabox":
|
| 151 |
+
try:
|
| 152 |
+
idx = int(float(text))
|
| 153 |
+
return f"x{idx % 10}, y{idx // 10}"
|
| 154 |
+
except ValueError:
|
| 155 |
+
return text
|
| 156 |
+
return text
|
| 157 |
+
|
| 158 |
|
| 159 |
def model_metadata_frame() -> pd.DataFrame:
|
| 160 |
rows = []
|
|
|
|
| 307 |
for col in ["x", "y", "z"]:
|
| 308 |
plot_df[col] = pd.to_numeric(plot_df[col], errors="coerce")
|
| 309 |
plot_df["condition_num"] = pd.to_numeric(plot_df["condition"], errors="coerce")
|
| 310 |
+
plot_df["condition_label"] = plot_df["condition"].map(
|
| 311 |
+
lambda value: condition_label(dataset, value)
|
| 312 |
+
)
|
| 313 |
plot_df = plot_df.dropna(subset=["x", "y", "z"])
|
| 314 |
if plot_df.empty:
|
| 315 |
return empty_figure("Latent-space samples are empty after filtering.")
|
| 316 |
|
| 317 |
+
trajectory_df = latent_trajectories[
|
| 318 |
+
(latent_trajectories["dataset"].astype(str) == str(dataset))
|
| 319 |
+
& (latent_trajectories["model"].astype(str) == str(model))
|
| 320 |
+
].copy()
|
| 321 |
+
for col in ["x", "y", "z"]:
|
| 322 |
+
if col in trajectory_df:
|
| 323 |
+
trajectory_df[col] = pd.to_numeric(trajectory_df[col], errors="coerce")
|
| 324 |
+
if not trajectory_df.empty:
|
| 325 |
+
trajectory_df["condition_label"] = trajectory_df["condition"].map(
|
| 326 |
+
lambda value: condition_label(dataset, value)
|
| 327 |
+
)
|
| 328 |
+
trajectory_df = trajectory_df.dropna(subset=["x", "y", "z"])
|
| 329 |
+
|
| 330 |
sessions = ordered_unique(plot_df["session_label"])
|
| 331 |
n_cols = 2
|
| 332 |
n_rows = int(np.ceil(len(sessions) / n_cols))
|
|
|
|
| 339 |
vertical_spacing=0.105,
|
| 340 |
)
|
| 341 |
|
| 342 |
+
condition_values = sorted(
|
| 343 |
+
plot_df["condition"].astype(str).unique(),
|
| 344 |
+
key=condition_sort_key,
|
| 345 |
+
)
|
| 346 |
use_categorical_conditions = len(condition_values) <= 12
|
| 347 |
direction_palette = [
|
| 348 |
"#B23AEE",
|
|
|
|
| 362 |
condition: direction_palette[idx % len(direction_palette)]
|
| 363 |
for idx, condition in enumerate(condition_values)
|
| 364 |
}
|
| 365 |
+
if dataset == "monkey":
|
| 366 |
+
condition_prefix = "Reach direction"
|
| 367 |
+
elif dataset == "ratinabox":
|
| 368 |
+
condition_prefix = "Position bin"
|
| 369 |
+
elif dataset == "allen_neuropixels":
|
| 370 |
+
condition_prefix = "Orientation"
|
| 371 |
+
elif dataset == "speech":
|
| 372 |
+
condition_prefix = "Cue"
|
| 373 |
+
else:
|
| 374 |
+
condition_prefix = "Condition"
|
| 375 |
|
| 376 |
for session_idx, session in enumerate(sessions):
|
| 377 |
session_df = plot_df[plot_df["session_label"].astype(str) == session]
|
|
|
|
| 385 |
cond_df = session_df[session_df["condition"].astype(str) == condition]
|
| 386 |
if cond_df.empty:
|
| 387 |
continue
|
| 388 |
+
trace_name = condition_label(dataset, condition)
|
| 389 |
+
session_traj = trajectory_df[
|
| 390 |
+
(trajectory_df["session_label"].astype(str) == session)
|
| 391 |
+
& (trajectory_df["condition"].astype(str) == condition)
|
| 392 |
+
].sort_values("time_index")
|
| 393 |
fig.add_trace(
|
| 394 |
go.Scatter3d(
|
| 395 |
x=cond_df["x"],
|
|
|
|
| 400 |
legendgroup=condition,
|
| 401 |
showlegend=session_idx == 0,
|
| 402 |
marker=dict(
|
| 403 |
+
size=1.9 if not session_traj.empty else 2.6,
|
| 404 |
+
opacity=0.28 if not session_traj.empty else 0.72,
|
| 405 |
color=condition_colors[condition],
|
| 406 |
),
|
| 407 |
customdata=np.stack(
|
| 408 |
[
|
| 409 |
cond_df["session_label"].astype(str),
|
| 410 |
+
cond_df["condition_label"].astype(str),
|
| 411 |
cond_df["trial_index"].astype(str),
|
| 412 |
cond_df["time_index"].astype(str),
|
| 413 |
],
|
|
|
|
| 415 |
),
|
| 416 |
hovertemplate=(
|
| 417 |
"session=%{customdata[0]}<br>"
|
| 418 |
+
f"{condition_prefix.lower()}=%{{customdata[1]}}<br>"
|
| 419 |
+
"trial=%{customdata[2]} time=%{customdata[3]}"
|
| 420 |
"<extra></extra>"
|
| 421 |
),
|
| 422 |
),
|
| 423 |
row=row,
|
| 424 |
col=col,
|
| 425 |
)
|
| 426 |
+
if not session_traj.empty:
|
| 427 |
+
fig.add_trace(
|
| 428 |
+
go.Scatter3d(
|
| 429 |
+
x=session_traj["x"],
|
| 430 |
+
y=session_traj["y"],
|
| 431 |
+
z=session_traj["z"],
|
| 432 |
+
mode="lines",
|
| 433 |
+
name=trace_name,
|
| 434 |
+
legendgroup=condition,
|
| 435 |
+
showlegend=False,
|
| 436 |
+
line=dict(color=condition_colors[condition], width=5),
|
| 437 |
+
hovertemplate=(
|
| 438 |
+
f"{condition_prefix.lower()}={trace_name}<br>"
|
| 439 |
+
"time=%{customdata}<extra></extra>"
|
| 440 |
+
),
|
| 441 |
+
customdata=session_traj["time_index"],
|
| 442 |
+
),
|
| 443 |
+
row=row,
|
| 444 |
+
col=col,
|
| 445 |
+
)
|
| 446 |
else:
|
| 447 |
fig.add_trace(
|
| 448 |
go.Scatter3d(
|
|
|
|
| 463 |
customdata=np.stack(
|
| 464 |
[
|
| 465 |
session_df["condition"].astype(str),
|
| 466 |
+
session_df["condition"].map(lambda value: condition_label(dataset, value)).astype(str),
|
| 467 |
session_df["trial_index"].astype(str),
|
| 468 |
session_df["time_index"].astype(str),
|
| 469 |
],
|
|
|
|
| 471 |
),
|
| 472 |
hovertemplate=(
|
| 473 |
"session=%{fullData.name}<br>"
|
| 474 |
+
f"{condition_prefix.lower()}=%{{customdata[1]}}<br>"
|
| 475 |
+
"trial=%{customdata[2]} time=%{customdata[3]}"
|
| 476 |
"<extra></extra>"
|
| 477 |
),
|
| 478 |
),
|
data/latent_trajectories.csv
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|