Shandong Provincial Hospital ECG Database (SPHDB)

Open Completed

Quick facts

Format12-lead · 10–56 s · 500 Hz · HDF5
Patients24,666
Records25,770
Leads12
LicenseCC BY 4.0
OriginShandong Provincial Hospital — China

Overview

25,770 twelve-lead clinical ECGs from 24,666 patients recorded at Shandong Provincial Hospital between August 2019 and August 2020 — the largest single-source ECG dataset in this catalogue, and the only one whose labels are a published clinical standard rather than a vocabulary invented for the release.

Three things make it distinctive:

The labels are AHA/ACC/HRS standardised diagnostic statements. 44 primary statements across 11 categories, each optionally qualified by one or more of 15 modifiers, so a record’s ground truth reads as 60+310;147 — frequent ventricular premature complexes, plus T-wave abnormality. The vocabulary is closed and fully documented in the shipped code.csv; every code in every one of the 25,770 records resolves against it. 3,724 records (14.45%) carry more than one statement.

It is the only HDF5 dataset in ECGBench. Each record is one .h5 file holding a single (12, N) float16 array named ecg, already in millivolts. Loading needs pip install ecgbench[hdf5]; nothing else about the API changes, and window= is pushed into h5py’s own slicing so a 56 s record decodes only the samples you ask for.

Patients repeat, and the folds account for it. 1,066 of the 24,666 patients contributed between two and five recordings, covering 2,170 records. ECGBench’s folds are grouped on patient_id, so no patient appears in two folds — verified, not assumed.

Record length varies from 10 s to 56 s in 39 distinct lengths, and the metadata’s N column gives it exactly for every record, so nothing has to open a signal file to learn a length. Age (18–95) and sex are complete for all 25,770 rows with no sentinels and no blanks.

AHA statement categories

CategoryMeaningStatementsRecords carrying ≥1
ANormal interpretation113,905
CSinus node rhythms34,643
DSupraventricular premature/escape complexes4622
ESupraventricular arrhythmias3787
FVentricular premature complexes11,067
HAV conduction9370
IIntraventricular/intra-atrial conduction62,195
JAxis and voltage3612
KChamber hypertrophy and enlargement3229
LST, T and U abnormalities75,125
MMyocardial infarction4260
**total****44****29,815 statement-record pairs**

About those counts

Every figure on this page was recomputed from the shipped metadata.csv joined against code.csv, and from the 25,770 HDF5 arrays themselves. The headline figures agree with the paper: 25,770 records, 24,666 patients, 55.36% male (14,265 M / 11,505 F), 14.45% multi-statement (3,724 records) and 46.04% carrying an abnormality (11,865 records).

Three things worth knowing about how those numbers were derived:

The category table is multi-label and does not sum to 25,770. It counts every category a record carries, so the 29,815 statement-record pairs exceed the record total by 4,045 — the second and later statements of the 3,724 multi-statement records. Categories B, G and N of the AHA standard have no statements in this release at all.

“Normal” is a code, and two records list it twice. 13,905 records carry primary code 1 (Normal ECG) and nothing else. Only 13,903 have an AHA_Code cell equal to the string 1, because A02322 and A05000 read 1;1 — the same statement repeated. ECGBench deduplicates the primary-code list, which is what makes is_normal give 13,905 and reproduce the paper’s 46.04% abnormal exactly; a string comparison misses by two.

There is no primary diagnosis, so ECGBench does not pretend to have one. Nothing in the release ranks a record’s statements. The order is nearly a numeric sort of the primary code — 24,961 of 25,770 records are in ascending order, and 2,915 of the 3,724 multi-statement ones — so the first statement is neither reliably a sort artefact nor a documented priority. The loader’s single-label reduction is therefore called stratify_code, not primary_code: it takes the globally rarest code each record carries, exists to make stratified folds well defined, and is not ground truth. Train on aha_primary_codes.

One small disagreement with the paper: it gives the age range as 18–100, where the shipped maximum is 95. The minimum, 18, matches — this is an adults-only cohort and nothing here supports paediatric use.

Where the files come from

