Evaluation metrics and interactive reports for synthetic ECG quality assessment
Project description
ECGEN-Eval
Evaluation metrics and interactive HTML reports for synthetic ECG quality assessment. Part of the ECGEN research suite (ECGEN-FM, ECGEN-VAE, Pulse2Pulse).
Installation
pip install -e . # core
pip install -e ".[dev]" # + pytest, black, ruff
Optional dependencies:
pip install wfdb # for loading PTB-XL / WFDB records
pip install scipy # for Welch PSD, resampling, and R-peak detection (recommended)
pip install dtaidistance # fast Cython DTW (falls back to pure-Python)
# Clinical feature extraction (Task 1.2)
pip install -e ".[features]" # installs neurokit2 + scipy
# or individually:
pip install neurokit2 scipy
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 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:
--metrics mmd prd psd psnr hrv— run a subset of metrics--feature-plots— add clinical feature extraction + 2D/3D distribution plots--fs 500— sampling frequency--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
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
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 ecgen_eval-0.5.0.tar.gz.
File metadata
- Download URL: ecgen_eval-0.5.0.tar.gz
- Upload date:
- Size: 2.3 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
94030bd88a677bfcfc540f16f0087f5259bf1dda27fd76a03192d8771b448c06
|
|
| MD5 |
8c6dfe0e835f7a799cb35655edef1049
|
|
| BLAKE2b-256 |
fa1576c3a7c659cbf8492c2cd5b3a605595ebc13dd47cf78a78ac0669f734ebd
|
Provenance
The following attestation bundles were made for ecgen_eval-0.5.0.tar.gz:
Publisher:
publish.yml on vlbthambawita/ECGEN-Eval
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ecgen_eval-0.5.0.tar.gz -
Subject digest:
94030bd88a677bfcfc540f16f0087f5259bf1dda27fd76a03192d8771b448c06 - Sigstore transparency entry: 1244566373
- Sigstore integration time:
-
Permalink:
vlbthambawita/ECGEN-Eval@191495961654baf4461c3ce163d6898614c82658 -
Branch / Tag:
refs/tags/v0.5.0 - Owner: https://github.com/vlbthambawita
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@191495961654baf4461c3ce163d6898614c82658 -
Trigger Event:
push
-
Statement type:
File details
Details for the file ecgen_eval-0.5.0-py3-none-any.whl.
File metadata
- Download URL: ecgen_eval-0.5.0-py3-none-any.whl
- Upload date:
- Size: 101.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
78e99f18681d34d2d1cbf965263053a90a4adc28f0c51fcaf75af80a59c268b1
|
|
| MD5 |
6ff1038af0c138f82750e72a44d55ce8
|
|
| BLAKE2b-256 |
58fdf1fd989aa4a1f4c63355baeceff343fe6be751ad309276b7087713fa317a
|
Provenance
The following attestation bundles were made for ecgen_eval-0.5.0-py3-none-any.whl:
Publisher:
publish.yml on vlbthambawita/ECGEN-Eval
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ecgen_eval-0.5.0-py3-none-any.whl -
Subject digest:
78e99f18681d34d2d1cbf965263053a90a4adc28f0c51fcaf75af80a59c268b1 - Sigstore transparency entry: 1244566439
- Sigstore integration time:
-
Permalink:
vlbthambawita/ECGEN-Eval@191495961654baf4461c3ce163d6898614c82658 -
Branch / Tag:
refs/tags/v0.5.0 - Owner: https://github.com/vlbthambawita
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@191495961654baf4461c3ce163d6898614c82658 -
Trigger Event:
push
-
Statement type: