Vital Signs 1: Simulating Vital Signs
Uses Only Free-Tier SpiceGrinder
Problem Statement
A new patient monitor needs realistic vitals to test against before it ships. The vitals we are monitoring are heart rate and blood pressure. Blood pressure isn’t just a pair of independent numbers; systolic and diastolic pressure move together. A monitor that doesn’t include that relationship lacks realism. A small fraction of readings need to look like an actual event: about 1 in 1,000, to simulate a genuine tachycardic episode, so the alert logic has something real to catch.
The model
Two populations, mixed 999-to-1. Both are a single MultivariateNormal with heart rate, systolic BP, diastolic BP and with the same covariance shape: for simplicity, we are modeling heart rate with no relationship to blood pressure (a zero cross-term), and systolic/diastolic carry a realistic ~0.7 correlation.
The only difference between the two populations is heart rate’s own mean: we choose a mean of 74 for a normal reading and 195 for a tachycardic one. As another simplification, we are choosing not to model an increase in blood pressure along with the elevated heart rate.
A Mix filter allows us to combine these separate data sets together, at the rate we need (1 in 1000 coming from the abnormal values).
<?xml version="1.0" encoding="UTF-8"?>
<dataset>
<root node="Vitals"/>
<nodes>
<Mix name="Vitals" synchronous="false">
<input name="NormalReading" weight="0.999"/>
<input name="AnomalousReading" weight="0.001"/>
</Mix>
<!-- dims: heart rate, systolic BP, diastolic BP. HR uncorrelated with BP (zero
cross-terms); SBP/DBP carry a ~0.7 correlation. -->
<MultivariateNormal name="NormalReading" means="74,120,80" covariance="64,0,0; 0,144,67.2; 0,67.2,64"/>
<!-- Anomalous population: tachycardia (elevated heart rate); BP distribution unchanged -->
<MultivariateNormal name="AnomalousReading" means="195,120,80" covariance="64,0,0; 0,144,67.2; 0,67.2,64"/>
</nodes>
</dataset>
The model produces data similar to: (heart rate, systolic, diastolic):
77.80, 128.44, 87.66
71.35, 105.65, 71.20
80.26, 120.94, 81.61
77.40, 124.31, 90.79
65.20, 112.31, 90.31
79.97, 111.78, 77.27
58.95, 111.23, 61.06
63.75, 115.94, 76.00
Why this works, and why it doesn’t
Running this at volume (5,000 rows with a seed of 42, if you want to reproduce it yourself) shows that the anomaly rate lands right where it should: 5 tachycardic readings, in line with the 1 in 1,000 probability. They land at rows 555, 681, 1239, 1918, and 2396.
Look at that list again: no pair of those rows are consecutive.
You can generate this file forever and never be guaranteed to see the pattern a real alert rule actually needs to be tested against: several abnormal readings in sequence, not one in isolation. That’s not bad luck, it’s math. Each row is an independent draw, so five landing in a row is 0.001 to the 5th power — a 1-in-10^15 event.
A mixture like this one matches the original problem statement. But it might be the wrong tool for testing whether your alert fires.