FAILURE MAP
← Case archive

FA-65146 / Epidemic compartment models / Open access

Epidemic growth rate, doubling time and R: slope estimator · case 01

Growth rate depends only on the first and last counts and on how many days were positive.

Verified by executionVariant 1 · 7 checks per implementationDownload source bundle ↓JSON ↗

ROOT CAUSE

An endpoint difference is divided by the number of points rather than fitting the regression.

VERIFIED REPAIR

Restore the slope estimator rule: `r = sxy / sxx`.

Unsuccessful approach: Endpoint slope over the day span still ignores interior observations.

Case contract

r is the least-squares slope of ln(count) against day index over days with positive counts (original day indices kept); doubling time ln2/r only when r>0 else None; latent time Tl=latent_fraction*Tg, infectious time Ti=Tg-Tl, R=(1+r*Tl)*(1+r*Ti); return [r rounded 6, doubling rounded 4, R rounded 6]; None with fewer than two positive days.

Why this case matters

Compartmental epidemic calculations drive outbreak forecasts, vaccine targets and hospital planning; a single wrong flow, rate conversion or boundary silently changes every downstream number.

1 / The failure

Exit 1
"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(counts, generation_mean, latent_fraction):
    pts = [(d, math.log(c)) for d, c in enumerate(counts) if c > 0]
    if len(pts) < 2:
        return None
    n = len(pts)
    mx = sum(d for d, _ in pts) / n
    my = sum(y for _, y in pts) / n
    sxx = sum((d - mx) ** 2 for d, _ in pts)
    sxy = sum((d - mx) * (y - my) for d, y in pts)
    r = (pts[-1][1] - pts[0][1]) / (n - 1)
    doubling = round(math.log(2) / r, 4) if r > 0 else None
    tl = latent_fraction * generation_mean
    ti = generation_mean - tl
    rep = (1 + r * tl) * (1 + r * ti)
    return [round(r, 6), doubling, round(rep, 6)]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[('regression: noisy growth', ([3, 5, 6, 10, 14, 19, 30], 5.0, 0.4), [0.372323, 1.8617, 3.693365]),
  ('regression: with reporting gap zero', ([2, 4, 0, 9, 13, 0, 30], 6.0, 0.5), [0.437065, 1.5859, 5.341617]),
  ('regression: declining', ([50, 41, 30, 26, 18, 15], 4.0, 0.3), [-0.246645, None, 0.217822]),
  ('control: flat', ([7, 7, 7, 7], 5.0, 0.5), [0.0, None, 1.0]),
  ('control: too few positives', ([0, 0, 4, 0], 5.0, 0.5), None),
  ('control: exact doubling per day', ([1, 2, 4, 8, 16], 3.0, 0.5), [0.693147, 1.0, 4.160461]),
  ('regression: leading zeros', ([0, 0, 1, 3, 5, 12], 7.0, 0.6), [0.796555, 0.8702, 14.037592])],
 [('regression: noisy growth', ([3, 5, 6, 10, 14, 19, 30], 5.0, 0.4), [0.372323, 1.8617, 3.693365]),
  ('control: flat', ([7, 7, 7, 7], 5.0, 0.5), [0.0, None, 1.0]),
  ('control: too few positives', ([0, 0, 4, 0], 5.0, 0.5), None),
  ('control: exact doubling per day', ([1, 2, 4, 8, 16], 3.0, 0.5), [0.693147, 1.0, 4.160461]),
  ('regression: leading zeros', ([0, 0, 1, 3, 5, 12], 7.0, 0.6), [0.796555, 0.8702, 14.037592]),
  ('regression: no latency', ([10, 13, 17, 22, 28], 5.0, 0.0), [0.258533, 2.6811, 2.292666]),
  ('control: two points', ([5, 9], 4.5, 0.25), [0.587787, 1.1792, 4.956834])],
 [('regression: noisy growth', ([3, 5, 6, 10, 14, 19, 30], 5.0, 0.4), [0.372323, 1.8617, 3.693365]),
  ('regression: with reporting gap zero', ([2, 4, 0, 9, 13, 0, 30], 6.0, 0.5), [0.437065, 1.5859, 5.341617]),
  ('control: flat', ([7, 7, 7, 7], 5.0, 0.5), [0.0, None, 1.0]),
  ('control: too few positives', ([0, 0, 4, 0], 5.0, 0.5), None),
  ('regression: leading zeros', ([0, 0, 1, 3, 5, 12], 7.0, 0.6), [0.796555, 0.8702, 14.037592]),
  ('regression: no latency', ([10, 13, 17, 22, 28], 5.0, 0.0), [0.258533, 2.6811, 2.292666]),
  ('control: two points', ([5, 9], 4.5, 0.25), [0.587787, 1.1792, 4.956834])],
 [('regression: noisy growth', ([3, 5, 6, 10, 14, 19, 30], 5.0, 0.4), [0.372323, 1.8617, 3.693365]),
  ('regression: with reporting gap zero', ([2, 4, 0, 9, 13, 0, 30], 6.0, 0.5), [0.437065, 1.5859, 5.341617]),
  ('regression: declining', ([50, 41, 30, 26, 18, 15], 4.0, 0.3), [-0.246645, None, 0.217822]),
  ('control: flat', ([7, 7, 7, 7], 5.0, 0.5), [0.0, None, 1.0]),
  ('control: too few positives', ([0, 0, 4, 0], 5.0, 0.5), None),
  ('control: exact doubling per day', ([1, 2, 4, 8, 16], 3.0, 0.5), [0.693147, 1.0, 4.160461]),
  ('regression: leading zeros', ([0, 0, 1, 3, 5, 12], 7.0, 0.6), [0.796555, 0.8702, 14.037592])],
 [('regression: noisy growth', ([3, 5, 6, 10, 14, 19, 30], 5.0, 0.4), [0.372323, 1.8617, 3.693365]),
  ('control: flat', ([7, 7, 7, 7], 5.0, 0.5), [0.0, None, 1.0]),
  ('control: too few positives', ([0, 0, 4, 0], 5.0, 0.5), None),
  ('control: exact doubling per day', ([1, 2, 4, 8, 16], 3.0, 0.5), [0.693147, 1.0, 4.160461]),
  ('regression: leading zeros', ([0, 0, 1, 3, 5, 12], 7.0, 0.6), [0.796555, 0.8702, 14.037592]),
  ('regression: no latency', ([10, 13, 17, 22, 28], 5.0, 0.0), [0.258533, 2.6811, 2.292666]),
  ('control: two points', ([5, 9], 4.5, 0.25), [0.587787, 1.1792, 4.956834])]]
