Long-Term ST Database (LTSTDB)

Open Completed

Quick facts

Format2- or 3-lead ambulatory · 12 lead layouts · 17–48 h · 250 Hz · WFDB · 3 sets of ST episode annotations
Patients80
Records86
Leads2
LicenseODC-By 1.0
OriginUniversity of Ljubljana; CNR Institute of Clinical Physiology, Pisa; MIT — Multi-national (Slovenia, Italy, USA)

Overview

Ischaemia detection over a whole day rather than a two-hour excerpt, and the largest annotated recording in this catalogue by both hours and beats. 86 continuous ambulatory records from 80 subjects, 1,992 hours of ECG at 250 Hz, 8,897,780 manually corrected reference beats, and 2,311 ST episodes under the release’s most inclusive criterion. Built between 1995 and 2002 by a project coordinated at the University of Ljubljana with the CNR Institute of Clinical Physiology in Pisa and MIT — the groups behind the European ST-T Database and the MIT-BIH Arrhythmia Database respectively — and supported by Medtronic and Zymed.

It exists to tell ischaemic ST change apart from everything that looks like it. Records were chosen to show a mixture of ischaemic episodes, axis-related non-ischaemic episodes, slow ST level drift and combinations of those, and the annotations mark which is which: 1,795 ischaemic episodes against 516 rate-related ones, plus 1,493 axis shifts and 895 conduction-change shifts, which are artefacts of body position and conduction rather than findings about the heart. A detector scored without those distractors is not being scored on the task this database was built for.

There are three sets of ST episode annotations, not one, and they disagree by a factor of two. .sta, .stb and .stc apply different amplitude and duration criteria to the same recordings, giving 2,311, 1,364 and 973 episodes. None is more correct than the others. Any figure from this database is meaningless without its criterion — see the table below.

Records hold two or three signals, in twelve layouts, and 22 of them do not name their leads at all. 68 records store two signals and 18 store three; the single largest layout is the 22 records whose headers describe both signals only as ECG and state “Electrode locations were not recorded.” No lead is present in all 86 records — the widest, MLIII, reaches 29. This is the only dataset in the catalogue that cannot be batched whole by any leads= selection.

Subject identity is published, inside the record name. sXYYYZ: X is the signal count, YYY the subject and Z that subject’s record number, so s20271–s20274 are one person. That is the grouping ECGBench folds on, and it matters: those four records hold 416 of the release’s 1,795 ischaemic episodes between them.

The three ST episode annotators — always quote which one a figure uses

FileCriterionIschaemicRate-relatedTotalRecords with ≥1 ischaemic
`.sta`75 µV, 30 s**1,795**516**2,311**68 of 86
`.stb`100 µV, 30 s1,1302341,36466 of 86
`.stc`100 µV, 60 s85711697364 of 86
*all three*axis shifts1,49359 of 86
*all three*conduction-change shifts8954 of 86
*all three*noise events3112 of 86
*all three*unreadable intervals607 of 86

Why the three criteria exist, and what the shared rows mean

An episode is detected on the ST deviation function — the measured ST level minus a piecewise-linear baseline the annotators placed themselves, so that postural drift is subtracted before anything is called an episode. It begins when the deviation first exceeds 50 µV, must then reach Vmin for at least Tmin, and ends when the deviation falls below 50 µV without exceeding it again within 30 s. The three files differ only in Vmin and Tmin, and the release ships all three because the appropriate threshold depends on the application. ECGBench exposes all three: the criterion-A columns are unsuffixed and the others carry _b and _c.

The last four rows carry no suffix because those quantities are identical in all three files. They are marks, not threshold crossings: an axis shift is a shift, whatever amplitude convention is applied to episodes around it.

Ischaemic and rate-related episodes must not be summed into “ST episodes” without saying so. Record s20011 holds 20 criterion-A episodes and every one is rate-related; its header explains why, and adds “It is recognized that this is an arbitrary decision.” 16 records hold rate-related episodes and no ischaemic ones, 55 the reverse, and 13 both.

ST episodes by direction and by signal, criterion A (75 µV / 30 s)

QuantityCountNote
ischaemic episodes**1,795**68 records
rate-related episodes**516**29 records
— ST depression1,896of the 2,311 combined
— ST elevation415of the 2,311 combined
ischaemic, signal 0856
ischaemic, signal 1786
ischaemic, signal 2153the 18 three-signal records only
episodes already running at sample 010no onset annotation; measured from 0
episodes still running at the last sample18no end annotation; closed at the record end
largest annotated deviation1,495 µVs20621 — a *rate-related* episode
time in ischaemic episode, union over leads151.8 h7.6% of the 1,992 recorded hours
time in ischaemic episode, summed over leads244.1 hleads annotated independently

