Patient Vital-Sign Anomaly (per-patient baseline)

Catch HR spikes against each patient's own personal baseline — the clinical pattern that beats fleet-wide thresholds and prevents alert fatigue.

All recipes· anomaly-detection· 3 minutesbeginnersql
Instance: localhost:8080

Opens your running SynapCores (Patient Vital-Sign Anomaly (per-patient baseline) will be staged for a preview — nothing runs until you click Run). No instance yet? Install free in ~30s.

Share

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.


Get SynapCores Community Edition →

Tags

anomaly-detectionstatisticalhealthcareclinicalpatient-monitoring

Run this on your own machine

Install SynapCores Community Edition free, paste the SQL or Cypher above into the bundled web UI, and watch it run.

Download Free CE