Skip to content

Ground suspected-case case-finding in contact tracing (headline; merges #389)#400

Open
seabbs-bot wants to merge 9 commits into
mainfrom
why-rt-down
Open

Ground suspected-case case-finding in contact tracing (headline; merges #389)#400
seabbs-bot wants to merge 9 commits into
mainfrom
why-rt-down

Conversation

@seabbs-bot

@seabbs-bot seabbs-bot commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Ground suspected-case case-finding in contact tracing (headline)

This began as an investigation into why the joint fit's reproduction number is
pulled down relative to the single-stream fits (leave-one-out traced it to the
DRC suspected surveillance reconciling a suspected-vs-confirmed growth conflict
through the reproduction-number walk, because the suspected stream had only a
constant ascertainment to absorb a change in case-finding effort). It now
merges and unifies with the contact-tracing work in #389 into a single headline
change: the observed contact-tracing follow-up rate drives both channels
case-finding acts through.

Model

  • Shared latent follow-up-rate process (contact_tracing_model). The daily
    "taux de suivi des contacts" is modelled as a logistic random walk q_t ∈ (0,1) over the whole grid, fitted to the reported rate where it exists.
    Because it is latent everywhere it is inferred before the reported window and
    projects into forecasts under its own dynamics rather than a fixed
    covariate stopping at the data boundary (so the case-finding anchor does not
    drop out when the data stop).
  • Non-BVD background scales with it, exp(β_contact·(q−q̄))
    (contact_background_model, from feat(model): add contact-tracing background covariate (#374) #389).
  • BVD suspected ascertainment carries a reporting-effort multiplier anchored
    to the same rate plus a random-walk deviation, exp(β_asc·(q−q̄) + w)
    (reporting_effort_walk_model). Both touch the suspected likelihood only, not
    the shared onsets or p_drc, so neither moves the outbreak size (Testing & case-finding operations vs model: retesting/rule-out and background scaling #374).
  • Headline, not opt-in: the live joint and every frozen re-fit pass the
    follow-up history with suspected_reporting_effort = true. Empty history /
    flag off recover the prior model. Also carries main's confirmed-case
    BetaBinomial overdispersion.

What the fits show (300 draws × 2, confirmatory)

Fit R_T R_T/R0 C_T β_contact β_asc divergences
headline (both channels) 1.32 0.87 4981 3.46 0.53 4
− ascertainment effort 1.35 0.90 5142 2.64 5
− contact grounding (free-walk effort) 1.41 0.93 5316 22
  • Both elasticities are positive and data-supported: contact tracing measurably
    scales detection of non-BVD (β_contact ≈ 3.5) and BVD (β_asc ≈ 0.5)
    suspects.
  • The outbreak size C_T is unchanged across the three (~5000), so the
    case-finding channels re-shape the suspected reporting without moving the
    size — no ascertainment / outbreak-size confounding.
  • Grounding the ascertainment in the observed rate collapses the divergences
    from 22 (free walk) to 4
    : the observed anchor identifies the ascertainment
    change far better than a free walk, so the grounded model is both more
    defensible and better-behaved.

Analysis and sensitivity

  • The main analysis reports the contact-tracing grounding of the reproduction
    number and the two elasticities (background_contact_coef, β_asc).
  • Two gated sensitivity re-fits on the sensitivity page: sens_no_contact
    (drop the grounding) and sens_no_effort (drop the ascertainment effort),
    each compared to the headline for R_T and C_T.

Files

  • src/models/priors.jlcontact_tracing_model (latent rate),
    contact_background_model (feat(model): add contact-tracing background covariate (#374) #389), reporting_effort_walk_model (anchored).
  • src/models/observations.jl — unified latent-q scaling of background and
    ascertainment in reported_cases_model.
  • src/models/joint.jlcontact_followup_history +
    suspected_reporting_effort wiring; background_contact_coef deterministic.
  • src/data.jl, data/observations.toml — the follow-up-rate stream (feat(model): add contact-tracing background covariate (#374) #389).
  • docs/fits/registry.jl, docs/examples/{_setup,analysis,sensitivity}.jl,
    docs/src/news.md, test/test_{contact_background,reporting_effort}.jl.

Supersedes the fixed-covariate form in #389 with the forecast-safe latent
process; #389 can close in favour of this.


This was opened by a bot. Please ping @seabbs for any questions.

seabbs-bot and others added 5 commits July 4, 2026 09:26
Add the situation-report contact-tracing follow-up rate ("taux de suivi
des contacts", 7 June-1 July) as an observed covariate for the non-BVD
suspected-case background, following the case-finding analysis in #374.

Active case-finding scales with the outbreak, so the non-BVD suspect
background has an outbreak-scaled component the calendar-time
background_walk cannot attribute. Scaling on the latent BVD signal would
reopen the p_drc/outbreak-size degeneracy, so the covariate is a fixed
offset on observed data instead.

- data: [contact_followup_history] manifest block + history_float loader
- model: reported_cases_model gains contact_covariate + contact_background_model
  coefficient; bg_daily += beta_contact * covariate, threaded through bvd_joint
- expand_covariate helper (interpolate across span, zero outside)
- analysis: sens_contact_bg re-fit (gated by BVD_RUN_SENSITIVITY), reported
  on the sensitivity page; news entry
- tests: test_contact_background.jl

Off by default (headline fits pass contact_covariate = nothing).

Co-authored-by: Sam Abbott <contact@samabbott.co.uk>
…374)

Per review: the contact-tracing background belongs in the headline model,
not a sensitivity toggle, and detection is a rate multiplier, so the
covariate should SCALE the background rather than add to it.

- multiplicative: bg_daily *= exp(β_contact * z̃), z̃ the follow-up rate
  mean-centred over its reported days (factor 1 at mean intensity and
  off-window); β_contact a non-negative log-scale elasticity, HalfNormal(0, 2)
- headline: the live joint and frozen fits now pass the covariate (frozen
  fits truncate it to their cut-off); refit variants inherit it
- the gated fit is flipped to sens_no_contact (covariate off) to show the
  effect on the headline C_T
- scales the non-BVD background only, not p_drc, so it stays off the
  outbreak-size identification path (#374)

Updates docstrings, manifest note, news and the sensitivity narrative.

Co-authored-by: Sam Abbott <contact@samabbott.co.uk>
In the joint fit the current reproduction number R_T sits below the
established R0, while the single-stream fits stay at or above R0. The DRC
suspected-case surveillance grows more slowly over June than the confirmed
cases (about 2x versus 5.5x), and the suspected likelihood reconciles the two
with only a constant ascertainment and a tight background, neither of which
can absorb a change in reporting effort over time. The discrepancy is instead
carried by the shared reproduction-number walk, which bends R_T down through
the one-sided intervention prior; the genetic anchor makes this visible by
pinning the outbreak size and age.

Add an opt-in reporting-effort multiplier (reporting_effort_walk_model): a
smooth weekly log random walk centred at one that scales the suspected-case
expected counts only, not the shared onset-to-report series the laboratory,
treatment and death streams reuse, so it is identified by the suspected-versus
-rest discrepancy. A suspected-specific reporting slowdown is then carried by
effort rather than by R_T. Off by default (suspected_reporting_effort =
false), so the headline fit is unchanged and sigma_eff -> 0 recovers the
constant ascertainment exactly.

Add a gated sensitivity re-fit (sens_reporting_effort) and a reporting-effort
section on the sensitivity page comparing R_T with and without the term, plus
tests.
@seabbs-bot seabbs-bot requested a review from seabbs as a code owner July 6, 2026 22:10
@seabbs-bot seabbs-bot marked this pull request as draft July 6, 2026 22:10
@seabbs seabbs marked this pull request as ready for review July 7, 2026 07:01
…nfirmed overdispersion)

Resolve overlap in docs/src/news.md by keeping both v1.8.0 Model entries.
Code files (priors.jl, observations.jl, BVDOutbreakSize.jl, registry.jl)
auto-merged; the reporting-effort term and main's confirmed-overdispersion
change are independent and coexist.
@codecov

codecov Bot commented Jul 7, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 88.33%. Comparing base (39e8a6c) to head (0fd40d8).
⚠️ Report is 2 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #400      +/-   ##
==========================================
+ Coverage   87.50%   88.33%   +0.83%     
==========================================
  Files          13       13              
  Lines        2433     2530      +97     
==========================================
+ Hits         2129     2235     +106     
+ Misses        304      295       -9     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

PR #397 replaced the plain Binomial confirmed-case likelihood with a
BetaBinomial, leaving Binomial imported but unused. ExplicitImports'
check_no_stale_explicit_imports flags it on Julia 1.12 (the Test matrix's
Julia 1 jobs), failing the quality tests on main and on any branch merged
from it. Remove the unused import.
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

📖 Documentation preview is ready!

View the docs for this PR at: http://epiforecasts.io/BVDOutbreakSize/previews/PR400/

This preview will be updated automatically when you push new commits.

…erge #389)

Merge the contact-tracing branch (#389) and unify it with the reporting-effort
work into one headline change: the observed contact-tracing follow-up rate
drives BOTH channels case-finding acts through.

- Latent follow-up-rate process (contact_tracing_model): the daily "taux de
  suivi des contacts" is modelled as a logistic random walk q_t over the whole
  grid, fitted to the reported rate where it exists. Being latent everywhere it
  is inferred before the reported window and PROJECTS INTO FORECASTS under its
  own dynamics rather than a fixed covariate stopping with the data.
- The non-BVD background scales with it (exp(β_contact·(q−q̄)),
  contact_background_model), and the BVD suspected ascertainment carries a
  reporting-effort multiplier anchored to the same rate plus a random-walk
  deviation (exp(β_asc·(q−q̄)+w), reporting_effort_walk_model). Both touch the
  suspected likelihood only, not the shared onsets or p_drc, so no
  ascertainment / outbreak-size confounding (#374).
- Headline: the live joint and every frozen re-fit pass the follow-up history
  and suspected_reporting_effort = true. Empty history / flag off recover the
  prior model.
- Sensitivity re-fits sens_no_contact (drop the grounding) and sens_no_effort
  (drop the ascertainment effort) on the sensitivity page; the main analysis
  reports the contact-tracing grounding of the reproduction number.
- Also carries #389's confirmed-case BetaBinomial overdispersion (from main).

Supersedes the fixed-covariate form in #389 with the forecast-safe latent
process. Tests: test_contact_background.jl, test_reporting_effort.jl.
Add the BVD-ascertainment elasticity to contact tracing (β_asc) and its
random-walk SD to the surveillance-parameter summary, alongside the non-BVD
background elasticity, so the headline table shows both case-finding channels.
@seabbs-bot seabbs-bot changed the title Opt-in suspected-case reporting-effort term (why R_T declines in the joint fit) Ground suspected-case case-finding in contact tracing (headline; merges #389) Jul 7, 2026
@seabbs

seabbs commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

This doesn't converge so we can't merge.

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.

2 participants