ECGEN-Eval
Evaluation metrics and interactive HTML reports for synthetic ECG quality assessment. Part of the ECGEN research suite (ECGEN-FM, ECGEN-VAE, Pulse2Pulse).
📖 Documentation: https://vajira.info/ECGEN-Eval/
Installation
Install the latest release from PyPI:
pip install ecgen-eval
Optional extras:
pip install "ecgen-eval[features]" # clinical feature extraction (neurokit2 + scipy)
pip install "ecgen-eval[fed]" # Fréchet ECG Distance (torch, transformers, huggingface_hub)
pip install "ecgen-eval[features,fed]" # both
Other optional packages:
pip install wfdb # for loading PTB-XL / WFDB records
pip install scipy # for Welch PSD, resampling, and R-peak detection (recommended)
To work on ECGEN-Eval itself, install from source instead; see the contributing guide.
Quick start
import numpy as np
from ecgen_eval import load_ecg_npy, AVAILABLE_METRICS
# Load model outputs (shape: N × L × T)
real = load_ecg_npy("real_ecgs.npy", label="PTB-XL")
synth = load_ecg_npy("synth_ecgs.npy", label="ECGEN-FM")
# Compute a metric
mmd = AVAILABLE_METRICS["mmd"]()
result = mmd.compute(real.data, synth.data, lead_names=real.lead_names)
print(f"MMD = {result.score:.6f}")
print(result.per_lead_scores)
Data loaders
| Function | Input | Use case |
|---|---|---|
load_ecg_npy(path) |
.npy / .npz (N, L, T) |
ECGEN-FM, ECGEN-VAE, Pulse2Pulse outputs |
load_ecg_wfdb(dir) |
WFDB .hea/.dat records |
PTB-XL and PhysioNet datasets |
load_ecg_folder(path) |
auto-detected | Any of the above + CSV |
load_ecg_folder auto-detects the format based on file extensions.
Metrics
All metrics are available via AVAILABLE_METRICS["key"]() and return a MetricResult
with .score, .per_lead_scores, and .extra.
Original metrics
| Key | Name | Lower / Higher | Paper |
|---|---|---|---|
mmd |
Maximum Mean Discrepancy | Lower ↓ | Gretton et al., JMLR 2012 |
dtw |
Dynamic Time Warping | Lower ↓ | Berndt & Clifford, KDD 1994 |
prd |
Percent Root-mean-square Difference | Lower ↓ | Zigel et al., IEEE TBME 2000 |
psd |
PSD Divergence (Jensen-Shannon) | Lower ↓ | Welch, IEEE Trans Audio 1967 · Applied: Golany & Radinsky, AAAI 2019 |
fd |
Fréchet Distance | Lower ↓ | Heusel et al., NeurIPS 2017 · Applied: Thambawita et al., Sci Rep 2021 |
New metrics (literature review 2018 – 2025)
| Key | Name | Lower / Higher | Paper |
|---|---|---|---|
psnr |
Peak Signal-to-Noise Ratio | Higher ↑ | Huynh-Thu & Ghanbari, Electron Lett 2008 |
ssim |
Structural Similarity Index (1-D) | Higher ↑ | Wang et al., IEEE TIP 2004 · ECG: Hayn et al., Physiol Meas 2018 |
swd |
Sliced Wasserstein Distance | Lower ↓ | Rabin et al., LNCS 2011 · Kolouri et al., NeurIPS 2019 |
mae |
Mean Absolute Error (nearest-neighbour) | Lower ↓ | Zigel et al., IEEE TBME 2000 |
hrv |
Heart Rate Variability (SDNN/rMSSD/pNN50) | Lower ↓ | Task Force ESC/NASPE, Circulation 1996 |
pr_dist |
Distribution Precision & Recall | Higher ↑ | Kynkäänniemi et al., NeurIPS 2019 |
sqi |
Signal Quality Index (QRS template) | Higher ↑ | Clifford et al., CinC 2017 |
nn_dist |
Nearest-Neighbour Distance (memorisation) | Lower ↓ | Meehan et al., AISTATS 2020 |
heart_rate |
Heart Rate Distribution (JS divergence) | Lower ↓ | Pan & Tompkins, IEEE TBME 1985 · Applied: Golany & Radinsky, AAAI 2019 |
spectral_entropy |
Spectral Entropy Divergence | Lower ↓ | Inouye et al., EEG Clin Neurophysiol 1991 |
Feature Extraction
Extract 10 clinical ECG features per recording and compare their distributions between real and synthetic datasets.
from ecgen_eval import load_ecg_npy
from ecgen_eval.features import extract_features
real = load_ecg_npy("real_ecgs.npy", label="PTB-XL")
synth = load_ecg_npy("synth_ecgs.npy", label="ECGEN-FM")
# Returns (beat_df, summary_df)
_, real_feat = extract_features(real)
_, synth_feat = extract_features(synth)
print(real_feat[["rr_mean_ms", "hr_bpm", "qrs_duration_ms",
"qt_interval_ms", "hrv_sdnn_ms"]].describe())
Extracted features (one value per recording):
| Feature | Column | Description |
|---|---|---|
| RR interval | rr_mean_ms, rr_std_ms |
Mean and std of peak-to-peak intervals |
| Heart rate | hr_bpm |
60 000 / mean(RR) |
| PR interval | pr_interval_ms |
P-onset to QRS-onset |
| QRS duration | qrs_duration_ms |
Width of QRS complex |
| QT interval | qt_interval_ms |
QRS-onset to T-end |
| QTc (Bazett) | qtc_bazett_ms |
QT / √RR(s) |
| ST deviation | st_deviation_mv |
Mean ST voltage vs isoelectric line |
| T-wave amplitude | t_amplitude_mv |
Signed T-peak amplitude |
| P-wave duration | p_wave_duration_ms |
P-onset to P-offset |
| HRV | hrv_sdnn_ms, hrv_rmssd_ms, hrv_pnn50 |
SDNN, RMSSD, pNN50 |
2D and 3D distribution plots
from ecgen_eval.visualization.feature_plots import (
plot_feature_2d,
plot_feature_pair_grid,
plot_feature_3d,
plot_feature_triple_grid,
)
# Single 2D scatter + KDE contour
fig = plot_feature_2d(real_feat, synth_feat, "rr_mean_ms", "hr_bpm")
fig.show()
# Grid of default feature pairs
fig = plot_feature_pair_grid(real_feat, synth_feat)
fig.show()
# Interactive 3D scatter
fig = plot_feature_3d(real_feat, synth_feat, "rr_mean_ms", "hr_bpm", "qrs_duration_ms")
fig.show()
CLI
# Run all metrics (except fed) and generate interactive HTML report
ecgen-eval eval \
--real /path/to/real_ecgs.npy \
--synthetic /path/to/model1_ecgs.npy \
--synthetic /path/to/model2_ecgs.npy \
--output report.html
# Include clinical feature distributions in the report (requires neurokit2)
ecgen-eval eval \
--real /path/to/real_ecgs.npy \
--synthetic /path/to/synth_ecgs.npy \
--feature-plots \
--output report.html
# Extract features only — save summary CSV
ecgen-eval features \
--real /path/to/real_ecgs.npy \
--synthetic /path/to/synth_ecgs.npy \
--output features.csv
# List available metrics
ecgen-eval list-metrics
# ECG waveform comparison only (no metrics)
ecgen-eval visualize \
--real /path/to/real.npy \
--synthetic /path/to/synth.npy \
--compare-mode overlay
Key options for eval:
-m mmd -m prd -m psd— run a subset of metrics (repeat-mper metric; default: all exceptfed)--feature-plots— add clinical feature extraction + 2D/3D distribution plots--fs 500— sampling frequency (also passed topsd,spectral_entropy,hrv,heart_rate)--n-samples 5— ECG samples shown in gallery--offline— embed Plotly.js for offline viewing--max-samples 500— cap loaded recordings per folder
Report
The HTML report includes:
- Dataset summary table
- Interactive ECG gallery (overlay / side-by-side / grid)
- Per-metric: score table, per-lead bar chart, PSD overlay (PSD metric), violin plot (DTW)
- Radar chart summarising all metrics
- Metrics summary table with Download CSV button
- Clinical Feature Summary table (mean ± std, real vs synthetic) — with
--feature-plots - 2D feature distribution plots (scatter + KDE contours) — with
--feature-plots - 3D feature distribution plots (interactive rotation) — with
--feature-plots - References / citations
Running tests
pytest tests/ -v
Project structure
ecgen_eval/
├── data/
│ ├── dataset.py ECGDataset container
│ └── loader.py npy / WFDB / CSV loaders
├── features/ Clinical ECG feature extraction (optional: neurokit2)
│ ├── __init__.py Exports FeatureExtractor, extract_features
│ ├── extractor.py FeatureExtractor class → (beat_df, summary_df)
│ ├── qrs_detection.py R-peak detection (neurokit2 primary, scipy fallback)
│ ├── interval_features.py RR, HR, PR, QRS, QT, QTc, P-wave duration
│ ├── amplitude_features.py ST deviation, T-wave amplitude
│ ├── hrv_features.py SDNN, RMSSD, pNN50
│ └── utils.py Bandpass filter, peak helpers
├── metrics/
│ ├── base.py BaseMetric + MetricResult
│ ├── mmd.py MMD
│ ├── dtw.py DTW
│ ├── prd.py PRD
│ ├── psd.py PSD divergence
│ ├── fd.py Fréchet Distance
│ ├── psnr.py PSNR
│ ├── ssim.py 1-D SSIM
│ ├── swd.py Sliced Wasserstein Distance
│ ├── mae.py MAE / RMSE
│ ├── hrv.py HRV statistics (SDNN, rMSSD, pNN50)
│ ├── pr_dist.py Distribution Precision & Recall
│ ├── sqi.py Signal Quality Index
│ ├── nn_dist.py Nearest-Neighbour Distance
│ ├── heart_rate.py Heart Rate Distribution
│ └── spectral_entropy.py Spectral Entropy Divergence
├── visualization/
│ ├── ecg_paper.py ECG graph-paper figure factory
│ ├── ecg_waveform.py Waveform comparison plots
│ ├── metric_plots.py Bar, violin, PSD overlay, radar
│ └── feature_plots.py 2D/3D clinical feature distribution plots
├── report/
│ └── html_report.py HTML report generator
└── cli.py Click CLI entry point (eval, visualize, features, list-metrics)
tests/
├── conftest.py
├── test_dataset.py
├── test_loader.py
├── test_metrics.py
└── test_features.py
docs/
└── ecg_features_review.md Literature review of the 10 clinical features
Metadata
Release files for ecgen-eval 0.6.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| ecgen_eval-0.6.0.tar.gz | 3.3 MB | With provenance | Details for ecgen_eval-0.6.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| ecgen_eval-0.6.0-py3-none-any.whl | Python 3 | none | any | Details for ecgen_eval-0.6.0-py3-none-any.whl |
Total release size: 3.5 MB
Release files / ecgen_eval-0.6.0.tar.gz
| Download URL | ecgen_eval-0.6.0.tar.gz |
|---|---|
| Size | 3.3 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
cfd472afe6b93f2b32c21cdcd3d0b1cf41265ed4c8fe27cf8b1c07b4cf56ea3b
|
|
BLAKE2b-256 checksum How to use checksums |
7a08792ef31d54e8c0b60e6cac25410ac12611ffb68d1bb4fa0876d596bd9686
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 28, 2026.
Transparency logRelease files / ecgen_eval-0.6.0-py3-none-any.whl
| Download URL | ecgen_eval-0.6.0-py3-none-any.whl |
|---|---|
| Size | 103.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
dca9300f54a83de2ba5aa44778f9de38bceab8fec6a234d7c15e3194b748efb2
|
|
BLAKE2b-256 checksum How to use checksums |
011a3aa3a82094874d6f4cf1dca05377b2789f5de2bdafa422e5d4dc831ac62d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 28, 2026.
Transparency logSecurity for ecgen-eval 0.6.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for ecgen_eval-0.6.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 28, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.