MIT-BIH ST Change Database

Open Completed

Quick facts

Format2-lead on 18 records, 1-lead on 10 (ECG1/ECG2, both unnamed) · 13.1–67.2 min · 360 Hz · WFDB · mostly exercise stress ECGs
Patients
Records28
Leads2
LicenseODC Attribution
OriginMIT — USA

Overview

28 short ECG recordings, most of them exercise stress tests, selected because they exhibit transient ST change — 13.49 hours at 360 Hz, digitised for Paul Albrecht’s 1983 MIT master’s thesis and published by PhysioNet in 1999. The last five records (323–327) are excerpts of long-term ambulatory recordings and show ST elevation; the rest show transient ST depression.

It is the smallest and thinnest release in this catalogue. There is no metadata file, no clinical table, and no header comment line of any kind — no age, no sex, no diagnosis, no recording date and no subject identifier anywhere in the 142 shipped files. Everything ECGBench exposes is derived from the signal headers and the reference beat annotations, except the exercise/long-term grouping, which is transcribed from one sentence of the landing page and flagged as such by a group_source column.

All 142 files verify against the release’s own SHA256SUMS.txt, all 28 records pass every ECGBench quality check, and clean/ therefore equals original/ — which is the exception rather than the rule among the MIT-BIH-family Holter datasets here.

The name promises ST annotations. There are none.

This is the single most important thing to know about the database, and its name works against you. PhysioNet states it on the landing page — the annotation files “contain only beat labels; they do not include ST change annotations, as in the European ST-T Database” — and the files bear it out: 76,175 of the 76,181 annotations in the 28 .atr files are beat labels, the other six are signal-quality markers, and there is not a single + rhythm marker, s ST episode or non-empty aux_note anywhere in the release.

There is no ST measurement, no episode boundary, no onset, no deviation in millivolts and no per-record ST label of any kind. What this release offers is 28 recordings selected for ST change, with reference beats.

Want Use
Annotated ST episodes with onset, extent and peak deviation European ST-T Database — 802 episodes
ST episodes over 24-hour records Long-Term ST Database
Recordings selected for ST change, with reference beats this database

The st_change_type label ECGBench exposes is the landing page’s own grouping of its records, transcribed. It is not derived from the signals and it is not an ST measurement — group_source says landing_page on every row so a reader of the CSV can tell without this page in front of them.

Ten of the 28 records hold one channel, not two

Nothing on the landing page mentions it, and the “2 leads” in the table above is true of 18 records. Records 313, 314, 315, 316, 317, 319, 320, 321, 322 and 323 declare a single signal.

The consequence is immediate rather than theoretical: ecg_collate_fn stacks signals with torch’s default_collate, so a batch drawing from both layouts raises

RuntimeError: stack expects each tensor to be equal size,
              but got [2, 10800] at entry 0 and [1, 10800] at entry 2

The fix is to ask for a lead every record has:

ECGDataset("stdb", split="train", leads=["ECG1"], window=(0, 10_800))

The config declares alternate_lead_names: {1: ["ECG1"]}, which is what makes leads=["ECG2"] raise for those ten records against a named layout instead of silently returning ECG1.

ECG1/ECG2 are channel positions, not lead names. Every signal line of every header ends in the bare description ECG, and the release states no electrode placement anywhere. Do not read them as MLII/V1 by analogy with mitdb — the temptation is strongest here because this release shares mitdb’s 360 Hz sampling rate and its three-digit record numbering, and nothing whatsoever supports it.

The 28 records, recomputed from the files

RecChMinGroupSTBeatsSVHR baseHR peakHR riseGain (adu/mV)Fold
300224.9exercisedepression2558029311421.2296 / 3006
301232.4exercisedepression24971115712769.9295 / 3008
302223.6exercisedepression2113006212461.8295 / 2131
303233.9exercisedepression30055168610115.1457 / 2919
304230.3exercisedepression185200548329.5292 / 3203
305213.1exercisedepression103622655711558.0204 / 2285
306267.2exercisedepression65270065180114.8204 / 2292
307236.8exercisedepression2469105810243.7176 / 1821
308229.4exercisedepression229979145711861.4296 / 27510
309241.4exercisedepression5149018517690.4206 / 2753
310219.0exercisedepression2410019417984.9296 / 1614
311230.5exercisedepression3009007715881.0178 / 2057
312227.9exercisedepression23401106114180.6298 / 1822
313123.2exercisedepression27012079182103.42951
314126.1exercisedepression2121516611245.62068
315126.2exercisedepression3274018016180.22315
316125.7exercisedepression3351019418994.52002
317127.7exercisedepression2776506916192.12656
318227.1exercisedepression3531019117079.1319 / 2704
319123.6exercisedepression2559008716476.62157
320132.2exercisedepression3135207915575.82233
321122.9exercisedepression2115007313158.216310
322113.2exercisedepression1508309213441.73669
323142.9**long-term****elevation**5290008417288.03624
324230.1**long-term****elevation**1740363467670.0300 / 3008
325221.2**long-term****elevation**14650076793.1300 / 4007
326236.6**long-term****elevation**2075336458667.9300 / 5005
327219.9**long-term****elevation**127000558227.7300 / 4006