for label, args, expected in fixtures[N - 1]:
    check(label, solve(*args), expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
Boundary fixtureActualExpectedOutcome
regression: noisy growth[0.383764, 1.8062, 3.802471][0.372323, 1.8617, 3.693365]Failed
regression: with reporting gap zero[0.677013, 1.0238, 9.187189][0.437065, 1.5859, 5.341617]Failed
regression: declining[-0.240795, None, 0.231641][-0.246645, None, 0.217822]Failed
control: flat[0.0, None, 1.0][0.0, None, 1.0]Passed
control: too few positivesNoneNonePassed
control: exact doubling per day[0.693147, 1.0, 4.160461][0.693147, 1.0, 4.160461]Passed
regression: leading zeros[0.828302, 0.8368, 14.86647][0.796555, 0.8702, 14.037592]Failed

SHA-256 / 9b8db7c46cc8a6ee34f4b40d6df1d99fff922d3aa6152563d4f3f0733b7cd6a4

2 / The unsuccessful fix

Exit 1
"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(counts, generation_mean, latent_fraction):
    pts = [(d, math.log(c)) for d, c in enumerate(counts) if c > 0]
    if len(pts) < 2:
        return None
    n = len(pts)
    mx = sum(d for d, _ in pts) / n
    my = sum(y for _, y in pts) / n
    sxx = sum((d - mx) ** 2 for d, _ in pts)
    sxy = sum((d - mx) * (y - my) for d, y in pts)
    r = (pts[-1][1] - pts[0][1]) / (pts[-1][0] - pts[0][0])
    doubling = round(math.log(2) / r, 4) if r > 0 else None
    tl = latent_fraction * generation_mean
    ti = generation_mean - tl
    rep = (1 + r * tl) * (1 + r * ti)
    return [round(r, 6), doubling, round(rep, 6)]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[('regression: noisy growth', ([3, 5, 6, 10, 14, 19, 30], 5.0, 0.4), [0.372323, 1.8617, 3.693365]),
  ('regression: with reporting gap zero', ([2, 4, 0, 9, 13, 0, 30], 6.0, 0.5), [0.437065, 1.5859, 5.341617]),
  ('regression: declining', ([50, 41, 30, 26, 18, 15], 4.0, 0.3), [-0.246645, None, 0.217822]),
  ('control: flat', ([7, 7, 7, 7], 5.0, 0.5), [0.0, None, 1.0]),
  ('control: too few positives', ([0, 0, 4, 0], 5.0, 0.5), None),
  ('control: exact doubling per day', ([1, 2, 4, 8, 16], 3.0, 0.5), [0.693147, 1.0, 4.160461]),
  ('regression: leading zeros', ([0, 0, 1, 3, 5, 12], 7.0, 0.6), [0.796555, 0.8702, 14.037592])],
 [('regression: noisy growth', ([3, 5, 6, 10, 14, 19, 30], 5.0, 0.4), [0.372323, 1.8617, 3.693365]),
  ('control: flat', ([7, 7, 7, 7], 5.0, 0.5), [0.0, None, 1.0]),
  ('control: too few positives', ([0, 0, 4, 0], 5.0, 0.5), None),
  ('control: exact doubling per day', ([1, 2, 4, 8, 16], 3.0, 0.5), [0.693147, 1.0, 4.160461]),
  ('regression: leading zeros', ([0, 0, 1, 3, 5, 12], 7.0, 0.6), [0.796555, 0.8702, 14.037592]),
  ('regression: no latency', ([10, 13, 17, 22, 28], 5.0, 0.0), [0.258533, 2.6811, 2.292666]),
  ('control: two points', ([5, 9], 4.5, 0.25), [0.587787, 1.1792, 4.956834])],
 [('regression: noisy growth', ([3, 5, 6, 10, 14, 19, 30], 5.0, 0.4), [0.372323, 1.8617, 3.693365]),
  ('regression: with reporting gap zero', ([2, 4, 0, 9, 13, 0, 30], 6.0, 0.5), [0.437065, 1.5859, 5.341617]),
  ('control: flat', ([7, 7, 7, 7], 5.0, 0.5), [0.0, None, 1.0]),
  ('control: too few positives', ([0, 0, 4, 0], 5.0, 0.5), None),
  ('regression: leading zeros', ([0, 0, 1, 3, 5, 12], 7.0, 0.6), [0.796555, 0.8702, 14.037592]),
  ('regression: no latency', ([10, 13, 17, 22, 28], 5.0, 0.0), [0.258533, 2.6811, 2.292666]),
  ('control: two points', ([5, 9], 4.5, 0.25), [0.587787, 1.1792, 4.956834])],
 [('regression: noisy growth', ([3, 5, 6, 10, 14, 19, 30], 5.0, 0.4), [0.372323, 1.8617, 3.693365]),
  ('regression: with reporting gap zero', ([2, 4, 0, 9, 13, 0, 30], 6.0, 0.5), [0.437065, 1.5859, 5.341617]),
  ('regression: declining', ([50, 41, 30, 26, 18, 15], 4.0, 0.3), [-0.246645, None, 0.217822]),
  ('control: flat', ([7, 7, 7, 7], 5.0, 0.5), [0.0, None, 1.0]),
  ('control: too few positives', ([0, 0, 4, 0], 5.0, 0.5), None),
  ('control: exact doubling per day', ([1, 2, 4, 8, 16], 3.0, 0.5), [0.693147, 1.0, 4.160461]),
  ('regression: leading zeros', ([0, 0, 1, 3, 5, 12], 7.0, 0.6), [0.796555, 0.8702, 14.037592])],
 [('regression: noisy growth', ([3, 5, 6, 10, 14, 19, 30], 5.0, 0.4), [0.372323, 1.8617, 3.693365]),
  ('control: flat', ([7, 7, 7, 7], 5.0, 0.5), [0.0, None, 1.0]),
  ('control: too few positives', ([0, 0, 4, 0], 5.0, 0.5), None),
  ('control: exact doubling per day', ([1, 2, 4, 8, 16], 3.0, 0.5), [0.693147, 1.0, 4.160461]),
  ('regression: leading zeros', ([0, 0, 1, 3, 5, 12], 7.0, 0.6), [0.796555, 0.8702, 14.037592]),
  ('regression: no latency', ([10, 13, 17, 22, 28], 5.0, 0.0), [0.258533, 2.6811, 2.292666]),
  ('control: two points', ([5, 9], 4.5, 0.25), [0.587787, 1.1792, 4.956834])]]
