Honest, leakage-safe resting-state EEG analysis for cross-hospital Parkinson's disease classification.
Project description
aperion
Resting-state EEG analysis for neurodevelopmental disorder research. The goal is a simple, honest pipeline that predicts Parkinson's disease (PD) vs healthy control (HC) from resting-state EEG — and, crucially, that keeps working when tested on a hospital it has never seen.
Status: chunks one–three complete.
- Plumbing + honesty gate — one dataset (ds002778), band-power features, logistic regression, shuffled-label leakage control.
- Aperiodic method — separate the 1/f background (offset, exponent) from the periodic peaks (FOOOF), scored with a nested SelectKBest + shrinkage-LDA harness.
- Cross-hospital — three hospitals (ds002778, ds003490, ds004584), leave-one-dataset-out evaluation, and a harmonizer to remove site effects.
Headline result (honest)
Trained on two hospitals, tested on an entirely unseen third (leave-one-dataset-out):
| Features | cross-hospital AUC (no harmonizer) | with harmonizer |
|---|---|---|
| band-power | 0.711 | 0.703 |
| aperiodic | 0.660 | 0.716 |
The harmonizer rescues the aperiodic method from behind to a tie with band-power (not a decisive win). The strongest evidence is the hospital-probe control: after harmonizing, the ability to predict which hospital a recording came from collapses (aperiodic 0.72 → 0.24, below chance) while disease AUC rises — i.e. it removed hospital effects, not disease signal. Benefit was heterogeneous (large on ds002778 / ds004584, flat on ds003490).
Project layout
NDDProject_EEG/
├── pyproject.toml # build (hatchling) + deps + optional [app]/[dev] extras
├── src/aperion/ # the importable package (LEAN: core deps only)
│ ├── datasets/ # dataset loaders (+ multi-cohort loader)
│ ├── preprocessing/ # one identical cleaning pipeline -> epochs
│ ├── features/ # band-power + aperiodic (FOOOF) features
│ ├── harmonize/ # PerHospitalStandardizer (site harmonizer)
│ ├── evaluate/ # subject-grouped CV, LODO, honesty/probe controls
│ ├── utils/ # structural harmonization (channels, sample rate)
│ ├── viz/ # PSD / group-comparison plots
│ ├── pipeline_api.py # raw -> subject-level features (single source of truth)
│ ├── model.py # trainable/saveable AperionModel (aperiodic + harmonizer)
│ └── inference.py # single-recording scoring for the app
├── app/ # SEPARATE demo web app (Streamlit) — imports aperion only
│ ├── streamlit_app.py
│ └── assets/ # saved results.json, figures, demo recordings
├── benchmarks/ # build_phase_b_artifacts.py (trains model, saves results)
├── models/ # saved model file (generated; gitignored)
├── examples/ · tests/ · docs/
Install
A virtual environment (.venv) is expected in the project root.
python -m venv .venv
source .venv/bin/activate # Windows: .\.venv\Scripts\Activate.ps1
pip install -e ".[dev]" # core + dev tools (pytest/ruff/black)
Core install pulls only mne, scikit-learn, numpy, scipy, pandas, matplotlib, fooof.
It does not install streamlit — the web app is an opt-in extra (see below).
Verify:
python -c "import aperion; print(aperion.__version__)" # -> 0.1.0
pytest
Quickstart
The feature extractors and the harmonizer are standard scikit-learn transformers, so they drop straight into a pipeline. This snippet is self-contained (toy spectra) and runs as-is:
import numpy as np
from aperion import AperiodicPeriodicExtractor, BandPowerExtractor, PerHospitalStandardizer
freqs = np.arange(2.0, 40.0, 0.5)
ch_names = ["Fz", "Cz", "Pz", "Oz"]
# power spectra shaped (n_recordings, n_channels, n_freqs) -- here a toy 1/f example
base = (10.0 ** 1.0) / (freqs ** 1.3)
spectra = np.tile(base, (6, len(ch_names), 1))
# 1) two interchangeable feature families
# (n_jobs=1 keeps FOOOF single-process — safest on Windows / inside scripts)
ap = AperiodicPeriodicExtractor(freqs, ch_names, n_jobs=1).fit_transform(spectra) # 1/f + peaks
bp = BandPowerExtractor(freqs, ch_names).fit_transform(spectra) # relative band power
# 2) remove per-hospital effects (fit on training sites; held-out site uses its own stats)
cohorts = np.array(["H1"] * 3 + ["H2"] * 3)
harm = PerHospitalStandardizer().fit(ap, cohorts=cohorts)
ap_harmonized = harm.transform(ap, cohorts=cohorts)
print(ap.shape, bp.shape, ap_harmonized.shape)
Score a real recording with the saved model (after running the build step below):
from aperion import load_model, load_recording, analyze_raw
model = load_model("models/aperion_model_v0.1.0.joblib")
raw = load_recording("data/raw/ds002778/sub-pd3/ses-off/eeg/sub-pd3_ses-off_task-rest_eeg.bdf")
result = analyze_raw(raw, model, cohort="ds002778")
print(f"P(Parkinson's) = {result['probability']:.2f} ± {result['uncertainty']:.2f}")
Reproduce the benchmark
Verify the headline cross-hospital result from the raw data (fixed seeds; requires the three
datasets in data/raw/):
python benchmarks/reproduce_cross_hospital.py
It rebuilds features, runs leave-one-dataset-out ± harmonizer for both feature types and the
hospital-probe control, and checks the two headline effects (harmonizer rescue + probe
collapse) against expected values — printing HEADLINE REPRODUCED on success.
Demo web app
The app is a research demonstration — not a medical device and not a diagnosis. It
lives in app/ and only imports/calls aperion (no data or modelling logic of its own);
it loads a pre-trained model and pre-computed result files rather than recomputing science.
1. Install the optional app dependencies (adds streamlit):
pip install -e ".[app]"
2. Build the model + result files once (the app loads these; requires the datasets in
data/raw/):
python benchmarks/build_phase_b_artifacts.py
3. Run the app locally:
streamlit run app/streamlit_app.py
# open http://localhost:8501
- Analyze page: pick a bundled demo recording (or upload one); see its spectrum split into the 1/f background and the peaks, plus the model's Parkinson's probability with an uncertainty band and a medical disclaimer.
- Results page: the honest cross-hospital story (harmonizer rescue, hospital-probe control, matched-size domain shift, and limitations), loaded from saved files.
Sharing it online (optional, not deployed here)
The simplest free option is Streamlit Community Cloud:
push this repo to GitHub, then on Community Cloud point a new app at
app/streamlit_app.py and set the dependencies to .[app]. You would need to include (or
regenerate) the models/ file and app/assets/ since large data is gitignored. This repo
does not deploy anything automatically.
Dependencies
- Core:
mne,scikit-learn,numpy,scipy,pandas,matplotlib,fooof [app]:streamlit·[dev]:pytest,ruff,black
License
MIT — see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file aperion-0.1.0.tar.gz.
File metadata
- Download URL: aperion-0.1.0.tar.gz
- Upload date:
- Size: 47.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
964aac1b55b56408b8f3ee8af185b31a5912e1916eedfad9f8f44fa06a0954a2
|
|
| MD5 |
1575db549b84458435402eb726474fc3
|
|
| BLAKE2b-256 |
64c9776ff34e635503001c3be05cad9961feb0eee7eaa21a6c3969f1f932e0b5
|
File details
Details for the file aperion-0.1.0-py3-none-any.whl.
File metadata
- Download URL: aperion-0.1.0-py3-none-any.whl
- Upload date:
- Size: 36.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
55ae13b1bd1a2ecd7a2173474a083f33937e0f0adb3a57466b7b1899f92d4066
|
|
| MD5 |
85161f193fc2c95496fb9443ef3985cd
|
|
| BLAKE2b-256 |
638dcc84e3d9f6f28e3236bf8440482144f9b09fb39e55041bc2fe2be9f81602
|