About those counts

Every figure on this page was recomputed from the 86 headers and their .atr, .sta, .stb and .stc files, after verifying the shipped data against the release’s own SHA256SUMS.txt. All 774 signal and annotation files match — but only after a repair: the local copy’s s30801.dat was a truncated download (13.9 MB against 64.4 MB) and its .hea, .sta, .stb, .stc and .stf were absent entirely, which would have failed one record of 86 for a reason the traceback would have blamed on the header.

The episode counts are the release’s own, and reproducing them exactly is what validates the counting convention. Each record ships a .cnt text file summarising its episodes, and ECGBench’s parse of the binary annotations agrees with it in all 258 blocks — 86 records × 3 criteria × 6 quantities. Getting there requires counting an episode at its extremum rather than its onset: an episode is an onset/extremum/end triple, but 10 of them were already in progress when the tape started and have no onset at all, so counting onsets gives 1,785 ischaemic episodes instead of 1,795 and disagrees with the shipped summaries for 14 of the 86 records.

The published record length is wrong for 16 of the 86. PhysioNet says the recordings are “between 21 and 24 hours in duration”. Measured from the headers, 70 are; eight are shorter, down to 16.9 h (s30771), and eight are longer, up to 47.8 h — s20611 holds 43,050,000 samples, nearly twice the median, and its .dat file size confirms the header. So expected_samples is deliberately empty and duration_seconds in the config is the median (23.4 h), not a uniform length. A window= must fit s30771.

Quantity Landing page Recomputed (v1.0.0) Diff
Records 86 86
Subjects 80 80
Three-signal records 18 18
From the European ST-T collection 10 10 (named in the headers)
From the 1995–98 pilot collection 11 11 (named in the headers)
Record duration 21–24 h 16.9–47.8 h, median 23.4 h 16 records outside

There is no changelog in the release directory, so the duration disagreement cannot be attributed to a reissue; the shipped headers and .dat sizes agree with each other, and the landing page does not agree with them. Treat the recomputed figures as authoritative for v1.0.0.

ischemic_secs can exceed a record’s own length, and that is correct. The signals are annotated independently, so ischaemia seen in two leads at once is two episodes and its seconds count twice: 244.1 h summed against 151.8 h in the bounded union. Use ischemic_secs_any_lead, or ischemic_fraction, for anything expressed as a proportion.

The twelve lead layouts, counted from the 86 headers

Layout (file order)SignalsRecordsLayout (file order)SignalsRecords
ECG / ECG2**22**MLIII / V321
V4 / MLIII220V5 / MLIII21
ML2 / MV2216V5 / V221
E-S / A-S / A-I315V2 / MLIII21
MLIII / V426V4 / V3 / II31
V6 / V5 / aVF31
V6 / II / V531

…and how many records contain each lead. None reaches 86.

LeadRecordsShare of the release
MLIII2934%
V42731%
ECG *(unnamed)*2226%
ML21619%
MV21619%
E-S / A-S / A-I15 each17%
V545%
V2, V3, V6, II2 each2%
aVF11%

What the twelve layouts mean for loading — read this before batching

config.record_lead_layouts declares all twelve, which makes ECGDataset(leads=[...]) open each record’s own header and resolve the requested names against it rather than trusting a position. Four configs in this catalogue need that; this is the only one where the lead count varies too, which is why alternate_lead_names — keyed by count — cannot express it.

Three consequences, in decreasing order of how likely they are to bite:

1. This dataset cannot be batched whole. ecg_collate_fn stacks with torch’s default_collate, which raises on a batch mixing 2- and 3-channel records, and no leads= value fixes that because no lead is in all 86 records. leads=["MLIII"] loads 29 and raises for the other 57. Filter first — n_leads and lead_names are columns of ecgbench.labels.ltstdb — or use batch_size=1. The example script does the filtering.

2. V4/MLIII and MLIII/V4 are both present, 20 records and 6. Those 26 records are indistinguishable by shape, by lead count and by anything in the exported fold CSVs, and signal[0] is a chest lead in one group and a limb lead in the other.

3. For the 22 ECG/ECG records, leads=["ECG"] returns signal 0 and there is no name that reaches signal 1. That is a property of the release rather than of ECGBench: the electrode positions were never recorded, so the two signals are genuinely indistinguishable by name. Use leads=None and index them if you need both. leads_named flags those records in the labels.

Reference beat annotations, recomputed from the 86 .atr files

Beat typeSymbolCountShareRecords containing it
normal beatN8,669,29797.432%86
bundle branch block beat (unspecified)B88,7200.997%2
premature ventricular contractionV72,8520.819%76
supraventricular premature or ectopic beatS57,3110.644%75
atrial premature beatA8,7300.098%7
fusion of ventricular and normal beatF5970.007%36
aberrated atrial premature beata1620.002%14
ventricular escape beatE710.001%2
atrial escape beate300.000%4
nodal (junctional) escape beatj60.000%2
unclassifiable beatQ20.000%2
nodal (junctional) premature beatJ10.000%1
paced beat/10.000%1
**total****8,897,780****100%****86**

The same beats under the AAMI EC57 reduction — use these to combine with mitdb, svdb, incartdb and edb

AAMI classBuilt fromCountShareRecords containing it
N — normal / bundle branch blockN, L, R, e, j, B8,758,05398.430%86
S — supraventricular ectopicA, a, J, S, n66,2040.744%82
V — ventricular ectopicV, E, r72,9230.820%76
F — fusionF5970.007%36
Q — unclassifiable / paced/, f, Q30.000%3

The .atr files hold beats and nothing else

Unusually for a MIT-BIH-family release, there is no rhythm (+) annotation, no signal-quality (~) annotation and no aux_note anywhere in the 86 .atr files — 8,897,780 annotations, every one a beat label. So this database offers no rhythm ground truth at all, and quality is annotated somewhere else: the 31 noise events and 60 unreadable intervals live in the ST files as noi and (urd/urd), covering 18.5 h in 7 records.

Beat coverage is otherwise complete. The first beat falls 0.02–9.18 s into the record and the last 0.13–1.96 s before its end, so every record is annotated over at least 99.98% of its length — there is no unannotated tail to window around, unlike nsrdb, which leaves 12.1% of its signal in silence.

The .ari files are the same detector’s uncorrected output and are deliberately not read; .atr is the manually corrected version. The 26 .hea- files are superseded pre-2008 headers. The .16a, .stf, .klt.zip, .tsr.zip, legendre/ and kl-single*/ products — ST measurements at eight points of every beat, ST level and deviation functions, and Karhunen-Loève and Legendre coefficient time series — are shipped and rich, and ECGBench does not parse them. Read them from the release directly.

Ischaemic-burden bands — the stratification label

BandIschaemic episodes (criterion A)RecordsEpisodes in the band
none0180
1-51 – 51445
6-206 – 2025259
21+21 or more291,491

Why the folds are grouped by subject and stratified on ischaemic burden

The grouping is published, unlike edb’s. sXYYYZ puts the subject number in the record name, the landing page states the rule, and four headers restate it in words (“Records s20271, s20272, s20273 and s20274 are from the same patient.”). Six records belong to subjects with more than one: 027 has four, and 073, 074 and 075 have two each. Ungrouped, subject 027 alone would scatter four recordings holding 23% of the release’s ischaemia across several folds, and any of them landing in both train and test is the same day of the same heart on both sides of the split. patient_id is the zero-padded three-digit field of the record name — "027", not 27 — which is why this config sets zero_padded_identifiers.

Stratification is the criterion-A ischaemic episode count, banded at fixed edges of 1, 6 and 21 episodes. It is what this database exists to measure and it is enormously uneven: 18 records hold no ischaemic episode at all while s20274 holds 143, and the top band holds 83% of all the episodes. A fold drawn without regard to it can easily be all quiet records or all busy ones. Fixed edges rather than quantiles, so a re-release with one extra episode cannot silently relabel records that did not change.

Nothing clinical works better. st_class puts 55 of the 86 records in one class and 2 in another; the header findings describe the subject rather than the recording; diagnoses is free text with more distinct values than there are folds to spread them over.

Unlike edb, every band clears the ten folds (14 is the smallest), so none is forced to skip any. With 86 records over 10 folds, train holds 70, val 8 and test 8, and every fold carries records from all four bands. stratify_class is a fold-construction label. Train on the episode counts, ischemic_fraction, peak_st_deviation_uv, or — for episode detection, which is the task — on the .sta/.stb/.stc files directly.

Cohort, recomputed from the 86 headers

QuantityValue
Subjects / records80 / 86
Subjects contributing >1 record4 (027 ×4; 073, 074, 075 ×2)
Sex (subjects)46 M, 29 F, 5 not recorded
Age23–87, mean 61.5, median 62; 5 records not recorded
Recorded hours1,991.8 h (median record 23.4 h)
Mean heart rate51.4 – 109.2 bpm per record
Holter recorderRemco 18, Zymed 18, ICR 10, Oxford Medilog 2, Del Mar Avionics 2, **not recorded 36**
Recording dates1984 – 2000
Hypertension / previous MI25 / 24 records; 11–12 records say 'No data'