for label, args, expected in fixtures[N - 1]:
    check(label, solve(*args), expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
Boundary fixtureActualExpectedOutcome
regression: noisy growth[0.383764, 1.8062, 3.802471][0.372323, 1.8617, 3.693365]Failed
regression: with reporting gap zero[0.451342, 1.5357, 5.541434][0.437065, 1.5859, 5.341617]Failed
regression: declining[-0.240795, None, 0.231641][-0.246645, None, 0.217822]Failed
control: flat[0.0, None, 1.0][0.0, None, 1.0]Passed
control: too few positivesNoneNonePassed
control: exact doubling per day[0.693147, 1.0, 4.160461][0.693147, 1.0, 4.160461]Passed
regression: leading zeros[0.828302, 0.8368, 14.86647][0.796555, 0.8702, 14.037592]Failed

SHA-256 / f7dcdeb63f63a29e8d2e69b6811ac9098421b979469b323687b3313b9661be5f

3 / The verified repair

Exit 0
"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(counts, generation_mean, latent_fraction):
    pts = [(d, math.log(c)) for d, c in enumerate(counts) if c > 0]
    if len(pts) < 2:
        return None
    n = len(pts)
    mx = sum(d for d, _ in pts) / n
    my = sum(y for _, y in pts) / n
    sxx = sum((d - mx) ** 2 for d, _ in pts)
    sxy = sum((d - mx) * (y - my) for d, y in pts)
    r = sxy / sxx
    doubling = round(math.log(2) / r, 4) if r > 0 else None
    tl = latent_fraction * generation_mean
    ti = generation_mean - tl
    rep = (1 + r * tl) * (1 + r * ti)
    return [round(r, 6), doubling, round(rep, 6)]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[('regression: noisy growth', ([3, 5, 6, 10, 14, 19, 30], 5.0, 0.4), [0.372323, 1.8617, 3.693365]),
  ('regression: with reporting gap zero', ([2, 4, 0, 9, 13, 0, 30], 6.0, 0.5), [0.437065, 1.5859, 5.341617]),
  ('regression: declining', ([50, 41, 30, 26, 18, 15], 4.0, 0.3), [-0.246645, None, 0.217822]),
  ('control: flat', ([7, 7, 7, 7], 5.0, 0.5), [0.0, None, 1.0]),
  ('control: too few positives', ([0, 0, 4, 0], 5.0, 0.5), None),
  ('control: exact doubling per day', ([1, 2, 4, 8, 16], 3.0, 0.5), [0.693147, 1.0, 4.160461]),
  ('regression: leading zeros', ([0, 0, 1, 3, 5, 12], 7.0, 0.6), [0.796555, 0.8702, 14.037592])],
 [('regression: noisy growth', ([3, 5, 6, 10, 14, 19, 30], 5.0, 0.4), [0.372323, 1.8617, 3.693365]),
  ('control: flat', ([7, 7, 7, 7], 5.0, 0.5), [0.0, None, 1.0]),
  ('control: too few positives', ([0, 0, 4, 0], 5.0, 0.5), None),
  ('control: exact doubling per day', ([1, 2, 4, 8, 16], 3.0, 0.5), [0.693147, 1.0, 4.160461]),
  ('regression: leading zeros', ([0, 0, 1, 3, 5, 12], 7.0, 0.6), [0.796555, 0.8702, 14.037592]),
  ('regression: no latency', ([10, 13, 17, 22, 28], 5.0, 0.0), [0.258533, 2.6811, 2.292666]),
  ('control: two points', ([5, 9], 4.5, 0.25), [0.587787, 1.1792, 4.956834])],
 [('regression: noisy growth', ([3, 5, 6, 10, 14, 19, 30], 5.0, 0.4), [0.372323, 1.8617, 3.693365]),
  ('regression: with reporting gap zero', ([2, 4, 0, 9, 13, 0, 30], 6.0, 0.5), [0.437065, 1.5859, 5.341617]),
  ('control: flat', ([7, 7, 7, 7], 5.0, 0.5), [0.0, None, 1.0]),
  ('control: too few positives', ([0, 0, 4, 0], 5.0, 0.5), None),
  ('regression: leading zeros', ([0, 0, 1, 3, 5, 12], 7.0, 0.6), [0.796555, 0.8702, 14.037592]),
  ('regression: no latency', ([10, 13, 17, 22, 28], 5.0, 0.0), [0.258533, 2.6811, 2.292666]),
  ('control: two points', ([5, 9], 4.5, 0.25), [0.587787, 1.1792, 4.956834])],
 [('regression: noisy growth', ([3, 5, 6, 10, 14, 19, 30], 5.0, 0.4), [0.372323, 1.8617, 3.693365]),
  ('regression: with reporting gap zero', ([2, 4, 0, 9, 13, 0, 30], 6.0, 0.5), [0.437065, 1.5859, 5.341617]),
  ('regression: declining', ([50, 41, 30, 26, 18, 15], 4.0, 0.3), [-0.246645, None, 0.217822]),
  ('control: flat', ([7, 7, 7, 7], 5.0, 0.5), [0.0, None, 1.0]),
  ('control: too few positives', ([0, 0, 4, 0], 5.0, 0.5), None),
  ('control: exact doubling per day', ([1, 2, 4, 8, 16], 3.0, 0.5), [0.693147, 1.0, 4.160461]),
  ('regression: leading zeros', ([0, 0, 1, 3, 5, 12], 7.0, 0.6), [0.796555, 0.8702, 14.037592])],
 [('regression: noisy growth', ([3, 5, 6, 10, 14, 19, 30], 5.0, 0.4), [0.372323, 1.8617, 3.693365]),
  ('control: flat', ([7, 7, 7, 7], 5.0, 0.5), [0.0, None, 1.0]),
  ('control: too few positives', ([0, 0, 4, 0], 5.0, 0.5), None),
  ('control: exact doubling per day', ([1, 2, 4, 8, 16], 3.0, 0.5), [0.693147, 1.0, 4.160461]),
  ('regression: leading zeros', ([0, 0, 1, 3, 5, 12], 7.0, 0.6), [0.796555, 0.8702, 14.037592]),
  ('regression: no latency', ([10, 13, 17, 22, 28], 5.0, 0.0), [0.258533, 2.6811, 2.292666]),
  ('control: two points', ([5, 9], 4.5, 0.25), [0.587787, 1.1792, 4.956834])]]
for label, args, expected in fixtures[N - 1]:
    check(label, solve(*args), expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
Boundary fixtureActualExpectedOutcome
regression: noisy growth[0.372323, 1.8617, 3.693365][0.372323, 1.8617, 3.693365]Passed
regression: with reporting gap zero[0.437065, 1.5859, 5.341617][0.437065, 1.5859, 5.341617]Passed
regression: declining[-0.246645, None, 0.217822][-0.246645, None, 0.217822]Passed
control: flat[0.0, None, 1.0][0.0, None, 1.0]Passed
control: too few positivesNoneNonePassed
control: exact doubling per day[0.693147, 1.0, 4.160461][0.693147, 1.0, 4.160461]Passed
regression: leading zeros[0.796555, 0.8702, 14.037592][0.796555, 0.8702, 14.037592]Passed

SHA-256 / 3fb9a4ce15d63c4c94c23e9a57afc36add7bc056b0466ce211c43b86c607e6fa

Verification & scope

Deterministic bounded teaching model with a stipulated contract; not a validated scientific or public-health modelling library. This reproducer isolates one failure mechanism. Results cover the supplied fixtures. Variants within a family share a test contract and should remain grouped when constructing evaluation splits. Related mechanisms with a shared evaluation_group must also remain together; these controlled models are not independent production incidents.

Observations recorded using Python 3.12.14 at 2026-09-29T14:47:31.352929+00:00.

Case digest / 3967f486273c90c93e0697cfb5b4504502c839ec13e121cad51e3456c398e9f3