OSCR

Subject-Wise Depression Screening from Eight-Channel Resting-State EEG Using Asymmetry-Aware Spectral Features and Connectivity Ablation.

Code ↔ Paper

10 matches between paragraphs of the paper and lines of its authors' code, computed by the harvester (lexical-v1). Click a colored paragraph or line to see its counterpart.

The 10 matches
  1. [1] § 2. Materials and Methods › 2.4. Classifiers ↔ EEG_Depression_Analysis_Public_Notebook.ipynb, lines 346–401 · score 0.97 · depthwise spatial convolution, temporal kernel length, pointwise filters, separable convolution, temporal filters, depth multiplier
  2. [2] § 2. Materials and Methods › 2.4. Classifiers ↔ EEG_Depression_Analysis_Public_Notebook.ipynb, lines 628–737 · score 0.78 · MLPClassifier, hidden layers, validation fraction, Extra Trees, activations, baseline
  3. [3] § 2. Materials and Methods › 2.2. Channel Selection and Signal Processing ↔ EEG_Depression_Analysis_Public_Notebook.ipynb, lines 447–530 · score 0.77 · inner training split, training partition, score normalisation, epochs, validation, fitted
  4. [4] § 2. Materials and Methods › 2.5. Evaluation Protocol and Experimental Design ↔ EEG_Depression_Analysis_Public_Notebook.ipynb, lines 875–987 · score 0.71 · fitted Extra Trees, Gini importance, importance ranking, evaluated Extra Trees, dimensional spectral, probed
  5. [5] § 3. Results › 3.3. Spectral Feature-Selection Ablation ↔ EEG_Depression_Analysis_Public_Notebook.ipynb, lines 875–987 · score 0.68 · Gini importance ranking, feature selection ablation, random seed, Extra Trees, row, refitted
  6. [6] § 2. Materials and Methods › 2.4. Classifiers ↔ EEG_Depression_Analysis_Public_Notebook.ipynb, lines 312–343 · score 0.66 · ReLU, adaptive, max, kernel, head, linear
  7. [7] § 3. Results › 3.1. Compact Eight-Channel Baselines ↔ EEG_Depression_Analysis_Public_Notebook.ipynb, lines 1098–1155 · score 0.66 · raw signal deep, EEGNet, ShallowConvNet, balanced accuracy, Extra Trees, spectral feature
  8. [8] § 2. Materials and Methods › 2.3. Feature Extraction › 2.3.2. Connectivity Features and Fusion ↔ EEG_Depression_Analysis_Public_Notebook.ipynb, lines 116–171 · score 0.58 · 12–30 Hz, 8–12 Hz, beta, bands, alpha, connectivity
  9. [9] § 2. Materials and Methods › 2.5. Evaluation Protocol and Experimental Design ↔ EEG_Depression_Analysis_Public_Notebook.ipynb, lines 537–553 · score 0.57 · percentile bootstrap, confidence interval, bounds, resamples
  10. [10] § 2. Materials and Methods › 2.3. Feature Extraction › 2.3.1. Spectral Features ↔ EEG_Depression_Analysis_Public_Notebook.ipynb, lines 116–171 · score 0.56 · 12–30 Hz, 8–12 Hz, beta, bands, alpha, channel

Paper

Loaded from Europe PMC by your browser, not stored by OSCR: doi.org · Europe PMC

The paper is loaded when this pane is shown.

The authors' code

Jupyter notebook · 1,297 lines · 48 KB · no license · 10 matches

  1. # %% [markdown]
  2. # # EEG-Based Depression Screening from 8-Channel Resting-State EEG
  3. # by Hassan Ugail, Newton Howard, Ali Ahmed Elmahmudi and Zied Mnasri
  4. #
  5. # ## Subject-wise evaluation, compact baselines, connectivity ablation, deep-learning baselines
  6. #
  7. # Cite as: H Ugail, N Howard, A A Elmahmudi and Z Mnasri, Subject-Wise Depression Screening from 8-Channel Resting-State EEG using Asymmetry-Aware Spectral Features and Connectivity Ablation, Sensors, To Appear, 2026.
  8. #
  9. # ## What this notebook does
  10. # It loads the processed outputs from the preprocessing notebook and reproduces the final experimental package:
  11. #
  12. # 1. **Correct subject identity handling**
  13. # - uses `subject_key = label + "_" + subject_id`
  14. # - avoids merging `H_S1` and `MDD_S1`
  15. #
  16. # 2. **Experiment 1 — Compact 8-channel baselines**
  17. # - Extra Trees on asymmetry-aware spectral features
  18. # - MLP on the same feature vector
  19. # - compact 1D CNN on raw 8-channel segments
  20. # - **EEGNet** (Lawhern et al., 2018) on raw 8-channel segments
  21. # - **ShallowConvNet** (Schirrmeister et al., 2017) on raw 8-channel segments
  22. #
  23. # 3. **Experiment 2 — Connectivity ablation**
  24. # - Extra Trees on spectral features only
  25. # - Extra Trees on connectivity features only
  26. # - Extra Trees on spectral + connectivity fusion
  27. # - MLP on spectral + connectivity fusion
  28. #
  29. # 4. **Experiment 3 — Spectral feature-selection ablation**
  30. # - Top-$K$ Extra Trees over $K \in \{5, 10, 15, 20, 30, 50, 70, 90\}$ features
  31. # - Tests whether the 90-dimensional spectral vector is over-parameterised
  32. #
  33. # 5. **Bootstrap inference**
  34. # - 95% percentile bootstrap confidence intervals of the mean across the 10 repeats
  35. # - Reported for every metric in every experiment
  36. #
  37. # 6. **Publication-ready outputs**
  38. # - manuscript tables with both SD and 95% CI columns
  39. # - compact results CSV files
  40. # - high-resolution figures with non-overlapping labels and 95% CI error bars
  41. #
  42. # ## Dataset
  43. # This notebook assumes you already generated the processed files from the public dataset:
  44. #
  45. # **Mumtaz, Wajid (2016). _MDD Patients and Healthy Controls EEG Data (New)._ figshare. Dataset.**
  46. # DOI: `10.6084/m9.figshare.4244171.v2`
  47. #
  48. # ## Input assumption
  49. # This notebook expects the processed outputs from the earlier preprocessing stage under:
  50. #
  51. # `/content/drive/MyDrive/.../run_subjectwise_baselines_v1/processed/`
  52. #
  53. # Note: "run_subjectwise_baselines_v1/processed/" is available at:
  54. # https://drive.google.com/drive/folders/1K4J-jqfWtoG7njM21aOgc2Ct7QgYf4W9?usp=sharing
  55. #
  56. # ## Output philosophy
  57. # Only a small set of result files is written so analysis can be done elsewhere if needed.
  58. # %%
  59. !pip install -q pandas numpy scipy scikit-learn matplotlib openpyxl torch
  60. # %%
  61. from google.colab import drive
  62. drive.mount('/content/drive')
  63. # %% [markdown]
  64. # ## Imports and configuration
  65. # %%
  66. import os
  67. import re
  68. import json
  69. import math
  70. import random
  71. import warnings
  72. from pathlib import Path
  73. from itertools import combinations
  74. import numpy as np
  75. import pandas as pd
  76. import matplotlib.pyplot as plt
  77. from scipy.signal import coherence
  78. from sklearn.base import clone
  79. from sklearn.pipeline import Pipeline
  80. from sklearn.impute import SimpleImputer
  81. from sklearn.preprocessing import StandardScaler
  82. from sklearn.model_selection import GroupShuffleSplit
  83. from sklearn.metrics import (
  84. accuracy_score, balanced_accuracy_score, f1_score, matthews_corrcoef,
  85. confusion_matrix, roc_auc_score, precision_score, recall_score
  86. )
  87. from sklearn.ensemble import ExtraTreesClassifier
  88. from sklearn.neural_network import MLPClassifier
  89. import torch
  90. import torch.nn as nn
  91. from torch.utils.data import TensorDataset, DataLoader
  92. warnings.filterwarnings("ignore")
  93. pd.set_option("display.max_columns", 200)
  94. plt.rcParams.update({
  95. "font.size": 11,
  96. "axes.titlesize": 15,
  97. "axes.labelsize": 13,
  98. "xtick.labelsize": 11,
  99. "ytick.labelsize": 11,
  100. "legend.fontsize": 11,
  101. "figure.dpi": 150,
  102. "savefig.dpi": 300,
  103. "axes.spines.top": False,
  104. "axes.spines.right": False,
  105. })
  106. # %%
  107. # =========================
  108. # USER CONFIGURATION
  109. # =========================
  110. BASE_DIR = Path("/content/drive/MyDrive/...")
  111. INPUT_DIR = BASE_DIR / "run_subjectwise_baselines_v1" / "processed"
  112. RUN_DIR = BASE_DIR / "REVISION/run_final_public_notebook_v1"
  113. RUN_DIR.mkdir(parents=True, exist_ok=True)
  114. # Output files
  115. FINAL_REPEAT_METRICS = RUN_DIR / "final_repeat_metrics.csv"
  116. FINAL_SUMMARY = RUN_DIR / "final_summary.csv"
  117. FINAL_SUBJECT_PREDICTIONS = RUN_DIR / "final_subject_predictions.csv"
  118. FINAL_TOP_FEATURES = RUN_DIR / "final_top_features.csv"
  119. FINAL_MANUSCRIPT_TABLE = RUN_DIR / "final_manuscript_table.csv"
  120. FINAL_MANUSCRIPT_TABLE_XLSX = RUN_DIR / "final_manuscript_table.xlsx"
  121. FINAL_FEATURE_SELECTION = RUN_DIR / "final_feature_selection.csv"
  122. FINAL_FEATURE_SELECTION_SUMMARY = RUN_DIR / "final_feature_selection_summary.csv"
  123. FIG_BASELINES = RUN_DIR / "fig_baselines_8ch.png"
  124. FIG_FUSION = RUN_DIR / "fig_fusion_ablation.png"
  125. FIG_IMPORTANCE = RUN_DIR / "fig_top_feature_importance.png"
  126. FIG_FEATURE_SELECTION = RUN_DIR / "fig_feature_selection.png"
  127. MANIFEST_PATH = RUN_DIR / "manifest.json"
  128. RANDOM_SEED = 42
  129. N_REPEATS = 10
  130. TEST_SIZE_SUBJECT_FRACTION = 0.20
  131. VAL_SIZE_FROM_TRAIN_SUBJECTS = 0.20
  132. BATCH_SIZE = 64
  133. CNN_EPOCHS = 25
  134. CNN_LR = 1e-3
  135. EARLY_STOPPING_PATIENCE = 5
  136. DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
  137. FULL_CHANNELS = [
  138. "Fp1", "Fp2", "F7", "F3", "Fz", "F4", "F8",
  139. "T7", "C3", "Cz", "C4", "T8",
  140. "P7", "P3", "Pz", "P4", "P8", "O1", "O2"
  141. ]
  142. CHANNELS_8CH = ["Fp1", "Fp2", "F3", "F4", "C3", "C4", "P3", "P4"]
  143. CONNECTIVITY_BANDS = {
  144. "alpha": (8.0, 12.0),
  145. "beta": (12.0, 30.0),
  146. }
  147. # Feature-selection ablation grid (top-K from Gini importance)
  148. K_GRID = [5, 10, 15, 20, 30, 50, 70, 90]
  149. # Bootstrap configuration for 95% CIs of the mean across repeats
  150. BOOTSTRAP_N = 10000
  151. BOOTSTRAP_CI = 95
  152. BOOTSTRAP_SEED = 42
  153. # %% [markdown]
  154. # ## Helper functions
  155. # %%
  156. def set_all_seeds(seed: int):
  157. random.seed(seed)
  158. np.random.seed(seed)
  159. torch.manual_seed(seed)
  160. if torch.cuda.is_available():
  161. torch.cuda.manual_seed_all(seed)
  162. def specificity_from_cm(cm: np.ndarray) -> float:
  163. if cm.shape != (2, 2):
  164. return np.nan
  165. tn, fp, fn, tp = cm.ravel()
  166. return tn / (tn + fp) if (tn + fp) > 0 else np.nan
  167. def safe_auc(y_true, y_prob) -> float:
  168. try:
  169. if len(np.unique(y_true)) < 2:
  170. return np.nan
  171. return roc_auc_score(y_true, y_prob)
  172. except Exception:
  173. return np.nan
  174. def compute_metrics(y_true, y_pred, y_prob) -> dict:
  175. cm = confusion_matrix(y_true, y_pred, labels=[0, 1])
  176. sens = recall_score(y_true, y_pred, zero_division=0)
  177. spec = specificity_from_cm(cm)
  178. prec = precision_score(y_true, y_pred, zero_division=0)
  179. return {
  180. "accuracy": accuracy_score(y_true, y_pred),
  181. "balanced_accuracy": balanced_accuracy_score(y_true, y_pred),
  182. "precision": prec,
  183. "recall_sensitivity": sens,
  184. "specificity": spec,
  185. "f1": f1_score(y_true, y_pred, zero_division=0),
  186. "mcc": matthews_corrcoef(y_true, y_pred) if len(np.unique(y_true)) > 1 and len(np.unique(y_pred)) > 1 else np.nan,
  187. "auroc": safe_auc(y_true, y_prob),
  188. "tn": int(cm[0, 0]),
  189. "fp": int(cm[0, 1]),
  190. "fn": int(cm[1, 0]),
  191. "tp": int(cm[1, 1]),
  192. }
  193. def aggregate_subject_predictions(df_pred: pd.DataFrame, subject_col: str = "subject_key") -> pd.DataFrame:
  194. agg = df_pred.groupby(subject_col).agg(
  195. y_true=("y_true", "first"),
  196. prob=("prob", "mean"),
  197. n_segments=("prob", "size"),
  198. label=("label", "first"),
  199. condition=("condition", lambda s: "|".join(sorted(pd.unique(s.astype(str))))),
  200. subject_id=("subject_id", "first"),
  201. ).reset_index()
  202. agg["y_pred"] = (agg["prob"] >= 0.5).astype(int)
  203. return agg
  204. def stratified_subject_holdout(subject_df: pd.DataFrame, test_fraction=0.2, seed=42):
  205. rng = np.random.RandomState(seed)
  206. train_subjects, test_subjects = [], []
  207. for label_value in sorted(subject_df["y"].unique()):
  208. sub = subject_df[subject_df["y"] == label_value]["subject_key"].tolist()
  209. sub = list(sub)
  210. rng.shuffle(sub)
  211. n_test = max(1, int(round(len(sub) * test_fraction)))
  212. test_subjects.extend(sub[:n_test])
  213. train_subjects.extend(sub[n_test:])
  214. return sorted(train_subjects), sorted(test_subjects)
  215. def make_subject_label_table(df: pd.DataFrame) -> pd.DataFrame:
  216. subj = df.groupby("subject_key").agg(
  217. y=("label", lambda s: 1 if s.iloc[0] == "MDD" else 0),
  218. label=("label", "first"),
  219. subject_id=("subject_id", "first")
  220. ).reset_index()
  221. return subj
  222. def get_spectral_feature_columns(feature_table: pd.DataFrame, selected_channels: list) -> list:
  223. meta_cols = {"file_name", "subject_id", "subject_key", "label", "condition"}
  224. cols = [c for c in feature_table.columns if c not in meta_cols]
  225. base_cols = []
  226. asym_cols = []
  227. for c in cols:
  228. if "__ASYM_" in c:
  229. asym_cols.append(c)
  230. continue
  231. parts = c.split("__")
  232. if len(parts) != 2:
  233. continue
  234. prefix, ch = parts
  235. if ch not in selected_channels:
  236. continue
  237. if prefix.endswith("_logabs") or prefix.endswith("_rel"):
  238. base_cols.append(c)
  239. keep_asym = []
  240. for c in asym_cols:
  241. m = re.search(r"__ASYM_([A-Za-z0-9]+)_([A-Za-z0-9]+)$", c)
  242. if m and m.group(1) in selected_channels and m.group(2) in selected_channels:
  243. keep_asym.append(c)
  244. return sorted(base_cols + keep_asym)
  245. def build_connectivity_features(X_raw_8ch, sfreq, channel_names, bands):
  246. edge_pairs = list(combinations(range(len(channel_names)), 2))
  247. edge_names = []
  248. rows = []
  249. for i, j in edge_pairs:
  250. for band_name in bands.keys():
  251. edge_names.append(f"coh_{band_name}__{channel_names[i]}__{channel_names[j]}")
  252. for seg in X_raw_8ch:
  253. feats = []
  254. for i, j in edge_pairs:
  255. x = seg[i]
  256. y = seg[j]
  257. f, cxy = coherence(x, y, fs=sfreq, nperseg=min(len(x), 512))
  258. for band_name, (fmin, fmax) in bands.items():
  259. mask = (f >= fmin) & (f < fmax)
  260. val = float(np.mean(cxy[mask])) if np.any(mask) else 0.0
  261. feats.append(val)
  262. rows.append(feats)
  263. return pd.DataFrame(rows, columns=edge_names)
  264. def fit_predict_prob(model, X_train, y_train, X_test):
  265. fitted = clone(model)
  266. fitted.fit(X_train, y_train)
  267. if hasattr(fitted, "predict_proba"):
  268. prob = fitted.predict_proba(X_test)[:, 1]
  269. else:
  270. score = fitted.decision_function(X_test)
  271. prob = 1 / (1 + np.exp(-score))
  272. pred = (prob >= 0.5).astype(int)
  273. return fitted, prob, pred
  274. # ---------------------------------------------------------------------
  275. # Deep-learning baselines: Compact 1D CNN, EEGNet, ShallowConvNet
  276. # ---------------------------------------------------------------------
  277. class CompactEEGCNN(nn.Module):
  278. """
  279. Compact 1D CNN baseline operating directly on (B, C, T) raw segments.
  280. """
  281. def __init__(self, n_channels: int, n_samples: int):
  282. super().__init__()
  283. self.net = nn.Sequential(
  284. nn.Conv1d(n_channels, 32, kernel_size=7, padding=3),
  285. nn.BatchNorm1d(32),
  286. nn.ReLU(),
  287. nn.MaxPool1d(2),
  288. nn.Conv1d(32, 64, kernel_size=5, padding=2),
  289. nn.BatchNorm1d(64),
  290. nn.ReLU(),
  291. nn.MaxPool1d(2),
  292. nn.Conv1d(64, 128, kernel_size=5, padding=2),
  293. nn.BatchNorm1d(128),
  294. nn.ReLU(),
  295. nn.AdaptiveAvgPool1d(1)
  296. )
  297. self.head = nn.Sequential(
  298. nn.Flatten(),
  299. nn.Dropout(0.3),
  300. nn.Linear(128, 64),
  301. nn.ReLU(),
  302. nn.Dropout(0.3),
  303. nn.Linear(64, 1)
  304. )
  305. def forward(self, x):
  306. x = self.net(x)
  307. x = self.head(x)
  308. return x.squeeze(1)
  309. class EEGNet(nn.Module):
  310. """
  311. EEGNet (Lawhern et al., 2018). Compact 8-channel adaptation in PyTorch.
  312. Inputs are shaped (B, C, T) and lifted to (B, 1, C, T) internally.
  313. Notes
  314. -----
  315. F1 is the number of temporal filters in Block 1, D is the depth multiplier
  316. for the depthwise spatial convolution, and F2 is the number of pointwise
  317. filters in the separable convolution of Block 2. The temporal kernel
  318. length defaults to half the sampling rate.
  319. """
  320. def __init__(self, n_channels, n_samples, sfreq=256.0,
  321. F1=8, D=2, F2=16, dropout=0.5, kern_length=None):
  322. super().__init__()
  323. if kern_length is None:
  324. kern_length = max(8, int(sfreq // 2))
  325. # Block 1: temporal convolution
  326. self.block1 = nn.Sequential(
  327. nn.Conv2d(1, F1, (1, kern_length),
  328. padding=(0, kern_length // 2), bias=False),
  329. nn.BatchNorm2d(F1),
  330. )
  331. # Block 1 cont: depthwise spatial convolution across channels
  332. self.depthwise = nn.Sequential(
  333. nn.Conv2d(F1, F1 * D, (n_channels, 1), groups=F1, bias=False),
  334. nn.BatchNorm2d(F1 * D),
  335. nn.ELU(),
  336. nn.AvgPool2d((1, 4)),
  337. nn.Dropout(dropout),
  338. )
  339. # Block 2: separable convolution (depthwise + pointwise)
  340. self.separable = nn.Sequential(
  341. nn.Conv2d(F1 * D, F1 * D, (1, 16),
  342. padding=(0, 8), groups=F1 * D, bias=False),
  343. nn.Conv2d(F1 * D, F2, (1, 1), bias=False),
  344. nn.BatchNorm2d(F2),
  345. nn.ELU(),
  346. nn.AvgPool2d((1, 8)),
  347. nn.Dropout(dropout),
  348. )
  349. with torch.no_grad():
  350. x = torch.zeros(1, 1, n_channels, n_samples)
  351. x = self.block1(x); x = self.depthwise(x); x = self.separable(x)
  352. n_flat = x.numel()
  353. self.head = nn.Linear(n_flat, 1)
  354. def forward(self, x):
  355. x = x.unsqueeze(1)
  356. x = self.block1(x)
  357. x = self.depthwise(x)
  358. x = self.separable(x)
  359. x = x.flatten(start_dim=1)
  360. x = self.head(x)
  361. return x.squeeze(1)
  362. class ShallowConvNet(nn.Module):
  363. """
  364. ShallowConvNet (Schirrmeister et al., 2017).
  365. Temporal convolution, spatial convolution across channels, square
  366. activation, mean pooling, log activation, dropout, and a linear head.
  367. Inputs are shaped (B, C, T) and lifted to (B, 1, C, T) internally.
  368. """
  369. def __init__(self, n_channels, n_samples,
  370. n_filters_time=40, filter_time_length=25,
  371. n_filters_spat=40, pool_time_length=75, pool_time_stride=15,
  372. dropout=0.5):
  373. super().__init__()
  374. self.temporal = nn.Conv2d(1, n_filters_time, (1, filter_time_length), bias=False)
  375. self.spatial = nn.Conv2d(n_filters_time, n_filters_spat, (n_channels, 1), bias=False)
  376. self.bn = nn.BatchNorm2d(n_filters_spat)
  377. self.pool = nn.AvgPool2d((1, pool_time_length), stride=(1, pool_time_stride))
  378. self.dropout = nn.Dropout(dropout)
  379. with torch.no_grad():
  380. x = torch.zeros(1, 1, n_channels, n_samples)
  381. x = self.temporal(x)
  382. x = self.spatial(x)
  383. x = self.bn(x)
  384. x = x * x
  385. x = self.pool(x)
  386. x = torch.log(torch.clamp(x, min=1e-7, max=1e7))
  387. n_flat = x.numel()
  388. self.head = nn.Linear(n_flat, 1)
  389. def forward(self, x):
  390. x = x.unsqueeze(1)
  391. x = self.temporal(x)
  392. x = self.spatial(x)
  393. x = self.bn(x)
  394. x = x * x
  395. x = self.pool(x)
  396. x = torch.log(torch.clamp(x, min=1e-7, max=1e7))
  397. x = self.dropout(x)
  398. x = x.flatten(start_dim=1)
  399. x = self.head(x)
  400. return x.squeeze(1)
  401. def fit_predict_torch_model(
  402. model_factory, X_train, y_train, groups_train, X_test, seed,
  403. val_fraction=0.2, batch_size=64, epochs=25, lr=1e-3, patience=5, device="cpu"
  404. ):
  405. """
  406. Generic subject-wise trainer for any PyTorch (B, C, T) -> logit model.
  407. model_factory(n_channels, n_samples) -> nn.Module returning a single
  408. pre-sigmoid output per segment. Validation subjects are drawn from the
  409. training partition with GroupShuffleSplit so that no subject appears in
  410. both training and validation. Per-channel z-score normalisation
  411. statistics are computed on the inner training split only.
  412. """
  413. set_all_seeds(seed)
  414. train_meta = pd.DataFrame({"subject_key": groups_train, "y": y_train}).drop_duplicates()
  415. gss = GroupShuffleSplit(n_splits=1, test_size=val_fraction, random_state=seed)
  416. tr_sub_idx, va_sub_idx = next(gss.split(train_meta[["y"]], train_meta["y"], groups=train_meta["subject_key"]))
  417. tr_subjects = set(train_meta.iloc[tr_sub_idx]["subject_key"])
  418. va_subjects = set(train_meta.iloc[va_sub_idx]["subject_key"])
  419. tr_mask = np.array([g in tr_subjects for g in groups_train])
  420. va_mask = np.array([g in va_subjects for g in groups_train])
  421. X_tr = X_train[tr_mask]
  422. y_tr = y_train[tr_mask]
  423. X_va = X_train[va_mask]
  424. y_va = y_train[va_mask]
  425. mean = X_tr.mean(axis=(0, 2), keepdims=True)
  426. std = X_tr.std(axis=(0, 2), keepdims=True) + 1e-6
  427. X_tr = (X_tr - mean) / std
  428. X_va = (X_va - mean) / std
  429. X_te = (X_test - mean) / std
  430. tr_ds = TensorDataset(torch.tensor(X_tr, dtype=torch.float32), torch.tensor(y_tr, dtype=torch.float32))
  431. va_ds = TensorDataset(torch.tensor(X_va, dtype=torch.float32), torch.tensor(y_va, dtype=torch.float32))
  432. te_x = torch.tensor(X_te, dtype=torch.float32)
  433. tr_loader = DataLoader(tr_ds, batch_size=batch_size, shuffle=True)
  434. va_loader = DataLoader(va_ds, batch_size=batch_size, shuffle=False)
  435. n_channels = X_train.shape[1]
  436. n_samples = X_train.shape[2]
  437. model = model_factory(n_channels, n_samples).to(device)
  438. optimizer = torch.optim.Adam(model.parameters(), lr=lr)
  439. criterion = nn.BCEWithLogitsLoss()
  440. best_state = None
  441. best_val = np.inf
  442. bad_epochs = 0
  443. for _ in range(epochs):
  444. model.train()
  445. for xb, yb in tr_loader:
  446. xb = xb.to(device); yb = yb.to(device)
  447. optimizer.zero_grad()
  448. logits = model(xb)
  449. loss = criterion(logits, yb)
  450. loss.backward()
  451. optimizer.step()
  452. model.eval()
  453. val_losses = []
  454. with torch.no_grad():
  455. for xb, yb in va_loader:
  456. xb = xb.to(device); yb = yb.to(device)
  457. logits = model(xb)
  458. val_losses.append(criterion(logits, yb).item())
  459. val_loss = float(np.mean(val_losses)) if val_losses else np.inf
  460. if val_loss < best_val:
  461. best_val = val_loss
  462. best_state = {k: v.cpu().clone() for k, v in model.state_dict().items()}
  463. bad_epochs = 0
  464. else:
  465. bad_epochs += 1
  466. if bad_epochs >= patience:
  467. break
  468. if best_state is not None:
  469. model.load_state_dict(best_state)
  470. model.eval()
  471. with torch.no_grad():
  472. logits = model(te_x.to(device)).cpu().numpy()
  473. prob = 1 / (1 + np.exp(-logits))
  474. pred = (prob >= 0.5).astype(int)
  475. return prob, pred
  476. # ---------------------------------------------------------------------
  477. # Bootstrap CI helper
  478. # ---------------------------------------------------------------------
  479. def bootstrap_ci_mean(values, n_bootstrap=10000, ci=95, seed=42):
  480. """
  481. Percentile bootstrap confidence interval of the mean.
  482. Returns (mean, lower, upper). NaNs are dropped before resampling. If
  483. fewer than two finite values remain, the bounds are returned as NaN.
  484. """
  485. values = np.asarray(values, dtype=float)
  486. values = values[~np.isnan(values)]
  487. if len(values) < 2:
  488. m = float(np.mean(values)) if len(values) else float("nan")
  489. return m, float("nan"), float("nan")
  490. rng = np.random.RandomState(seed)
  491. n = len(values)
  492. boots = rng.choice(values, size=(n_bootstrap, n), replace=True).mean(axis=1)
  493. alpha = (100 - ci) / 2.0
  494. return float(np.mean(values)), float(np.percentile(boots, alpha)), float(np.percentile(boots, 100 - alpha))
  495. def make_public_label(model_name: str) -> str:
  496. mapping = {
  497. "extratrees_8ch_features": "Extra Trees (features)",
  498. "mlp_8ch_features": "MLP (features)",
  499. "cnn1d_8ch_raw": "1D CNN (raw 8ch)",
  500. "eegnet_8ch_raw": "EEGNet (raw 8ch)",
  501. "shallowconvnet_8ch_raw": "ShallowConvNet (raw 8ch)",
  502. "extratrees_spectral": "ET (spectral)",
  503. "extratrees_connectivity": "ET (connectivity)",
  504. "extratrees_fusion": "ET (fusion)",
  505. "mlp_fusion": "MLP (fusion)",
  506. }
  507. return mapping.get(model_name, model_name)
  508. # %% [markdown]
  509. # ## Load processed data and create the corrected subject key
  510. # %%
  511. feature_table = pd.read_csv(INPUT_DIR / "feature_table.csv")
  512. X_segments = np.load(INPUT_DIR / "X_segments.npy")
  513. y_segments = np.load(INPUT_DIR / "y.npy")
  514. groups_segments = np.load(INPUT_DIR / "groups.npy", allow_pickle=True)
  515. conditions_segments = np.load(INPUT_DIR / "conditions.npy", allow_pickle=True)
  516. feature_table["subject_id"] = feature_table["subject_id"].astype(str)
  517. feature_table["label"] = feature_table["label"].astype(str)
  518. feature_table["condition"] = feature_table["condition"].astype(str)
  519. feature_table["subject_key"] = feature_table["label"] + "_" + feature_table["subject_id"]
  520. group_subject_ids = pd.Series(groups_segments.astype(str))
  521. group_labels = pd.Series(np.where(y_segments == 1, "MDD", "H"))
  522. group_subject_keys = (group_labels + "_" + group_subject_ids).values
  523. subject_df = make_subject_label_table(feature_table)
  524. print("feature_table:", feature_table.shape)
  525. print("X_segments:", X_segments.shape)
  526. print("Unique subject_key count:", len(subject_df))
  527. print("Subjects by class:")
  528. print(subject_df["label"].value_counts())
  529. # %% [markdown]
  530. # ## Prepare feature matrices and raw 8-channel tensor
  531. # %%
  532. channel_idx = [FULL_CHANNELS.index(ch) for ch in CHANNELS_8CH]
  533. X_raw_8ch = X_segments[:, channel_idx, :]
  534. sfreq = 256.0
  535. spectral_cols = get_spectral_feature_columns(feature_table, CHANNELS_8CH)
  536. X_spectral = feature_table[spectral_cols].copy()
  537. X_connectivity = build_connectivity_features(
  538. X_raw_8ch=X_raw_8ch,
  539. sfreq=sfreq,
  540. channel_names=CHANNELS_8CH,
  541. bands=CONNECTIVITY_BANDS
  542. )
  543. X_fusion = pd.concat([X_spectral.reset_index(drop=True), X_connectivity.reset_index(drop=True)], axis=1)
  544. y_all = feature_table["label"].map({"H": 0, "MDD": 1}).astype(int).values
  545. subject_keys_all = feature_table["subject_key"].astype(str).values
  546. subject_ids_all = feature_table["subject_id"].astype(str).values
  547. conditions_all = feature_table["condition"].astype(str).values
  548. print("Spectral features:", X_spectral.shape)
  549. print("Connectivity features:", X_connectivity.shape)
  550. print("Fusion features:", X_fusion.shape)
  551. print("Raw 8ch tensor:", X_raw_8ch.shape)
  552. # %% [markdown]
  553. # ## Define the experimental models
  554. # %%
  555. compact_models = {
  556. "extratrees_8ch_features": Pipeline([
  557. ("imputer", SimpleImputer(strategy="median")),
  558. ("clf", ExtraTreesClassifier(
  559. n_estimators=300,
  560. class_weight="balanced",
  561. random_state=RANDOM_SEED,
  562. n_jobs=-1
  563. ))
  564. ]),
  565. "mlp_8ch_features": Pipeline([
  566. ("imputer", SimpleImputer(strategy="median")),
  567. ("scaler", StandardScaler()),
  568. ("clf", MLPClassifier(
  569. hidden_layer_sizes=(128, 64),
  570. activation="relu",
  571. alpha=1e-4,
  572. batch_size=64,
  573. learning_rate_init=1e-3,
  574. max_iter=200,
  575. early_stopping=True,
  576. validation_fraction=0.15,
  577. random_state=RANDOM_SEED
  578. ))
  579. ]),
  580. }
  581. # Deep raw-signal baselines: each entry maps a model id to a factory taking
  582. # (n_channels, n_samples) and returning an nn.Module. All are trained with
  583. # fit_predict_torch_model under the same subject-wise inner validation split.
  584. deep_models = {
  585. "cnn1d_8ch_raw": (
  586. lambda n_channels, n_samples: CompactEEGCNN(n_channels, n_samples)
  587. ),
  588. "eegnet_8ch_raw": (
  589. lambda n_channels, n_samples: EEGNet(
  590. n_channels=n_channels, n_samples=n_samples, sfreq=sfreq,
  591. F1=8, D=2, F2=16, dropout=0.5
  592. )
  593. ),
  594. "shallowconvnet_8ch_raw": (
  595. lambda n_channels, n_samples: ShallowConvNet(
  596. n_channels=n_channels, n_samples=n_samples,
  597. n_filters_time=40, filter_time_length=25,
  598. n_filters_spat=40, pool_time_length=75, pool_time_stride=15,
  599. dropout=0.5
  600. )
  601. ),
  602. }
  603. fusion_models = {
  604. "extratrees_spectral": {
  605. "X": X_spectral,
  606. "model": Pipeline([
  607. ("imputer", SimpleImputer(strategy="median")),
  608. ("clf", ExtraTreesClassifier(
  609. n_estimators=300,
  610. class_weight="balanced",
  611. random_state=RANDOM_SEED,
  612. n_jobs=-1
  613. ))
  614. ])
  615. },
  616. "extratrees_connectivity": {
  617. "X": X_connectivity,
  618. "model": Pipeline([
  619. ("imputer", SimpleImputer(strategy="median")),
  620. ("clf", ExtraTreesClassifier(
  621. n_estimators=300,
  622. class_weight="balanced",
  623. random_state=RANDOM_SEED,
  624. n_jobs=-1
  625. ))
  626. ])
  627. },
  628. "extratrees_fusion": {
  629. "X": X_fusion,
  630. "model": Pipeline([
  631. ("imputer", SimpleImputer(strategy="median")),
  632. ("clf", ExtraTreesClassifier(
  633. n_estimators=300,
  634. class_weight="balanced",
  635. random_state=RANDOM_SEED,
  636. n_jobs=-1
  637. ))
  638. ])
  639. },
  640. "mlp_fusion": {
  641. "X": X_fusion,
  642. "model": Pipeline([
  643. ("imputer", SimpleImputer(strategy="median")),
  644. ("scaler", StandardScaler()),
  645. ("clf", MLPClassifier(
  646. hidden_layer_sizes=(128, 64),
  647. activation="relu",
  648. alpha=1e-4,
  649. batch_size=64,
  650. learning_rate_init=1e-3,
  651. max_iter=200,
  652. early_stopping=True,
  653. validation_fraction=0.15,
  654. random_state=RANDOM_SEED
  655. ))
  656. ])
  657. },
  658. }
  659. # %% [markdown]
  660. # ## Run all experiments under repeated subject-wise holdout
  661. # %%
  662. all_metric_rows = []
  663. all_subject_pred_rows = []
  664. feature_importance_rows = []
  665. for repeat_idx in range(N_REPEATS):
  666. seed = RANDOM_SEED + repeat_idx
  667. set_all_seeds(seed)
  668. train_subjects, test_subjects = stratified_subject_holdout(
  669. subject_df,
  670. test_fraction=TEST_SIZE_SUBJECT_FRACTION,
  671. seed=seed
  672. )
  673. train_mask = np.isin(subject_keys_all, train_subjects)
  674. test_mask = np.isin(subject_keys_all, test_subjects)
  675. y_train = y_all[train_mask]
  676. y_test = y_all[test_mask]
  677. g_train = subject_keys_all[train_mask]
  678. g_test = subject_keys_all[test_mask]
  679. sid_test = subject_ids_all[test_mask]
  680. cond_test = conditions_all[test_mask]
  681. label_test = np.where(y_test == 1, "MDD", "H")
  682. print(f"===== Repeat {repeat_idx+1}/{N_REPEATS} | seed={seed} =====")
  683. tmp = pd.DataFrame({"subject_key": train_subjects + test_subjects, "split": ["train"]*len(train_subjects)+["test"]*len(test_subjects)})
  684. tmp = tmp.merge(subject_df[["subject_key", "label"]], on="subject_key", how="left")
  685. print(tmp.groupby(["split", "label"]).size())
  686. # -----------------------------------------------------------------
  687. # Experiment 1a: tabular feature-based compact models
  688. # -----------------------------------------------------------------
  689. Xf_train = X_spectral.loc[train_mask].reset_index(drop=True)
  690. Xf_test = X_spectral.loc[test_mask].reset_index(drop=True)
  691. for model_name, model in compact_models.items():
  692. fitted, prob, pred = fit_predict_prob(model, Xf_train, y_train, Xf_test)
  693. seg_df = pd.DataFrame({
  694. "repeat": repeat_idx, "seed": seed, "model": model_name,
  695. "subject_key": g_test, "subject_id": sid_test, "label": label_test,
  696. "condition": cond_test, "y_true": y_test, "prob": prob, "y_pred": pred,
  697. })
  698. subj_df = aggregate_subject_predictions(seg_df, subject_col="subject_key")
  699. subj_df["repeat"] = repeat_idx
  700. subj_df["seed"] = seed
  701. subj_df["model"] = model_name
  702. all_subject_pred_rows.append(subj_df)
  703. metrics = compute_metrics(subj_df["y_true"].values, subj_df["y_pred"].values, subj_df["prob"].values)
  704. metrics.update({"repeat": repeat_idx, "seed": seed, "model": model_name, "experiment": "compact_baselines", "n_subjects": len(subj_df), "n_segments": len(seg_df)})
  705. all_metric_rows.append(metrics)
  706. if model_name == "extratrees_8ch_features":
  707. clf = fitted.named_steps["clf"]
  708. feat_names = list(Xf_train.columns)
  709. importances = clf.feature_importances_
  710. top_idx = np.argsort(importances)[::-1][:15]
  711. for rank, idx in enumerate(top_idx, start=1):
  712. feature_importance_rows.append({
  713. "repeat": repeat_idx, "model": model_name, "rank": rank,
  714. "feature": feat_names[idx], "importance": float(importances[idx]),
  715. "family": "spectral",
  716. })
  717. # -----------------------------------------------------------------
  718. # Experiment 1b: deep raw-signal baselines (1D CNN, EEGNet, ShallowConvNet)
  719. # -----------------------------------------------------------------
  720. Xr_train = X_raw_8ch[train_mask]
  721. Xr_test = X_raw_8ch[test_mask]
  722. for model_name, factory in deep_models.items():
  723. prob_dl, pred_dl = fit_predict_torch_model(
  724. model_factory=factory,
  725. X_train=Xr_train, y_train=y_train, groups_train=g_train, X_test=Xr_test,
  726. seed=seed, val_fraction=VAL_SIZE_FROM_TRAIN_SUBJECTS, batch_size=BATCH_SIZE,
  727. epochs=CNN_EPOCHS, lr=CNN_LR, patience=EARLY_STOPPING_PATIENCE, device=DEVICE,
  728. )
  729. seg_df = pd.DataFrame({
  730. "repeat": repeat_idx, "seed": seed, "model": model_name,
  731. "subject_key": g_test, "subject_id": sid_test, "label": label_test,
  732. "condition": cond_test, "y_true": y_test, "prob": prob_dl, "y_pred": pred_dl,
  733. })
  734. subj_df = aggregate_subject_predictions(seg_df, subject_col="subject_key")
  735. subj_df["repeat"] = repeat_idx
  736. subj_df["seed"] = seed
  737. subj_df["model"] = model_name
  738. all_subject_pred_rows.append(subj_df)
  739. metrics = compute_metrics(subj_df["y_true"].values, subj_df["y_pred"].values, subj_df["prob"].values)
  740. metrics.update({"repeat": repeat_idx, "seed": seed, "model": model_name, "experiment": "compact_baselines", "n_subjects": len(subj_df), "n_segments": len(seg_df)})
  741. all_metric_rows.append(metrics)
  742. # -----------------------------------------------------------------
  743. # Experiment 2: connectivity ablation (unchanged)
  744. # -----------------------------------------------------------------
  745. for model_name, spec in fusion_models.items():
  746. X_df = spec["X"]
  747. model = spec["model"]
  748. X_train_df = X_df.loc[train_mask].reset_index(drop=True)
  749. X_test_df = X_df.loc[test_mask].reset_index(drop=True)
  750. fitted, prob, pred = fit_predict_prob(model, X_train_df, y_train, X_test_df)
  751. seg_df = pd.DataFrame({
  752. "repeat": repeat_idx, "seed": seed, "model": model_name,
  753. "subject_key": g_test, "subject_id": sid_test, "label": label_test,
  754. "condition": cond_test, "y_true": y_test, "prob": prob, "y_pred": pred,
  755. })
  756. subj_df = aggregate_subject_predictions(seg_df, subject_col="subject_key")
  757. subj_df["repeat"] = repeat_idx
  758. subj_df["seed"] = seed
  759. subj_df["model"] = model_name
  760. all_subject_pred_rows.append(subj_df)
  761. metrics = compute_metrics(subj_df["y_true"].values, subj_df["y_pred"].values, subj_df["prob"].values)
  762. metrics.update({"repeat": repeat_idx, "seed": seed, "model": model_name, "experiment": "fusion_ablation", "n_subjects": len(subj_df), "n_segments": len(seg_df)})
  763. all_metric_rows.append(metrics)
  764. if model_name.startswith("extratrees"):
  765. clf = fitted.named_steps["clf"]
  766. feat_names = list(X_df.columns)
  767. importances = clf.feature_importances_
  768. top_idx = np.argsort(importances)[::-1][:15]
  769. family = "spectral" if model_name == "extratrees_spectral" else ("connectivity" if model_name == "extratrees_connectivity" else "fusion")
  770. for rank, idx in enumerate(top_idx, start=1):
  771. feature_importance_rows.append({
  772. "repeat": repeat_idx, "model": model_name, "rank": rank,
  773. "feature": feat_names[idx], "importance": float(importances[idx]),
  774. "family": family,
  775. })
  776. # %% [markdown]
  777. # ## Experiment 3 — Spectral feature-selection ablation
  778. #
  779. # To test for potential overfitting from a 90-dimensional spectral feature set, we run a top-$K$ Gini-importance ablation. For each repeat we fit Extra Trees on the full 90-feature set, rank the columns by importance, then refit and re-evaluate Extra Trees on the top $K \in \{5, 10, 15, 20, 30, 50, 70, 90\}$ features under the same subject-wise test split. Subject-level metrics are reported as mean and 95% bootstrap CI across the 10 repeats.
  780. # %%
  781. fs_metric_rows = []
  782. for repeat_idx in range(N_REPEATS):
  783. seed = RANDOM_SEED + repeat_idx
  784. set_all_seeds(seed)
  785. train_subjects, test_subjects = stratified_subject_holdout(
  786. subject_df, test_fraction=TEST_SIZE_SUBJECT_FRACTION, seed=seed
  787. )
  788. train_mask = np.isin(subject_keys_all, train_subjects)
  789. test_mask = np.isin(subject_keys_all, test_subjects)
  790. y_train = y_all[train_mask]
  791. y_test = y_all[test_mask]
  792. g_test = subject_keys_all[test_mask]
  793. sid_test = subject_ids_all[test_mask]
  794. cond_test = conditions_all[test_mask]
  795. label_test = np.where(y_test == 1, "MDD", "H")
  796. Xf_train = X_spectral.loc[train_mask].reset_index(drop=True)
  797. Xf_test = X_spectral.loc[test_mask].reset_index(drop=True)
  798. # Fit on the full feature set to obtain Gini-importance ranking
  799. full_pipe = Pipeline([
  800. ("imputer", SimpleImputer(strategy="median")),
  801. ("clf", ExtraTreesClassifier(
  802. n_estimators=300, class_weight="balanced",
  803. random_state=seed, n_jobs=-1
  804. ))
  805. ])
  806. full_pipe.fit(Xf_train, y_train)
  807. importances = full_pipe.named_steps["clf"].feature_importances_
  808. feature_names = list(Xf_train.columns)
  809. n_total = len(feature_names)
  810. sorted_idx = np.argsort(importances)[::-1]
  811. for K in K_GRID:
  812. k_use = min(K, n_total)
  813. keep_idx = sorted_idx[:k_use]
  814. keep_cols = [feature_names[i] for i in keep_idx]
  815. Xs_train = Xf_train[keep_cols]
  816. Xs_test = Xf_test[keep_cols]
  817. sub_pipe = Pipeline([
  818. ("imputer", SimpleImputer(strategy="median")),
  819. ("clf", ExtraTreesClassifier(
  820. n_estimators=300, class_weight="balanced",
  821. random_state=seed, n_jobs=-1
  822. ))
  823. ])
  824. _, prob, pred = fit_predict_prob(sub_pipe, Xs_train, y_train, Xs_test)
  825. seg_df = pd.DataFrame({
  826. "repeat": repeat_idx, "seed": seed,
  827. "model": f"extratrees_topk_{K}",
  828. "subject_key": g_test, "subject_id": sid_test,
  829. "label": label_test, "condition": cond_test,
  830. "y_true": y_test, "prob": prob, "y_pred": pred,
  831. })
  832. subj_df = aggregate_subject_predictions(seg_df, subject_col="subject_key")
  833. m = compute_metrics(subj_df["y_true"].values, subj_df["y_pred"].values, subj_df["prob"].values)
  834. m.update({
  835. "repeat": repeat_idx, "seed": seed,
  836. "K": K, "n_features_used": k_use,
  837. "experiment": "feature_selection",
  838. "model": f"extratrees_topk_{K}",
  839. "n_subjects": len(subj_df), "n_segments": len(seg_df),
  840. })
  841. fs_metric_rows.append(m)
  842. print(f"FS ablation repeat {repeat_idx+1}/{N_REPEATS} done")
  843. fs_metrics_df = pd.DataFrame(fs_metric_rows)
  844. fs_metrics_df.to_csv(FINAL_FEATURE_SELECTION, index=False)
  845. # Per-K summary with bootstrap 95% CI of the mean
  846. fs_summary_rows = []
  847. for K, grp in fs_metrics_df.groupby("K"):
  848. rec = {"K": int(K), "n_features_used": int(grp["n_features_used"].iloc[0])}
  849. for raw_name, out_name in [
  850. ("balanced_accuracy", "balanced_accuracy"),
  851. ("auroc", "auroc"),
  852. ("recall_sensitivity", "sensitivity"),
  853. ("specificity", "specificity"),
  854. ("precision", "precision"),
  855. ("f1", "f1"),
  856. ("mcc", "mcc"),
  857. ]:
  858. vals = grp[raw_name].values
  859. m, lo, hi = bootstrap_ci_mean(vals, n_bootstrap=BOOTSTRAP_N, ci=BOOTSTRAP_CI, seed=BOOTSTRAP_SEED)
  860. rec[f"{out_name}_mean"] = m
  861. rec[f"{out_name}_std"] = float(np.nanstd(vals, ddof=1)) if np.sum(~np.isnan(vals)) > 1 else float("nan")
  862. rec[f"{out_name}_ci_lower"] = lo
  863. rec[f"{out_name}_ci_upper"] = hi
  864. fs_summary_rows.append(rec)
  865. fs_summary_df = pd.DataFrame(fs_summary_rows).sort_values("K").reset_index(drop=True)
  866. fs_summary_df.to_csv(FINAL_FEATURE_SELECTION_SUMMARY, index=False)
  867. print("Saved:")
  868. print(FINAL_FEATURE_SELECTION)
  869. print(FINAL_FEATURE_SELECTION_SUMMARY)
  870. display(fs_summary_df[[
  871. "K", "balanced_accuracy_mean", "balanced_accuracy_ci_lower", "balanced_accuracy_ci_upper",
  872. "auroc_mean", "auroc_ci_lower", "auroc_ci_upper",
  873. ]])
  874. # %% [markdown]
  875. # ## Save tables
  876. # %%
  877. repeat_metrics_df = pd.DataFrame(all_metric_rows)
  878. subject_predictions_df = pd.concat(all_subject_pred_rows, axis=0, ignore_index=True) if all_subject_pred_rows else pd.DataFrame()
  879. top_features_df = pd.DataFrame(feature_importance_rows)
  880. # Mean and SD across the 10 repeats per (experiment, model)
  881. summary_df = (
  882. repeat_metrics_df
  883. .groupby(["experiment", "model"], as_index=False)
  884. .agg(
  885. balanced_accuracy_mean=("balanced_accuracy", "mean"),
  886. balanced_accuracy_std=("balanced_accuracy", "std"),
  887. auroc_mean=("auroc", "mean"),
  888. auroc_std=("auroc", "std"),
  889. sensitivity_mean=("recall_sensitivity", "mean"),
  890. sensitivity_std=("recall_sensitivity", "std"),
  891. specificity_mean=("specificity", "mean"),
  892. specificity_std=("specificity", "std"),
  893. precision_mean=("precision", "mean"),
  894. precision_std=("precision", "std"),
  895. f1_mean=("f1", "mean"),
  896. f1_std=("f1", "std"),
  897. mcc_mean=("mcc", "mean"),
  898. mcc_std=("mcc", "std"),
  899. n_subjects_mean=("n_subjects", "mean"),
  900. )
  901. .sort_values(["experiment", "balanced_accuracy_mean", "auroc_mean"], ascending=[True, False, False])
  902. .reset_index(drop=True)
  903. )
  904. summary_df["public_label"] = summary_df["model"].map(make_public_label)
  905. # Bootstrap 95% CI of the mean across the 10 repeats, per (experiment, model)
  906. metric_columns = {
  907. "balanced_accuracy": "balanced_accuracy",
  908. "auroc": "auroc",
  909. "recall_sensitivity": "sensitivity",
  910. "specificity": "specificity",
  911. "precision": "precision",
  912. "f1": "f1",
  913. "mcc": "mcc",
  914. }
  915. ci_records = []
  916. for (exp_name, model_name), grp in repeat_metrics_df.groupby(["experiment", "model"]):
  917. rec = {"experiment": exp_name, "model": model_name}
  918. for raw_name, out_name in metric_columns.items():
  919. vals = grp[raw_name].values
  920. m, lo, hi = bootstrap_ci_mean(vals, n_bootstrap=BOOTSTRAP_N, ci=BOOTSTRAP_CI, seed=BOOTSTRAP_SEED)
  921. rec[f"{out_name}_ci_lower"] = lo
  922. rec[f"{out_name}_ci_upper"] = hi
  923. ci_records.append(rec)
  924. ci_df = pd.DataFrame(ci_records)
  925. summary_df = summary_df.merge(ci_df, on=["experiment", "model"], how="left")
  926. # Top features summary (unchanged)
  927. if len(top_features_df):
  928. top_features_summary = (
  929. top_features_df
  930. .groupby(["model", "family", "feature"], as_index=False)
  931. .agg(
  932. importance_mean=("importance", "mean"),
  933. importance_count=("importance", "size"),
  934. )
  935. .sort_values(["model", "importance_mean"], ascending=[True, False])
  936. .groupby("model", as_index=False)
  937. .head(12)
  938. .reset_index(drop=True)
  939. )
  940. else:
  941. top_features_summary = pd.DataFrame()
  942. # Manuscript table now includes both SD and 95% CI columns for the headline
  943. # metrics. Editors and reviewers can pick whichever uncertainty representation
  944. # they prefer when reading Tables 1 and 2.
  945. manuscript_table = summary_df[[
  946. "experiment", "public_label",
  947. "balanced_accuracy_mean", "balanced_accuracy_std",
  948. "balanced_accuracy_ci_lower", "balanced_accuracy_ci_upper",
  949. "auroc_mean", "auroc_std",
  950. "auroc_ci_lower", "auroc_ci_upper",
  951. "sensitivity_mean", "sensitivity_std",
  952. "specificity_mean", "specificity_std",
  953. "precision_mean", "precision_std",
  954. "f1_mean", "f1_std",
  955. "mcc_mean", "mcc_std",
  956. ]].copy()
  957. repeat_metrics_df.to_csv(FINAL_REPEAT_METRICS, index=False)
  958. summary_df.to_csv(FINAL_SUMMARY, index=False)
  959. subject_predictions_df.to_csv(FINAL_SUBJECT_PREDICTIONS, index=False)
  960. top_features_summary.to_csv(FINAL_TOP_FEATURES, index=False)
  961. manuscript_table.to_csv(FINAL_MANUSCRIPT_TABLE, index=False)
  962. manuscript_table.to_excel(FINAL_MANUSCRIPT_TABLE_XLSX, index=False)
  963. print("Saved:")
  964. print(FINAL_REPEAT_METRICS)
  965. print(FINAL_SUMMARY)
  966. print(FINAL_SUBJECT_PREDICTIONS)
  967. print(FINAL_TOP_FEATURES)
  968. print(FINAL_MANUSCRIPT_TABLE)
  969. print(FINAL_MANUSCRIPT_TABLE_XLSX)
  970. display(manuscript_table)
  971. # %% [markdown]
  972. # ## Figure 1 — Compact 8-channel baselines
  973. # %%
  974. fig, ax = plt.subplots(figsize=(10.0, 4.8), constrained_layout=True)
  975. df_plot = summary_df[summary_df["experiment"] == "compact_baselines"].copy()
  976. order = [
  977. "extratrees_8ch_features",
  978. "mlp_8ch_features",
  979. "cnn1d_8ch_raw",
  980. "eegnet_8ch_raw",
  981. "shallowconvnet_8ch_raw",
  982. ]
  983. df_plot["order"] = df_plot["model"].map({m: i for i, m in enumerate(order)})
  984. df_plot = df_plot.sort_values("order")
  985. labels = df_plot["model"].map(make_public_label).tolist()
  986. means = df_plot["balanced_accuracy_mean"].values
  987. lower = df_plot["balanced_accuracy_ci_lower"].values
  988. upper = df_plot["balanced_accuracy_ci_upper"].values
  989. err_low = means - lower
  990. err_high = upper - means
  991. # Wong colour-blind-safe palette. Feature-based models use cool tones,
  992. # raw-signal deep models use warm tones, and the strongest model (Extra
  993. # Trees on spectral features) anchors the palette in deep bluish-green.
  994. bar_colors = [
  995. "#009E73", # Extra Trees (spectral) - winner, deep bluish-green
  996. "#56B4E9", # MLP (spectral) - feature-based, sky blue
  997. "#E69F00", # 1D CNN (raw) - raw-signal, orange
  998. "#D55E00", # EEGNet (raw) - raw-signal, vermilion
  999. "#CC79A7", # ShallowConvNet (raw) - raw-signal, reddish purple
  1000. ]
  1001. edge_colors = ["#006B4F", "#2A7BB0", "#A06D00", "#933F00", "#8C4F75"]
  1002. ax.set_axisbelow(True)
  1003. bars = ax.bar(
  1004. range(len(labels)), means,
  1005. yerr=[err_low, err_high],
  1006. color=bar_colors, edgecolor=edge_colors, linewidth=0.9,
  1007. capsize=4, error_kw={"elinewidth": 1.2, "ecolor": "#333333"},
  1008. alpha=0.95,
  1009. )
  1010. for i, v in enumerate(means):
  1011. ax.text(i, upper[i] + 0.015, f"{v:.3f}", ha="center", va="bottom", fontsize=11)
  1012. ax.set_xticks(range(len(labels)))
  1013. ax.set_xticklabels(labels, rotation=12, ha="right")
  1014. ax.set_ylabel("Subject-level balanced accuracy")
  1015. ax.set_title("Compact 8-channel baselines (mean and 95% bootstrap CI)")
  1016. ax.set_ylim(0.0, 1.05)
  1017. ax.set_yticks(np.arange(0.0, 1.01, 0.2))
  1018. ax.grid(axis="y", linewidth=0.4, alpha=0.3)
  1019. fig.savefig(FIG_BASELINES, bbox_inches="tight")
  1020. plt.show()
  1021. print(FIG_BASELINES)
  1022. # %% [markdown]
  1023. # ## Figure 2 — Spectral vs connectivity vs fusion
  1024. # %%
  1025. fig, ax = plt.subplots(figsize=(8.8, 4.8), constrained_layout=True)
  1026. df_plot = summary_df[summary_df["experiment"] == "fusion_ablation"].copy()
  1027. order = ["extratrees_spectral", "extratrees_connectivity", "extratrees_fusion", "mlp_fusion"]
  1028. df_plot["order"] = df_plot["model"].map({m: i for i, m in enumerate(order)})
  1029. df_plot = df_plot.sort_values("order")
  1030. labels = df_plot["model"].map(make_public_label).tolist()
  1031. means = df_plot["balanced_accuracy_mean"].values
  1032. lower = df_plot["balanced_accuracy_ci_lower"].values
  1033. upper = df_plot["balanced_accuracy_ci_upper"].values
  1034. err_low = means - lower
  1035. err_high = upper - means
  1036. # Distinct colour per feature condition. The spectral bar uses the same
  1037. # green as the winning model in Figure 1, providing visual continuity
  1038. # across the two experiments. Connectivity gets a warm orange to mark its
  1039. # different feature family. Fusion gets a deep unifying blue. MLP fusion
  1040. # gets a complementary reddish purple.
  1041. bar_colors = [
  1042. "#009E73", # ET (spectral) - matches Fig 1 winner
  1043. "#E69F00", # ET (connectivity) - warm orange for connectivity family
  1044. "#0072B2", # ET (fusion) - deep blue for combined representation
  1045. "#CC79A7", # MLP (fusion) - reddish purple
  1046. ]
  1047. edge_colors = ["#006B4F", "#A06D00", "#004F7C", "#8C4F75"]
  1048. ax.set_axisbelow(True)
  1049. bars = ax.bar(
  1050. range(len(labels)), means,
  1051. yerr=[err_low, err_high],
  1052. color=bar_colors, edgecolor=edge_colors, linewidth=0.9,
  1053. capsize=4, error_kw={"elinewidth": 1.2, "ecolor": "#333333"},
  1054. alpha=0.95,
  1055. )
  1056. for i, v in enumerate(means):
  1057. ax.text(i, upper[i] + 0.015, f"{v:.3f}", ha="center", va="bottom", fontsize=11)
  1058. ax.set_xticks(range(len(labels)))
  1059. ax.set_xticklabels(labels)
  1060. ax.set_ylabel("Subject-level balanced accuracy")
  1061. ax.set_title("Spectral vs connectivity vs fusion (mean and 95% bootstrap CI)")
  1062. ax.set_ylim(0.0, 1.05)
  1063. ax.set_yticks(np.arange(0.0, 1.01, 0.2))
  1064. ax.grid(axis="y", linewidth=0.4, alpha=0.3)
  1065. fig.savefig(FIG_FUSION, bbox_inches="tight")
  1066. plt.show()
  1067. print(FIG_FUSION)
  1068. # %% [markdown]
  1069. # ## Figure 3 — Top feature importance preview
  1070. # %%
  1071. fig, axs = plt.subplots(1, 2, figsize=(12.5, 4.8), constrained_layout=True)
  1072. # (a) Spectral winner. Sequential green ramp keeps the spectral colour
  1073. # family from Figures 1 and 2, with deeper colour for higher importance.
  1074. spec_df = top_features_summary[top_features_summary["model"] == "extratrees_8ch_features"].head(10).copy()
  1075. spec_df = spec_df.sort_values("importance_mean", ascending=True)
  1076. spec_colors = plt.cm.YlGn(np.linspace(0.45, 0.85, len(spec_df)))
  1077. axs[0].set_axisbelow(True)
  1078. axs[0].barh(spec_df["feature"], spec_df["importance_mean"],
  1079. color=spec_colors, edgecolor="#2D5F3E", linewidth=0.6)
  1080. axs[0].set_title("(a) Top spectral/asymmetry features")
  1081. axs[0].set_xlabel("Mean importance")
  1082. axs[0].grid(axis="x", linewidth=0.4, alpha=0.3)
  1083. # (b) Connectivity winner. Sequential orange ramp matches the
  1084. # connectivity colour family used for the connectivity bar in Figure 2.
  1085. conn_df = top_features_summary[top_features_summary["model"] == "extratrees_connectivity"].head(10).copy()
  1086. conn_df = conn_df.sort_values("importance_mean", ascending=True)
  1087. conn_colors = plt.cm.YlOrBr(np.linspace(0.40, 0.80, len(conn_df)))
  1088. axs[1].set_axisbelow(True)
  1089. axs[1].barh(conn_df["feature"], conn_df["importance_mean"],
  1090. color=conn_colors, edgecolor="#7A4F1F", linewidth=0.6)
  1091. axs[1].set_title("(b) Top connectivity features")
  1092. axs[1].set_xlabel("Mean importance")
  1093. axs[1].grid(axis="x", linewidth=0.4, alpha=0.3)
  1094. fig.savefig(FIG_IMPORTANCE, bbox_inches="tight")
  1095. plt.show()
  1096. print(FIG_IMPORTANCE)
  1097. # %% [markdown]
  1098. # ## Figure 4 — Spectral feature-selection ablation curve
  1099. #
  1100. # Subject-level balanced accuracy and 95% bootstrap CI as a function of the number of selected features. The full 90-feature baseline is shown as a horizontal reference. If accuracy plateaus well below $K=90$, the spectral representation is not over-parameterised; if it improves at smaller $K$, top-$K$ pruning offers a parsimonious alternative.
  1101. # %%
  1102. fig, ax = plt.subplots(figsize=(8.6, 4.8), constrained_layout=True)
  1103. xs = fs_summary_df["K"].values
  1104. means = fs_summary_df["balanced_accuracy_mean"].values
  1105. los = fs_summary_df["balanced_accuracy_ci_lower"].values
  1106. his = fs_summary_df["balanced_accuracy_ci_upper"].values
  1107. # Reuse the spectral-winner colour from Figure 1 since this figure
  1108. # describes the same Extra Trees on spectral features at varying K.
  1109. PRIMARY = "#009E73"
  1110. PRIMARY_DARK = "#006B4F"
  1111. ax.set_axisbelow(True)
  1112. ax.fill_between(xs, los, his, color=PRIMARY, alpha=0.18, label="95% bootstrap CI")
  1113. ax.plot(xs, means, "o-", color=PRIMARY, linewidth=2.0, markersize=7,
  1114. markerfacecolor="white", markeredgecolor=PRIMARY_DARK, markeredgewidth=1.8,
  1115. label="Top-$K$ Extra Trees mean")
  1116. # Reference line for the full 90-feature baseline
  1117. full_row = fs_summary_df[fs_summary_df["K"] == 90]
  1118. if len(full_row):
  1119. ref = float(full_row["balanced_accuracy_mean"].iloc[0])
  1120. ax.axhline(ref, linestyle="--", color="#555555", linewidth=1.0, alpha=0.7,
  1121. label=f"Full 90-feature baseline ({ref:.3f})")
  1122. for x, m, lo, hi in zip(xs, means, los, his):
  1123. ax.text(x, hi + 0.012, f"{m:.3f}", ha="center", va="bottom", fontsize=10)
  1124. ax.set_xlabel("Number of selected spectral and asymmetry features ($K$)")
  1125. ax.set_ylabel("Subject-level balanced accuracy")
  1126. ax.set_title("Spectral feature-selection ablation (Extra Trees, mean and 95% CI)")
  1127. ax.set_xticks(xs)
  1128. ymin = float(np.nanmin(los)) - 0.04
  1129. ymax = max(1.02, float(np.nanmax(his)) + 0.05)
  1130. ax.set_ylim(max(0.0, ymin), ymax)
  1131. ax.grid(axis="y", linewidth=0.4, alpha=0.3)
  1132. ax.legend(loc="lower right", frameon=False)
  1133. fig.savefig(FIG_FEATURE_SELECTION, bbox_inches="tight")
  1134. plt.show()
  1135. print(FIG_FEATURE_SELECTION)

EEG_Depression_Analysis_Public_Notebook.ipynb at commit 0a6888f, no license · at the source

Overview

  1. Centre for Visual Computing and Intelligent Systems, University of Bradford, Bradford BD7 1DP, UK
  2. The Howard Brain Sciences Foundation, Washington, DC 20001, USA
Institutions: University of Bradford (United Kingdom); Howard Brain Sciences Foundation (United States)
Journal: Sensors (Basel, Switzerland), volume 26, issue 10, article 3065
Dates: received 9 April 2026; accepted 11 May 2026; published online 12 May 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.3390/s26103065 · PMID 42197874 · PMCID PMC13210619 · OpenAlex W7160913777
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: EEG (modality), other (modality), human (organism), depression (population), cellular / molecular (subfield)
Methods: Spectral & time-frequency, Connectivity, Preprocessing, Statistics, Machine learning
Keywords: electroencephalography, major depressive disorder, subject-wise evaluation, data leakage, spectral features, frontal alpha asymmetry, beta-band power, inter-channel coherence, Extra Trees, wearable EEG
MeSH: Depression*, Electroencephalography*, Major Depressive Disorder*, Adult, Algorithms, Convolutional Neural Networks, Female, Humans, Male, Rest, Signal Processing, Computer-Assisted (* major topic)
Topic: EEG and Brain-Computer Interfaces (Cognitive Neuroscience, Neuroscience), according to OpenAlex
Citations: not cited yet (Europe PMC); 34 references in the paper

Abstract

Major depressive disorder remains difficult to diagnose objectively, as routine assessment is still largely dependent on clinical interview and rating scales. Resting-state electroencephalography (EEG) is an attractive complementary modality because it is non-invasive, low-cost, and compatible with wearable sensing, but many reported EEG classification results are weakened by segment-level leakage and unclear subject identity handling. This study evaluates whether depression can be distinguished from healthy controls using a compact eight-channel resting-state EEG configuration under a strictly leakage-free subject-wise protocol. Using a widely used public EEG dataset, we first corrected a previously overlooked subject-identity ambiguity by constructing a class-aware composite key, yielding 56 valid unique participants. We then applied ten repeated subject-wise holdout splits and compared five compact baselines spanning Extra Trees and a multi-layer perceptron on asymmetry-aware spectral features and three convolutional networks on raw signals, including the EEG-specific EEGNet and ShallowConvNet architectures. Uncertainty was quantified through 95% bootstrap confidence intervals of the mean across repeats. The best model, an Extra Trees classifier using eight-channel spectral and asymmetry features, achieved a mean balanced accuracy of 93.5% with a 95% bootstrap confidence interval of 89.6% to 96.8% and a mean area under the receiver operating characteristic curve of 98.6% with a 95% bootstrap confidence interval of 96.2% to 100.0%. A connectivity ablation showed that inter-channel coherence was informative in isolation but did not improve performance when naively fused with spectral features. A feature-selection ablation did not show evidence that the 90-dimensional spectral representation was dominated by noisy or uninformative dimensions under this evaluation protocol. These results support compact, subject-wise evaluated EEG screening pipelines while highlighting the importance of rigorous leakage control.

Reproduced under the paper's license (CC BY), from the paper cited above.

Repository

Its files are read in the Code ↔ Paper reader above, with 10 matches between paragraphs and lines of code.

ugail/EEG_Based_Depression_Screening

License: none: the authors keep all their rights
State: the link answers, verified on 28 September 2026
Evidence: files inventoried
Commit: 0a6888f3caaafcad451a447db807154eaa1f73d7, 15 May 2026
Languages: Jupyter (1)
Size: 9 files, 1 script
Software Heritage: not archived
Found in: “Data Availability Statement”
Holds: README, 1 notebook
Not found: license file, CITATION.cff, environment file, tests, continuous integration, documentation
Tools: Matplotlib (1 file), NumPy (1 file), pandas (1 file), PyTorch (1 file), scikit-learn (1 file), SciPy (1 file)
Availability: 1 check, the latest on 28 September 2026: the link answers
  • 28 September 2026: the link answers
2 files

The paper's code and data availability statement is in the Data section.

Tracing map

Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.

What the map holds:

  • 1 repository of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
  • 1 script, each with its path and the digest of its content;
  • 10 matches between paragraphs of the paper and lines of the code (method lexical-v1);
  • neither the text of the paper nor the code itself.

Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.

Data

Datasets cited

Data Availability Statement

The EEG dataset analysed in this study is publicly available on figshare, DOI https://doi.org/10.6084/m9.figshare.4244171.v2. The code and data generated during the experimentation and analysis are publicly available at https://github.com/ugail/EEG_Based_Depression_Screening (accessed on 9 May 2026).

Reproduced under the paper's license (CC BY), from the paper cited above.

Versions

The history of this record: each version stored by the harvester or made by a correction of its authors or of the maintainers of its code, and what changed in its facts. The texts of the paper (its abstract, its availability statements) are not part of it; versions that changed only those are not listed.

Version 1, 28 September 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 4 authors, 10 keywords, 11 MeSH terms, 1 funder, 29 references.

Cite

This paper

Ugail, H., Howard, N., Elmahmudi, A. A., & Mnasri, Z. (2026). Subject-Wise Depression Screening from Eight-Channel Resting-State EEG Using Asymmetry-Aware Spectral Features and Connectivity Ablation. Sensors (Basel, Switzerland), 26(10), 3065. https://doi.org/10.3390/s26103065

BibTeX

@article{ugail2026subject,
author = {Ugail, Hassan and Howard, Newton and Elmahmudi, Ali Ahmed and Mnasri, Zied},
title = {{Subject-Wise Depression Screening from Eight-Channel Resting-State EEG Using Asymmetry-Aware Spectral Features and Connectivity Ablation}},
journal = {Sensors (Basel, Switzerland)},
year = {2026},
month = may,
volume = {26},
number = {10},
pages = {3065},
publisher = {Multidisciplinary Digital Publishing Institute (MDPI)},
issn = {1424-8220},
doi = {10.3390/s26103065},
url = {https://doi.org/10.3390/s26103065},
pmid = {42197874},
pmcid = {PMC13210619}
}

RIS

TY - JOUR
AU - Ugail, Hassan
AU - Howard, Newton
AU - Elmahmudi, Ali Ahmed
AU - Mnasri, Zied
TI - Subject-Wise Depression Screening from Eight-Channel Resting-State EEG Using Asymmetry-Aware Spectral Features and Connectivity Ablation
T2 - Sensors (Basel, Switzerland)
J2 - Sensors (Basel)
PY - 2026
DA - 2026/05/12
VL - 26
IS - 10
SP - 3065
SN - 1424-8220
PB - Multidisciplinary Digital Publishing Institute (MDPI)
DO - 10.3390/s26103065
UR - https://doi.org/10.3390/s26103065
LA - en
ER -

CSL-JSON

{
"id": "10.3390/s26103065",
"type": "article-journal",
"title": "Subject-Wise Depression Screening from Eight-Channel Resting-State EEG Using Asymmetry-Aware Spectral Features and Connectivity Ablation",
"container-title": "Sensors (Basel, Switzerland)",
"author": [
{
"family": "Ugail",
"given": "Hassan"
},
{
"family": "Howard",
"given": "Newton"
},
{
"family": "Elmahmudi",
"given": "Ali Ahmed"
},
{
"family": "Mnasri",
"given": "Zied"
}
],
"container-title-short": "Sensors (Basel)",
"volume": "26",
"issue": "10",
"page": "3065",
"DOI": "10.3390/s26103065",
"PMID": "42197874",
"PMCID": "PMC13210619",
"ISSN": "1424-8220",
"publisher": "Multidisciplinary Digital Publishing Institute (MDPI)",
"URL": "https://doi.org/10.3390/s26103065",
"language": "en",
"issued": {
"date-parts": [
[
2026,
5,
12
]
]
}
}

The tracing map gets a citation of its own once an author has validated it and it has a DOI.

Similar papers

The papers with a page that share the most with this one: the tools found in their code, their categories, datasets, cited references and authors, the rarest counting most.

[1] doi:10.1007/s00422-026-01049-1 [code]
A three-component dynamical index of consciousness-related neural organisation.
Journal: Biological cybernetics
In common: scikit-learn, pandas, SciPy, 2 other tools, EEG, author Hassan Ugail
[2] doi:10.3389/fnins.2026.1874302 [code]
Architecture-data matching for EEG-EMG decoding: compact deep models match classical spectral decoders on the WAY-EEG-GAL grasp-and-lift dataset.
Journal: Frontiers in neuroscience
In common: PyTorch, scikit-learn, pandas, 3 other tools, other, EEG, 3 references
[3] doi:10.1371/journal.pone.0347671 [code]
RMETNet: A cross-subject motor imagery EEG signal classification model based on TSLANet and riemannian geometry features.
Journal: PloS one
In common: PyTorch, scikit-learn, pandas, 3 other tools, EEG, 3 references
[4] doi:10.1038/s41598-026-68186-2 [code]
NeuroStream: spectral-spatio-temporal deep learning for visual stimulus classification from EEG.
Journal: Scientific reports
In common: PyTorch, scikit-learn, pandas, 3 other tools, EEG, 3 references
[5] doi:10.1002/hbm.70528 [code]
Explainable AI Insights Into EEG Classification and Its Alignment to Neural Correlates.
Journal: Human brain mapping
In common: PyTorch, scikit-learn, pandas, 3 other tools, EEG, 2 references
[6] doi:10.1371/journal.pone.0351872 [code]
Decoding visual object recognition from EEG signals.
Journal: PloS one
In common: PyTorch, scikit-learn, pandas, 3 other tools, EEG, 2 references
[7] doi:10.1038/s41746-026-02778-0 [code]
Trust-gated synthetic EEG augmentation reduces performance drops when generalizing to new patients.
Journal: NPJ digital medicine
In common: PyTorch, scikit-learn, pandas, 3 other tools, EEG, 2 references
[8] doi:10.3390/bios16070394
Hybrid Edge-Cloud Asymmetric Analytics for Portable Multimodal BCI Biosensors.
Journal: Biosensors
In common: other, EEG, 5 references
[9] doi:10.1038/s41398-026-04079-2 [code]
Neural Responses to Affective Sentences Reveal Signatures of Depression.
Journal: Translational psychiatry
In common: PyTorch, scikit-learn, pandas, 3 other tools, depression, EEG, 1 reference
[10] doi:10.1162/imag.a.1301 [code]
MEG-GPT: A transformer-based foundation model for magnetoencephalography data.
Journal: Imaging neuroscience (Cambridge, Mass.)
In common: scikit-learn, pandas, SciPy, 2 other tools, 3 references

Contribute

The authors of this paper can claim it, correct its record and validate its tracing map, and the maintainers of its code (its owner, or a public member of its organization) correct what it says of their repository; anyone signed in can ask for its removal. Every request goes to OSCR's own machine, which answers it; your account page follows them.

Sign in with ORCID to claim this paper as one of its authors, correct its record or validate its tracing map: when the paper's metadata lists your ORCID iD, you are recognized at once. Maintainers of its code: sign in with GitHub, then claim the repository on your account page.

Request its removal

To ask OSCR to remove this record, the copies of its authors' scripts or its tracing map, use the removal request page: signed in, you say who you are, what to remove and why, then review and confirm the request. Published rules decide every request (how).

Discussion, reproductions, activity

Discussion: questions and error reports about this paper and its code, from signed-in readers and its authors. It opens with sign-in.

Reproductions: reports from readers who ran the authors' code: what they reproduced, with which environment, commit and data. It opens with sign-in.

Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.