Reference pipeline for the paper "An interpretable tensor model for identifying atrial fibrillation in sinus-rhythm ECG: block-term tensor regression on PTB-XL, benchmarked against deep and foundation models" (Cornelissen & Faes). It applies real Block-Term Tensor Regression (Faes et al., the vendored bttrlib/) to PTB-XL and benchmarks it, under identical patient-grouped folds, against a clinical-feature logistic regression, an Attia-style 1-D CNN, an unsupervised CP/PARAFAC surrogate, and frozen HuBERT-ECG foundation-model embeddings. The glass-box BTTR matches or exceeds every baseline at no accuracy cost and yields directly inspectable (time x lead) weight maps. NB: PTB-XL has no outcome follow-up, so the AF-in-SR task is concurrent-association identification, not future-onset prediction.
bttrlib/bttr.py + bttrlib/ace.py implement supervised, deflation-based BTTR: a mode-1 cross-covariance tensor between X and the +/-1 label is Tucker-decomposed each iteration; Automatic Component Extraction (ACE) selects each block's multilinear rank by a BIC sweep; a score vector is SVD-extracted and both X and the target are deflated before the next block, for K blocks. With enforce_rank_1=True each block is a readable (time x lead) weight map. This is structurally distinct from the unsupervised parafac CP surrogate in core/baselines.py (a separate baseline, not the method). bttrlib/ is vendored - never algorithm-edited (one tensorly>=0.8 compat shim only).
bttrlib/- vendored real BTTR:bttr.py(deflation loop),ace.py(automatic rank), imported as a top-level package (from bttrlib import BTTR).core/- shared library:config.py(paths + cohort/BTTR/tensorisation settings; editPTBXL_DIR),data.py(PTB-XL loading),preprocess.py(wavelet denoise + tensorisation:window/median_beat/median_beat_rr),bttr_model.py(sklearn-styleBTTRClassifier+block_maps()),baselines.py(clinical features + CP surrogate),evaluate.py(patient-grouped CV + bootstrap CI).experiments/- the keeper drivers that produce the paper numbers.paper.py- the single entry point that runs the whole flow.results/- generated CSVs + block-map.npz/.png(regenerated bypaper.py).
The algorithm lives in bttrlib/ (vendored BTTR) wrapped by core/bttr_model.py; the benchmark is driven by experiments/.
PTB-XL 1.0.3 (PhysioNet, DOI 10.13026/x4td-x982), used at 100 Hz. Open access (PhysioNet account, no DUA). Fetch selectively from the PhysioNet S3 open mirror s3://physionet-open/ptb-xl/: ptbxl_database.csv, scp_statements.csv, and the cohort records100/ files (a few MB; the full 1.8 GB zip is not needed). Set PTBXL_DIR in core/config.py to point at the download. HuBERT-ECG foundation-model weights (public; DOI 10.1101/2024.11.14.24317328) are fetched by the HuBERT step.
pip install -r requirements.txt
python paper.py # runs the full flow; writes results/ (copy the two used figures into the manuscript figures dir)Individual steps run as python -m experiments.<name>. The HuBERT step needs transformers + torch + network (downloads the weights once); the CNN baseline needs torch; both are benchmark baselines, not dependencies of BTTR.
| Script | Produces | Manuscript element |
|---|---|---|
experiments/final_eval.py |
results/final_table.csv |
Table 1 (BTTR vs CP vs CNN vs HuBERT vs clinical) |
experiments/plot_forest.py |
results/forest_auroc.png |
the forest figure |
experiments/paired_test.py |
stdout | Table 2 (paired differences) + Table 3 CNN rows |
experiments/harden_afsr.py |
results/harden_afsr.csv |
Table 3 BTTR aligned/unaligned rows + the K-sweep; also the cohort builder used by the others |
experiments/run_analyses.py |
results/analyses.csv |
map-stability r = 0.88 |
experiments/interpret_afsr.py |
results/interpret_afsr_afibonly_blocks.{npz,png} (+ secondary) |
the block-map figure |
experiments/weight_fractions.py |
results/weight_fractions.csv |
the temporal weight fractions (atrial/QRS/ST/T) |
experiments/run.py --task detect |
results/results_detect.csv |
the detection-context numbers |
experiments/nested_afsr.py |
results/nested_afsr.csv |
the nested-CV robustness cross-check |
experiments/attia_cnn.py (the CNN baseline, cnn_oof), hubert_probe.py (HuBERT embeddings + probe), and interpret.py (the block-map export helper) are imported by the drivers above.