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).
| Records | 750 |
| Labels | 456 false / 294 true |
| Sampling rate | 250 Hz |
| Channels | 2 ECG leads + 1–2 of PLETH / ABP, sometimes RESP |
| Alarm types | VTA 341 · ETC 140 · ASY 122 · EBR 89 · VFB 58 |
| Record length | 375 …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 score | sensitivity | specificity | AUROC | |
|---|---|---|---|---|
| Model 1 — naive (RF, thr 0.5) | 0.55 | 0.69 | 0.91 | 0.87 |
| Model 1 — thr tuned to official score | 0.65 | — | — | 0.87 |
| Model 2 — full (LGBM, 5× FN weight, tuned thr) | 0.73 | 0.90 | 0.82 | 0.94 |
| Model 2 — 15% holdout | 0.81 | 0.93 | 0.87 | 0.96 |
| keep-all-alarms baseline | 0.39 | 1.00 | 0.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)