Open-source project
pyRiemann/pyRiemann avatar
pyRiemann/pyRiemann

pyRiemann: Riemannian geometry for SPD matrices in a scikit-learn pipeline

Machine learning for multivariate data through the Riemannian geometry of positive definite matrices in Python

777 stars190 forksPythonBSD-3-Clause

At a glance

What is it?
pyRiemann turns multichannel time series into covariance matrices and classifies them on the manifold of symmetric positive definite matrices, using the scikit-learn API. It is built for EEG, MEG and EMG work first, and for radar and hyperspectral imagery second.
Who is it for?
Adopt pyRiemann if your features are covariance matrices, your data is multichannel time series, and you want a scikit-learn compatible classifier rather than a hand-rolled implementation of the affine-invariant metric. Do not adopt it if you need a general Riemannian manifold library, since the API is scoped to SPD and HPD matrices, or if you cannot run Python 3.11 or newer, which setup.py sets as the floor.
Can I use it commercially?
Yes. BSD-3-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 16 days ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 27, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The problem pyRiemann solves: classifying covariance matrices without flattening them

A multichannel recording gives you a matrix of channels by time. The usual next step is to estimate a covariance matrix per epoch, which compresses that recording into a compact object. The trouble starts there. Covariance matrices are symmetric positive definite, and they live on a curved manifold, not in a flat vector space. Flattening them and feeding them to a linear classifier means treating the space of SPD matrices as Euclidean, which distorts distances between them. pyRiemann exists to avoid that flattening. It provides estimators that operate directly on the manifold, plus tangent space projections when you do want a vector representation that preserves the geometry locally. The intended audience is narrow and clear: researchers working on brain-computer interfaces with motor imagery, event-related potentials or steady-state visually evoked potentials, and remote sensing practitioners estimating covariances over spatial coordinates of radar or hyperspectral images. The README states the package aims to be generic for multivariate data analysis but was designed around biosignals. That ordering matters when you decide whether it fits your problem.

How the covariance-to-classifier data flow actually works

The pipeline has three stages, and the README demonstrates all of them. First, an estimation step turns raw epochs into matrices: `pyriemann.estimation.Covariances` takes input shaped n_epochs by n_channels by n_times and returns one covariance matrix per epoch. Second, a geometry step either stays on the manifold or maps to a tangent space. `pyriemann.classification.MDM` is the minimum distance to mean classifier, which computes class means on the manifold and assigns each trial to the nearest one. `pyriemann.tangentspace.TangentSpace` instead projects each matrix to a vector tangent to a reference point, after which any ordinary scikit-learn estimator applies. Third, a standard classifier consumes the result. Because every object follows the scikit-learn estimator interface, `make_pipeline` and `cross_val_score` work without adapters. The README also describes extended labels that support multisource transfer learning between sessions or subjects, which is the mechanism for reusing a calibration across recording conditions rather than collecting new labelled data for each one. On the numerical side, the core utility functions accept both NumPy and PyTorch arrays through the Python Array API, so passing PyTorch tensors enables GPU execution and autograd. That is a property of the utility layer, not a promise that every estimator is differentiable.

Installing pyRiemann and running a first MDM classification

The README gives three installation routes. The simplest is PyPI, and the package requires Python 3.11, 3.12 or 3.13 according to the badge and `python_requires=">=3.11"` in setup.py.

bash
pip install pyriemann

conda-forge is the alternative, which is the route to take if your environment is already managed by conda.

bash
conda install -c conda-forge pyriemann

For unreleased code, the README offers a git install, and for local development an editable install from a clone. Once installed, the first real use is a cross-validated classification. The example below is adapted from the README, with the data placeholders left as the README leaves them.

python
import pyriemann
from sklearn.model_selection import cross_val_score

X = ...  # EEG data, n_epochs x n_channels x n_times
y = ...  # labels

cov = pyriemann.estimation.Covariances().fit_transform(X)
mdm = pyriemann.classification.MDM()
accuracy = cross_val_score(mdm, cov, y)

print(accuracy.mean())

The output is a mean cross-validated accuracy, one number per fold averaged. If you prefer a tangent space pipeline, the README shows the same data flowing through `Covariances`, `TangentSpace` and a linear SVM inside `make_pipeline`. The repository ships an examples directory with subfolders for biosignal-mi, biosignal-erp, biosignal-ssvep, covariance-estimation, image-radar, fnirs, transfer and simulated data, so there is a runnable starting point closer to your own setup than the README snippet.

