Port the LCFS consumption, ETB VAT, and ETB services/NHS layers as declarative source stages - #730
Conversation
…clarative source stages Three stages inserted between regional_property_uprating and the SPI spine trio (incumbent order: consumption before SPI row-stacking): lcfs_consumption (18-output seeded weighted QRF chain in the incumbent's conditioning order, WAS-bridged has_fuel flag, NEED energy raking via the new generic iterative_proportional_fit operation, donor-support clipping, non-ICE petrol/diesel zeroing), etb_vat (single-output QRF; VAT rates from a cited anchors resource with parameter_path lockstep - the incumbent's silent 0.03-vs-0.025 fallback dies), and etb_services (3-output chain, rail_usage ratio, deterministic NHS age-gender person allocation with the signed 85+ fold-in / full-denominator / half-open-band fixes). Supporting: three new generic operation kinds (iterative_proportional_fit, bridge_donor_column_via_qrf, allocate_per_capita_from_cell_table) plus a condition parameter on assign_binary_from_rate; five cited build/uk resources (NEED 2023 kWh targets + Ofgem Q2 2026 rates, LCFS/ETB/services policy anchors, the NHS age-gender table) flagged chronicle_candidate; sha-bound SDC-rounded support bounds for all three donors with licensed round-trip tests; the uk_aggregate_admin gate (NEED/Ofgem + NHS anchors); gate-battery digests re-cut; coverage-manifest family entries; driver wiring with caller-supplied LCFS/ETB tabs and input-artifact pins; the carried #717 review fixes (fail-closed SPI support weight kind, last_result resume comments, the seed+1 convention note). full_rate_vat_expenditure_rate is deliberately absent from nonnegative_outputs (negative donor-realized support, 4 of 4,199 rows - the net_financial_wealth precedent). Donor uprating is identity at this vintage (LCFS survey year == 2023 build year) and is deferred to the FRS 2024-25 refresh (#687) rather than shipped as a dead declared op. Implements microcosm#682 (workstream E6 of #665). Codex implemented under /codex-implement-plan; Claude review adjudicated the deviations and added the engine-tree anchor lockstep test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Covers the domestic-energy fold, rail_usage ratio, petrol/diesel zeroing idempotence, and the NHS age-gender allocation recomputed from the committed resource, under row permutation keyed by entity id and scoped to the FRS spine rows (stacked SPI clones are E7's receipt surface). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s and the fitted-QRF predict API The licensed twin build surfaced two defects PR CI could not see (the bridge had no direct unit coverage): the LCFS donor frame carries hbai_household_net_income / is_adult / is_child, which the bridge must rename to the WAS predictor names before predicting (incumbent consumption.py:556-574), and encode_qrf_predictor_pair hardcoded the WAS wealth predictor list (now parameterized, default unchanged). Also FittedRegimeGatedQRF exposes predict, not draw. Adds a native-name regression test that executes the bridge end to end. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…compute The spi_support_channel stage scales survey-channel weights by (1 - share) after the E6 stages ran; the NHS allocation's budget normalization is absolute, so the receipt restores the declared share before recomputing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Second licensed-build crash in the PR-CI-blind class: is_adult / is_child / is_SP_age materialize at person grain (36,248 values) and were assigned directly to the household index (16,754). Mirrors the lcfs/services native-entity aggregation (the E5 review class); adds a native-grain fake-engine regression test that executes recipient_predictors. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… engine variables Third licensed-build crash in the PR-CI-blind class: count_*_education are not policyengine-uk variables — the incumbent derives them from person current_education (etb.py:180-186). The transform now materializes the seven real engine variables and derives the three counts at person grain before household aggregation; the manifest declares the derivation under derived_predictors; native-grain regression test added. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fourth licensed-build crash: banded labels read "01-04 years" and the parser never stripped the unit suffix (the incumbent slices the first five characters). The unit test had used bare synthetic labels; it now parses every label in the committed resource. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fifth licensed-build crash: the frame keeps household weights in the typed vector, not as a table column, but the weighted person counts read household['household_weight']. allocate_nhs_by_age_gender now assigns the passed array onto the table; the test fixture drops the column to match the real frame. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Licensed acceptance caught the gap: the manifest declares the incumbent's four-margin post-imputation rake (income -> tenure -> accommodation -> region, 50 iterations, weighted), but only the income margin was implemented — the incumbent artifact hits the tenure/accommodation NEED cells to ~1 percent while ours was off by up to ±32 percent. rake_energy_to_need now takes the three categorical groupers, builds their MarginSpecs from the committed NEED resource (one source of values for the raking and the aggregate_admin anchors), and sweeps the incumbent's order; unmapped categories stay exempt. Four-margin regression test added. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
5deaa09 to
fea19ef
Compare
juaristi22
left a comment
There was a problem hiding this comment.
Addressed the adversarial-review findings in commit fea19ef2, and rebased the PR onto current main after #714 merged.
Resolved:
- E6 VAT, services/NHS, LCFS, and NEED transforms now load their committed anchor resources at runtime; manifest values are checked against the resource-backed configuration.
- Anchor resources now include authoritative source URLs.
- IPF records mapped positive targets that cannot be reached from zero current means, with an explicit strict mode for fail-closed callers.
- ETB VAT donor cleaning now rejects zero denominators and non-finite derived rates.
- Updated the authoritative UK YAML stage projection, source schema, coverage manifest, and identity-receipt tool for the retargeted branch.
Validation:
microcosm-build: 210 passed, 7 skipped.microcosm-datacontract suite: 181 passed.- Ruff and
git diff --check: passed.
vahid-ahmadi
left a comment
There was a problem hiding this comment.
Reviewed with the usual method — the three transforms and the raking kernel against the incumbent's consumption.py/vat.py/services.py, the anchors and bounds resources, and both review rounds. The E6 test files pass for me (55 + the licensed skip) on the rebased head.
Verified faithful at the quirk level, which is where this layer's risk lives: the NTS 0.90 ICE share, the 0.52 electricity-share fallback, the weighta rename, and — the one I checked most carefully — the NEED raking. Your kernel's semantics match the incumbent's loop exactly (weighted cell means, current <= 0 cells skipped, ×50 iterations, income → tenure → accommodation → region order), and the additions are strictly evidence: the raking_zero_current_cells attr and the strict mode from the adversarial round record what the incumbent silently skips. The MarginSpec means-based design is honest about why it works in means, and microcosm/build/raking.py is genuinely country-neutral.
The VAT fallback death deserves its own sentence. The incumbent's _get_vat_parameters docstring promises the layered fallback exists "so callers never silently get wrong numbers" — while its terminal fallback is the silent wrong number at any vintage where 0.03 isn't the reduced-rate share (2023's is 0.025). Replacing that with a cited anchors resource, parameter_path-declared and lockstep-tested against the engine tree, so a missing rate fails loud, is the correct posture — and extending the E7 drift-assert pattern from manifest parameters to engine-parameter anchors makes it a house pattern now. The queued upstream report (with the NHS roll-in) following the EDUCQUAL precedent is the right disposition; #463's list grows to five.
The licensed funnel's sixth defect is worth a design note, not just a fix. "The manifest declared four margins; the transform ran one" is precisely the class the drift asserts were invented for — but it escaped them, because the asserts verify declaration equality, not execution. The regression tests now execute the previously-untested path, which covers this instance; the general lesson (each declared operation parameter needs either a drift assert or an executed-effect receipt, and the raking margins had neither until the licensed run) belongs wherever the E7 pattern is documented, so E8+ inherits it deliberately.
Also confirmed: the three #717 carryovers are all swept here as recorded (fail-closed weight kind with a regression test, the last_result resume comments, the seed + 1 convention named at both sites) — that closes my #717 review in full except the incapacity_benefit_reported promotion question, which I'll assume rides E10 with the rest of the composition story unless the licensed run said otherwise. The ETB year-quirk note ("year == max resolves to 2023 on this tab, so the incumbent's apparent mismatch is vacuous at this vintage") is the right kind of finding: documented as vacuous now with the reason, so the 2024-25 refresh (#723) knows to re-check it rather than inherit it.
María's two adjudication asks look correctly scoped to her, and both proposed dispositions read right to me — zero-inflation divergence as a signed method class with the weighted-distributional comparison binding at E10, and the income-band re-bucketing residuals as a documented symmetric class (both pipelines rake before the certified pair rewrites incomes, so the residual is definitional, not directional).
Nothing blocking from my side.
Workstream E6 of the UK migration (#145, master epic #665). Closes #682. Stacked on #714 (
uk-was-wealth-681); base flips to main when #714 merges.What
Ports the incumbent LCFS consumption, ETB VAT, and ETB services/NHS layers as three declarative source stages inserted between
regional_property_upratingand the SPI spine trio (the incumbent's execution order: consumption before SPI row-stacking, so channel clones inherit donor consumption):lcfs_consumption(household) — LCFS 2023-24 household+person donor (UKDS SN 9468, DOI 10.5255/UKDA-SN-9468-3, sha+size-pinned, caller-supplied local inputs), the incumbent's donor cleaning verbatim (lowercase headers, the three rename maps, person-income join,WEEKS_IN_YEAR = 365.25/7,weighta×1000, three-code bus fare, the lossy tenure/accommodation collapses, the four-case electricity/gas split with the 0.52 fallback), the training-side single-pass NEED income calibration, the WAS-bridgedhas_fuel_consumptionflag (WAS round-8 donor reused from E5's pin; recipient side an identity-keyed conditional draw onnum_vehicles > 0× the cited NTS 0.90 ICE share), a seeded weighted 18-outputfit_weighted_qrf_chainin the incumbent's conditioning order, donor-support clipping (raked energy columns exempt), the four-margin ×50 weighted NEED raking as the new genericiterative_proportional_fitoperation (income → tenure → accommodation → region, zeros preserved, unmapped categories and Scotland/NI untouched per NEED's England & Wales scope), the domestic-energy fold recompute, andzero_when_falsepetrol/diesel zeroing for non-fuel households.etb_vat(household) — ETB 1977-24 donor (UKDS SN 8856, DOI 10.5255/UKDA-SN-8856-4),year == 2023filter, the incumbent's target construction with VAT rates from a cited anchors resource (0.20 / 0.025 @2023,parameter_path-declared, engine-tree lockstep-tested) — the incumbent's silentexcept → (0.2, 0.03)fallback dies; missing rates fail loud. Weighted single-output QRF, donor-support clip.etb_services(household + person) — same ETB donor atyear == max(which resolves to 2023 on this tab: the file labels FYE-2024 as year 2023, so the incumbent's apparent VAT/services/fare-index year mismatch is vacuous on this vintage), education counts derived from personcurrent_education(the incumbent's construction), native-entity predictor aggregation, weighted 3-output chain, donor-support clip,rail_usage = rail_subsidy_spending / 1.110(compute_ratio, fare index anchored withparameter_path), and the deterministic NHS age-gender person allocation from the committed 252-row public table with the signed 85+ fold-in / full-table £202bn denominator / half-open band fixes.Supporting: three new generic operation kinds (
iterative_proportional_fitbacked by the country-neutralmicrocosm/build/raking.py,bridge_donor_column_via_qrf,allocate_per_capita_from_cell_table) plus aconditionparameter onassign_binary_from_rate; five citedbuild/uk/resources (NEED 2023 kWh margins + Ofgem Q2 2026 rates, LCFS/VAT/services policy anchors withparameter_pathfields andchronicle_candidateflags, the NHS age-gender table); sha-bound SDC-rounded support bounds for all three donors (generated from the pinned licensed tabs, placeholder-refusing CI tests, licensed-only round-trip viaPOPULACE_UK_LCFS_HH_TAB/POPULACE_UK_LCFS_PERSON_TAB/POPULACE_UK_ETB_TAB); theuk_aggregate_admingate (first UK binding of the shared gate; NEED/Ofgem energy anchors + the NHS budget as cited TargetSpecs, release-blocking, passed at build time); gate-battery digests re-cut; coverage-manifest family entries for the three stages; driver wiring with_input_artifact_pins; 22 FitWeightRecords across the four fits (allexplicit); the--check e6identity receipt; and the hygieneuv.lockregen (themicrocosm-frameukextra from #717 was never locked; CI is unaffected — it syncs without--locked).UK-specificity note (required by #682)
Generic contributions:
iterative_proportional_fit+raking.py, the bridge-QRF and cell-table allocation kinds, theconditionparameter, the anchors-with-parameter_pathlockstep pattern, the parameterizedencode_qrf_predictor_pair. UK-specific: LCFS/ETB column semantics and cleaning, the NEED margin tables and enum maps, the fivebuild/uk/resources, the threeuk_runtimetransforms, and the gate declarations. The incumbent package name appears nowhere in the tree.Signed differences
default_rng(42)per output — every household draws the identical quantile index for all 18 outputs). For zero-inflated targets this is not cosmetic: the incumbent's comonotone median-biased draws collapse them (itsdfe_education_spendinglands at weighted mean ~£10 with nonzero share 0.0004 against an ETB donor whose annualized mean is ~£3,348 at share 0.2546; its petrol mean roughly doubles the donor's). Ours reproduces the donor distributions (education ~£3,210 at share 0.227; petrol ~£550 vs donor ~£631). Parity is weighted-distributional; the eFRS register restates the incumbent's collapse for these columns, so register deltas on them are method-class — flagged for reviewer adjudication rather than silently excluded.weighta, ETBhhold_adj_weight, WASR8xshhwgtas explicit QRF weights; the ETB selections gain the weight column the incumbent discards.exceptmasks the live 0.025 reduced-rate share with 0.03.default_rng(42)on both the WAS donor and the FRS recipient.WEEKS_IN_YEAR365.25/7 (LCFS) vs 52 (ETB); the lossy tenure/accommodation collapses; CONVERTED_HOUSE/OTHER/UNKNOWN and Scotland/NI margin exemptions; the 0.52mean_elec_sharefallback; donor dropna row loss (logged); NEED targets at FY26/27 prices in a 2023 build;domestic = elec + gasholding only in the build year; recipient predictors at the frame period vs the incumbent's implicit default-period reads;state_pensiondead rename dropped.Licensed acceptance — DONE (evidence:
data/ukds/acceptance/e6/)uk_aggregate_adminNEED anchors.under_15ka small-denominator band.run_incumbent_consumption_e6.pyat pin ebf733c, same FRS/WAS/LCFS/ETB tabs, LCFS uprating target pinned to 2023): weighted quantiles per output; the NHS level shift quantified above; the zero-inflation divergences per signed difference 1.predictAPI,etb_vatperson-entity aggregation, the education-count derivation, the NHS age-label parser, NHS weight threading, and the four-margin raking wiring (the manifest declared four margins; the transform ran one).Reviewer (María) adjudication asks: (1) the donor-faithful vs incumbent-collapse divergence class on zero-inflated outputs (signed difference 1) — proposed disposition: signed method-class receipts, weighted-distributional comparison binds at E10; (2) the income-band re-bucketing residuals as a documented symmetric class.
Carried from the #717 review
Per the recorded disposition ("deferred to E6 to avoid a CI-only re-spin of an already-reviewed PR"): the SPI support transform now fails closed on a non-importance builder weight kind (regression-tested),
last_resultcarries resume-semantics comments, and the stage-2seed + 1RNG-stream convention is named at both sites.Out of scope
The incumbent's post-calibration rail/bus/fuel/bus-fare aggregate scalers run after weights exist and are adjudicated on #623 — DfT-total comparisons do not apply to E6's pre-scaling outputs. DfT anchors + calibration targets: E9 (#685) / WS-C, salvaging #153.
mortgage_debt/consumer_debt: #685. FRS 2024-25 refresh incl. the donor uprating machinery: #687. Upstream defect reports (NHS roll-in, VAT fallback) to policyengine-uk-data follow the EDUCQUAL precedent after this lands.🤖 Generated with Claude Code