About those counts

Nothing in this release publishes a table to disagree with — there is no paper table, no per-record listing on the landing page and no shipped metadata — so every figure above is computed from the files rather than reconciled against a published one. Three things it shows that the release does not state:

The heart rate contradicts the grouping for one record. The landing page names the five long-term excerpts per record and leaves the other 23 to the word “most”, so the exercise assignment is by exclusion. hr_rise_bpm (peak minus opening 60-second mean) is the measurable check, and an exercise test has a characteristic ramp-and-recover shape. Records 324, 325 and 326 rise 0.0, 3.1 and 7.9 bpm and never pass 79 bpm — flat, as an ambulatory excerpt should be. Record 323 ramps 84 → 172 bpm and is still at 117 bpm in its final minute, which looks nothing like the other four, and it is also the only single-channel record among them. Within the exercise group the rise runs 15.1–114.8 bpm, weakest at 303 (15.1), 300 (21.2) and 304 (29.5) — the release’s “most” showing up in the data. ECGBench still labels by the landing page; the column is how you check it.

Ectopy is almost absent, and what there is sits in three records. 1,137 of 76,175 beats are not normal. Record 305 holds 265 of the release’s 322 V, and 324 and 326 together hold 699 of its 815 S. Nine records have no ectopic beat at all and nineteen have fewer than five, so there is no arrhythmia task here and no usable ectopy stratification.

The gain is per record and per channel — 31 distinct values from 161 to 500 adu/mV, with record 326 declaring 300 for one channel and 500 for the other, and no two records sharing a full pair. wfdb applies each record’s own, so signals arrive in millivolts regardless; it matters because the 12-bit rail moves with it, which is what amplitude_range_mv is computed from (±2047/161 = ±12.71 mV, rounded outward and never attained — the widest amplitude in the release is +6.94 mV in record 302).

Beat annotation is effectively complete: it starts 0.2–1.0 s into each record and ends 0.1–0.9 s before its end, covering 99.77–99.98% of every record. There is no multi-hour unannotated tail to window around, unlike nsrdb. Signal quality is annotated in exactly one record — 319 carries the release’s only six ~ markers, and 86.2% of it is marked not-clean — so the absence of noise markers in the other 27 means nobody marked them, not that the signal is clean.

The default val and test splits hold two records each

28 records over 10 folds gives eight folds of three and two of two, and the default 8/1/1 layout therefore makes val and test two records each. Both happen to be exercise/depression records, so neither default evaluation split contains a single ST-elevation record. That is arithmetic, not a defect: there are only five elevation records in the whole release.

If you need the elevation group represented in evaluation, do not use the default split — pass split=None with fold_numbers to select by fold across the whole partition, or run your own cross-validation over folds.csv:

ECGDataset("stdb", split=None, fold_numbers=[4, 5, 6, 7, 8])

Folds are stratified on the ST-change group crossed with the channel count (depression_2ch 14, depression_1ch 9, elevation_2ch 4, elevation_1ch 1), so every fold holds at least one two-channel record and nine of the ten hold a single-channel record. There is no patient grouping, and here that is a gap rather than a choice: the release identifies its subjects in no way at all, so whether any two of these 28 recordings came from the same person is unknowable from the files. A record-level split is the strongest guarantee available.

Loading with ECGBench

from ecgbench import ECGDataset

# leads=["ECG1"] is what makes this batchable: ten of the 28 records hold
# one channel, and a batch mixing layouts raises in collation.
ds = ECGDataset(
    "stdb",
    split="train",
    labels=True,
    leads=["ECG1"],
    window=(0, 10_800),      # 30 s; must fit record 305's 282,341 samples
    data_path="/path/to/stdb/1.0.0/",
)

sample = ds[0]
sample["signal"].shape        # (1, 10800)
sample["record_id"]           # 300
sample["labels"]["n_channels"]      # 2
sample["labels"]["st_change_type"]  # 'depression'  <- transcribed, not measured
sample["labels"]["group_source"]    # 'landing_page'
sample["labels"]["hr_rise_bpm"]     # 21.2         <- measured, checks the above

Building the splits

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