Skip to content

Riemannian tangent-space EEG path - #4

Merged
aaygan29 merged 3 commits into
mainfrom
riemann-eeg
Aug 11, 2026
Merged

Riemannian tangent-space EEG path#4
aaygan29 merged 3 commits into
mainfrom
riemann-eeg

Conversation

@aaygan29

Copy link
Copy Markdown
Owner

Builds the Riemannian EEG arm as the proper follow-up flagged in #3, in three small commits. This is the one I said needed a dedicated path rather than a base-learner swap — here it is, done correctly.

Why it needed its own path

EEG carries much of its discriminative signal in inter-channel covariance, which lives on the manifold of SPD matrices, not a flat vector space. The framework's StandardScaler z-scores each covariance entry independently (destroying the positive-definite structure) and SMOTE interpolates in straight lines (off-manifold). So you can't just slot a "riemann" base learner into the flat pipeline — the covariance has to be projected into a flat tangent space first.

What's shipped

BaggingClassifier
  └── Pipeline(RiemannianTangentSpace → StandardScaler → SMOTE → logistic)
  • models/riemann.pyRiemannianTangentSpace transformer (scipy-only, log-Euclidean metric). Isometric vectorisation (√2 off-diagonal so Euclidean norm = Frobenius), identity-shrinkage regularisation so rank-deficient short-epoch covariances still admit a matrix log, and a training-set log-Euclidean mean reference computed in fit only (leakage-safe).
  • make_balanced_pipeline gains pre_steps, and build_modality_model's riemann path prepends the tangent transform, classifies with logistic in tangent space, and forces max_features=1.0 (a column subset of a flattened covariance isn't a covariance).
  • EEGLoader covariance mode — emits per-trial inter-channel covariance, sharing flatten_spd with the transformer as one source of truth (a round-trip test guards against drift). Mutually exclusive with band power / ERP.
block = EEGLoader(include_bandpower=False, include_erp=False,
                  include_covariance=True).from_arrays(epochs, sfreq, times, subj, stim)
# config: model.base_learner.eeg = riemann

Two honest caveats (in the code and docs)

  • It does not beat plain logistic on clean synthetic data, because raw covariance entries are already linearly separable there. Its advantage is on ill-conditioned real EEG, the regime the method exists for. The tests assert the path recovers covariance structure above chance — not a synthetic "beats logistic" win, which would be cherry-picking.
  • This is the log-Euclidean metric (closed-form, no dependency). pyriemann's affine-invariant metric is a further upgrade, noted as the next step if a real EEG dataset warrants it.

Tests

18 new (test_riemann.py 12, test_eeg_covariance.py 6): isometric/invertible vectorisation, rank-deficient survival, leakage-safe reference mean, non-covariance rejection, pipeline wiring, loader↔transformer round-trip, mutual-exclusivity, and end-to-end above-chance positive controls. Full suite 147 passing, no new dependency.

Aayush Gandhi added 3 commits August 11, 2026 08:35
EEG carries much of its signal in inter-channel covariance, which lives on the
manifold of SPD matrices, not a flat vector space. Scaling and SMOTE-ing the raw
entries throws away that geometry. This adds the missing path documented in
docs/estimators.md: a log-Euclidean tangent projection at the front of the
pipeline, so downstream scaler/SMOTE/logistic operate on genuinely Euclidean
tangent vectors.

models/riemann.py: RiemannianTangentSpace transformer (scipy-only). Vectorises
covariances isometrically (sqrt(2) off-diagonal so Euclidean norm == Frobenius),
regularises rank-deficient covariances via identity shrinkage before the matrix
log (short-epoch covariances are singular), and centres at the training-set
log-Euclidean mean (leakage-safe: computed in fit only). pyriemann's
affine-invariant metric is a further upgrade, noted for later.

Wiring: make_balanced_pipeline gains pre_steps, inserted before the scaler.
build_modality_model's `riemann` path prepends the tangent transform, uses
logistic in tangent space, and forces max_features=1.0 (a column subset of a
flattened covariance is not a covariance).

12 tests: isometric vectorisation, invertibility, rank-deficient survival,
leakage-safe reference mean, non-covariance rejection, pipeline wiring, and an
honest above-chance positive control (NOT a synthetic "beats logistic" claim,
since raw entries are already separable on clean data).
EEGLoader gains an include_covariance mode that emits per-trial inter-channel
covariance, flattened with the same sqrt(2) off-diagonal convention the tangent
transformer uses. Both share models.riemann.flatten_spd as the single source of
truth, and a round-trip test guards against the loader and transformer drifting
apart.

Covariance is mutually exclusive with band-power/ERP: the tangent map needs the
covariance matrix intact, not concatenated with unrelated columns, so the
constructor raises if you ask for both.

6 tests: shape/naming, the min-samples guard, provenance labelling, the
loader<->transformer round-trip, the mutual-exclusivity guard, and an end-to-end
EEGLoader(covariance) -> riemann positive control above chance.
estimators.md now describes riemann as a real estimator: how to produce
covariance features, the tangent-first pipeline that keeps scaling/SMOTE valid,
the leakage-safe training-mean reference, and two honest caveats (it does not
beat plain logistic on clean synthetic data, where raw entries are already
separable; log-Euclidean is used, with pyriemann's affine-invariant metric noted
as the further upgrade). Removes the old "not a drop-in" Riemannian subsection,
leaving mixed-effects as the one remaining documented-but-not-built model.
@aaygan29
aaygan29 merged commit b2cc4cf into main Aug 11, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant