MIT-BIH Arrhythmia Database

Open Completed

Quick facts

Format2-lead (usually MLII + V1) · 30 min · 360 Hz · WFDB
Patients47
Records48
Leads2
LicenseODC-By 1.0
OriginBeth Israel Hospital / MIT — USA

Overview

The reference arrhythmia benchmark. 48 half-hour excerpts of two-channel ambulatory ECG, digitised at 360 Hz from Holter tapes recorded at the Beth Israel Hospital Arrhythmia Laboratory between 1975 and 1979, carrying 109,494 beat annotations across fifteen types plus rhythm episodes, signal-quality changes and artefact markers. Each record was annotated independently by two or more cardiologists and the disagreements resolved.

The database is two deliberate halves, and ECGBench stratifies on that. Records 100–124 (23 of them) were chosen at random from a pool of over 4000 recordings, to be representative of routine clinical material. Records 200–234 (25) were selected from the same pool for rare but clinically important phenomena. With only 48 records over 10 folds, a fold drawn without regard to that split can easily be all one half — the difference between a test set with ventricular flutter in it and one without.

The two leads are not the same two in every record. This is the one dataset in the catalogue where records store the same number of leads under different names, so nothing about a signal’s shape reveals which lead is which. See “Lead layout” below — it changes how you must call leads=.

Records are long — 650,000 samples, about 5 MB each. Take a fixed window=(start, length), which is read at load time rather than cropped afterwards, or use batch_size=1.

Folds are grouped by analog tape. 48 records come from 47 subjects: records 201 and 202 were cut from the same tape (1960), which the shipped directory states and the tape number in the header confirms. One subject out of 47 — but it is the only one there is, and ungrouped those two land in different folds most of the time.

Lead layout — why you must select leads by name here

In most records the upper signal is a modified limb lead II (MLII), taken from chest electrodes, and the lower a modified V1. Not always. Counted from all 48 headers:

Layout Records Which
MLII, V1 40 the rest
MLII, V5 2 100, 123
MLII, V2 2 103, 117
V5, V2 2 102, 104
MLII, V4 1 124
V5, MLII 1 114 — the two signals are reversed

Records 102 and 104 have no MLII at all: surgical dressings made a modified II impossible, so V5 was used for the upper signal. Record 114 has the predominant pair the wrong way round, which the source documents as something that “happens occasionally in clinical practice” and which arrhythmia detectors should cope with.

Every one of the 48 records stores exactly 2 leads, so alternate_lead_names — which maps a lead count to a layout — cannot express any of this. The config declares record_lead_layouts instead, and ECGDataset then resolves the requested lead names against each record’s own header:

ds = ECGDataset("mitdb", split="train", data_path=..., leads=["MLII"])
ds[0]["signal"]    # record 100: MLII, read from position 0
# record 114: MLII, read from position 1 — an index would have returned V5
# record 102: raises ValueError; it stores V5/V2 and has no MLII to return

Without this, signal[0] is a limb-type lead in 46 records and a chest lead in 2, and leads=["MLII"] returns V5 for three of them with no error.

Reference beat annotations

Beat typeSymbolCountShareRecords containing it
normal beatN75,05268.54%40
left bundle branch block beatL8,0757.37%4
right bundle branch block beatR7,2596.63%6
premature ventricular contractionV7,1306.51%37
paced beat/7,0286.42%4
atrial premature beatA2,5462.33%27
fusion of paced and normal beatf9820.90%3
fusion of ventricular and normal beatF8030.73%17
nodal (junctional) escape beatj2290.21%5
aberrated atrial premature beata1500.14%7
ventricular escape beatE1060.10%2
nodal (junctional) premature beatJ830.08%5
unclassifiable beatQ330.03%6
atrial escape beate160.01%1
supraventricular premature beatS20.00%1
**total beats****109,494**48

Time spent in each annotated rhythm

RhythmCodeMinutesShareRecords containing it
normal sinus rhythmN1,055.473.08%42
atrial fibrillationAFIB132.69.18%8
paced rhythmP110.27.63%4
ventricular bigeminyB41.82.90%12
sinus bradycardiaSBR30.12.08%1
ventricular trigeminyT19.01.32%12
atrial flutterAFL13.10.91%3
pre-excitation (WPW)PREX12.30.85%1
second degree heart blockBII11.70.81%1
nodal (AV junctional) rhythmNOD4.70.32%3
ventricular tachycardiaVT3.60.25%13
supraventricular tachyarrhythmiaSVTA3.50.24%7
idioventricular rhythmIVR2.40.17%2
ventricular flutterVFL2.40.17%1
atrial bigeminyAB1.50.10%1
**total annotated****1,444.3**48

