Back to trackLesson 2 of 7

Vital Signs 2: Forcing a Run

Problem statement

The problem statement in Lesson 1 called for about 1 in 1,000 readings to be anomalous. But that couldn’t reliably produce what an alert test needs: several abnormal readings in a row.

So our new problem statement is to create a model that will still generate data at the expected rates, but also ensure that we get recognizable runs or sequences of abnormal results instead of “blips”.

Attempt 1: turn up the rate (still Free-tier)

The obvious first thing to try: since 0.1% almost never strings together, let’s just raise the rate in order to tilt probability to our side.

<Mix name="Vitals" synchronous="false">
  <input name="NormalReading" weight="0.75"/>
  <input name="AnomalousReading" weight="0.25"/>
</Mix>

Now, a full quarter of readings come from the tachycardic population; that’s not very realistic, of course, but it should get us the streaks we need. Running it at 1,000 rows, we observe our longest run of consecutive anomalies is 4. Raising that to 5,000 rows, it happens 6 times. Out of 912 separate anomalous episodes total, 689 of them (75%) are still a single isolated reading, not a run at all.

This may be good enough for some purposes, but not others, and it doesn’t match the requirements of the problem statement. We now have the streaks we’re looking for, but no longer the desired rate, and the end result is closer to noise than anything realistic.

Attempt 2: force the run to occur (Pro-tier)

Perturb doesn’t mess with the odds. It runs the baseline for a scheduled stretch, then switches to the disturbance population for a defined number of readings, then switches back — on a timer, not a coin flip.

<?xml version="1.0" encoding="UTF-8"?>
<dataset>
  <root node="Vitals"/>
  <nodes>
    <Perturb name="Vitals" repeat="true">
      <input name="NormalReading" role="normal"/>
      <input name="AnomalousReading" role="disturbance"/>
      <input name="WaitGen" role="wait"/>
      <input name="DurGen" role="duration"/>
    </Perturb>
    <MultivariateNormal name="NormalReading" means="74,120,80" covariance="64,0,0; 0,144,67.2; 0,67.2,64"/>
    <MultivariateNormal name="AnomalousReading" means="195,120,80" covariance="64,0,0; 0,144,67.2; 0,67.2,64"/>
    <Constant name="WaitGen" value="4995" type="double"/>
    <Constant name="DurGen" value="5" type="double"/>
  </nodes>
</dataset>

We used the same two distributions as in Lesson 1. WaitGen/DurGen set the schedule: 4995 normal readings, then a run of exactly 5, repeating, keeping the 0.1% average rate of anomalies.

Examining the output, we see the transition into the first run, right where we expect it (looking at rows 4993–5000):

74.31, 123.92, 81.84
76.74, 126.33, 89.02
72.72, 128.44, 89.73
75.36, 129.29, 80.68
193.43, 116.78, 79.75
196.90, 128.42, 87.07
203.74, 143.49, 99.54
189.12, 116.18, 81.85

The run starts exactly when it was told to and then holds for exactly 5 rows. From this model, you will consistently get a run of five anomalies for every 5000 rows you generate. The baseline stays the normal population from Lesson 1. The run you need to test against is no longer a matter of luck.

What this gives you: a scheduled disturbance instead of a random one. This is the difference between a fixture that might contain the case you’re testing and one that does, on a timer you control and in the shape you need.

Note that we used Constant to produce fixed values for the wait and duration parameters; to inject randomness, you could use a statistical distribution for each input, as long as they’re guaranteed to produce positive numbers. You can also create more complex models if you need to include streaks with different frequency or duration shapes.