Where pyRiemann stops being the right tool

The package is scoped to symmetric positive definite and Hermitian positive definite matrices. If your features are not of that form, nothing here applies, and forcing a covariance interpretation onto arbitrary data will not rescue the approach. A second boundary is the data regime. Covariance estimation from short epochs with many channels produces ill-conditioned matrices, and the manifold operations become numerically fragile. The README does not document a default shrinkage policy for the `Covariances` estimator, so conditioning is something you have to check yourself rather than assume. Third, the transfer learning support depends on extended labels, which means your dataset has to carry the session or subject structure the API expects; retrofitting that onto an existing label array is work the documentation does not walk you through. Finally, the Array API backend is described as covering pyRiemann's core utility functions. That phrasing is narrower than "the whole library runs on PyTorch", and treating it as the latter will lead to surprises when an estimator you need turns out to be NumPy-only.

pyRiemann against MNE-Python and hand-written geometry code

The obvious comparison is MNE-Python, which also handles EEG and MEG and is listed in pyRiemann's own docs extra requirements. The difference is the layer each one occupies. MNE covers reading, filtering, epoching and visualisation of neurophysiological recordings, and it does not provide a manifold classifier. pyRiemann starts where epoching ends: it assumes you already have clean epochs shaped n_epochs by n_channels by n_times and takes over from the covariance onward. In practice they compose rather than compete, and the docs extra pulling in mne suggests the authors expect exactly that. The second alternative is implementing the geometry yourself with NumPy and SciPy. That is entirely feasible for a single metric and a single classifier, and it removes a dependency. What you lose is the breadth: pyRiemann ships multiple estimators behind one interface, tangent space projections, transfer learning hooks and a test suite that runs under pytest. If your project needs one distance and one mean, writing it yourself is defensible. If you need to compare several approaches on the same data, the package saves you from reimplementing each one.

Maintenance, licensing and what an upgrade costs you

The repository is not archived, and the last push was on 2026-09-08. Releases are frequent: v0.12 landed on 2026-07-01, v0.11 on 2026-04-16 and v0.10 on 2026-01-06. That cadence is a real cost as well as a benefit, because each release can change estimator behaviour and you are the one who has to re-validate a published accuracy figure. The dependency floor is worth checking before you pin anything: setup.py requires numpy>=1.25.0, scikit-learn>=0.24, array-api-compat>=1.14 and array-api-extra>=0.6, plus scipy, joblib and matplotlib. The Array API dependencies are the ones most likely to constrain an older environment. Licensing is BSD 3-clause, stated in the README and in setup.py as "BSD (3-clause)". That is a permissive licence, but the practical question is what your own distribution does with the attribution and the disclaimer, which is a matter for your legal review rather than something the repository decides for you. If you cite the package in academic work, the README provides a BibTeX entry with the Zenodo DOI 10.5281/zenodo.593816 and version v0.12.

Editorial conclusion

Adopt pyRiemann if your features are covariance matrices, your data is multichannel time series, and you want a scikit-learn compatible classifier rather than a hand-rolled implementation of the affine-invariant metric. Do not adopt it if you need a general Riemannian manifold library, since the API is scoped to SPD and HPD matrices, or if you cannot run Python 3.11 or newer, which setup.py sets as the floor. Before committing, verify that your covariance estimates are well conditioned (shrinkage is available in the estimation module), and check that the estimator you plan to use is exported by the installed version rather than only present in the examples folder.

Frequently asked questions

Can you explain Riemannian geometry?

The README does not give a tutorial on the mathematics. It states that the package works through the Riemannian geometry of symmetric positive definite matrices, and it points to a cited primer and review by Congedo, Barachant and Bhatia for the background.

Is Riemannian geometry hard?

The README does not address the difficulty of the mathematics. It presents the geometry through scikit-learn style estimators such as MDM and TangentSpace, so the numerical work is wrapped in a familiar API rather than exposed as raw manifold operations.

What is the key difference between Euclidean and Riemannian geometry?

The README does not spell out the comparison. What it does show is the consequence: pyRiemann classifies covariance matrices on the manifold of SPD matrices, and offers a tangent space projection when a vector representation is needed instead.

Official sources

  1. License: BSD-3-Clause
  2. Project website
  3. pyRiemann/pyRiemann on GitHub
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/pyriemann-pyriemann.svg)](https://hysenlabs.com/projects/pyriemann-pyriemann)