Skip to content

Add the frs_relationships UK spine stage and rebind the ONS household-composition cells (#791) - #903

Merged
juaristi22 merged 4 commits into
mainfrom
uk-household-composition-791
Sep 11, 2026
Merged

Add the frs_relationships UK spine stage and rebind the ONS household-composition cells (#791)#903
juaristi22 merged 4 commits into
mainfrom
uk-household-composition-791

Conversation

@juaristi22

@juaristi22 juaristi22 commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator

Closes #791. Plan of record: María's local repos/uk-791-relationship-to-head-plan.md (approved 2026-09-11; plans are kept outside the tree by her convention); receipts R1–R11 in experiments/791-household-composition-receipts.md.

What changes

Spine. A new stage frs_relationships (after age_tail; manifest 30 → 31 entries, executed roster 28 → 29) re-reads the pinned FRS 2024-25 adult, child and househol tabs and derives four frame columns from the household grid (relhrp, R01R14, hrpid, hrpnum): person.relationship_to_head, person.ons_family_role, person.ons_family_index, household.ons_household_type. The household column is valued with Chronicle's ons-families-households-2025 ons.household_type value ids verbatim, so the frame value, the manifest declaration, the contract row and the published fact are one string. The derivation follows the ONS Families and households definitions (family = couple, or lone parent with a child; a child of any age with no partner or own child belongs to the parent's family; dependent = under 16 or 16–18 in full-time education; one family plus other individuals is one-family; no family present = two or more unrelated adults; two or more families = multi-family). Codes and rules are declared as a schema-validated derive_ons_household_composition manifest operation whose parameters the runtime lockstep-asserts (the age_tail pattern), with the FRS SN 9563 / ONS / Chronicle provenance recorded beside each list.

Gates. uk_stage_frs_relationships_composition (stage health at the assembled boundary: one HEAD per household agreeing with hrpid and hrpnum, contiguous person numbers, no double partners, no unmapped codes, family-index and enum consistency, the ten-cell partition covering every household exactly once, grid reciprocity within the reviewed tolerance of 3 ordered pairs on the 2024-25 tape) and uk_ons_household_type_enum_domain; both in the spine scope and mirrored in the data contract. The four columns are on the export allowlist.

Contract and registers. All ten ons.household_composition.* rows bind on household.ons_household_type == <value id> (one reduce-less condition each; notes rewritten with the translation record); the three microcosm#791 measure exclusions are retired (register 51 → 48; the 2026-08-26 tranche 39 → 36); a compile-time check refuses any ons_household_type condition outside the declared domain or a partition that does not cover it once; the census-households bridge is bound and the local adjudication wording, the uk-data parity register and the cross-grain doctrine note say so.

Executor fix (found by this work). A new dense column on a filtered population (a sampled rung) was widened to float64 because the executor's placeholder was built on a fresh RangeIndex and inserted label-aligned; the placeholder now binds to the table's index, with a regression test.

Re-adjudication of the register (the three retired entries)

  • Tape probe (R2): every invariant clean on all 16,288 households; the ten cells within 25 % of the published 2025 values at design weights before any calibration (multi-family 0.87, unrelated adults 0.73, the rest 0.91–1.10).
  • Full licensed build spine-r (R4): 29 stages, 17/17 spine gates, both new gates passed with the R2 counts.
  • Twin diff vs spine-q (R5): only the four candidate-only columns; no value, row, index, weight or attribute moves.
  • Parity instrument (R6): verdict and register usage identical to spine-q (the pinned reference's pre-existing 20 unsigned differences, Make the UK eFRS parity re-pin one reviewed command; hold every identity mirror in lockstep (companion to #747) #749); the four columns are outside the instrument.
  • Derivation delta (R7): the old bindings were not a partition (6,563 households in two or more cells); multi-family 18.8× → 0.74, unrelated adults 4.0× → 0.62, lone parent non-dependent 2.7× → 0.88 of published, at design weights.
  • National calibration (R8): 367 targets, final loss 0.0114, 0 targets outside the 25 % fence, 6/6 terminal gates; the ten cells within ±0.9 % (the three retired cells within ±0.13 %); the +21.9 % collateral on lone_parent_dependent_children seen on the Q50f run is +0.21 % here.
  • Rowwise dry run (R9): unbound_bridges == []; the composition bridge binds both lower grains with declared factor 1.0 and zero shift, so the 1,011 census cells are numerically unchanged.

Review round 1 (Vahid, at 7779747)

Should-fix 1 is in (dated re-wording trailer on the census_disclosure_control_noise adjudication). Should-fix 2: the plan of record lives outside the tree by María's convention (her local repos/uk-791-relationship-to-head-plan.md); the receipts carry what the tree needs of it. The skip-generation age switch and the ONS source for "two or more unrelated adults" are now on the contract notes and in R1/R11. age NaN refuses instead of reading as 0; stage_health.py carries only the composition hunks. The cached-reference regeneration test in test_uk_parity_reference.py is retired (María's call): it regenerated the reference under the installed engine, was skipped in CI, and has failed on main since the 2.97.0 lock; the other thirteen reference integrity tests stay.

Follow-ups (not in this PR)

🤖 Generated with Claude Code

@vahid-ahmadi

Copy link
Copy Markdown
Contributor

Review at 7779747b (Claude Code, high effort; worktree checkout; without the engine, the tape or the pinned feed: 709 passed, 9 skipped, 0 failed across the sixteen touched and neighbouring suites; ruff check and ruff format --check clean on all 29 changed files; both census tools, the coverage manifest, the spec-engine coverage check and ci_test_groups --verify current; CI green on every lane that has finished. Draft, so a comment rather than a review.)

The derivation is right. I ran derive_frs_relationships directly on seventeen synthetic households built from the FRS grid codes — single under and over 65, married, cohabiting, civil-partner and same-sex couples, couples with dependent, non-dependent and three-plus children, lone parents with dependent and adult children, two three-generation households (grandparent head with a lone-parent daughter and grandchild → one family with the grandparent as other individual; grandparent couple with a partnered child and grandchild → multi-family), grandparent with grandchild and no parent, unrelated adults, siblings, couple plus lodger, a step-child through the cohabitee, a child whose parent is not the head, a one-sided parent link, and a 17-year-old in education with her own baby. Every one lands in the cell the ONS Families and households definitions give, and I checked those definitions against the 2025 bulletin glossary. Partners come from the R-grid only with relhrp used solely for the head relationship, own-child detection is column-any so the parent need not be the head, and 16–18 in full-time education is child-tab membership or the adult fted/educft flag with age 19+ non-dependent regardless.

Registration is lockstep: sources.yaml and source_stages.json both 30 → 31 with the stage in the same position, _STAGE_CONSUMES and _STAGE_CELLS declared, the spine fixture 40 → 41 nodes with only downstream input keys moving, and the policy, gates, spine and release-cut digests and the UK spec sha re-pinned. The ten value ids are one string across runtime tuple, manifest, contract conditions and ledger selectors. The three #791 exclusions are gone (51 → 48, tranche 39 → 36), the ten rows bind on one condition each with the compile-time partition check wired in, and the #900 fence is replaced honestly — a partial bridge stays unbound with reviewed records, a full one reconciles with unbound_bridges == []. The published 2025 values in the receipts match the parity fixture exactly. The gate fails closed on every malformed grid I could think of and the transform refuses first. The executor dtype fix is real and tested.

Should-fix

1. The adjudication register is re-worded under an unchanged signature. local_binding_adjudications.json:85 changes the census_disclosure_control_noise reason from "today it is reviewed-unbound" to "bound from microcosm#791 (2026-09-11)…", but approved_by, approved_on: 2026-08-31 and the "re-worded 2026-09-10 (#887 round 1)" trailer are untouched. You did the right thing on #900 by dating the re-wording; do the same here. The census JSON and local_target_census.py mirrors regenerate from it.

2. The plan of record is not in the tree. Body and receipts cite repos/uk-791-relationship-to-head-plan.md (approved 2026-09-11); there is no repos/ directory at this head. Commit it or point at where it lives.

Questions

  • Skip-generation households are age-dependent by inheritance from FRS coding. Codes 15/16 never form a family, so a grandparent raising a grandchild under 16 with no parent present is unrelated_adult_households — consistent with Families and households, where family means couple or lone parent with child (the "grandparent plus grandchild is a family" rule is Census 2021's). But receipts R1 records that the FRS codes a 16–19 student grandchild's grandparents as guardian (7/3), so the same household becomes lone_parent_dependent_children once the child turns 16. The unrelated_adult_households contract note lists skip-generation there unconditionally; one sentence on the age switch would save the next reader.
  • The note also says ONS counts siblings, foster and skip-generation under "two or more unrelated adults". The bulletin does not define that category and the methodology page 404s, so the R1 quote is the only support I can reach. Not disputing it; flagging it as unverifiable from here.

Nits

  • _evaluate_enum_domain falls back to the engine variable when the enum-domain artifact is absent; only the spine driver supplies it, and the spine scope is evaluated only there, so it is latent rather than live.
  • The H2 parity fixture's synthetic tabs emit only head plus optional child, so the couple and multi-family paths are covered by unit tests, not by the parity oracle.
  • age NaN coerces to 0 → dependent at frs_relationships.py:398; unreachable today because age is validated upstream, but a fillna(0) that silently biases is worth a refusal instead.
  • About 60% of the stage_health.py diff is reformatting of unrelated gates, which buries the composition-gate hunk.

#904 overlap

Both drafts sit on the same base. A trial merge auto-resolves five of the seven shared files and conflicts on two: test_uk_measure_simulation.py (48/36 here vs 50/38 there, different census markers removed) and the UK spec sha in test_spec_engine_country_bundles.py. Whichever lands second needs count 47, tranche 35, both markers dropped and a third sha — the auto-merge would hide that. This one is self-contained and small; I would land it first and rebase #904.

Could not verify

The R2–R9 tape, build and calibration numbers, the educft versus fted tape column, the three-pair reciprocity count, and the Chronicle value ids against Chronicle itself (checked against the ledger selectors generated from the pinned feed).


Nothing blocks. 1 and 2 before this leaves draft; the rest at your discretion. The derivation itself I am satisfied with.

juaristi22 added a commit that referenced this pull request Sep 11, 2026
…regeneration test (#791)

Should-fixes: the census_disclosure_control_noise adjudication reason carries a
dated re-wording trailer for the bound bridge beside the #887 one; the plan of
record is committed as docs/uk-relationship-to-head-plan-791.md and the
receipts and PR body point at it. Questions: the unrelated-adults and
lone-parent contract notes record the skip-generation age switch (under 16
with no parent present: two unrelated adults; 16-19 in full-time education:
the FRS codes the grandparents as legal guardian, so a family with a dependent
child) and cite the ONS "Families and households statistics explained"
definition by URL. Nits: a non-numeric age refuses instead of reading as 0;
stage_health.py is the main text plus the composition hunks only.

Retire test_cached_reference_regeneration_matches_committed_surface (Maria's
call): it regenerated the pinned eFRS parity reference under the installed
engine, was skipped in CI and has failed on main since the 2.97.0 lock; the
other thirteen reference integrity tests stay.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@juaristi22

Copy link
Copy Markdown
Collaborator Author

Thanks, all addressed at b4ab81cf (fixes at 12462fb0, plan pointer at b4ab81cf).

Should-fix 1: the census_disclosure_control_noise adjudication reason now carries a dated re-wording trailer, "re-worded 2026-09-11 (microcosm#791 …)", beside the #887 one, under the unchanged 2026-08-31 approval.

Should-fix 2: the plan of record lives outside the tree by María's convention (her local repos/uk-791-relationship-to-head-plan.md); the receipts carry what the tree needs of it, and the PR body and experiments/791-household-composition-receipts.md now say so rather than citing a path that is not in the repo.

Your skip-generation question is on the unrelated_adult_households and lone_parent_dependent_children_households contract notes: under 16 with no parent present the household is two unrelated adults (codes 15/16 never form a family); a 16–19 grandchild in full-time education is coded by the FRS as the grandparents' legal dependent (7/3), so the same household becomes a family with a dependent child. The "two or more unrelated adults" definition is cited by URL from ONS's "Families and households statistics explained" (2021), which is the source R1 quotes.

Nits: a non-numeric age now refuses (FRSRelationshipsError, test added) instead of reading as 0; stage_health.py is the main text plus the two composition hunks only. The enum-domain artifact fallback and the head-plus-child-only H2 fixture stay as you noted; the couple and multi-family paths are held by the unit tests.

The cached-reference regeneration test in test_uk_parity_reference.py is retired on María's call: it regenerated the reference under the installed engine, was skipped in CI, and has failed on main since the 2.97.0 lock; the other thirteen reference integrity tests stay, including the cached-artifact sha check. Landing order as you suggest: this one first, #904 re-pins the counts and the spec sha.

🤖 Posted with Claude Code

juaristi22 and others added 4 commits September 11, 2026 18:04
…-composition cells (#791)

The FRS household grid (relhrp, R01-R14, hrpid, hrpnum) becomes four exported
frame columns - person.relationship_to_head, person.ons_family_role,
person.ons_family_index and household.ons_household_type - derived per the
ONS Families and households definitions in a new stage after age_tail. The
household type carries Chronicle's ons.household_type value ids verbatim, so
the frame, the manifest, the contract rows and the published facts share one
string. The derivation is declared as a schema-validated
derive_ons_household_composition manifest operation that the runtime
lockstep-asserts, gated at the assembled boundary by a stage-health check and
an enum-domain gate, and mirrored into the data contract's part scopes and
digests.

All ten ons.household_composition.* rows bind on the new column and the three
microcosm#791 measure exclusions are retired (register 51 -> 48), which binds
the national_household_composition_partition_vs_census_households bridge. A
compile-time check refuses any ons_household_type condition outside the
declared domain or a partition that does not cover it once.

Found and fixed on the way: the graph executor's placeholder for a new dense
column on a filtered population (a sampled rung) was built on a fresh
RangeIndex and inserted label-aligned, widening int64 to float64; it now binds
to the table's index, with a regression test.

Receipts in experiments/791-household-composition-receipts.md: tape probe
clean on all 16,288 households; full licensed build 17/17 spine gates; twin
diff vs spine-q = only the four columns; calibration 367 targets, loss 0.0114,
0 outside the 25% fence, the ten cells within +/-0.9%; rowwise dry run binds
the bridge with declared factor 1.0.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit 7779747)
…regeneration test (#791)

Should-fixes: the census_disclosure_control_noise adjudication reason carries a
dated re-wording trailer for the bound bridge beside the #887 one; the plan of
record is committed as docs/uk-relationship-to-head-plan-791.md and the
receipts and PR body point at it. Questions: the unrelated-adults and
lone-parent contract notes record the skip-generation age switch (under 16
with no parent present: two unrelated adults; 16-19 in full-time education:
the FRS codes the grandparents as legal guardian, so a family with a dependent
child) and cite the ONS "Families and households statistics explained"
definition by URL. Nits: a non-numeric age refuses instead of reading as 0;
stage_health.py is the main text plus the composition hunks only.

Retire test_cached_reference_regeneration_matches_committed_surface (Maria's
call): it regenerated the pinned eFRS parity reference under the installed
engine, was skipped in CI and has failed on main since the 2.97.0 lock; the
other thirteen reference integrity tests stay.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit 12462fb)
…eceipts point at her local copy

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit b4ab81c)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@juaristi22
juaristi22 force-pushed the uk-household-composition-791 branch from b4ab81c to 48b1566 Compare September 11, 2026 16:22
@juaristi22
juaristi22 marked this pull request as ready for review September 11, 2026 16:27
@juaristi22
juaristi22 merged commit 3094bfe into main Sep 11, 2026
23 checks passed
MaxGhenis added a commit that referenced this pull request Sep 11, 2026
…mark the stamp as in-process

Two attested identities moved with the merge ref, both re-pinned to the
values the merged tree computes: the UK country bundle (main's #903 added
the frs_relationships spine stage on top of this branch's attested kernel
changes, so neither side's pin survives the merge), and the multispine
pool tool's constants-adapter country_spec digest, which had already moved
to the branch's attested value recorded in docs/evidence/spec-engine/
us-f0-coverage.json when the seed identities were re-pinned but was still
asserted at main's value in test_us_multispine_pool_tool.py.

survey_atomic_geography._population_stamp now says it is an in-process
seal that must never be persisted or compared across reconstructions
(microcosm#907 / #909), naming what a cross-call pin has to carry instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.

UK spine: relationship-to-head frame column, so the ONS household-composition split is measurable

2 participants