Skip to content

Epic — Reconciliation places every country's mass: donor fill for empty-grid draws (3.0.0) #286

Closed

Description

@Polichinel

What this epic delivers

Reconciliation places every country's fatalities on the grid. After this epic, for
every (month, country, draw) the reconciled grid cells sum to the country forecast. When
the grid is all zeros in a draw, the pattern is borrowed from a donor draw in the same month
and country. When no donor exists, the call fails loud. This ships as the new default in
views-frames 3.0.0.

The problem

Reconciliation scales a grid (pgm) forecast so each country's cells sum to that country's
(cm) forecast. Scaling is multiplication, so it can't do one thing: if every cell in a
country is zero in a draw, no multiplier produces a positive total.
Today those draws stay
at zero, and the country's fatalities vanish without a word
(src/views_frames_reconcile/proportional.py:81-98). The contract calls this "an edge, not
a failure" (docs/CICs/Reconcile.md §3/§6), and the published conformance check
(conformance.py:72-91) requires it.

Measured on the first production run (chrome_crate → chrome_box, 2026-10-30,
64,742 cells × 36 months × 1,024 draws × 3 targets; #283):

target fatality mass lost draws affected
lr_sb_best 4.023% 164,429
lr_ns_best 7.321% 188,481
lr_os_best 2.943% 193,903

The scaling itself was exact (0 failures across 22.3M triples). All of the loss is this
case. pipeline-core neither causes nor hides it (views-pipeline-core#579). The test suites
in both repos were green because no fixture ever produced the case (#285).

Why it matters

This forecast is bound for a UN partner. The platform's premise is that the country
forecast is the authority
: reconciliation exists to impose the country's numbers on the
grid. Today the code honours that in one direction (a zero country wipes the grid's mass)
and quietly abandons it in the other (a positive country with an empty grid loses its
mass). That's half a rule, and it fails silently, which breaks ADR-003 (fail loud).

The decision (made 2026-10-31; ADR-030 records it)

Analysis on the real archive (2026-10-31, read-only against the delivery):

  • Every one of the 546,813 empty-grid draws has donors in the same month and country.
    The minimum pool is 10 draws and the median is 130–211. Neither a neighbouring-month
    fallback nor a no-donor case ever happened.
  • Empty-grid draws carry real deaths: country value median 3–5, mean 28–78, never below 1.
  • A draw's country value barely predicts its grid pattern (Spearman 0.06–0.09), so
    k-nearest matching on the country value would behave like a random pick on this data.

So:

  1. Borrow from the same month. For an empty-grid draw with a positive country value,
    copy the pre-scaling pattern of a donor draw chosen at random from the draws in the same
    (month, country) group that have grid mass, then scale as usual.
  2. Random donor, with a seed you can reproduce: default_rng([seed, time, country]).
  3. Fail loud when there is no donor.
  4. No neighbouring-month step and no k-nearest, for now. Both are noted, not built.
  5. It's the new default, with no opt-out → 3.0.0. Under GOVERNANCE.md this is a MAJOR
    for two reasons: a call that succeeds today (a grid with no donor) starts raising, and
    the published conformance laws change. The coordination is small: only
    views-pipeline-core (pinned <3.0.0) and views-models use the package.

"Country is the authority" is a premise, not a finding. ADR-030 states it as a decision,
and a register entry keeps the empirical test open.

Where we end up

The reconciled grid sums to the country forecast in every draw, or the call raises.
The tests would catch it if that ever stopped being true, and mutation testing has shown
it. The contract, the conformance suite and the docs say so. 3.0.0 is on PyPI, and both
consumers have adoption issues.

Scope boundaries

In scope: a new donor_fill.py module and its wiring in module.py; the result
reporting; the fail-loud path; the conformance laws; the CIC/ADR/README/CHANGELOG/register;
realistic test fixtures and mutation testing; a real-data proof; the 3.0.0 release.

Not in scope:

  • proportional.py and grouping.py stay byte-for-byte unchanged (CIC §11, ADR-024
    rule). The torch-oracle kernel parity test stays green as proof.
  • The draw-pairing question (C-62 / ADR-024), which is a separate problem.
  • Neighbouring-month borrowing and k-nearest donors. They're noted below and come back only
    on evidence.
  • Any edit to another repository. Downstream work is listed as notes, not done here.

The stories

# Story Labels Waits for
S1 [#287] ADR-030: the decision, the 3.0.0 bump, register entries planning, adr, needs-decision —
S2 [#285] Realistic test fixtures + baseline mutation audit testing S1
S3 [#288] donor_fill: borrow a donor pattern for empty-grid draws implementation S1, S2
S4 [#284] Fail loud when an empty-grid draw has no donor implementation S3
S5 [#289] Mutation stress-test of the new path (release gate) testing S3, S4
S6 [#290] Restate the conformance laws and the CIC documentation, implementation S3, S4
S7 [#291] README, docstrings, CHANGELOG, register documentation S6
S8 [#292] Real-data proof on the chrome_box archive testing S3, S4
S9 [#293] Adoption issues in views-pipeline-core and views-models (maintainer) release S6
S10 [#294] Release 3.0.0 (maintainer tags) release S5, S7, S8, S9
S11 [#295] Close-out planning S10
S1 → S2 → S3 → S4 ─┬→ S5 ──────────────┐
                   ├→ S6 → S7 ─────────┼→ S10 → S11
                   ├→ S8 ──────────────┤
                   └→ S6 → S9 ─────────┘

The tracking issue #296 lists every issue in order.

Epic-level acceptance criteria

  • For every (time, country, draw) with a positive country value, the reconciled grid
    sums to it within rtol=1e-4, atol=1e-3. Otherwise the call raises ValueError.
  • A filled draw's cells are a scaled copy of one donor draw from the same group: same
    zeros, same proportions.
  • Same inputs + same seed → bit-identical output, whatever the call chunking or group
    order.
  • proportional.py and grouping.py are unchanged; the kernel oracle parity test is
    green; the e2e oracle test is identical outside filled draws.
  • Every mutation recorded in S5 is killed.
  • On the real chrome_box archive: 0 unplaced mass, 0 scaling failures, and the
    filled-draw count equals the 546,813 measured.
  • CIC, conformance, ADR-030, README, CHANGELOG and register all agree.
    bash docs/validate_docs.sh passes.
  • Full gate green: uv run pytest --cov --cov-fail-under=100, ruff check .,
    ruff format --check ., mypy src/ (and --python 3.11), lint-imports.
  • 3.0.0 on PyPI, verified from a clean venv. Adoption issues filed before the tag.

Noted, not worked here

  • views-pipeline-core: reconcile_frames.py:19-22 points at closed S3 — [research] Principled joint probabilistic reconciliation — design + ADR (C-37) #145; #579 drafted
    the fix. Widen the pin to <4 on adoption (S9).
  • views-models: reconciliation/residual.py should report 0 unplaced on 3.0.0. Its
    MANIFEST wording may change (S9).
  • The 2026-10-30 chrome_box UN delivery: whether to re-run it on 3.0.0 is the
    maintainer's call.
  • k-nearest donors: matching needs each donor's pre-reconciliation grid total. The
    archive keeps only scaled output, so testing it needs views-models to retain a raw grid
    pool. Only if the maintainer asks them.
  • Neighbouring-month fallback: only if a real run ever hits the no-donor error.

Refs: #283 (problem + measurement), views-pipeline-core#579 (downstream investigation),
ADR-023, ADR-024, C-62.

Activity

  1. added
    epicEpic tracking issue
    planningPlanning / epic-level work
    reconcileviews_frames_reconcile package
    on Oct 31, 2026
  2. Polichinel commented on Nov 1, 2026

    @Polichinel
    ContributorAuthor

    All stories are done: S1–S8 merged (#297), S9 adoption issues (#293), S10 released 3.0.0 (#294), S11 close-out (#299). Still open, by decision: C-100 (the country-authority premise) and the parked follow-ups (k-nearest donors, the neighbouring-month fallback).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    epicEpic tracking issueplanningPlanning / epic-level workreconcileviews_frames_reconcile package

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions