PTB Diagnostic ECG Database

Open Completed

Quick facts

Format15-lead (12 + 3 Frank) · 32-120 s · 1,000 Hz
Patients290
Records549
Leads15
LicenseODC-By 1.0
OriginUniv. Clinic Benjamin Franklin — Germany — Berlin

Overview

549 records from 290 subjects, recorded at the Benjamin Franklin University Clinic in Berlin. This is the most unusual dataset in the catalogue on three counts, all verified against the files.

15 signals, not 12. The conventional leads plus the three Frank vectorcardiography leads vx, vy, vz. Leads 1-12 live in the .dat file and 13-15 in a companion .xyz; wfdb reads both as one record. Names are lowercase in every header. ECGDataset(leads=[...]) takes the standard twelve by name.

Records are variable length — 11 distinct lengths between 32 s and 120 s, at 1000 Hz. They cannot be batched as they are: torch cannot stack tensors of differing width. Take a fixed window=(start, length), or use batch_size=1. For the same reason expected_samples is deliberately empty in the config, which disables the truncation check rather than failing every short record.

No metadata file ships at all. Every clinical field is in the per-record .hea comment block — 47 keys covering the admission diagnosis, infarction localisation, a full haemodynamics panel, coronary stenosis findings and the therapy history. ECGBench parses them into a metadata CSV on first run.

113 of the 290 patients contributed more than one recording (up to seven), so folds are grouped by patient. No patient spans a fold.

Admission diagnosis

DiagnosisRecordsPatients
Myocardial infarction368148
Healthy control8052
(none recorded)2722
Cardiomyopathy1715
Bundle branch block1715
Dysrhythmia1614
Hypertrophy77
Valvular heart disease66
Myocarditis44
Stable angina22
Heart failure (NYHA 2/3/4)1 each1 each
Palpitation11
Unstable angina11

About those counts

Recomputed from the # Reason for admission: field of all 549 headers. Single-label — one diagnosis per record, and no patient has conflicting diagnoses across their recordings, so the record and patient columns are both meaningful and neither sums to a multi-label total.

Two disagreements with the README, worth knowing:

27 records carry no diagnosis (n/a in the header). ECGBench labels those UNKNOWN and, because seven classes have fewer than 10 records, pools those into OTHER for stratification. primary_diagnosis is that pooled view — train on diagnosis, which is verbatim.

Validation summary (1000 Hz)

VersionRecordsNote
original549all records, with is_valid + quality_issues
clean54699.5% pass rate
excluded32 amplitude outliers past ±15 mV, 1 with flat leads

Building the splits

ecgbench splits --dataset ptbdb --data-path /path/to/ptbdb/1.0.0/

Loading with ECGBench

from ecgbench import ECGDataset

# Variable-length records: take a fixed window to allow batching, and the
# standard twelve leads by name out of the fifteen stored. window= is read
# at load time, so the other 22-110 s are never decoded.
STANDARD_12 = ["i", "ii", "iii", "avr", "avl", "avf",
               "v1", "v2", "v3", "v4", "v5", "v6"]

ds = ECGDataset(
    "ptbdb",
    split="train",
    data_path="/path/to/ptbdb/1.0.0/",
    leads=STANDARD_12,
    window=(0, 10_000),                  # first 10 s at 1000 Hz
    labels=True,
)

ds[0]["signal"].shape                 # (12, 10000)
ds[0]["labels"]["diagnosis"]          # 'Myocardial infarction'
ds[0]["labels"]["age"]                # 77.0

# Without leads= you get all 15 signals including vx, vy, vz; without the
# window, a DataLoader raises as soon as a batch mixes two record lengths.