Conversation
EMPSTATI 11 is "Other Inactive" in the UKDS FRS 2024-25 data dictionary (adult table; 9 is "Permanently sick/disabled"). The UK runtime kept the incumbent truncated map's artifact and sent 11 to LONG_TERM_DISABLED, plus a post-map fillna that sent any code above the domain there too. That put other-inactive adults into the ESA health-condition and support-group proxies. FRS_EMPSTATI_EMPLOYMENT_STATUS now maps codes 1-11 one data-dictionary label per line, matching policyengine-uk-data#526. People outside adult.tab stay CHILD; an adult with a blank or unknown code refuses the build, with the error naming the codes and suppressing adult counts under 10. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The stage notes in uk/source_stages.json and uk/spec/sources.yaml (and the charter-H2 fixture's copy) described code 11 as the incumbent truncated-map artifact. They now state the data-dictionary mapping, the CHILD rule for people outside adult.tab and the refusal for blank or unknown adult codes, citing uk-data#526 the way other stage notes cite the incumbent (the source manifest refuses the package name). release_input_coverage_manifest.json pins sha256(source_stages.json) in all 15 families; regenerated with tools/build_uk_release_input_coverage_manifest.py (--check passes). The notes are outside stage_contract_sha256, so uk_spine.json does not move. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The receipt records the pinned FRS 2024-25 adult.tab's EMPSTATI code counts (every adult carries 1-11; code 11 is 916 adults, 2.05m grossed) and the consumers of employment_status. The one-off differential loads uk-data#526's code table and derive function from the PR head by AST and agrees with microcosm on 34 fixed cases, 2,000 Hypothesis examples and all 34,966 licensed FRS 2024-25 people (0 mismatches). Aggregates only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ten mutations of the mapping and the ESA proxy statuses, each applied to a temporary copy of microcosm-build's sources placed first on PYTHONPATH; all ten fail the employment, legacy-proxy and engine enum tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#526's mapping is byte-identical to d3984002; only its refusal message changed. The differential now records each side's refusal exception type: under pandas 3, uk-data's new message raises TypeError for a blank code next to another unknown code (ValueError under its locked pandas 2.3.3). Statuses and refusals still agree everywhere, with 0 mismatches over 34,966 licensed FRS 2024-25 people. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#526's refusal message now formats float codes explicitly, so it refuses with ValueError under pandas 3 too; the mapping is unchanged since d3984002. The differential asserts ValueError on both sides and still finds 0 mismatches over 34,966 licensed FRS 2024-25 people. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The changelog fragment cites uk-data#526 instead of the incumbent package name, which test_no_incumbent_data_package_references_in_live_tree refuses (CI engine-free's only failure). - The differential suppresses a mismatch count from 1 to 9 and is re-pinned to uk-data#526's head 5f9912df (frs.py byte-identical to 89c48e07). - The ESA support-group property asserts equality with reported, working age, code 9 and no hours; the mutation check gains "support group ignores hours worked" (11/11 killed). - The receipt describes each parity instrument separately and says CI loads only the committed eFRS extraction. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adults are not survey households, so even a count of 10 or more adults could describe fewer than 10 households in a build log. The refusal now names the offending codes only (uk-data#526 made the same change at 1832adeb). A Hypothesis test checks that no digit appears outside the code list, and the mutation check's "adult count printed" mutation is killed (11/11). The differential is re-pinned to uk-data#526's 1832adeb (mapping and accept/refuse logic unchanged since d3984002; 0 mismatches over 34,966 licensed FRS 2024-25 people) and suppresses its status table by households, not people. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What was wrong
The UK
frs_employmentstage mapped FRS EMPSTATI with a table that kept the incumbent's truncated-map artifact on purpose: code 11 went toLONG_TERM_DISABLED, and a post-map.fillna("LONG_TERM_DISABLED")sent any code above the domain there too. In the UKDS FRS 2024-25 data dictionary (SN 9563, adult table, EMPSTATI "Adult - Employment Status - ILO definition"), 11 is "Other Inactive" and 9 is "Permanently sick/disabled". policyengine-uk'sEmploymentStatushasOTHER_INACTIVE.In the pinned FRS 2024-25
adult.tab, 916 adults (2.05m grossed) have code 11, beside 1,678 (3.24m) with code 9. All 2,594 came outLONG_TERM_DISABLED. Working-age code-11 adults with a reported status therefore entered the ESA health-condition proxy, and those with no hours worked also the support-group proxy. Code-11 adults at State Pension age were in neither, because both proxies require working age.This ports the incumbent's fix, PolicyEngine/policyengine-uk-data#526, so the two stay at parity.
The fix
FRS_EMPSTATI_EMPLOYMENT_STATUS(immutable) maps codes 1-11 one data-dictionary label per line; 11 isOTHER_INACTIVE.derive_employment_status_from_frs(empstati, is_adult_record):adult.tabareCHILD. The stage still alignsadult.tabbyperson_id, so child-table people arrive as NaN; they are now recognised byadult.tabmembership rather than by the NaN → 0 →CHILDmap entry.frs_legacy_proxiesis unchanged. It reads the frame'semployment_status, andESA_HEALTH_EMPLOYMENT_STATUSESis (LONG_TERM_DISABLED,SHORT_TERM_DISABLED), so code-11 adults leave both ESA proxies.legacy_jobseeker_proxyreadsUNEMPLOYEDonly.uk/spec/sources.yaml, thesource_stages.jsonmirror and the charter-H2 fixture's copy no longer describe the truncated-map artifact. They citeuk-data#526, because the source manifest refuses the incumbent package name in stage text.Impact on the licensed input (aggregates only)
Every one of the 27,714 adults in the pinned
adult.tab(sha2564eaea080…658d) carries a code from 1 to 11: none is blank, non-integer or out of range. So the new refusal does not stop the real build.child.tabhas no EMPSTATI column.LONG_TERM_DISABLEDadultsOTHER_INACTIVEadultsThe full per-code table is in
experiments/uk-frs-empstati-other-inactive/README.md.Differential against uk-data#526 (
differential_vs_uk_data_526.py, run once by hand; uk-data is not a microcosm dependency and #526 is unreleased). It loads #526's code table and derive function from the PR head (1832adeb; its mapping and accept/refuse logic are unchanged sinced3984002, and only the refusal message has changed) by AST:ValueError(the differential's run atfb026659found that Two-signal FLSA overtime incidence: usual-hours leg joins the reference-week snapshot (#451 item 4 residual) #526's message raisedTypeErrorunder pandas 3 for a blank code next to another unknown code; its owner fixed that in89c48e07);Who reads
employment_status(code read this session):dynamics/labour_supply.pyreads only the self-employed andSTUDENTstatuses. The three proxies are not engine variables. Engine outputs and calibration do not move._nonzero_share).UNEMPLOYED(_nondefault_signal_mask), soOTHER_INACTIVEcounts the same asLONG_TERM_DISABLEDdid.input_mass.py), and its reference totals leave out the proxies, which are not engine variables.employment_status.Timing against uk-data#526
No microcosm test, gate or CI job compares
employment_statuscategories or the ESA proxies with a released uk-data H5:uv.lock.employment_statusonly as a non-empty share.So this PR does not have to wait for #526's release.
Until the uk-data batch (d833) ships #526, microcosm's code-11 adults and ESA proxies differ from the pinned incumbent. No committed instrument measures that difference. A microcosm merge is not a data release; the next licensed UK build picks the change up.
Pins
release_input_coverage_manifest.json: all 15source_manifest_sha256values (sha256 ofsource_stages.json), regenerated withtools/build_uk_release_input_coverage_manifest.py;--checkpasses.stage_contract_sha256.uk_spine.jsonis unchanged. Regenerating the whole charter-H2 fixture withtools/graph_uk_spine_fixture.py --output <scratch>reproduces every committed file byte for byte (diff -rqexit 0), including the hand-editedfixture.jsonnotes.uk.stage.frs_employment@1moves at run time, but nothing commits it.adult.tabcarries codes 1-8 only, so its outputs and the integration-uk smoke build are unchanged.Invariants (property-tested with Hypothesis)
CHILDwhatever their code. Adults with codes 1-11 get exactly their data-dictionary status.CHILDare a bijection onto policyengine-uk'sEmploymentStatus(engine-uk test).Tests
tests/engine_free/uk/test_uk_frs_employment.py:tests/engine_free/uk/test_uk_frs_legacy_proxies.py: every code 1-11 through the stage derivation into the proxies, and the ESA proxy property.tests/engine/uk/test_uk_frs_employment.py: the bijection onto the engine enum, and the ESA statuses are engine statuses.test_engine_is_reported_unavailable_without_the_uk_extra, asserts the UK engine is not installed; my local venv has theukextra, and the engine-free CI job does not install it.test_uk_release_input_coverage.py(includingtest_shipped_manifest_is_current), the manifest tests and the charter-H2test_acceptance_h_parity.py: 56 passed.--run-integration: 2 passed.tools/ci_test_plan.py verifypasses, andruff checkis clean on every touched file.experiments/uk-frs-empstati-other-inactive/mutation_check.py, run against a temporary copy of the sources): 11 of 11 mutations are killed by the three test files. They include the old behaviour (11 →LONG_TERM_DISABLED), unknown adult codes defaulting toLONG_TERM_DISABLED, every row treated as an adult, adult records found by code presence instead ofadult.tabmembership, 9 and 10 swapped, an adult count printed, children given a non-CHILD status, code 0 accepted for adults, non-integer codes truncated, the support group ignoring hours worked, andOTHER_INACTIVEadded to the ESA health statuses.Review
Both rounds were independent reviews through subfleet, served by GPT-6.1 Sol after the Opus lane hit its limit.
Round 1, at
f4154ce64: REQUEST_CHANGES, one should-fix and four nits. All five are fixed inc3761888:The response is in the session's review notes. CI's first engine-free run also caught the changelog fragment naming the incumbent package, which the live-tree scan refuses; that is fixed in the same commit.
Round 2, at exactly
c37618889a300e6014ee4d7509b4184264071218: APPROVE, with no code findings. Its two PR-body nits (the proxy-impact wording above and a leftover drafting marker) are fixed in this body.axiom: n/a: data mapping of an FRS survey code to an input column; no policy rule changes.
🤖 Generated with Claude Code