ICU False Arrhythmia Alarm Reduction

September 18, 2026 · View on GitHub

Reducing false life-threatening arrhythmia alarms in the ICU using multi-modal bedside-monitor signals (ECG + PPG/ABP), on the PhysioNet/CinC Challenge 2015 training set.

Portfolio project. Retrospective research use only — not a validated clinical device.

Why this problem

Up to ~90% of ICU arrhythmia alarms are false, driving alarm fatigue. The task: given the 5 minutes of monitor data leading up to an alarm, decide whether the alarm is true (real event) or false (artifact / noise / lead-off). The challenge scores this with a deliberately asymmetric metric:

Score = (TP + TN) / (TP + TN + FP + 5·FN)

— a suppressed true alarm (FN) costs 5× a retained false alarm.

Dataset

PhysioNet/CinC Challenge 2015, challenge-2015/1.0.0, training set only (the 500-record test set is not public).

Records750
Labels456 false / 294 true
Sampling rate250 Hz
Channels2 ECG leads + 1–2 of PLETH / ABP, sometimes RESP
Alarm typesVTA 341 · ETC 140 · ASY 122 · EBR 89 · VFB 58
Record length375 …l records = 330 s (incl. +30 s retrospective tail) · 375 …s records = 300 s (stop at the alarm)

Evaluation protocol: stratified 5-fold CV (by alarm type × label) reporting mean ± std of the official score and sensitivity/specificity, plus a fixed 15% stratified hold-out for a single final number.

Approach

  • Model 1 — naive baseline. Every channel, off-the-shelf peak detectors, a small generic per-channel statistical feature set, logistic regression / RF. No cross-channel logic, no signal-quality gating, no per-alarm-type branching.
  • Model 2 — comprehensive. Signal-quality gating → multi-modal peak detection with cross-channel agreement features (does an independent pulsatile channel confirm what the ECG claims?) → per-alarm-type feature branches → calibrated gradient-boosted meta-classifier with the 5× FN penalty baked into training and threshold selection.
  • Model 1 → Model 2 delta is a clean ablation of "does the feature engineering the literature recommends actually help", holding the input channels fixed.

See physionet-false-alarm-project-plan.md for the full plan and literature review.

Setup

python3 -m venv .venv && .venv/bin/pip install -r requirements.txt

# Download the 750-record training set (~400 MB) into data/raw/training/
.venv/bin/python -m src.data_io download

# Build data/processed/manifest.csv (one row per record)
.venv/bin/python -m src.data_io manifest

# Tests (header parsing runs offline against a cached fixture)
.venv/bin/python -m pytest -q

Layout

src/
  data_io.py        WFDB download, header parsing, windowed signal loading
  signal_quality.py SQI functions                          (Model 2)
  peak_detection.py QRS + pulse peak detection, cross-channel matching
  features.py       feature engineering for Model 1 and Model 2
  models_naive.py   Model 1
  models_full.py    Model 2 (+ optional deep branch)
  evaluate.py       official score, sens/spec/AUROC, calibration, per-type breakdown
  deploy.py         edge-deployment benchmark: stage profiling, CPU-core
                     scaling, accuracy/latency/size trade-off
notebooks/exploration.ipynb
reports/model_card.md
tests/

Running Model 1

.venv/bin/python -m src.features --mode realtime        # cache feature matrix
.venv/bin/python -m src.models_naive --mode realtime    # CV + holdout + report

Outputs: reports/model1_results_{mode}.md / .json, reports/figures/*.png, and the fitted pipeline at data/processed/model1_*.joblib.

Running Model 2

.venv/bin/python -m src.features --which model2 --mode realtime   # ~10 min, cached
.venv/bin/python -m src.models_full --mode realtime               # CV + holdout + reports
.venv/bin/python -m src.models_full --compare-only                # rebuild the comparison

Model 1 vs Model 2 (real-time, out-of-fold, seed 42)

official scoresensitivityspecificityAUROC
Model 1 — naive (RF, thr 0.5)0.550.690.910.87
Model 1 — thr tuned to official score0.650.87
Model 2 — full (LGBM, 5× FN weight, tuned thr)0.730.900.820.94
Model 2 — 15% holdout0.810.930.870.96
keep-all-alarms baseline0.391.000.00

Model 2 adds SQI gating, cross-channel agreement (ECG vs PPG/ABP), and per-alarm-type feature branches, fed to one cost-weighted LightGBM. Both models use every channel, so the +0.18 official-score gain is the feature engineering + cost-aware modelling, not extra data. Per alarm type the biggest wins are on VTA (0.45→0.65) and EBR (0.44→0.71). Top features span all four branches plus cross-channel and SQI — see reports/model2_results_realtime.md.

Edge-deployment benchmark

.venv/bin/python -m src.features --which model2_fast --mode realtime   # ~2 min
.venv/bin/python -m src.deploy full-report --mode realtime             # ~3 min

Profiling the 179-feature pipeline found the real bottleneck isn't peak detection — it's neurokit2's windowed ECG/PPG quality index (nk.ecg_quality / nk.ppg_quality), ~90% of total latency at ~614 ms/record. A cheap kurtosis/flatline/clipping-based SQI proxy (signal_quality.py, method="fast") cuts that to ~88 ms — ~4.8× faster end to end (664 → 138 ms/record) — and, retrained on identical hyperparameters and split, scores at least as well on the official metric (0.73→0.75 OOF, 0.81→0.84 holdout, real-time setting). Latency barely moves with CPU core count (1 vs 20 cores): the pipeline is single-threaded per record, so a 1-core embedded target costs almost nothing extra over an unconstrained workstation for this workload. Model 2's LightGBM artifact (~425 KB) is ~10× smaller on disk than Model 1's random forest despite 2.5× the features. Full detail: reports/deploy_results_realtime.md.

Status

  • Repo scaffold, environment, data pipeline (data_io.py)
  • Exploratory analysis
  • Model 1 — naive baseline (features.py, models_naive.py, evaluate.py)
  • Model 2 — SQI (signal_quality.py) + cross-channel (peak_detection.py) + per-alarm-type branches → LightGBM (models_full.py)
  • Model 1 vs Model 2 comparison (reports/model_comparison.md), model card
  • Stretch: edge-deployment benchmark (deploy.py) — CPU-core scaling, stage-level profiling, and an accuracy/latency/size trade-off
  • Stretch: deep-learning branch (not attempted — CPU/BLAS-bound signal pipeline above turned out to be the more valuable finding)