Skip to content

fix(forecast): project each single-stream fit from its own trajectory - #706

Merged
seabbs merged 2 commits into
mainfrom
fix/stream-forecast-trajectory
Sep 15, 2026
Merged

seabbs merged 2 commits into
mainfrom
fix/stream-forecast-trajectory

Conversation

@seabbs-bot

Copy link
Copy Markdown
Collaborator

What

Every single-stream (individual) fit was forecast by inverting its cumulative
total under exponential growth, while the joint was forecast from its own
fitted trajectory.

forecast_stream takes a stream's cut-off daily rate as the last increment of
its cumulative trajectory, and falls back to _approx_daily — inverting the
cumulative total under exponential growth over the outbreak age — when the
chain carries none. #613 added the four cumulative_* := aliases to
bvd_joint for exactly this reason, noting that the inversion "collapses
towards zero once the fitted growth rate is at or below zero, which is not what
a stream still reporting daily is doing". The single-stream composers never got
the same aliases, so every individual fit kept taking the fallback.

That is the individual-versus-joint gap visible in data/forecast_overlay.csv,
where the individual fits sit below both the joint and the observed counts at
release after release (confirmed deaths at 2026-08-24: individual median 136,
joint 315, observed 292).

What changed

  • cases_only_model, deaths_only_model, confirmed_only_model and
    confirmed_deaths_only_model each expose their own stream's cumulative
    trajectory under the same un-prefixed name bvd_joint uses, so one
    _STREAM_SPEC entry keeps serving both fit kinds.
  • The confirmed-case testing-onset baseline re-add moves into
    _cumulative_confirmed, shared by the joint and the confirmed-only composer,
    so cumulative_confirmed is the same quantity on both.
  • _STREAM_SPEC's trajectory comment no longer says only the joint carries
    one.

No change to the joint's numbers: its cumulative_confirmed is the same
arithmetic, now through the shared helper.

Scope note

This touches src/models/joint.jl, which is hashed into every fit cache key,
so it forces a cold refit of the whole matrix.

Testing

  • New :slow test item samples each of the four composers from the prior and
    asserts the trajectory key is present, is length n, never turns back on
    itself, and resolves through the same _daily_at_cutoff_any the forecaster
    uses. Red before the fix (KeyError: key :cumulative_reports not found).
  • test_forecast.jl, test_boxed_captures.jl, test_cases_model.jl,
    test_confirmed_cases_model.jl, test_confirmed_deaths.jl,
    test_confirmed_cases_daily.jl, test_reported_cases_daily.jl,
    test_suspected_daily.jl, test_suspected_daily_deaths.jl: 344 passing.
    Rebased onto the feat(patch): meta-population model over the three affected provinces #412 patch-model merge and re-run there: the whole of
    test_forecast.jl, 213 passing, including feat(patch): meta-population model over the three affected provinces #412's own province-forecast
    items.

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

🤖 Generated with Claude Code

https://claude.ai/code/session_01XABDXNzg5H61QAxBAT2Tfx

seabbs-bot and others added 2 commits September 15, 2026 21:56
…fits

`forecast_stream` reads each stream's cut-off daily rate off the chain's
cumulative trajectory, falling back to inverting the cumulative total
under exponential growth when the chain carries none. Only `bvd_joint`
exposes those trajectories, so every individual fit takes the fallback.

Red: the four single-stream composers carry no `cumulative_*` alias.

Co-authored-by: Sam Abbott <contact@samabbott.co.uk>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XABDXNzg5H61QAxBAT2Tfx
`forecast_stream` reads a stream's cut-off daily rate as the last
increment of its cumulative trajectory, and falls back to inverting the
cumulative total under exponential growth when the chain carries none.
composers never got them, so every individual fit took the fallback.

That inversion collapses towards zero as the fitted growth rate reaches
zero, which is what the outbreak's streams are now doing, so the
individual fits project below the joint and below the data they are
scored against in `stream_forecasts.csv`, the validation panels and the
by-release overlay.

Each composer now exposes its own stream's trajectory under the same
un-prefixed name the joint uses, so one `_STREAM_SPEC` entry serves both
fit kinds. The confirmed-case baseline re-add moves into
`_cumulative_confirmed`, shared by the joint and the confirmed-only
composer so the two carry the same quantity under the same name.

Co-authored-by: Sam Abbott <contact@samabbott.co.uk>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XABDXNzg5H61QAxBAT2Tfx
@seabbs-bot
seabbs-bot requested a review from seabbs as a code owner September 15, 2026 21:38

@seabbs-review-bot seabbs-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR gives each of the four single-stream composers (deaths_only_model, cases_only_model, confirmed_only_model, confirmed_deaths_only_model) the same un-prefixed cumulative-trajectory := alias bvd_joint already exposes, so forecast_stream reads the cut-off daily rate directly instead of falling back to an exponential-growth inversion that collapses toward zero. The confirmed-cases logic is correctly deduplicated into a shared _cumulative_confirmed function used by both bvd_joint and confirmed_only_model, field names match the underlying submodels exactly, and the added test (paired with the prior red commit) demonstrates the behaviour change. Verdict: sound change; one documentation-placement nit.

Automated first pass by seabbs-review-bot (Claude sonnet), triggered by: first pass. Not a human review. Comment @seabbs-review-bot to ask for another pass: @seabbs any time, the author's agent once it has pushed changes. Add the no-review label to opt this PR out. Ping @seabbs with any questions.

Comment thread src/models/joint.jl
return cumsum(confirmed_daily) .+ [t >= cap ? base : 0 for t in 1:n]
end

## Why every composer exposes its stream's cumulative trajectory as a `:=`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion This 9-line rationale comment floats between _cumulative_confirmed and the unrelated exports_only_model docstring, so a reader meets it right before a composer that never gets a trajectory alias (:exports has an empty trajectory list in _STREAM_SPEC). It also restates, at greater length, what src/forecast.jl's _STREAM_SPEC comment already says under the trajectory: bullet, and what each of the four new := lines already says tersely at its own call site. Either delete this block (the forecast.jl doc and the per-line comments already cover it) or fold it into the _STREAM_SPEC docstring where the reader is actually resolving spec.trajectory.

@seabbs-review-bot seabbs-review-bot Bot added the llm-reviewed Reviewed by the wait-for-review loop label Sep 15, 2026
@seabbs
seabbs merged commit 7e5c107 into main Sep 15, 2026
25 of 26 checks passed
@seabbs
seabbs deleted the fix/stream-forecast-trajectory branch September 15, 2026 22:07
@codecov

codecov Bot commented Sep 15, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.74%. Comparing base (54cc27c) to head (045ebfa).
⚠️ Report is 2 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #706   +/-   ##
=======================================
  Coverage   96.73%   96.74%           
=======================================
  Files          16       16           
  Lines        4536     4540    +4     
=======================================
+ Hits         4388     4392    +4     
  Misses        148      148           

☔ 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.

seabbs-bot added a commit that referenced this pull request Sep 16, 2026
#706 gave the same reason six times: a block comment before the composers,
a two-line comment on each of the four composers, and again in bvd_joint.
Keep the block, drop the four copies, trim the rest.

Co-authored-by: Sam Abbott <contact@samabbott.co.uk>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XABDXNzg5H61QAxBAT2Tfx
seabbs-bot added a commit that referenced this pull request Sep 16, 2026
#706 gave the same reason six times: a block comment before the composers,
a two-line comment on each of the four composers, and again in bvd_joint.
Keep the block, drop the four copies, trim the rest.

Co-authored-by: Sam Abbott <contact@samabbott.co.uk>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XABDXNzg5H61QAxBAT2Tfx
seabbs-bot added a commit that referenced this pull request Sep 17, 2026
The V2.0.0 tag points at 7bebf1c, which is main's head and this
branch's parent, so every pull request the branch had filed under
v2.1.0 already shipped inside that release.
Listing them as changes since v2.0.0 told a reader work was new when
the V2.0.0 download already carried it.

The entries for #706, #707, #709, #710, #712, #714 and #716 move into
the v2.0.0 subsection that fits each by content, and the compat-bound
entry for #696 to #701 moves with them into a new Dependencies
subsection.
None of those was already described there.

#716 contradicted the Model subsection, which still gave the sampler
configuration as 900 draws and 400 adaptation steps.
That line now states the 750 draws and 500 adaptation steps the release
actually shipped, and the Performance entry gives the reasoning without
restating the numbers as a change.

The v2.1.0 heading stays, with one line and no entries, as the landing
place for the work still in flight.
The version bump in Project.toml and CITATION.cff is unchanged.
seabbs pushed a commit that referenced this pull request Sep 17, 2026
The V2.0.0 tag points at 7bebf1c, which is main's head and this
branch's parent, so every pull request the branch had filed under
v2.1.0 already shipped inside that release.
Listing them as changes since v2.0.0 told a reader work was new when
the V2.0.0 download already carried it.

The entries for #706, #707, #709, #710, #712, #714 and #716 move into
the v2.0.0 subsection that fits each by content, and the compat-bound
entry for #696 to #701 moves with them into a new Dependencies
subsection.
None of those was already described there.

#716 contradicted the Model subsection, which still gave the sampler
configuration as 900 draws and 400 adaptation steps.
That line now states the 750 draws and 500 adaptation steps the release
actually shipped, and the Performance entry gives the reasoning without
restating the numbers as a change.

The v2.1.0 heading stays, with one line and no entries, as the landing
place for the work still in flight.
The version bump in Project.toml and CITATION.cff is unchanged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

llm-reviewed Reviewed by the wait-for-review loop

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants