Skip to content

feat(thresholds): add geometric endemic channel strategy - #591

Draft
knutdrand wants to merge 1 commit into
masterfrom
feat/endemic-channel-strategies
Draft

knutdrand wants to merge 1 commit into
masterfrom
feat/endemic-channel-strategies

Conversation

@knutdrand

Copy link
Copy Markdown
Contributor

What

Adds a geometric endemic channel strategy alongside the existing seasonal and percentile ones:

threshold = expm1(mean(log1p(cases)) + k * std(log1p(cases)))

over historical same-month (or same-week) values — the geometric mean times the geometric standard deviation raised to the power k. std_multiplier takes a scalar or a list, so one request can return several lines.

Why

Case counts are strongly right-skewed, and seasonal's arithmetic mean is pulled up by single epidemic years. Working on a log scale centres the channel on the geometric mean instead, which AM-GM keeps at or below the arithmetic mean for any non-constant history.

Computing on log1p/expm1 rather than a plain geometric mean is deliberate: it keeps zero counts, which a plain geometric mean cannot represent, and an all-zero season yields a finite threshold of 0 rather than dropping the rows.

Caveat, stated in the strategy description

Only the centre is guaranteed lower than the arithmetic channel. The band is multiplicative, so the log-scale standard deviation is a relative spread, and on a short baseline a single extreme year can make the back-transformed band wider than mean + k*std:

baseline (one 20x spike) seasonal geometric
n=5 2179 2652 (wider)
n=8 1681 1211
n=20 1045 445

That matters because percentile defaults to a 5-year baseline, which is exactly where the effect reverses. The registry description, module docstring and contributor guide all say this, and the test asserts the guaranteed property (k=0, AM-GM) rather than the data-dependent inequality.

Notes

  • No endpoint changes — registration through @threshold plus membership in the ThresholdParams union is enough.
  • Contributor guide gains a built-in strategy table covering all three strategies and when to reach for each.
  • An earlier revision of this branch also added a median_iqr (Tukey fence) strategy; it was dropped as redundant next to percentile.

Testing

  • make lint clean (ruff, mypy, pyright)
  • Full suite: 1501 passed, 122 skipped, 4 xfailed
  • New tests cover values, parity with the module-level helper, zero counts, the AM-GM centre against an epidemic-year baseline, multi-line output, weekly data, frequency mismatch, and params validation, plus an end-to-end endpoint test

Add a geometric channel alongside the seasonal and percentile strategies:
expm1(mean(log1p(x)) + k*std(log1p(x))) over historical same-month (or
same-week) values, i.e. the geometric mean times the geometric standard
deviation to the power k. Computing on a log1p scale keeps zero counts, which a
plain geometric mean cannot represent.

The channel is centred on the geometric mean, which AM-GM keeps at or below the
arithmetic mean, so a past epidemic year moves it far less than it moves
mean + k*std. The band is multiplicative rather than additive, so on a short
baseline a single extreme year can make it wider than mean + k*std; the
strategy description and module docstring say so, since the default percentile
baseline is five years.

std_multiplier takes a scalar or a list, so one request can return several
lines.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant