#!/usr/bin/env python # coding: utf-8 """ Group-level permutation inference for searchlight decoding accuracy This script aggregates subject-level permutation results, computes group-level statistics using a permutation-based null distribution, and applies FDR correction separately for each hemisphere. Author: Theo Marins """ # ============================================================================ # Imports # ============================================================================ import os import pickle import numpy as np from statsmodels.stats.multitest import multipletests # ============================================================================ # Configuration # ============================================================================ PERM_DIR = os.path.join(BASE_DIR, "permutations_HA") # or "permutations_AA" or "permutations_ws" STATS_DIR = PERM_DIR / "stats_HA" # or "stats_AA" os.makedirs(STATS_DIR, exist_ok=True) subjects = ["01", "02", "04", "05", "07", "08", "11", "16", "20", "22", "23", "26"] hemispheres = ["l", "r"] # ============================================================================ # Group-level aggregation # ============================================================================ group_results = {} for hemi in hemispheres: print(f"\nProcessing hemisphere: {hemi}") subject_scores = [] subject_perms = [] included_subjects = [] # ------------------------------------------------------------------------ # Load subject-level data # ------------------------------------------------------------------------ for sub in subjects: scores_file = os.path.join(PERM_DIR, f"score--{hemi}--sub{sub}.pkl") perm_file = os.path.join(PERM_DIR, f"perm_scores--{hemi}--sub{sub}.pkl") if not (os.path.exists(scores_file) and os.path.exists(perm_file)): print(f" Skipping sub-{sub} ({hemi}): missing files") continue with open(scores_file, "rb") as f: scores = np.asarray(pickle.load(f), dtype=float) with open(perm_file, "rb") as f: perms = np.asarray(pickle.load(f), dtype=float) if perms.ndim != 2: raise ValueError( f"Permutation scores must be 2D (V × P), got {perms.shape} for sub-{sub}" ) if scores.shape[0] != perms.shape[0]: raise ValueError( f"Vertex mismatch for sub-{sub}: scores={scores.shape[0]}, perms={perms.shape[0]}" ) subject_scores.append(scores) subject_perms.append(perms) included_subjects.append(sub) if len(included_subjects) < 2: raise RuntimeError(f"Not enough subjects for group inference ({hemi})") # ------------------------------------------------------------------------ # Stack arrays # ------------------------------------------------------------------------ X_obs = np.stack(subject_scores, axis=0) # (S, V) X_perm = np.stack(subject_perms, axis=0) # (S, V, P) S, V = X_obs.shape P = X_perm.shape[2] # ------------------------------------------------------------------------ # Group statistic (mean across subjects) # ------------------------------------------------------------------------ mean_obs = X_obs.mean(axis=0) # (V,) mean_null = X_perm.mean(axis=0) # (V, P) # One-sided permutation p-values with +1 correction C = (mean_null >= mean_obs[:, None]).sum(axis=1) pvals = (C + 1) / (P + 1) # Standard error across subjects (for plotting) se_obs = X_obs.std(axis=0, ddof=1) / np.sqrt(S) # ------------------------------------------------------------------------ # FDR correction (Benjamini–Hochberg) # ------------------------------------------------------------------------ reject, pvals_fdr, _, _ = multipletests(pvals, alpha=0.05, method="fdr_bh") masked_accuracy = np.where(reject, mean_obs, 0.0) # ------------------------------------------------------------------------ # Store results # ------------------------------------------------------------------------ group_results[hemi] = { "subjects_used": included_subjects, "mean_accuracy": mean_obs, "se_accuracy": se_obs, "pvals_uncorrected": pvals, "pvals_fdr": pvals_fdr, "reject_fdr": reject, "masked_accuracy_fdr": masked_accuracy, "mean_null": mean_null, # (V, P) } # ============================================================================ # Save results # ============================================================================ with open(STATS_DIR, "wb") as f: pickle.dump(group_results, f)