Skip to main content

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.

  1. Plumbing + honesty gate — one dataset (ds002778), band-power features, logistic regression, shuffled-label leakage control.
  2. Aperiodic method — separate the 1/f background (offset, exponent) from the periodic peaks (FOOOF), scored with a nested SelectKBest + shrinkage-LDA harness.
  3. 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 demonstrationnot 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

aperion-0.1.0.tar.gz (47.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

aperion-0.1.0-py3-none-any.whl (36.6 kB view details)

Uploaded Python 3

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

Hashes for aperion-0.1.0.tar.gz
Algorithm Hash digest
SHA256 964aac1b55b56408b8f3ee8af185b31a5912e1916eedfad9f8f44fa06a0954a2
MD5 1575db549b84458435402eb726474fc3
BLAKE2b-256 64c9776ff34e635503001c3be05cad9961feb0eee7eaa21a6c3969f1f932e0b5

See more details on using hashes here.

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

Hashes for aperion-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 55ae13b1bd1a2ecd7a2173474a083f33937e0f0adb3a57466b7b1899f92d4066
MD5 85161f193fc2c95496fb9443ef3985cd
BLAKE2b-256 638dcc84e3d9f6f28e3236bf8440482144f9b09fb39e55041bc2fe2be9f81602

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page