Download validate_data.py from Tang-Lab/benchdash: direct link, hf CLI and curl.
- Browser
- Download file 33.5 kB
-
https://huggingface.co/spaces/Tang-Lab/benchdash/resolve/main/validate_data.py
- Command line
-
hf download hf://spaces/Tang-Lab/benchdash/validate_data.py
-
curl -L -o validate_data.py https://huggingface.co/spaces/Tang-Lab/benchdash/resolve/main/validate_data.py
33.5 kB
| #!/usr/bin/env python3 | |
| """Validate the bundled BEND-BCI Space tables and their paper provenance. | |
| The local checks run in the standalone Hugging Face Space. Passing | |
| ``--canonical-root`` additionally compares every manuscript-facing summary | |
| against ``paper/results`` in the main benchmark repository. | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| from pathlib import Path | |
| from typing import Iterable | |
| import numpy as np | |
| import pandas as pd | |
| DATASETS = { | |
| "monkey", | |
| "allen_neuropixels", | |
| "speech", | |
| "mc_pacman", | |
| "ratinabox", | |
| } | |
| CONSISTENCY_DATASETS = DATASETS - {"mc_pacman"} | |
| LATENT_COORDINATE_SPACE = "per_session_whitened_reference_aligned_3d" | |
| FIGURE5_TARGET_SESSION = "sub-C_ses-CO-20150716_behavior+ecephys" | |
| # Coverage is defined in Supplementary Table 2. | |
| EXPECTED_COVERAGE = { | |
| "clean_prediction_summary.csv": (115, 112), | |
| "robustness_summary.csv": (115, 112), | |
| "scalability_summary.csv": (115, 112), | |
| "consistency_summary.csv": (54, 46), | |
| "neuron_shap_summary.csv": (106, 106), | |
| "trial_shapley_summary.csv": (81, 81), | |
| "trial_shapley_retrain_summary.csv": (99, 99), | |
| } | |
| REQUIRED_COLUMNS = { | |
| "dataset_overview.csv": { | |
| "dataset", "dataset_name", "species", "task", "array_shape", | |
| "target", "score", "bin_ms", "recordings", "source_label", | |
| "source_url", "example_trial_index", "example_features_shown", | |
| }, | |
| "dataset_example_neural.csv": { | |
| "dataset", "trial_index", "time_index", "time_ms", | |
| "feature_display_index", "feature_index", "neural_value", | |
| }, | |
| "dataset_example_targets.csv": { | |
| "dataset", "trial_index", "time_index", "time_ms", "target_0", | |
| "target_1", "target_label", | |
| }, | |
| "dataset_targets.csv": { | |
| "dataset", "trial_index", "trial_id", "condition_id", "target_label", | |
| "time_index", "time_ms", "target_0", "target_1", "is_example", | |
| }, | |
| "feature_example_raster.csv": { | |
| "dataset", "trial_index", "display_index", "feature_index", | |
| "feature_group", "validation_value", "group_order", "n_time", | |
| "t0_index", "bin_ms", | |
| }, | |
| "clean_prediction_summary.csv": { | |
| "model", "dataset", "status", "metric", "score", "decoder", | |
| }, | |
| "robustness_summary.csv": { | |
| "model", "dataset", "status", "metric", "noise_levels", "scores", | |
| "raw_auc", | |
| }, | |
| "scalability_summary.csv": { | |
| "model", "dataset", "status", "training_time_sec", | |
| "inference_time_sec", "peak_ram_gb", "peak_vram_gb", | |
| }, | |
| "consistency_summary.csv": { | |
| "model", "dataset", "is_active_model", "mean_r2", "n_sessions", | |
| "sessions", "latent_dim", "scoring_modes", "normalizations", | |
| }, | |
| "neuron_shap_summary.csv": { | |
| "model", "dataset", "is_active_model", "auc", "spearman_corr", | |
| "shap_mean_value", "shap_min_value", "shap_max_value", | |
| "shap_fraction_positive", "shap_fraction_negative", | |
| }, | |
| "neuron_attributions.csv": { | |
| "model", "dataset", "feature_index", "feature_group", | |
| "signed_attribution", "attribution_rank", "attribution_bin", | |
| "validation_value", | |
| }, | |
| "trial_shapley_summary.csv": { | |
| "model", "dataset", "is_active_model", "analysis", "perturbation_auc", | |
| "rotation_angle_deg", "rotation_subspace_dim_spec", | |
| "trial_selection_mode", "converged", "shapley_mean_value", | |
| "shapley_min_value", "shapley_max_value", | |
| "shapley_fraction_positive", "shapley_fraction_negative", | |
| }, | |
| "trial_shapley_retrain_summary.csv": { | |
| "analysis", "model", "is_active_model", "condition", "metric", "score", | |
| }, | |
| "trial_historical_trajectories.csv": { | |
| "model", "target_session", "trial_index", "trial_id", | |
| "direction_index", "direction_label", "time_index", "target_x", | |
| "target_y", "current_only_x", "current_only_y", | |
| "historical_selected_x", "historical_selected_y", "current_only_r2", | |
| "historical_selected_r2", | |
| }, | |
| "latent_samples.csv": { | |
| "model", "dataset", "session", "session_label", "x", "y", "z", | |
| "condition", "trial_index", "time_index", "eval_time_index", | |
| "coordinate_space", "reference_session", "alignment", "landmark_type", | |
| "n_alignment_landmarks", "is_reference", "session_order", | |
| }, | |
| "latent_trajectories.csv": { | |
| "model", "dataset", "session", "session_label", "x", "y", "z", | |
| "condition", "time_index", "eval_time_index", "n_points", | |
| "coordinate_space", "reference_session", "alignment", "landmark_type", | |
| "n_alignment_landmarks", "is_reference", "session_order", | |
| }, | |
| } | |
| UNIQUE_KEYS = { | |
| "dataset_overview.csv": ["dataset"], | |
| "dataset_example_neural.csv": ["dataset", "time_index", "feature_display_index"], | |
| "dataset_example_targets.csv": ["dataset", "time_index"], | |
| "dataset_targets.csv": ["dataset", "trial_index", "time_index"], | |
| "feature_example_raster.csv": ["dataset", "feature_index"], | |
| "clean_prediction_summary.csv": ["model", "dataset"], | |
| "robustness_summary.csv": ["model", "dataset"], | |
| "scalability_summary.csv": ["model", "dataset"], | |
| "consistency_summary.csv": ["model", "dataset"], | |
| "neuron_shap_summary.csv": ["model", "dataset"], | |
| "neuron_attributions.csv": ["model", "dataset", "feature_index"], | |
| "trial_shapley_summary.csv": ["model", "dataset"], | |
| "trial_shapley_retrain_summary.csv": ["analysis", "model", "condition"], | |
| } | |
| CANONICAL_NAMES = { | |
| "clean_prediction_summary.csv": "metrics_summary.csv", | |
| "robustness_summary.csv": "robustness_summary.csv", | |
| "scalability_summary.csv": "scalability_summary.csv", | |
| "consistency_summary.csv": "consistency_summary.csv", | |
| "neuron_shap_summary.csv": "neuron_shap_summary.csv", | |
| "trial_shapley_summary.csv": "trial_shapley_summary.csv", | |
| "trial_shapley_retrain_summary.csv": "trial_shapley_retrain_summary.csv", | |
| } | |
| class ValidationError(RuntimeError): | |
| """Raised when Space data violates its manuscript-facing contract.""" | |
| def _active_mask(frame: pd.DataFrame) -> pd.Series: | |
| if "status" in frame.columns: | |
| return frame["status"].fillna("").eq("present") | |
| if "is_active_model" in frame.columns: | |
| return frame["is_active_model"].astype(str).str.lower().eq("true") | |
| return pd.Series(True, index=frame.index) | |
| def _require(condition: bool, message: str, errors: list[str]) -> None: | |
| if not condition: | |
| errors.append(message) | |
| def _same_values(left: pd.DataFrame, right: pd.DataFrame) -> None: | |
| pd.testing.assert_frame_equal( | |
| left.reset_index(drop=True), | |
| right.reset_index(drop=True), | |
| check_dtype=False, | |
| check_exact=True, | |
| check_categorical=False, | |
| ) | |
| def _validate_latent_cell( | |
| frame: pd.DataFrame, | |
| *, | |
| table_label: str, | |
| model: str, | |
| dataset: str, | |
| sessions: list[str], | |
| landmark_type: str, | |
| errors: list[str], | |
| ) -> None: | |
| """Validate reference/alignment metadata for one method-dataset cell.""" | |
| cell = frame[ | |
| frame["model"].astype(str).eq(model) | |
| & frame["dataset"].astype(str).eq(dataset) | |
| ] | |
| for session_order, session in enumerate(sessions): | |
| session_rows = cell[cell["session"].astype(str).eq(session)] | |
| if session_rows.empty: | |
| errors.append(f"{table_label}: missing {model}/{dataset}/{session}") | |
| continue | |
| expected_reference = "true" if session_order == 0 else "false" | |
| observed_reference = set( | |
| session_rows["is_reference"].dropna().astype(str).str.lower() | |
| ) | |
| _require( | |
| observed_reference == {expected_reference}, | |
| f"{table_label}: {model}/{dataset}/{session} is_reference " | |
| f"values {sorted(observed_reference)}", | |
| errors, | |
| ) | |
| expected_alignment = "identity" if session_order == 0 else "proper_similarity_procrustes" | |
| observed_alignment = set(session_rows["alignment"].dropna().astype(str)) | |
| _require( | |
| observed_alignment == {expected_alignment}, | |
| f"{table_label}: {model}/{dataset}/{session} alignment " | |
| f"values {sorted(observed_alignment)}", | |
| errors, | |
| ) | |
| observed_reference_sessions = set( | |
| session_rows["reference_session"].dropna().astype(str) | |
| ) | |
| _require( | |
| observed_reference_sessions == {sessions[0]}, | |
| f"{table_label}: {model}/{dataset}/{session} reference metadata " | |
| f"{sorted(observed_reference_sessions)}", | |
| errors, | |
| ) | |
| observed_landmarks = set(session_rows["landmark_type"].dropna().astype(str)) | |
| _require( | |
| observed_landmarks == {landmark_type}, | |
| f"{table_label}: {model}/{dataset}/{session} landmarks " | |
| f"{sorted(observed_landmarks)}", | |
| errors, | |
| ) | |
| orders = pd.to_numeric(session_rows["session_order"], errors="coerce") | |
| _require( | |
| orders.notna().all() | |
| and np.isfinite(orders.to_numpy()).all() | |
| and orders.eq(session_order).all(), | |
| f"{table_label}: {model}/{dataset}/{session} has invalid session_order", | |
| errors, | |
| ) | |
| landmark_counts = pd.to_numeric( | |
| session_rows["n_alignment_landmarks"], errors="coerce" | |
| ) | |
| _require( | |
| landmark_counts.notna().all() | |
| and np.isfinite(landmark_counts.to_numpy()).all() | |
| and landmark_counts.ge(3).all(), | |
| f"{table_label}: {model}/{dataset}/{session} has invalid landmark count", | |
| errors, | |
| ) | |
| def validate_local(data_dir: Path) -> dict[str, pd.DataFrame]: | |
| """Validate schemas, coverage, uniqueness, and fixed analysis conventions.""" | |
| frames: dict[str, pd.DataFrame] = {} | |
| errors: list[str] = [] | |
| for name, columns in REQUIRED_COLUMNS.items(): | |
| path = data_dir / name | |
| if not path.exists(): | |
| errors.append(f"missing required table: {path}") | |
| continue | |
| frame = pd.read_csv(path, low_memory=False) | |
| missing = sorted(columns - set(frame.columns)) | |
| _require(not missing, f"{name}: missing columns {missing}", errors) | |
| if missing: | |
| continue | |
| frames[name] = frame | |
| for name, (n_rows, n_active) in EXPECTED_COVERAGE.items(): | |
| if name not in frames: | |
| continue | |
| frame = frames[name] | |
| _require(len(frame) == n_rows, f"{name}: expected {n_rows} rows, found {len(frame)}", errors) | |
| active = int(_active_mask(frame).sum()) | |
| _require(active == n_active, f"{name}: expected {n_active} available rows, found {active}", errors) | |
| for name, keys in UNIQUE_KEYS.items(): | |
| if name not in frames or not set(keys).issubset(frames[name].columns): | |
| continue | |
| duplicates = frames[name].duplicated(keys, keep=False) | |
| _require(not duplicates.any(), f"{name}: duplicate keys for {keys}", errors) | |
| for name in ("clean_prediction_summary.csv", "robustness_summary.csv", "scalability_summary.csv"): | |
| if name in frames: | |
| observed = set(frames[name]["dataset"].dropna().astype(str)) | |
| _require(observed == DATASETS, f"{name}: dataset set is {sorted(observed)}", errors) | |
| for name in ( | |
| "dataset_overview.csv", | |
| "dataset_example_neural.csv", | |
| "dataset_example_targets.csv", | |
| "dataset_targets.csv", | |
| "feature_example_raster.csv", | |
| ): | |
| if name in frames: | |
| observed = set(frames[name]["dataset"].dropna().astype(str)) | |
| _require(observed == DATASETS, f"{name}: dataset set is {sorted(observed)}", errors) | |
| if "dataset_overview.csv" in frames: | |
| overview = frames["dataset_overview.csv"] | |
| _require(len(overview) == 5, "dataset overview: expected five rows", errors) | |
| shown = pd.to_numeric(overview["example_features_shown"], errors="coerce") | |
| _require( | |
| shown.notna().all() and shown.between(1, 80).all(), | |
| "dataset overview: invalid example feature counts", | |
| errors, | |
| ) | |
| if "dataset_example_neural.csv" in frames: | |
| examples = frames["dataset_example_neural.csv"] | |
| values = pd.to_numeric(examples["neural_value"], errors="coerce") | |
| _require( | |
| values.notna().all() and np.isfinite(values.to_numpy()).all(), | |
| "dataset examples: neural values must be finite", | |
| errors, | |
| ) | |
| trials_per_dataset = examples.groupby("dataset")["trial_index"].nunique() | |
| _require( | |
| trials_per_dataset.eq(1).all(), | |
| "dataset examples: expected one trial per dataset", | |
| errors, | |
| ) | |
| if "dataset_example_targets.csv" in frames: | |
| targets = frames["dataset_example_targets.csv"] | |
| trials_per_dataset = targets.groupby("dataset")["trial_index"].nunique() | |
| _require( | |
| trials_per_dataset.eq(1).all(), | |
| "dataset targets: expected one trial per dataset", | |
| errors, | |
| ) | |
| classification = targets[ | |
| targets["dataset"].isin({"allen_neuropixels", "speech"}) | |
| ] | |
| _require( | |
| len(classification) == 2 and classification["target_label"].notna().all(), | |
| "dataset targets: classification labels are missing", | |
| errors, | |
| ) | |
| if "dataset_targets.csv" in frames: | |
| targets = frames["dataset_targets.csv"] | |
| expected_rows = { | |
| "monkey": 15_950, | |
| "allen_neuropixels": 598, | |
| "speech": 168, | |
| "mc_pacman": 40_544, | |
| "ratinabox": 15_000, | |
| } | |
| observed_rows = targets.groupby("dataset").size().to_dict() | |
| _require( | |
| observed_rows == expected_rows, | |
| f"all targets: row counts are {observed_rows}", | |
| errors, | |
| ) | |
| for column in ["trial_index", "condition_id", "time_index"]: | |
| values = pd.to_numeric(targets[column], errors="coerce") | |
| _require( | |
| values.notna().all() and np.allclose(values, np.round(values)), | |
| f"all targets: invalid {column} values", | |
| errors, | |
| ) | |
| example_mask = targets["is_example"].astype(str).str.lower().eq("true") | |
| example_trials = targets.loc[example_mask].groupby("dataset")[ | |
| "trial_index" | |
| ].nunique() | |
| _require( | |
| example_trials.reindex(sorted(DATASETS)).eq(1).all(), | |
| "all targets: expected one highlighted trial per dataset", | |
| errors, | |
| ) | |
| classification = targets[targets["dataset"].isin({"allen_neuropixels", "speech"})] | |
| class_sets = classification.groupby("dataset")["condition_id"].apply( | |
| lambda values: set(pd.to_numeric(values, errors="coerce").astype(int)) | |
| ) | |
| _require( | |
| class_sets.map(lambda values: values == set(range(8))).all() | |
| and classification["target_label"].notna().all(), | |
| "all targets: classification labels or class coverage differ", | |
| errors, | |
| ) | |
| continuous = targets[targets["dataset"].isin({"monkey", "mc_pacman", "ratinabox"})] | |
| target_0 = pd.to_numeric(continuous["target_0"], errors="coerce") | |
| _require( | |
| target_0.notna().all() and np.isfinite(target_0.to_numpy()).all(), | |
| "all targets: continuous target_0 values must be finite", | |
| errors, | |
| ) | |
| two_dimensional = continuous[continuous["dataset"].isin({"monkey", "ratinabox"})] | |
| target_1 = pd.to_numeric(two_dimensional["target_1"], errors="coerce") | |
| _require( | |
| target_1.notna().all() and np.isfinite(target_1.to_numpy()).all(), | |
| "all targets: two-dimensional target values must be finite", | |
| errors, | |
| ) | |
| if "feature_example_raster.csv" in frames: | |
| raster = frames["feature_example_raster.csv"] | |
| value_columns = sorted( | |
| column for column in raster.columns if column.startswith("value_") | |
| ) | |
| _require( | |
| len(value_columns) == 300, | |
| "feature raster: expected 300 time-value columns", | |
| errors, | |
| ) | |
| for dataset, group in raster.groupby("dataset"): | |
| n_time = pd.to_numeric(group["n_time"], errors="coerce") | |
| _require( | |
| n_time.notna().all() and n_time.nunique() == 1, | |
| f"feature raster: inconsistent time length for {dataset}", | |
| errors, | |
| ) | |
| if n_time.notna().all(): | |
| active_columns = value_columns[: int(n_time.iloc[0])] | |
| active = group[active_columns].apply(pd.to_numeric, errors="coerce") | |
| _require( | |
| active.notna().all().all() | |
| and np.isfinite(active.to_numpy(dtype=float)).all(), | |
| f"feature raster: nonfinite activity for {dataset}", | |
| errors, | |
| ) | |
| if all( | |
| name in frames | |
| for name in ( | |
| "dataset_overview.csv", | |
| "dataset_example_neural.csv", | |
| "dataset_example_targets.csv", | |
| "dataset_targets.csv", | |
| ) | |
| ): | |
| overview_trials = frames["dataset_overview.csv"].set_index("dataset")[ | |
| "example_trial_index" | |
| ].astype(int) | |
| neural_trials = frames["dataset_example_neural.csv"].groupby("dataset")[ | |
| "trial_index" | |
| ].first().astype(int) | |
| target_trials = frames["dataset_example_targets.csv"].groupby("dataset")[ | |
| "trial_index" | |
| ].first().astype(int) | |
| all_targets = frames["dataset_targets.csv"] | |
| all_target_mask = all_targets["is_example"].astype(str).str.lower().eq("true") | |
| all_target_trials = all_targets.loc[all_target_mask].groupby("dataset")[ | |
| "trial_index" | |
| ].first().astype(int) | |
| _require( | |
| overview_trials.equals(neural_trials.reindex(overview_trials.index)) | |
| and overview_trials.equals(target_trials.reindex(overview_trials.index)) | |
| and overview_trials.equals(all_target_trials.reindex(overview_trials.index)), | |
| "dataset examples: manifest, neural and target trial indices differ", | |
| errors, | |
| ) | |
| if "clean_prediction_summary.csv" in frames: | |
| prediction = frames["clean_prediction_summary.csv"] | |
| _require(prediction["model"].nunique() == 23, "prediction: expected 23 methods", errors) | |
| metrics = set(prediction.loc[_active_mask(prediction), "metric"].dropna()) | |
| _require(metrics == {"accuracy", "r2"}, f"prediction: unexpected metrics {sorted(metrics)}", errors) | |
| if "consistency_summary.csv" in frames: | |
| consistency = frames["consistency_summary.csv"] | |
| active = consistency.loc[_active_mask(consistency)] | |
| observed = set(active["dataset"].dropna().astype(str)) | |
| _require(observed == CONSISTENCY_DATASETS, f"consistency: dataset set is {sorted(observed)}", errors) | |
| norms = set(active["normalizations"].dropna().astype(str)) | |
| _require(norms == {"per_session_whitening"}, f"consistency: unexpected normalization {sorted(norms)}", errors) | |
| if "neuron_shap_summary.csv" in frames: | |
| feature = frames["neuron_shap_summary.csv"] | |
| allen = feature[feature["dataset"].eq("allen_neuropixels")] | |
| _require(not allen.empty and allen["spearman_corr"].notna().all(), "feature attribution: Allen Spearman values missing", errors) | |
| other = feature[~feature["dataset"].eq("allen_neuropixels")] | |
| _require(other["auc"].notna().all(), "feature attribution: ROC-AUC values missing", errors) | |
| _require(feature["shap_min_value"].lt(0).any(), "feature attribution: signed negative values absent", errors) | |
| if "neuron_attributions.csv" in frames: | |
| features = frames["neuron_attributions.csv"] | |
| observed = set(features["dataset"].dropna().astype(str)) | |
| _require(observed == DATASETS, f"neuron attributions: dataset set is {sorted(observed)}", errors) | |
| _require( | |
| features[["model", "dataset"]].drop_duplicates().shape[0] == 106, | |
| "neuron attributions: expected 106 method-dataset pairs", | |
| errors, | |
| ) | |
| values = pd.to_numeric(features["signed_attribution"], errors="coerce") | |
| _require( | |
| values.notna().all() and np.isfinite(values.to_numpy()).all(), | |
| "neuron attributions: signed values must be finite", | |
| errors, | |
| ) | |
| _require( | |
| values.lt(0).any() and values.gt(0).any(), | |
| "neuron attributions: expected positive and negative signed values", | |
| errors, | |
| ) | |
| _require( | |
| set(features["attribution_bin"].dropna().astype(str)) | |
| == {"Top", "Middle", "Bottom", "Tied"}, | |
| "neuron attributions: invalid rank bins", | |
| errors, | |
| ) | |
| if "neuron_shap_summary.csv" in frames: | |
| expected_counts = ( | |
| frames["neuron_shap_summary.csv"] | |
| .set_index(["model", "dataset"])["shap_n_values"] | |
| .astype(int) | |
| ) | |
| observed_counts = features.groupby(["model", "dataset"]).size() | |
| _require( | |
| observed_counts.equals(expected_counts.reindex(observed_counts.index)), | |
| "neuron attributions: feature counts differ from summary", | |
| errors, | |
| ) | |
| if "feature_example_raster.csv" in frames: | |
| raster = frames["feature_example_raster.csv"] | |
| raster_counts = raster.groupby("dataset")["feature_index"].nunique() | |
| attribution_counts = features.groupby("dataset")["feature_index"].nunique() | |
| _require( | |
| raster_counts.equals(attribution_counts.reindex(raster_counts.index)), | |
| "feature raster: feature counts differ from attributions", | |
| errors, | |
| ) | |
| raster_groups = raster[["dataset", "feature_index", "feature_group"]] | |
| attribution_groups = features[ | |
| ["dataset", "feature_index", "feature_group"] | |
| ].drop_duplicates() | |
| merged = raster_groups.merge( | |
| attribution_groups, | |
| on=["dataset", "feature_index"], | |
| how="outer", | |
| suffixes=("_raster", "_attribution"), | |
| indicator=True, | |
| ) | |
| _require( | |
| merged["_merge"].eq("both").all() | |
| and merged["feature_group_raster"].eq( | |
| merged["feature_group_attribution"] | |
| ).all(), | |
| "feature raster: group labels differ from attributions", | |
| errors, | |
| ) | |
| if "trial_shapley_summary.csv" in frames: | |
| trial = frames["trial_shapley_summary.csv"] | |
| _require(set(trial["analysis"].dropna()) == {"subspace_rotation"}, "trial valuation: noncanonical analysis", errors) | |
| angles = set(pd.to_numeric(trial["rotation_angle_deg"], errors="coerce").dropna()) | |
| _require(angles == {75.0}, f"trial valuation: rotation angles {sorted(angles)}", errors) | |
| dims = set(trial["rotation_subspace_dim_spec"].dropna().astype(str)) | |
| _require(dims == {"full"}, f"trial valuation: subspace specs {sorted(dims)}", errors) | |
| modes = set(trial["trial_selection_mode"].dropna().astype(str)) | |
| _require(modes == {"random"}, f"trial valuation: selection modes {sorted(modes)}", errors) | |
| _require(trial["perturbation_auc"].notna().all(), "trial valuation: detection AUC missing", errors) | |
| _require(trial["shapley_min_value"].lt(0).any(), "trial valuation: signed negative values absent", errors) | |
| if "trial_shapley_retrain_summary.csv" in frames: | |
| retrain = frames["trial_shapley_retrain_summary.csv"] | |
| expected = { | |
| "within_session_cleaning": {"mixed_full", "data_shapley", "oracle"}, | |
| "cross_session_old_trial_selection": { | |
| "target_only", "all_sessions", "oldonly_dshap_negative_removal", | |
| }, | |
| } | |
| observed = { | |
| analysis: set(group["condition"].dropna().astype(str)) | |
| for analysis, group in retrain.groupby("analysis") | |
| } | |
| _require(observed == expected, f"trial retraining: conditions {observed}", errors) | |
| if "trial_historical_trajectories.csv" in frames: | |
| historical = frames["trial_historical_trajectories.csv"] | |
| _require( | |
| len(historical) == 135 * 35, | |
| "historical trajectories: expected 135 trials × 35 time bins", | |
| errors, | |
| ) | |
| keys = ["trial_index", "time_index"] | |
| _require( | |
| not historical.duplicated(keys, keep=False).any(), | |
| f"historical trajectories: duplicate keys for {keys}", | |
| errors, | |
| ) | |
| _require( | |
| set(historical["model"].dropna().astype(str)) == {"rnn"}, | |
| "historical trajectories: expected the Figure 5e RNN example", | |
| errors, | |
| ) | |
| _require( | |
| set(historical["target_session"].dropna().astype(str)) | |
| == {FIGURE5_TARGET_SESSION}, | |
| "historical trajectories: unexpected target session", | |
| errors, | |
| ) | |
| _require( | |
| historical["trial_index"].nunique() == 135 | |
| and historical["trial_id"].nunique() == 135, | |
| "historical trajectories: expected 135 held-out trials and trial IDs", | |
| errors, | |
| ) | |
| time_counts = historical.groupby("trial_index")["time_index"].nunique() | |
| _require( | |
| len(time_counts) == 135 and time_counts.eq(35).all(), | |
| "historical trajectories: every trial must contain 35 time bins", | |
| errors, | |
| ) | |
| direction_indices = pd.to_numeric( | |
| historical["direction_index"], errors="coerce" | |
| ) | |
| _require( | |
| direction_indices.notna().all() | |
| and set(direction_indices.astype(int)) == set(range(8)), | |
| "historical trajectories: expected all eight reach directions", | |
| errors, | |
| ) | |
| for column in ( | |
| "target_x", | |
| "target_y", | |
| "current_only_x", | |
| "current_only_y", | |
| "historical_selected_x", | |
| "historical_selected_y", | |
| "current_only_r2", | |
| "historical_selected_r2", | |
| ): | |
| values = pd.to_numeric(historical[column], errors="coerce") | |
| _require( | |
| values.notna().all() and np.isfinite(values.to_numpy()).all(), | |
| f"historical trajectories: non-finite {column} values", | |
| errors, | |
| ) | |
| _require( | |
| historical["current_only_r2"].nunique() == 1 | |
| and historical["historical_selected_r2"].nunique() == 1, | |
| "historical trajectories: expected one R² value per condition", | |
| errors, | |
| ) | |
| for name in ("latent_samples.csv", "latent_trajectories.csv"): | |
| if name not in frames: | |
| continue | |
| latent = frames[name] | |
| observed = set(latent["dataset"].dropna().astype(str)) | |
| _require(observed.issubset(CONSISTENCY_DATASETS), f"{name}: unsupported datasets {sorted(observed - CONSISTENCY_DATASETS)}", errors) | |
| for column in ("x", "y", "z"): | |
| values = pd.to_numeric(latent[column], errors="coerce") | |
| _require( | |
| values.notna().all() and np.isfinite(values.to_numpy()).all(), | |
| f"{name}: non-finite {column} values", | |
| errors, | |
| ) | |
| for column in ( | |
| "coordinate_space", | |
| "reference_session", | |
| "alignment", | |
| "landmark_type", | |
| "n_alignment_landmarks", | |
| "is_reference", | |
| "session_order", | |
| ): | |
| _require( | |
| latent[column].notna().all(), | |
| f"{name}: missing {column} values", | |
| errors, | |
| ) | |
| spaces = set(latent["coordinate_space"].dropna().astype(str)) | |
| _require( | |
| spaces == {LATENT_COORDINATE_SPACE}, | |
| f"{name}: coordinate spaces {sorted(spaces)}", | |
| errors, | |
| ) | |
| if { | |
| "consistency_summary.csv", "latent_samples.csv", "latent_trajectories.csv" | |
| }.issubset(frames): | |
| consistency = frames["consistency_summary.csv"] | |
| consistency = consistency.loc[_active_mask(consistency)].copy() | |
| samples = frames["latent_samples.csv"] | |
| trajectories = frames["latent_trajectories.csv"] | |
| expected_sample_sessions: set[tuple[str, str, str]] = set() | |
| expected_trajectory_sessions: set[tuple[str, str, str]] = set() | |
| for row in consistency.itertuples(index=False): | |
| sessions = ( | |
| [] | |
| if pd.isna(row.sessions) | |
| else [item.strip() for item in str(row.sessions).split(";") if item.strip()] | |
| ) | |
| if not sessions: | |
| errors.append(f"consistency: {row.model}/{row.dataset} has no sessions") | |
| continue | |
| try: | |
| expected_n_sessions = int(row.n_sessions) | |
| except (TypeError, ValueError): | |
| expected_n_sessions = -1 | |
| _require( | |
| len(sessions) == expected_n_sessions, | |
| f"consistency: {row.model}/{row.dataset} lists {len(sessions)} " | |
| f"sessions but n_sessions={row.n_sessions}", | |
| errors, | |
| ) | |
| expected_sample_sessions.update( | |
| (str(row.model), str(row.dataset), session) for session in sessions | |
| ) | |
| if str(row.dataset) != "ratinabox": | |
| expected_trajectory_sessions.update( | |
| (str(row.model), str(row.dataset), session) for session in sessions | |
| ) | |
| _validate_latent_cell( | |
| samples, | |
| table_label="latent samples", | |
| model=str(row.model), | |
| dataset=str(row.dataset), | |
| sessions=sessions, | |
| landmark_type=str(row.scoring_modes), | |
| errors=errors, | |
| ) | |
| if str(row.dataset) != "ratinabox": | |
| _validate_latent_cell( | |
| trajectories, | |
| table_label="latent trajectories", | |
| model=str(row.model), | |
| dataset=str(row.dataset), | |
| sessions=sessions, | |
| landmark_type=str(row.scoring_modes), | |
| errors=errors, | |
| ) | |
| observed_sample_sessions = set( | |
| samples[["model", "dataset", "session"]].astype(str).itertuples(index=False, name=None) | |
| ) | |
| observed_trajectory_sessions = set( | |
| trajectories[["model", "dataset", "session"]].astype(str).itertuples(index=False, name=None) | |
| ) | |
| _require( | |
| observed_sample_sessions == expected_sample_sessions, | |
| "latent samples: active consistency session coverage differs", | |
| errors, | |
| ) | |
| _require( | |
| observed_trajectory_sessions == expected_trajectory_sessions, | |
| "latent trajectories: applicable consistency session coverage differs", | |
| errors, | |
| ) | |
| _require( | |
| len(observed_sample_sessions) == 173, | |
| f"latent samples: expected 173 sessions, found {len(observed_sample_sessions)}", | |
| errors, | |
| ) | |
| if errors: | |
| raise ValidationError("\n".join(f"- {item}" for item in errors)) | |
| return frames | |
| def validate_canonical(data_dir: Path, canonical_root: Path) -> None: | |
| """Require Space summaries to equal the current paper result tables.""" | |
| results_dir = canonical_root / "paper" / "results" | |
| errors: list[str] = [] | |
| for dashboard_name, paper_name in CANONICAL_NAMES.items(): | |
| dashboard_path = data_dir / dashboard_name | |
| paper_path = results_dir / paper_name | |
| if not dashboard_path.exists(): | |
| errors.append(f"missing dashboard table: {dashboard_path}") | |
| continue | |
| if not paper_path.exists(): | |
| errors.append(f"missing canonical table: {paper_path}") | |
| continue | |
| try: | |
| _same_values(pd.read_csv(dashboard_path), pd.read_csv(paper_path)) | |
| except AssertionError as exc: | |
| first_line = str(exc).splitlines()[0] if str(exc) else "values differ" | |
| errors.append(f"{dashboard_name} != {paper_name}: {first_line}") | |
| if errors: | |
| raise ValidationError("\n".join(f"- {item}" for item in errors)) | |
| def build_parser() -> argparse.ArgumentParser: | |
| parser = argparse.ArgumentParser(description=__doc__) | |
| parser.add_argument("--data-dir", type=Path, default=Path(__file__).resolve().parent / "data") | |
| parser.add_argument( | |
| "--canonical-root", | |
| type=Path, | |
| help="Main benchmark repository root; enables exact paper/results comparisons.", | |
| ) | |
| return parser | |
| def main(argv: Iterable[str] | None = None) -> int: | |
| args = build_parser().parse_args(argv) | |
| frames = validate_local(args.data_dir) | |
| if args.canonical_root is not None: | |
| validate_canonical(args.data_dir, args.canonical_root.resolve()) | |
| print(f"Validated {len(frames)} BEND-BCI Space tables in {args.data_dir}") | |
| if args.canonical_root is not None: | |
| print("Canonical paper/results comparison passed") | |
| return 0 | |
| if __name__ == "__main__": | |
| raise SystemExit(main()) | |