CPSC 2018 (China Physiological Signal Challenge 2018)

Open Completed

Quick facts

Format12-lead · 6–144 s · 500 Hz · WFDB (.hea/.mat)
Patients
Records6,877
Leads12
LicenseCC BY 4.0
Origin11 hospitals (ICBEB, Nanjing) — China

Overview

The public training set of the China Physiological Signal Challenge 2018: 6,877 twelve-lead recordings from 11 Chinese hospitals, 500 Hz, labelled with one normal and eight abnormal classes. The challenge’s own 2,954-record test set was never released and the organisers say it will stay private, so this is the whole publicly available dataset.

Two things make it awkward in a way most 12-lead resting datasets are not:

Record length varies by a factor of 24. 6 s to 144 s, median 12 s, 1,650 distinct lengths. Only 2,416 of 6,877 records are the familiar 10 s. Any fixed window has to fit the shortest record — window=(0, 3000), 6 s at 500 Hz — and a model handed raw lengths can partly read the class off the duration, because the premature-beat classes are systematically the longest recordings.

It is multi-label, but the primary label did not survive. 470 records carry two classes and 6 carry three. CPSC’s original REFERENCE.csv distinguished First, Second and Third label; the WFDB copy everyone actually uses sorted each record’s codes by class index and dropped the distinction. See “About those counts” for what that costs.

This dataset is contained whole in Challenge 2020 and 2021. All 6,877 waveform files are byte-identical to their counterparts there, under the same names. Training on either challenge and evaluating here is testing on training data.

The nine classes

#ClassSNOMED-CTRecords (any label)Published (first label)Diff
1Normal (`NSR`)4267830069189180
2Atrial fibrillation (`AF`)1648890031,2211,098+123
3First-degree AV block (`IAVB`)270492004722704+18
4Left bundle branch block (`LBBB`)164909002236207+29
5Right bundle branch block (`RBBB`)591180011,8571,695+162
6Premature atrial contraction (`PAC`)284470004616556+60
7Premature ventricular contraction (`PVC`)164884008700672+28
8ST-segment depression (`STD`)429622005869825+44
9ST-segment elevation (`STE`)164931005220202+18
**total class-record pairs****7,359****6,877****+482**

About those counts

Every figure on this page was recomputed from the 6,877 shipped headers. The 6,877 records and the 3,699 male / 3,178 female split match the challenge’s published figures exactly. Three things do not.

The class table above shows two different quantities, and the difference is fully explained. The challenge’s Table 1 is labelled “according to the ‘First label’ annotations” and sums to 6,877 — one class per record. The “any label” column counts every class a record carries and sums to 7,359. The gap is exactly 482 = 470 × 1 + 6 × 2, the second and third labels of the 476 multi-label records. Neither column is wrong; they answer different questions. Use the any-label column — the data is multi-label and dx exposes all of it.

The primary diagnosis is not recoverable from the shipped files, so ECGBench does not pretend to have one. The challenge page documents A0043 as First label 5 (RBBB), Second label 2 (AF). Its header reads #Dx: 164889003,59118001 — AF first. The conversion sorted every #Dx list into CPSC class-index order, which is why the first code cannot be read as a primary diagnosis and why the loader’s single-label reduction is named stratify_dx, not primary_dx. It exists to make stratified folds well defined; do not train on it.

The published 60 s maximum is wrong for the shipped release. Table 1 gives min 6.00 s, max 60.00 s, mean 15.79 s over the training set. The files run 6.0 s to 144.0 s, mean 15.95 s, with 27 records (0.39%) longer than 60 s — the longest being A4133 at 144 s. Clipping the durations at 60 s reproduces the published mean of 15.79 (15.84), so the published table is consistent with a copy in which those 27 records were shorter or excluded. Size any window to the 6 s minimum regardless.

Two further details, both about age:

Where the files come from

The official icbeb.org download links are dead. ECGBench was built and verified against the Kaggle mirror, a flat Training_WFDB/ directory of 6,877 .hea/.mat pairs. That mirror is not the original MATLAB release: it is the WFDB conversion prepared for the PhysioNet/CinC Challenge 2020, so labels arrive as SNOMED-CT codes in the header #Dx field and no REFERENCE.csv ships.

The mirror publishes no checksums, so authenticity was established by comparison instead. All 6,877 .mat files are byte-identical to training/cpsc_2018/ in PhysioNet’s Challenge 2020 v1.0.2, and that release’s entire 13,761-file cpsc_2018 subtree verifies against its own published SHA256SUMS.txt (13,761 of 13,761 match). Only the headers differ, and only in formatting and the 104 railed ages described above.

If you have a Challenge 2020 or 2021 download already, you have this dataset — point --data-path at a directory holding a Training_WFDB/ with those records in it.

Validation summary (500 Hz)

VersionRecordsNote
original6,877all records, with is_valid + quality_issues
clean6,76098.3% pass rate
excluded11799 with out-of-range amplitudes, 32 with a dead lead (14 with both)

About the 117 excluded records

amplitude_range_mv is ECGBench’s standard [-10, 10] here, and the exclusions it produces are genuine defects rather than a badly chosen threshold:

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.

Folds are stratified on the rarest class each record carries, which leaves nine classes with the smallest at 220 records; nothing needs pooling into an OTHER bucket, unlike Challenge 2020. Folds are not grouped by patient, because no patient identifiers ship. The challenge describes 6,877 recordings from 11 hospitals and never mentions repeat patients, but nothing in the files proves one record per patient either, so this page leaves the patient count blank rather than guessing.

Building the splits

ecgbench splits --dataset cpsc_2018 --data-path /path/to/CPSC_2018/

Loading with ECGBench

from ecgbench import ECGDataset

# Records run 6-144 s, so a fixed window must fit the SHORTEST one.
# window= is pushed into the wfdb reader, so the 144 s records decode 6 s
# rather than all of it.
ds = ECGDataset(
    "cpsc_2018",
    split="train",
    data_path="/path/to/CPSC_2018/",
    window=(0, 3000),        # first 6 s at 500 Hz
    labels=True,
)

len(ds)                                    # 5408
ds[0]["signal"].shape                      # (12, 3000)
ds[0]["record_id"]                         # 'A0001'
ds[0]["labels"]["dx"]                      # '59118001'
ds[0]["labels"]["dx_abbreviations"]        # 'RBBB'
ds[0]["labels"]["dx_names"]                # 'Right bundle branch block'
ds[0]["labels"]["duration_seconds"]        # 15.0  — the FULL record
ds[0]["labels"]["age"], ds[0]["labels"]["sex"]   # '74', 'Male'

# Multi-label: build the 9-way target from dx_abbreviations, not from
# stratify_dx_abbreviation (which is a rarest-class reduction for folds).
from ecgbench.labels.cpsc_2018 import CPSC_CLASSES
names = [abbr for _, _, abbr, _ in CPSC_CLASSES]
y = [int(n in ds[0]["labels"]["dx_abbreviations"].split(",")) for n in names]
dict(zip(names, y))
# {'NSR': 0, 'AF': 0, 'IAVB': 0, 'LBBB': 0, 'RBBB': 1,
#  'PAC': 0, 'PVC': 0, 'STD': 0, 'STE': 0}

# Standard lead order and standard spelling — verified in all 6,877 headers.
ds.config.lead_names
# ['I','II','III','aVR','aVL','aVF','V1','V2','V3','V4','V5','V6']