Patient Vital-Sign Anomaly (per-patient baseline)
Tested against SynapCores CE v1.7.0.1-ce (the currently-shipped release on Docker Hub:
synapcores/community:v1.7.0.1-ce).
Objective
A 22-year-old marathon runner with a resting HR of 48 spiking to 95 is not the same event as an 80-year-old with a resting HR of 75 spiking to 95 — but a global threshold treats them identically. Personal- baseline detection ranks anomalies relative to each patient.
Why this matters: hospitals report 40–80% of monitor alarms are false positives. Alarm fatigue causes nurses to silence real events. A per-patient baseline cuts the false-positive rate at the source.
Step 1 — Schema + multi-patient hourly vitals
5 patients × 24 hourly readings. Each patient has exactly one tachycardia event at a random hour — different magnitudes and baselines.
DROP TABLE IF EXISTS vitals;
CREATE TABLE vitals (
id INTEGER PRIMARY KEY,
patient_id TEXT,
hour INTEGER,
hr_bpm INTEGER
);
INSERT INTO vitals VALUES
(1,'P1',0,64),
(2,'P1',1,61),
(3,'P1',2,65),
(4,'P1',3,62),
(5,'P1',4,62),
(6,'P1',5,66),
(7,'P1',6,59),
(8,'P1',7,65),
(9,'P1',8,60),
(10,'P1',9,65),
(11,'P1',10,66),
(12,'P1',11,60),
(13,'P1',12,99),
(14,'P1',13,63),
(15,'P1',14,60),
(16,'P1',15,64),
(17,'P1',16,65),
(18,'P1',17,66),
(19,'P1',18,58),
(20,'P1',19,66),
(21,'P1',20,59),
(22,'P1',21,64),
(23,'P1',22,60),
(24,'P1',23,63),
(25,'P2',0,59),
(26,'P2',1,63),
(27,'P2',2,59),
(28,'P2',3,61),
(29,'P2',4,63),
(30,'P2',5,60),
(31,'P2',6,59),
(32,'P2',7,60),
(33,'P2',8,97),
(34,'P2',9,59),
(35,'P2',10,61),
(36,'P2',11,61),
(37,'P2',12,62),
(38,'P2',13,57),
(39,'P2',14,60),
(40,'P2',15,57),
(41,'P2',16,62),
(42,'P2',17,60),
(43,'P2',18,56),
(44,'P2',19,57),
(45,'P2',20,61),
(46,'P2',21,63),
(47,'P2',22,62),
(48,'P2',23,56),
(49,'P3',0,69),
(50,'P3',1,74),
(51,'P3',2,73),
(52,'P3',3,75),
(53,'P3',4,73),
(54,'P3',5,73),
(55,'P3',6,71),
(56,'P3',7,70),
(57,'P3',8,68),
(58,'P3',9,73),
(59,'P3',10,72),
(60,'P3',11,68),
(61,'P3',12,72),
(62,'P3',13,75),
(63,'P3',14,73),
(64,'P3',15,75),
(65,'P3',16,117),
(66,'P3',17,68),
(67,'P3',18,76),
(68,'P3',19,70),
(69,'P3',20,72),
(70,'P3',21,73),
(71,'P3',22,69),
(72,'P3',23,73),
(73,'P4',0,62),
(74,'P4',1,58),
(75,'P4',2,57),
(76,'P4',3,63),
(77,'P4',4,57),
(78,'P4',5,59),
(79,'P4',6,59),
(80,'P4',7,65),
(81,'P4',8,59),
(82,'P4',9,58),
(83,'P4',10,65),
(84,'P4',11,63),
(85,'P4',12,58),
(86,'P4',13,65),
(87,'P4',14,58),
(88,'P4',15,61),
(89,'P4',16,63),
(90,'P4',17,60),
(91,'P4',18,58),
(92,'P4',19,57),
(93,'P4',20,111),
(94,'P4',21,58),
(95,'P4',22,59),
(96,'P4',23,65),
(97,'P5',0,74),
(98,'P5',1,72),
(99,'P5',2,75),
(100,'P5',3,77),
(101,'P5',4,74),
(102,'P5',5,75),
(103,'P5',6,77),
(104,'P5',7,72),
(105,'P5',8,76),
(106,'P5',9,73),
(107,'P5',10,77),
(108,'P5',11,73),
(109,'P5',12,74),
(110,'P5',13,114),
(111,'P5',14,74),
(112,'P5',15,72),
(113,'P5',16,71),
(114,'P5',17,72),
(115,'P5',18,77),
(116,'P5',19,72),
(117,'P5',20,79),
(118,'P5',21,73),
(119,'P5',22,75),
(120,'P5',23,79)
;
Step 2 — Per-patient baseline + peak
SELECT patient_id,
AVG(hr_bpm) AS personal_avg,
MIN(hr_bpm) AS personal_low,
MAX(hr_bpm) AS personal_peak,
MAX(hr_bpm) - AVG(hr_bpm) AS peak_above_baseline
FROM vitals
GROUP BY patient_id;
-- → Every patient shows a 35–49 BPM spike above their own baseline.
-- The spike is the same alert event regardless of absolute BPM.
Step 3 — Score every reading against the patient's own σ
SELECT id, patient_id, hour, hr_bpm,
AVG(hr_bpm) OVER (PARTITION BY patient_id) AS personal_mu,
STDDEV(hr_bpm) OVER (PARTITION BY patient_id) AS personal_sigma
FROM vitals
ORDER BY patient_id, hour;
Step 4 — Surface the actual alarms
SELECT patient_id, hour, hr_bpm
FROM (
SELECT patient_id, hour, hr_bpm,
AVG(hr_bpm) OVER (PARTITION BY patient_id) AS pmu,
STDDEV(hr_bpm) OVER (PARTITION BY patient_id) AS psig
FROM vitals
) t
WHERE hr_bpm > pmu + 2.5 * psig
ORDER BY patient_id, hour;
-- → exactly 5 rows: one tachycardia event per patient
Productionizing
Stream bedside-monitor data into AIDB via REST. Recompute personal
baselines on a 24h rolling window so a chronically tachycardic patient
doesn't constantly fire. Add spo2 and resp_rate and use a single
multivariate distance (Mahalanobis-lite) for richer signal.