About those counts

Every figure on this page was recomputed from the 48 headers and 48 .atr files, after verifying all 226 signal and annotation files against the release’s own SHA256SUMS.txt. Most of them reconcile with the published record exactly, which is unusual in this catalogue and worth stating:

The rhythm table measures time, not markers. A rhythm annotation opens an episode that runs until the next one, so a record with two AFIB markers and two sinus markers may be 99% sinus. The 1,444.3 minutes annotated is 9 seconds short of the 1,444.5 minutes the 48 records contain, because in a few records the first rhythm annotation comes a beat or two after the start.

Beat classes are not a record-level label, and they are wildly imbalanced. Almost every record carries several beat types, so there is no single beat class per record; and several classes live in one or two records (all 472 ventricular-flutter waves are in record 207, 427 of the 428 missed-beat markers are in record 231, and the 7,028 paced beats come from 4 records). The stratify_class used for folds is neither — it is the random/selected halves, and it is for fold construction only.

Validation summary (360 Hz)

VersionRecordsNote
original48all records, with is_valid + quality_issues
clean4593.8% pass rate
excluded3103, 116, 223 — each saturates the 11-bit ADC

About the excluded records, and the amplitude range

amplitude_range_mv is [-5.11, 5.11] here rather than ECGBench’s usual [-10, 10], and it is set from the hardware rather than from physiology. The recordings are format 212 at 11 bits with a gain of 200 adu/mV and adc_zero 1024, so the full scale is exactly −5.120 to +5.115 mV — no sample in this database can lie outside it, and any range wider than that is a check that provably never fires.

At the rail it does something real. Three records reach it, i.e. clip:

Record Lead Samples at the rail Peak
116 MLII 0.14% ±5.12 mV
223 V1 0.11% ±5.12 mV
103 V2 0.04% ±5.12 mV

The next-highest peak anywhere in the release is 4.94 mV (record 200), so the threshold separates the saturating records from every other one cleanly. The clipping is brief — well under a second in total per record — so for beat detection these records are perfectly usable; take the original version if you want them, where they are present and flagged.

There are no NaN samples and no flat or all-zero leads anywhere in this release, and all 48 records are exactly 650,000 samples, so the truncation check is enabled rather than skipped.

What is not in this dataset

The download contains three things ECGBench deliberately ignores, all checked against the files rather than assumed:

The shipped ANNOTATORS file lists atr and nothing else, which is what ECGBench reads.

Note also that the two channels are not synchronous: the directory records skew of up to 40 ms between them, part fixed per recorder and part variable from tape wobble. Anything comparing the channels sample-for-sample has to allow for it.

Building the splits

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

Loading with ECGBench

from ecgbench import ECGDataset

# 650,000-sample records: a window is needed to batch at all, and because
# window= is pushed into the reader it also avoids decoding the other 1795 s.
ds = ECGDataset(
    "mitdb",
    split="train",
    data_path="/path/to/mitdb/1.0.0/",
    window=(0, 3600),        # first 10 s at 360 Hz
    labels=True,
)

len(ds)                                  # 37
ds[0]["signal"].shape                    # torch.Size([2, 3600])
ds[0]["record_id"]                       # 100
ds[0]["labels"]["lead_names"]            # 'MLII|V5' — this record's own layout
ds[0]["labels"]["patient_id"]            # 'tape1085' — folds are grouped by this
ds[0]["labels"]["dominant_rhythm"]       # 'N'
ds[0]["labels"]["n_beats"]               # 2273
ds[0]["labels"]["pvc_fraction"]          # 0.00044

# Select by NAME. Records 102 and 104 store V5/V2 and raise rather than
# returning V5 where MLII was asked for; record 114 returns MLII from
# position 1, where an index-based selection would have returned V5.
mlii = ECGDataset("mitdb", split="train", data_path="...",
                  window=(0, 3600), leads=["MLII"])
mlii[0]["signal"].shape                  # torch.Size([1, 3600])

# Rhythm burden per record, straight off the reference annotations:
ds.labels_df["rhythm_secs_AFIB"].max()   # 1769.92 -- record 210, AFIB for
                                         # all but 36 s of its 1805.6 s