The clinical record in the headers is unusually complete — and unusually free-text

Each .hea carries a 28-field indented tree: age, sex, annotator commentary on the recording itself, symptoms during monitoring, a diagnosis list, current medications, angioplasty and bypass history, eleven structured cardiac-history fields, and the findings of previous stress tests, thallium or stress echocardiography, LV function studies, echocardiography, coronary arteriography and baseline ECG. All 28 fields appear in all 86 headers.

The structured fields look boolean and are not. “Previous Myocardial Infarction” answers include No, No data, Yes, Yes, anterior, Yes, in 1996 and 1997 and Yes, had myocardial infarction in 1995; “Left ventricular hypertrophy” includes Septum 13 mm; “Intraventricular conduction block” includes Right bundle branch block and Borderline. ECGBench exposes each as a nullable boolean plus the verbatim text in a <field>_text column — read the text whenever the detail matters. NA means the header said “No data”, which is 11 to 45 records depending on the field.

Two provenance fields are parsed out of the annotator commentary. edb_record names the European ST-T record cut from the same tape, for the ten records that have one — see the overlap warning at the top of this page. pilot_record is the name eleven records carried in the 1995–98 pilot collection, which was never published; do not read it as a record id here, because one of the eleven collides — s20071’s pilot name is s20511, and s20511 is also a record in this release, belonging to a different subject.

Building the splits

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

Loading with ECGBench

from ecgbench import ECGDataset

# Records run 15,200,000 to 43,050,000 samples per channel — 17 to 48 hours —
# so window= is not optional here. It is pushed into the reader, so it avoids
# decoding the rest of the day rather than cropping afterwards, and it must fit
# the SHORTEST record (s30771, 15,200,000 samples).
ds = ECGDataset(
    "ltstdb",
    split="train",
    data_path="/path/to/ltstdb/1.0.0/",
    window=(0, 2500),          # first 10 s at 250 Hz
    labels=True,
)

len(ds)                                      # 70
ds[0]["signal"].shape                        # torch.Size([2, 2500])
ds[0]["record_id"]                           # 's20011'

ds.lead_names                                # ('ECG', 'ECG')  <- the MODAL layout,
                                             #    which names nothing, and is NOT
                                             #    this record's
ds[0]["labels"]["lead_names"]                # 'ML2|MV2'   <- what s20011 stores
ds[0]["labels"]["n_leads"]                   # 2   (18 of the 86 records store 3)

# Criterion A (.sta, 75 uV / 30 s) is unsuffixed; _b and _c are the stricter two.
ds[0]["labels"]["n_ischemic_episodes"]       # 0
ds[0]["labels"]["n_rate_related_episodes"]   # 20  <- NOT ischaemia, and the header
                                             #        says the call was arbitrary
ds[0]["labels"]["n_rate_related_episodes_b"] # 4   <- same record, 100 uV / 30 s
ds[0]["labels"]["n_axis_shifts"]             # 7   <- positional artefact
ds[0]["labels"]["peak_st_deviation_uv"]      # 243  <- vs the annotator-placed
                                             #         baseline, not isoelectric
ds[0]["labels"]["ischemic_fraction"]         # 0.0
ds[0]["labels"]["st_class"]                  # 'rate_related_only'
ds[0]["labels"]["ischemic_burden_band"]      # 'none'  <- for folds, not training
ds[0]["labels"]["patient_id"]                # '001'   <- from the record name
ds[0]["labels"]["duration_hours"]            # 22.8831
ds[0]["labels"]["n_beats"]                   # 100053
ds[0]["labels"]["age"], ds[0]["labels"]["sex"]       # 58.0, 'M'
ds[0]["labels"]["diagnoses"]                 # 'No coronary artery disease'

# Batching needs a RECORD FILTER, not just leads=. No lead is in all 86 records
# and 18 store three signals, so a mixed batch raises in default_collate.
holds_mliii = ds.labels_df["lead_names"].str.split("|").apply(lambda n: "MLIII" in n)
int(holds_mliii.sum())                       # 23 of the 70 records in this split

# Selecting by NAME re-resolves against each record's own header, and raises for
# the records that lack the lead rather than returning whichever one sits there.
mliii = ECGDataset("ltstdb", split="train", data_path="/path/to/ltstdb/1.0.0/",
                   window=(0, 2500), leads=["MLIII"])
mliii[0]    # ValueError: Record 's20011' stores ['ML2', 'MV2'], and this dataset
            # uses more than one lead layout. Lead 'MLIII' is not in 'ltstdb'.

# See examples/load_ltstdb.py for the full walkthrough.