refactor(scoring): one implementation of the harmonisation-break correction - #587
Conversation
There was a problem hiding this comment.
This consolidates the harmonisation-break net - gross arithmetic into one function (confirmed_break_steps in src/data.jl), with confirmed_break_correction and the scorer's break_day_corrections both now delegating to it instead of carrying separate copies. I traced both call paths against the original logic and they preserve the prior behaviour exactly, and the new cross-checking tests (comparing the scorer against the package helper over the whole grid, and against a hand-computed net-minus-gross) give this real coverage. Verdict: sound refactor, worth merging as is.
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.
Merge note: this PR conflicts with #586Verified with Resolution
The trap. The This was opened by a bot. Please ping @seabbs for any questions. |
|
@seabbs-bot resolve ths conflicts |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #587 +/- ##
=======================================
Coverage 93.99% 94.00%
=======================================
Files 15 15
Lines 3497 3502 +5
=======================================
+ Hits 3287 3292 +5
Misses 210 210 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
`scripts/score_releases.jl` and `src/data.jl` each carried the harmonisation-break `net - gross` arithmetic. `src/data.jl` now owns it as `confirmed_break_steps`, with `confirmed_break_correction` the half-open window sum over it, and the scorer keeps only what is its own: which confirmed stream a label reads (`break_stream_kind`) and the grid-day to calendar-date mapping. Sharing did not need `stream_history` in `src/` after all. The shared piece is keyed on which confirmed stream to read, not on resolving every scored stream, so the stream dispatch stays in the script. No scored number changes. A full `scripts/score_releases.jl` run before and after writes byte-identical tables, and the two implementations agree over 175,780 (stream, window) combinations on the current manifest and 106,600 over the synthetic fixtures the test files pin. Adds regression tests for the two scoring behaviours audited alongside this: the basis every stream is scored on (an increment for the cumulative counts, a level for the occupancy stocks) and the manifest's break-day correction reaching the truth for both confirmed streams. Closes #575. Co-authored-by: Sam Abbott <contact@samabbott.co.uk>
…'s data The manifest-driven items asserted values that only hold for the manifest as it stands. `data/observations.toml` advances with each situation report and gains break days as they are found, so a data update could turn them red with no code having changed, and a strictly positive correction on every listed day in both confirmed streams is a property of today's data rather than of the scorer. The behaviour is now pinned on fixtures built in the test: the confirmed-death stream's own net, gross, window edge and scored truth, which had no coverage of its own, and the scorer against `confirmed_break_correction` over every window of a two-break-day fixture. The scorer takes calendar dates and the helper grid days, so that mapping is the one thing that could drift apart. The two live-manifest items stay, as identities rather than values: the correction over `(d - 1, d]` is `net - gross` for each listed day, the two paths agree and stay non-negative over every window of the grid, and a scored truth is its cumulative difference less whatever harmonisation the window carries. Those hold for any manifest, including one listing no break days. Co-authored-by: Sam Abbott <contact@samabbott.co.uk>
The comment called its two break days "the current manifest". They are a constructed declaration, and the manifest lists one day. That was the last comment in the file claiming a fixture is the live data, which is the distinction the items around it now draw. Co-authored-by: Sam Abbott <contact@samabbott.co.uk>
The basis test compared the stream maps against a literal copy of themselves, so it caught a stream declared on the wrong basis but not one scored on the wrong basis: nothing in it reached truth_at. Loop the maps instead and put each stream through truth_at from two different window openings. An increment moves when the window opens earlier and a level does not, which separates the two without pinning a value per stream, and a stream added to a map is covered on the day it is added. Co-authored-by: Sam Abbott <contact@samabbott.co.uk>
6bb5453 to
1b0147c
Compare
|
📖 Documentation preview is ready! View the docs for this PR at: http://epiforecasts.io/BVDOutbreakSize/previews/PR587/ This preview will be updated automatically when you push new commits. |
Closes #575.
What
scripts/score_releases.jlandsrc/data.jleach carried a copy of the harmonisation-breaknet - grossarithmetic.This leaves one implementation.
src/data.jlgainsconfirmed_break_steps, which returns the(grid day, correction)pairs, andconfirmed_break_correctionbecomes the half-open window sum over it.The scorer keeps only what is genuinely its own: mapping a stream label onto which confirmed stream it reads (
break_stream_kind) and mapping grid days onto calendar dates.The blocker recorded in #575 was that sharing would mean moving
stream_historyandSTREAM_ASSEMBLEDintosrc/.It does not.
The shared piece needs to know which confirmed stream to read, not how to resolve every scored stream, so the stream dispatch stays in the script and
src/gains nothing that has to know about assembled streams.No scored number changes.
Equivalence
The consolidated
break_correctionwas compared against the pre-consolidation implementation, run verbatim side by side:truth_aton the current manifest, every window and both confirmed streams: equal to the raw cumulative difference less the correction throughout.A full
scripts/score_releases.jlrun before and after the change produces byte-identical scoring tables.Audit of the two other concerns raised alongside #575
Both were checked against the code and the data; neither is a bug, so neither is "fixed" here.
Each is pinned by a regression test instead, which is the part of this PR that is not a pure refactor.
Are cumulative streams scored on the level rather than the increment?
No.
forecast_archivestoresconfirmed_new,confirmed_deaths_new,recovered_newandonset_reports_new, andforecast_streamreturns the new count over the horizon for everykind = :cumulativestream.truth_atmatches them: the cumulative-count streams takecum_at(target) - cum_at(made)and only the three occupancy stocks take a level.On the current manifest, the (2026-07-08, 2026-07-22] window scores confirmed cases at 841 against a cumulative total of 2905, and confirmed deaths at 470 against 1269.
test_scoring.jlnow pins the basis of every scored stream, so a stream added on the wrong one fails rather than scoring a cumulative total against an increment forecast.Are the harmonisation jumps applied correctly?
Yes, everywhere they should be.
On the one break day the manifest lists, 2026-07-22, the correction is 272 cases and 174 deaths (a
netof 369 and 236 against printed 24h counts of 97 and 62), and both implementations agree on both streams.It reaches the forecast truth, the persistence baseline centre and the baseline's own step pool; the per-horizon and per-release tables aggregate scored rows, so they inherit it.
The window is half open on the left in both, so a release made on the break day is not charged for it.
A break day cannot be silently skipped on the current manifest:
check_break_grosserrors at load if a listed day matches no vintage in either confirmed history.In the current release set the correction is visible in the scored output.
The 21-day window from 2026-07-01 scores confirmed cases at 1173, not the raw 1445, and the 14-day window from 2026-07-08 at 841, not 1113.
Confirmed deaths over the same 14-day window score at 470, not 644.
The stale committed scoring tables, and a fallback that hides them
data/forecast_overlay.csvin the repository recordsobserved = 1113.0for confirmed cases atmade_date2026-07-08, horizon 14, target 2026-07-22.That is the raw cumulative difference. A live run of the scorer gives 841 for the same window, the notified count with the 272-case harmonisation taken out.
Those tables were last written on 2026-07-29 and so predate the correction landing in #524. The 1113 is almost certainly the number that prompted the report that the jumps were not being applied: the code is right and the committed artefact is three weeks stale.
They are meant to be only a fallback.
docs.ymlre-runsscripts/score_releases.jlbefore rendering, so the published report uses fresh, corrected scores. The most recent build onmainloggedWrote ... scored forecasts, and filtering out the script text GitHub echoes in the run-block header leaves zero real fallbacks across the recent Documenter runs, so the report is correct today and the fallback has not fired.The hazard is that it is silent.
.github/workflows/docs.yml:196isOn any failure the render continues against CSVs that are known to be wrong, with one log line to say so. The scorer makes around a hundred
ghrelease-asset downloads per run, and this repo saw transient CDN 429/503 failures on other jobs today, so the mode is reachable rather than theoretical. A report that quietly publishedobserved = 1113for a window whose truth is 841 would look like a scoring bug rather than a skipped refresh.My recommendation is to make it fail loudly. The rescore is cheap next to the fits and the render, and a hard failure is recoverable by re-running the job, whereas a silent substitution of stale data is not detectable from the rendered page. Refreshing or deleting the committed CSVs helps too, but on its own it only moves the staleness rather than removing the silence.
Out of scope for this PR, which is behaviour-neutral. Worth its own issue.
Tests
test/test_break_day_correction.jl: the correction on the confirmed-death stream, which had no coverage of its own, with its net, gross, window edge and scored truth pinned on a fixture; and the scorer againstconfirmed_break_correctionover every window of a two-break-day fixture, since the scorer takes calendar dates and the helper grid days and that is the one thing that could drift apart.test/test_break_day_correction.jl, live manifest: two items that assert identities rather than values. The correction over(d - 1, d]equalsnet - grossfor each listed day, the scorer and the helper agree and stay non-negative over every window of the grid, and a scored truth is its cumulative difference less whatever harmonisation the window carries. These hold for any manifest, including one listing no break days, so a data update cannot turn them red on its own.test/test_scoring.jl:truth_atreturns an increment for every cumulative stream and a level for every occupancy stock, plus the pinned stream-basis map.Scoped to this checkout's own
test/root, since the repository has sibling worktrees an unscoped run would collect from.task formatis clean and thetest/package/quality set is 187 pass, 323 pre-existing broken, 0 fail.This was opened by a bot. Please ping @seabbs for any questions.