| Format | 12-lead · 10.24 s · 400 Hz · HDF5 |
|---|---|
| Patients | 1,631 |
| Records | 1,631 |
| Leads | 12 |
| License | CC BY 4.0 |
| Origin | UFMG; Uppsala University; EPFL — Brazil / Sweden / Switzerland |
1,631 twelve-lead ECGs from SaMi-Trop, an NIH-funded prospective cohort of patients with chronic Chagas cardiomyopathy recruited in northern Minas Gerais, Brazil, and followed for mortality. It is the third release in this catalogue from the Telehealth Network of Minas Gerais after CODE-15% and CODE-test, and the only one of the three that is not a classification dataset.
A record is a row, not a file. One HDF5 file holds a single
(1631, 4096, 12) array, so a signal reference reads
exams.hdf5:tracings:417. Loading needs pip install ecgbench[hdf5].
There is no diagnostic vocabulary at all. Unlike its two siblings,
which share this exact file format, SaMi-Trop ships no abnormality flags.
The only ECG label is a binary normal_ecg. What it ships instead is the
thing the dataset exists for: complete mortality follow-up for every
one of the 1,631 patients, with 104 deaths over a median 2.07 years.
normal_ecg is not a healthy control. Every patient here already has
chronic Chagas cardiomyopathy, so a normal tracing means a normal tracing
in a diseased patient. The 286 normal records are not usable as healthy
controls — the mistake to avoid when pooling this release with others. And
a record that is not normal carries no statement of what is wrong with
it.
One recording per patient, because the release is each patient’s first
exam. So patient_id_column is null and the folds are a plain stratified
split — the rare case in this catalogue where that is genuinely safe rather
than merely unchecked. There is no patient identifier in the release at
all, which is sound given one record each.
Its lead order is the standard I, II, III, aVR, aVL, aVF, checked
against the arrays rather than assumed, because the sibling CODE-test
release from the same network is not standard.
| Field | Value | Note |
|---|---|---|
| `normal_ecg` | 286 of 1,631 (17.5%) | the **only** ECG label |
| not normal | 1,345 (82.5%) | no statement of *what* is abnormal |
| `death` | 104 (6.4%) | complete — no missing outcomes |
| `followup_years` | median 2.07 (0.07-3.39) | time to death or censoring |
| `age` | 26-98, median 59 | whole years |
| `is_male` | 534 M / 1,097 F | 67.3% female |
| `nn_predicted_age` | 22.6-95.9 | **a model output**, not an observation |
Every figure on this page was recomputed from the shipped exams.csv,
whose md5 matches Zenodo’s published value
(6c9007a0427f7c3d9e1b6fb091231a67) exactly, as does exams.zip
(a7b65f115b0222ad2ecbc6a422496fdd). exams.hdf5 has no published
checksum of its own — it ships inside exams.zip, which does.
The headline figures agree with the release: 1,631 records, 1,631 patients. Note that the cohort is 1,959 patients; this release is the first ECG of the 1,631 who have one, and the release does not say how the 328 absent patients differ.
The CSV is joined to the waveforms by row position, and nothing else
could be. exams.hdf5 contains only a tracings array — no exam_id
dataset, unlike CODE-15%’s parts. That is exactly the situation in which
CODE-15% turned out not to be in file order, so the alignment was
tested rather than assumed: QRS amplitude is reliably larger in men, and
splitting the precordial peak-to-peak by the CSV’s own is_male in row
order gives a Welch t of 4.98, where the largest |t| over 2,000 random
permutations of the same rows is 3.44. The aligned ordering beats every
permutation. ECGBench asserts the 1,631-row count on every run, because a
positional join against a file of the wrong length produces 1,631
confidently wrong rows rather than an error.
The stratification label is not a diagnosis. stratify_class is
mortality-first — DEATH (104), NORMAL (283), ABNORMAL_ALIVE (1,244) —
so the rare outcome is what the folds balance. It is three classes and not
the death × normal_ecg cross because only 3 records are both dead and
normal, and a 3-member class cannot be spread over ten folds at all. Never
train on it.
Amplitudes are noisy, like its siblings’. The median per-record peak is 4.3 mV, so the validation range is ±20 mV rather than ECGBench’s usual ±10, matching CODE-15% because these are the same telehealth instruments. ±10 would exclude 180 records (11.0%); ±20 excludes 14 (0.9%).
| Version | Records | Excluded | Why |
|---|---|---|---|
| `original` | 1,631 | — | all records, with `is_valid` and `quality_issues` |
| `clean` | 1,615 | 16 | 14 `amplitude_outlier` (a lead beyond ±20 mV), 2 `missing_leads` (a lead recorded as exactly zero) |
from ecgbench import ECGDataset
# Fold CSVs come from the Hub; signals and labels from your local copy.
ds = ECGDataset(
"sami_trop",
split="train",
data_path="/path/to/SaMi-Trop/",
labels=True,
# The shortest real signal in the release is 1,568 samples, so a
# wider window risks reading only zero padding on those records.
window=(0, 1568),
)
print(len(ds)) # 1290
s = ds[0]
print(tuple(s["signal"].shape)) # (12, 1568)
print(s["record_id"]) # 3629
print(s["labels"]["normal_ecg"]) # False
print(s["labels"]["death"]) # False
print(s["labels"]["followup_years"]) # 1.93...
print(s["labels"]["stratify_class"]) # ABNORMAL_ALIVE
# A survival target, which is what this release supports.
df = ds.labels_df
print(int(df["death"].sum()), "deaths of", len(df)) # 81 deaths of 1290
# Lead order is standard here and NOT in the sibling CODE-test release,
# so select by name whenever you use both.
three = ECGDataset("sami_trop", split="train",
data_path="/path/to/SaMi-Trop/",
window=(0, 1568), leads=["aVR", "aVL", "aVF"])
print(tuple(three[0]["signal"].shape)) # (3, 1568)