The release ships four files: metadata.csv, code.csv, records.tar.gz (2.3 GB) and two PDFs documenting the coding rules. figshare serves them as separate downloads rather than one archive, so there is no single URL to auto-download from — pass --data-path.

records.tar.gz is an uncompressed tar despite the name. Extract it with tar -xf records.tar.gz; tar -xzf fails with “not in gzip format”. It expands to records/A00001.h5 … one file per record.

No checksums ship, so authenticity was established by internal consistency instead. On the copy ECGBench was built against:

Validation summary (500 Hz)

VersionRecordsNote
original25,770all records, with is_valid + quality_issues
clean25,44798.7% pass rate
excluded323all 323 fail amplitude_outlier and nothing else

About the 323 excluded records

amplitude_range_mv is ECGBench’s standard [-10, 10] here, and the exclusions are genuine artefacts rather than a badly chosen threshold: the median per-record peak amplitude across the whole dataset is 1.74 mV, so a record reaching past 10 mV has a railed or artefact-dominated lead.

No record fails any other check: no NaN samples, no dead or flat leads, no unreadable files, in all 25,770.

Fold membership is identical between original/ and clean/clean/ is a row subset, not a re-split — so a model trained on clean/ can be scored against original/ for the same fold without re-partitioning.

Splits

SplitFoldscleanoriginal
train1–820,36420,616
val92,5422,577
test102,5412,577

How the folds were made

Ten folds, grouped on patient_id and stratified on stratify_code, the rarest AHA primary code each record carries. The 9 codes with fewer than ten records (49 records in total) are pooled into an OTHER bucket first, so ten-fold stratification stays well defined.

What that produced, measured on the output:

Rarest-first stratification is used because there is no primary diagnosis to stratify on instead. It keeps all 44 codes representable, where taking the first listed statement collapses to 40 classes and leaves 12 below ten records rather than 9.

Building the splits

# records.tar.gz is an UNCOMPRESSED tar despite the name.
tar -xf records.tar.gz

ecgbench splits --dataset sph --data-path /path/to/SPH/

Loading with ECGBench

# pip install ecgbench[torch,hdf5]   <- h5py is needed for this dataset
from ecgbench import ECGDataset

# Records run 10-56 s, so a fixed window must fit the SHORTEST one.
# window= is pushed into h5py's slicing, so a 56 s record decodes 10 s.
ds = ECGDataset(
    "sph",
    split="train",
    data_path="/path/to/SPH/",
    window=(0, 5000),        # first 10 s at 500 Hz
    labels=True,
)

len(ds)                                        # 20364
ds[0]["signal"].shape                          # (12, 5000)
ds[0]["record_id"]                             # 'A00001'
ds[0]["labels"]["patient_id"]                  # 'S00001'
ds[0]["labels"]["aha_code"]                    # '22;23'  — as shipped
ds[0]["labels"]["aha_primary_codes"]           # '22;23'  — ground truth
ds[0]["labels"]["aha_primary_descriptions"]    # 'Sinus bradycardia;Sinus arrhythmia'
ds[0]["labels"]["aha_primary_categories"]      # 'C'
ds[0]["labels"]["aha_modifier_codes"]          # ''  — this record has none
ds[0]["labels"]["duration_seconds"]            # 10.0  — the FULL record
ds[0]["labels"]["age"], ds[0]["labels"]["sex"] # 55, 'M'

# Multi-label: build the 44-way target from aha_primary_codes, not from
# stratify_code (a rarest-code reduction that exists only for the folds).
from ecgbench.labels.sph import load_code_table
codes = load_code_table("/path/to/SPH/")
primaries = list(codes.index[~codes["is_modifier"]])
carried = ds[0]["labels"]["aha_primary_codes"].split(";")
y = {c: int(c in carried) for c in primaries}
sum(y.values())                                # 2

# Standard lead order and standard spelling — derived from the signals.
ds.config.lead_names
# ['I','II','III','aVR','aVL','aVF','V1','V2','V3','V4','V5','V6']

# Samples are already millivolts, so signal_unit_scale is 1.0.
ds.config.signal_unit_scale                    # 1.0