diff --git a/changelog.d/970-cgt-incidence-anchor.added.md b/changelog.d/970-cgt-incidence-anchor.added.md new file mode 100644 index 0000000000..0e010f8318 --- /dev/null +++ b/changelog.d/970-cgt-incidence-anchor.added.md @@ -0,0 +1 @@ +UK spine stage `cgt_incidence_anchor` (31st stage, after `hmrc_cgt_asset_type_spine`): a weights-only structural stage that moves the mass of sub-exempt and loss-making clone households back to their paired originals until the two groups match the Advani-Summers reporter composition at the redrawn liable mass. No cell is written; every clone/original pair and the household total are conserved; liable clones and band donors are untouched. Gated at stage time by `uk_stage_cgt_incidence_anchor_composition`; the H2 parity fixture, the release input coverage manifest, the spine gate pins and the identity-stability tool (pair-sum restoration, anchor recompute in the E8 receipt) move with it (microcosm#970). diff --git a/changelog.d/970-cgt-sub-aea-remainder.changed.md b/changelog.d/970-cgt-sub-aea-remainder.changed.md new file mode 100644 index 0000000000..701419dfa3 --- /dev/null +++ b/changelog.d/970-cgt-sub-aea-remainder.changed.md @@ -0,0 +1 @@ +UK CGT amounts stage (`hmrc_cgt_gains_spine`): gainers beyond the published taxpayer mass now take amounts from the Advani-Summers within-band gains distribution restricted to (0, annual exempt amount], rank-preserving within each total-income band and deterministic, instead of being capped at exactly the exempt amount. The shared Advani-Summers surface (loader, band lookup, degree-1 quantile spline and its inverse) moves to `uk_runtime/advani_summers.py`; the stage receipt carries a `remainder` block the stage-health gate checks (microcosm#970). diff --git a/changelog.d/970-uk-cgt-projection-entrants-gate.added.md b/changelog.d/970-uk-cgt-projection-entrants-gate.added.md new file mode 100644 index 0000000000..0c06456aa4 --- /dev/null +++ b/changelog.d/970-uk-cgt-projection-entrants-gate.added.md @@ -0,0 +1 @@ +UK calibration seam gate `uk_cgt_projection_entrants` (terminal, release-blocking, absence blocks): projects the calibrated frame's sub-exempt capital gains along the engine's per-capita GDP growth path against the frozen annual exempt amount to a 2030 horizon and fails when the cumulative stock of crossers in any year exceeds the vendored HMRC Table 2.1a taxpayers in the thinnest liable band (73,000 in 2024-25), a plausibility ceiling; the engine's growth path and exempt amount are pinned in the manifest and drift-checked. The seam supplies a `cgt_projection` artifact read from the installed engine (`uk_runtime/cgt_projection.py`), or stated from the manifest pins when no engine is installed (the receipt names the source, and the release certifier refuses a seam part whose fence read the pins, so an engine-free seam evaluates the gate but cannot certify a cut); the UK gate battery digests, the certification calibration-seam digests and their data-shard mirrors move (microcosm#970). diff --git a/docs/evidence/uk-cgt-970/README.md b/docs/evidence/uk-cgt-970/README.md new file mode 100644 index 0000000000..b1edc7ff13 --- /dev/null +++ b/docs/evidence/uk-cgt-970/README.md @@ -0,0 +1,14 @@ +# Evidence for microcosm#970 (UK CGT sub-exempt gainers) + +Aggregate-only extracts of the licensed receipts behind `experiments/970-cgt-sub-exempt-remainder.md`. The +licensed artifacts themselves (spine and calibrated H5 files, full sidecars, the FRS/SPI/WAS/LCFS/ETB inputs) live +outside the tree under `data/ukds/acceptance/970-cgt-sub-exempt/` and `runs/uk-623-first-calibrated/`. + +- `v20-fence-receipt.json`: the projection fence on the calibrated v20 artifact, directly and as the signed seam + receipt of calibrating the v20 spine through the branch (blocked at terminal, no H5). +- `spine-970-receipts.json`: the rebuilt 31-stage spine's anchor and remainder receipts and spine gate verdicts. +- `v22-calibration-receipts.json`: the v22 seam verdict, fence details, calibration summary, CGT target errors, the + pass-2 score against the incumbent, and the clone/donor split of the remaining entrants. + +Redaction: per-record values are withheld (the remainder's per-band and overall minimum and maximum amounts); +quantiles, counts, masses and target errors are kept. diff --git a/docs/evidence/uk-cgt-970/spine-970-receipts.json b/docs/evidence/uk-cgt-970/spine-970-receipts.json new file mode 100644 index 0000000000..b2fcb70546 --- /dev/null +++ b/docs/evidence/uk-cgt-970/spine-970-receipts.json @@ -0,0 +1,598 @@ +{ + "anchor_gate_details": { + "check": "cgt_incidence_anchor", + "clone_mass": 677698.9176763435, + "effective_pair_relative_tolerance": 1.7763568394002505e-15, + "liable_clone_mass": 486719.6331493518, + "liable_mass": 557419.6331493518, + "loss_after": 150249.8381002053, + "loss_before": 2707829.4981969413, + "loss_relative_error": 0.0, + "loss_target": 150249.8381002053, + "max_pair_relative_error": 2.2182797604217762e-16, + "original_mass": 28294734.082323655, + "pair_count": 26288, + "stage": "cgt_incidence_anchor", + "sub_exempt_after": 40729.44642678636, + "sub_exempt_before": 11291667.368653707, + "sub_exempt_relative_error": 0.0, + "sub_exempt_target": 40729.44642678636, + "transferred_mass": 13808517.582323655 + }, + "branch_head": "24509d78f3053000948fa07ac60574453d8e9b89", + "cgt_incidence_anchor": { + "after": { + "liable": 486719.6331493518, + "loss": 150249.8381002053, + "sub_exempt": 40729.44642678636 + }, + "annual_exempt_amount": 3000.0, + "before": { + "liable": 486719.6331493518, + "loss": 2707829.4981969413, + "sub_exempt": 11291667.368653707 + }, + "by_income_band": [ + { + "clone_households": 19225, + "income_measure": "component_sum_without_allowance", + "liable": 179214.04181245598, + "loss_after": 40626.20512005527, + "loss_before": 1887636.9748926982, + "lower_limit": 0, + "sub_exempt_after": 14164.230405519753, + "sub_exempt_before": 8389932.914161285 + }, + { + "clone_households": 2914, + "income_measure": "component_sum_without_allowance", + "liable": 84162.12543268935, + "loss_after": 20466.46125433182, + "loss_before": 319510.26975591714, + "lower_limit": 37700, + "sub_exempt_after": 6185.432627976916, + "sub_exempt_before": 1226156.0207129894 + }, + { + "clone_households": 3332, + "income_measure": "component_sum_without_allowance", + "liable": 117975.67386957117, + "loss_after": 47292.54754708536, + "loss_before": 389778.7962459927, + "lower_limit": 50000, + "sub_exempt_after": 13129.255218319226, + "sub_exempt_before": 1409011.7835760033 + }, + { + "clone_households": 329, + "income_measure": "component_sum_without_allowance", + "liable": 30538.71940899144, + "loss_after": 8114.747746516141, + "loss_before": 37009.77522046153, + "lower_limit": 100000, + "sub_exempt_after": 2052.5984517018605, + "sub_exempt_before": 119188.95183765839 + }, + { + "clone_households": 327, + "income_measure": "component_sum_without_allowance", + "liable": 30168.26753024156, + "loss_after": 22746.128017281582, + "loss_before": 50265.02263825849, + "lower_limit": 125140, + "sub_exempt_after": 3972.651205192077, + "sub_exempt_before": 113879.31641096616 + }, + { + "clone_households": 161, + "income_measure": "component_sum_without_allowance", + "liable": 44660.80509540233, + "loss_after": 11003.748414935091, + "loss_before": 23628.659443613236, + "lower_limit": 200000, + "sub_exempt_after": 1225.2785180765288, + "sub_exempt_before": 33498.381954805314 + } + ], + "capped_households": 0, + "composition": { + "exempt_quantile": 0.2551838064116276, + "implied_reporter_mass": 748398.9176763435, + "liable_share": 0.7448161935883724, + "zero_quantile": 0.20076169880991612 + }, + "donor_mass": 392000.0, + "liable_mass": 557419.6331493518, + "liable_persons": 1003, + "mass_by_clone_flag": { + "false": 28294734.082323655, + "true": 677698.9176763435 + }, + "max_pair_relative_error": 2.2182797604217762e-16, + "pair_count": 26288, + "scale": { + "loss": 3.0881630893756133, + "sub_exempt": 0.2425546739205345 + }, + "stage": "cgt_incidence_anchor", + "targets": { + "loss": 150249.8381002053, + "sub_exempt": 40729.44642678636 + }, + "transferred_mass": 13808517.582323655, + "trimmed_households": 25436, + "zero_gain_clone_households": 0 + }, + "entity_row_counts": { + "benunit": 61213, + "household": 52846, + "person": 113590 + }, + "hmrc_cgt_gains_spine_remainder": { + "annual_exempt_amount": 3000.0, + "band_rows": [ + { + "exempt_quantile": 0.2353658536585366, + "mass": 2139571.649486485, + "minimum_total_income": 0.0, + "persons": 3934, + "zero_quantile": 0.18048780487804877 + }, + { + "exempt_quantile": 0.22887323943661972, + "mass": 82146.60952224987, + "minimum_total_income": 10000.0, + "persons": 154, + "zero_quantile": 0.16549295774647887 + }, + { + "exempt_quantile": 0.2469387755102041, + "mass": 81827.70653167195, + "minimum_total_income": 10300.0, + "persons": 160, + "zero_quantile": 0.20102040816326533 + }, + { + "exempt_quantile": 0.24488636363636365, + "mass": 90899.66148028057, + "minimum_total_income": 10600.0, + "persons": 185, + "zero_quantile": 0.19375 + }, + { + "exempt_quantile": 0.23750000000000002, + "mass": 94908.3553377529, + "minimum_total_income": 11000.0, + "persons": 173, + "zero_quantile": 0.18392857142857144 + }, + { + "exempt_quantile": 0.255, + "mass": 152868.2084464865, + "minimum_total_income": 11400.0, + "persons": 348, + "zero_quantile": 0.18913043478260871 + }, + { + "exempt_quantile": 0.24000000000000002, + "mass": 94974.51170772573, + "minimum_total_income": 11800.0, + "persons": 192, + "zero_quantile": 0.18 + }, + { + "exempt_quantile": 0.24366197183098592, + "mass": 110269.50604374261, + "minimum_total_income": 12100.0, + "persons": 217, + "zero_quantile": 0.18028169014084508 + }, + { + "exempt_quantile": 0.24782608695652175, + "mass": 105680.60590288212, + "minimum_total_income": 12500.0, + "persons": 215, + "zero_quantile": 0.1826086956521739 + }, + { + "exempt_quantile": 0.23461538461538461, + "mass": 126293.81693461843, + "minimum_total_income": 12900.0, + "persons": 224, + "zero_quantile": 0.17692307692307693 + }, + { + "exempt_quantile": 0.24565217391304348, + "mass": 118999.85215801164, + "minimum_total_income": 13300.0, + "persons": 241, + "zero_quantile": 0.18043478260869567 + }, + { + "exempt_quantile": 0.2526595744680851, + "mass": 98921.35621355017, + "minimum_total_income": 13700.0, + "persons": 176, + "zero_quantile": 0.18958333333333333 + }, + { + "exempt_quantile": 0.24797297297297297, + "mass": 151076.7135107979, + "minimum_total_income": 14100.0, + "persons": 278, + "zero_quantile": 0.18716216216216217 + }, + { + "exempt_quantile": 0.23695652173913043, + "mass": 120330.1312426922, + "minimum_total_income": 14500.0, + "persons": 234, + "zero_quantile": 0.17173913043478262 + }, + { + "exempt_quantile": 0.2661290322580645, + "mass": 121735.70680632457, + "minimum_total_income": 14900.0, + "persons": 215, + "zero_quantile": 0.20135135135135135 + }, + { + "exempt_quantile": 0.2554945054945055, + "mass": 111097.78362736889, + "minimum_total_income": 15300.0, + "persons": 205, + "zero_quantile": 0.19473684210526315 + }, + { + "exempt_quantile": 0.25, + "mass": 110642.04152597653, + "minimum_total_income": 15700.0, + "persons": 193, + "zero_quantile": 0.1875 + }, + { + "exempt_quantile": 0.24178082191780823, + "mass": 127252.46809856928, + "minimum_total_income": 16100.0, + "persons": 233, + "zero_quantile": 0.18013698630136987 + }, + { + "exempt_quantile": 0.23541666666666666, + "mass": 122527.4193943528, + "minimum_total_income": 16600.0, + "persons": 218, + "zero_quantile": 0.17291666666666666 + }, + { + "exempt_quantile": 0.24807692307692308, + "mass": 110597.72357947817, + "minimum_total_income": 17000.0, + "persons": 200, + "zero_quantile": 0.19038461538461537 + }, + { + "exempt_quantile": 0.24797297297297297, + "mass": 118953.04905542974, + "minimum_total_income": 17400.0, + "persons": 218, + "zero_quantile": 0.18716216216216217 + }, + { + "exempt_quantile": 0.24797297297297297, + "mass": 107451.61115222395, + "minimum_total_income": 17900.0, + "persons": 188, + "zero_quantile": 0.18716216216216217 + }, + { + "exempt_quantile": 0.24391891891891893, + "mass": 126718.46877625333, + "minimum_total_income": 18300.0, + "persons": 230, + "zero_quantile": 0.18310810810810813 + }, + { + "exempt_quantile": 0.24615384615384617, + "mass": 92903.10740766935, + "minimum_total_income": 18800.0, + "persons": 173, + "zero_quantile": 0.18846153846153846 + }, + { + "exempt_quantile": 0.2391304347826087, + "mass": 119968.15518516049, + "minimum_total_income": 19200.0, + "persons": 214, + "zero_quantile": 0.17391304347826086 + }, + { + "exempt_quantile": 0.24583333333333335, + "mass": 112644.84880189197, + "minimum_total_income": 19700.0, + "persons": 212, + "zero_quantile": 0.18333333333333335 + }, + { + "exempt_quantile": 0.23846153846153847, + "mass": 113426.25443335756, + "minimum_total_income": 20200.0, + "persons": 207, + "zero_quantile": 0.1807692307692308 + }, + { + "exempt_quantile": 0.23076923076923078, + "mass": 128919.81753697227, + "minimum_total_income": 20700.0, + "persons": 234, + "zero_quantile": 0.17307692307692307 + }, + { + "exempt_quantile": 0.23125, + "mass": 140627.86803524132, + "minimum_total_income": 21200.0, + "persons": 254, + "zero_quantile": 0.16875 + }, + { + "exempt_quantile": 0.244, + "mass": 126774.0355893502, + "minimum_total_income": 21800.0, + "persons": 215, + "zero_quantile": 0.184 + }, + { + "exempt_quantile": 0.25280898876404495, + "mass": 132696.89799377287, + "minimum_total_income": 22300.0, + "persons": 238, + "zero_quantile": 0.18785714285714286 + }, + { + "exempt_quantile": 0.2411764705882353, + "mass": 133231.58903810475, + "minimum_total_income": 22900.0, + "persons": 241, + "zero_quantile": 0.175 + }, + { + "exempt_quantile": 0.2392857142857143, + "mass": 145978.29661732126, + "minimum_total_income": 23500.0, + "persons": 260, + "zero_quantile": 0.175 + }, + { + "exempt_quantile": 0.25297619047619047, + "mass": 154994.91562635888, + "minimum_total_income": 24100.0, + "persons": 284, + "zero_quantile": 0.192 + }, + { + "exempt_quantile": 0.24817073170731707, + "mass": 165568.3789614147, + "minimum_total_income": 24800.0, + "persons": 268, + "zero_quantile": 0.19329268292682927 + }, + { + "exempt_quantile": 0.24383561643835616, + "mass": 178329.25985056855, + "minimum_total_income": 25400.0, + "persons": 317, + "zero_quantile": 0.1821917808219178 + }, + { + "exempt_quantile": 0.25290697674418605, + "mass": 150477.8436987711, + "minimum_total_income": 26100.0, + "persons": 272, + "zero_quantile": 0.1949367088607595 + }, + { + "exempt_quantile": 0.24375, + "mass": 161699.23194476188, + "minimum_total_income": 26800.0, + "persons": 273, + "zero_quantile": 0.18125000000000002 + }, + { + "exempt_quantile": 0.23421052631578948, + "mass": 150495.19155830264, + "minimum_total_income": 27500.0, + "persons": 278, + "zero_quantile": 0.175 + }, + { + "exempt_quantile": 0.2291139240506329, + "mass": 167861.78455847496, + "minimum_total_income": 28300.0, + "persons": 290, + "zero_quantile": 0.17215189873417722 + }, + { + "exempt_quantile": 0.25, + "mass": 177605.55446968038, + "minimum_total_income": 29100.0, + "persons": 315, + "zero_quantile": 0.18571428571428572 + }, + { + "exempt_quantile": 0.23170731707317074, + "mass": 194596.45637822867, + "minimum_total_income": 30000.0, + "persons": 341, + "zero_quantile": 0.17682926829268292 + }, + { + "exempt_quantile": 0.2453846153846154, + "mass": 171552.69524965092, + "minimum_total_income": 30900.0, + "persons": 317, + "zero_quantile": 0.17615384615384616 + }, + { + "exempt_quantile": 0.24797297297297297, + "mass": 173461.74454100273, + "minimum_total_income": 31800.0, + "persons": 291, + "zero_quantile": 0.18716216216216217 + }, + { + "exempt_quantile": 0.24347826086956523, + "mass": 188236.70897177444, + "minimum_total_income": 32800.0, + "persons": 322, + "zero_quantile": 0.17826086956521742 + }, + { + "exempt_quantile": 0.23875000000000002, + "mass": 154528.60124980073, + "minimum_total_income": 33900.0, + "persons": 271, + "zero_quantile": 0.1825 + }, + { + "exempt_quantile": 0.23653846153846153, + "mass": 206591.52047206165, + "minimum_total_income": 35000.0, + "persons": 367, + "zero_quantile": 0.17884615384615385 + }, + { + "exempt_quantile": 0.24025974025974026, + "mass": 172915.5087442557, + "minimum_total_income": 36200.0, + "persons": 300, + "zero_quantile": 0.18181818181818182 + }, + { + "exempt_quantile": 0.2388888888888889, + "mass": 191039.9868028383, + "minimum_total_income": 37400.0, + "persons": 330, + "zero_quantile": 0.18333333333333335 + }, + { + "exempt_quantile": 0.2692307692307692, + "mass": 133751.579277577, + "minimum_total_income": 38700.0, + "persons": 235, + "zero_quantile": 0.20460526315789473 + }, + { + "exempt_quantile": 0.25290697674418605, + "mass": 192458.533772917, + "minimum_total_income": 39900.0, + "persons": 334, + "zero_quantile": 0.19423076923076923 + }, + { + "exempt_quantile": 0.26666666666666666, + "mass": 213356.90909349598, + "minimum_total_income": 41500.0, + "persons": 360, + "zero_quantile": 0.20443037974683545 + }, + { + "exempt_quantile": 0.2558139534883721, + "mass": 214090.74037782958, + "minimum_total_income": 43300.0, + "persons": 380, + "zero_quantile": 0.19683544303797468 + }, + { + "exempt_quantile": 0.24634146341463414, + "mass": 246836.30262321822, + "minimum_total_income": 45500.0, + "persons": 423, + "zero_quantile": 0.19146341463414634 + }, + { + "exempt_quantile": 0.2358823529411765, + "mass": 285750.699733868, + "minimum_total_income": 48300.0, + "persons": 488, + "zero_quantile": 0.18294117647058825 + }, + { + "exempt_quantile": 0.2426829268292683, + "mass": 304526.1277696466, + "minimum_total_income": 51900.0, + "persons": 504, + "zero_quantile": 0.1878048780487805 + }, + { + "exempt_quantile": 0.24505494505494507, + "mass": 319359.95534824807, + "minimum_total_income": 56700.0, + "persons": 538, + "zero_quantile": 0.1956043956043956 + }, + { + "exempt_quantile": 0.24021739130434783, + "mass": 318586.89431503334, + "minimum_total_income": 63400.0, + "persons": 537, + "zero_quantile": 0.19130434782608696 + }, + { + "exempt_quantile": 0.2405263157894737, + "mass": 311922.13447131857, + "minimum_total_income": 73400.0, + "persons": 488, + "zero_quantile": 0.1931578947368421 + }, + { + "exempt_quantile": 0.25255102040816324, + "mass": 251369.64526729056, + "minimum_total_income": 90100.0, + "persons": 405, + "zero_quantile": 0.20817307692307693 + }, + { + "exempt_quantile": 0.30223880597014924, + "mass": 188086.63635155128, + "minimum_total_income": 128200.0, + "persons": 235, + "zero_quantile": 0.24761904761904763 + } + ], + "bands_with_remainder": 61, + "income_measure": "component_sum_without_allowance", + "mass": 11612967.368653707, + "note": "per-band and overall min/max amounts withheld (per-record values); persons and mass per band kept", + "persons": 20642, + "policy": "advani_summers_within_band_exempt_range", + "resource": "advani_summers_capital_gains_distribution.json", + "resource_sha256": "22720d11009b0b5d46b140d5c8566f183204dccc4949070050214ab61fd1516c", + "resource_vintage": "2017-18" + }, + "household_weight_total": 29364432.999999996, + "spine_gate_statuses": { + "uk_brma_enum_domain": "passed", + "uk_capital_gains_asset_type_enum_domain": "passed", + "uk_ons_household_type_enum_domain": "passed", + "uk_stage_age_tail_targets": "passed", + "uk_stage_cgt_band_donors_support": "passed", + "uk_stage_cgt_incidence_anchor_composition": "passed", + "uk_stage_cgt_incidence_clone_mass": "passed", + "uk_stage_etb_services_support": "passed", + "uk_stage_etb_vat_support": "passed", + "uk_stage_frs_hmrc_spine_leaves_signal": "passed", + "uk_stage_frs_relationships_composition": "passed", + "uk_stage_hmrc_cgt_asset_type_spine_summary": "passed", + "uk_stage_hmrc_cgt_gains_spine_summary": "passed", + "uk_stage_hmrc_spi_income_spine_identity": "passed", + "uk_stage_lcfs_consumption_energy_rake": "passed", + "uk_stage_lcfs_consumption_support": "passed", + "uk_stage_salary_sacrifice_realization": "passed", + "uk_stage_spi_support_channel_mass": "passed", + "uk_stage_student_loans_realization": "passed", + "uk_stage_uc_deduction_attributes": "passed", + "uk_stage_was_wealth_support": "passed" + }, + "spine_h5_sha256": "60c8e1941ce66c1c15e46ff4b083f1d8af80c476ee7a9a524df182e77f78dc3c", + "what": "Stage receipts of the 31-stage licensed spine rebuilt on the branch (same inputs as spine-s); aggregates only." +} diff --git a/docs/evidence/uk-cgt-970/v20-fence-receipt.json b/docs/evidence/uk-cgt-970/v20-fence-receipt.json new file mode 100644 index 0000000000..0f75e290bc --- /dev/null +++ b/docs/evidence/uk-cgt-970/v20-fence-receipt.json @@ -0,0 +1,124 @@ +{ + "branch_head": "98ac4757 (direct) / 98ac4757 (seam)", + "direct": { + "details": { + "base_year": 2024, + "bound": 73000.0, + "bound_source": "hmrc_cgt_conditioning_facts.json: HMRC Table 2.1a tax year 2024, taxpayers with gains from 3,000 to 6,000", + "cumulative_gains_factor_by_year": { + "2025": 1.0438, + "2026": 1.07427896, + "2027": 1.108978170408, + "2028": 1.143356493690648, + "2029": 1.1771998459038913, + "2030": 1.214281641049864 + }, + "entrants_by_year": { + "2025": 10832804.010787928, + "2026": 10866856.974258387, + "2027": 10897468.20161053, + "2028": 10923076.027606929, + "2029": 10958313.550277675, + "2030": 10978624.221754039 + }, + "exempt_amount_by_year": { + "2024": 3000.0, + "2025": 3000.0, + "2026": 3000.0, + "2027": 3000.0, + "2028": 3000.0, + "2029": 3000.0, + "2030": 3000.0 + }, + "exempt_amount_parameter": "gov.hmrc.cgt.annual_exempt_amount", + "gains_growth_parameter": "gov.economic_assumptions.yoy_growth.obr.per_capita.gdp", + "horizon_year": 2030, + "max_entrants": 10978624.221754039, + "projection_engine": "policyengine-uk==2.98.0", + "sub_exempt": { + "p10": 3000.0, + "p50": 3000.0, + "p90": 3000.0, + "rows": 20642, + "weighted_at_exempt_amount": 10815254.376677955, + "weighted_persons": 11664956.898602244 + }, + "worst_year": 2030 + }, + "passed": false + }, + "engine": "policyengine-uk==2.98.0", + "seam": { + "blocked_at_phase": "terminal", + "evidence_sha256_keys": [ + "uk_cgt_projection_entrants" + ], + "fence": { + "details": { + "base_year": 2024, + "bound": 73000.0, + "bound_source": "hmrc_cgt_conditioning_facts.json: HMRC Table 2.1a tax year 2024, taxpayers with gains from 3,000 to 6,000", + "cumulative_gains_factor_by_year": { + "2025": 1.0438, + "2026": 1.07427896, + "2027": 1.108978170408, + "2028": 1.143356493690648, + "2029": 1.1771998459038913, + "2030": 1.214281641049864 + }, + "entrants_by_year": { + "2025": 10832804.010787928, + "2026": 10866856.974258387, + "2027": 10897468.20161053, + "2028": 10923076.027606929, + "2029": 10958313.550277675, + "2030": 10978624.221754039 + }, + "exempt_amount_by_year": { + "2024": 3000.0, + "2025": 3000.0, + "2026": 3000.0, + "2027": 3000.0, + "2028": 3000.0, + "2029": 3000.0, + "2030": 3000.0 + }, + "exempt_amount_parameter": "gov.hmrc.cgt.annual_exempt_amount", + "gains_growth_parameter": "gov.economic_assumptions.yoy_growth.obr.per_capita.gdp", + "horizon_year": 2030, + "max_entrants": 10978624.221754039, + "projection_engine": "policyengine-uk==2.98.0", + "sub_exempt": { + "p10": 3000.0, + "p50": 3000.0, + "p90": 3000.0, + "rows": 20642, + "weighted_at_exempt_amount": 10815254.376677955, + "weighted_persons": 11664956.898602244 + }, + "worst_year": 2030 + }, + "failures": [ + "microcosm_uk_2024: 10,978,624 weighted sub-exempt gainers cross the frozen annual exempt amount by 2030 under the engine's uprating, above the bound of 73,000 (hmrc_cgt_conditioning_facts.json: HMRC Table 2.1a tax year 2024, taxpayers with gains from 3,000 to 6,000)." + ], + "status": "failed" + }, + "gate_statuses": { + "uk_aggregate_admin": "passed", + "uk_calibration_reference_coverage": "passed", + "uk_cgt_projection_entrants": "failed", + "uk_target_fit": "failed", + "uk_weight_ess": "passed", + "uk_weight_ratio": "passed", + "uk_zero_weight_strata": "passed" + }, + "gates_manifest_sha256": "59323ffc470884c49f77105a80cb05eb6cc5321e9bfb109733ff51ea9f557d94", + "policy_sha256": "daa0b7fd4abeea97ffeeca227b1524458d00cc5f1bf45278b437dacb79cf4032", + "release_id": "uk-spine-assessment-v20-970-fence", + "staging_h5_written": false, + "target_fit_failures": [ + "microcosm_uk_2024: hmrc/self_employment_income_income_band_20_000_to_30_000@2025 relative error +25.3% exceeds 25%." + ] + }, + "what": "The #970 projection fence evaluated on the calibrated v20 artifact (spine-assessment-v20/microcosm_uk_2024.h5, sha256 8883e592\u2026), directly and as the signed seam receipt of calibrating the v20 spine (spine-s.h5 4d9752fd\u2026) through the branch; aggregates only." +} diff --git a/docs/evidence/uk-cgt-970/v22-calibration-receipts.json b/docs/evidence/uk-cgt-970/v22-calibration-receipts.json new file mode 100644 index 0000000000..c1d0d132ed --- /dev/null +++ b/docs/evidence/uk-cgt-970/v22-calibration-receipts.json @@ -0,0 +1,1207 @@ +{ + "blocked_at_phase": null, + "calibration": { + "effective_sample_size": 4906.858878502037, + "final_loss": 0.010380570173263132, + "fraction_within_10pct": 0.9655172413793104, + "initial_loss": 0.3014686405658722, + "n_nonzero": 52846, + "n_records": 52846, + "realized_max_weight_ratio": 10.000000000000002, + "top_1pct_weight_share": 0.25588111843469913 + }, + "cgt_targets": [ + { + "name": "hmrc.cgt.gains_by_age_band.age_16_to_24@2025", + "relative_error": -0.04580519286131672, + "target": 319000000.0 + }, + { + "name": "hmrc.cgt.gains_by_age_band.age_25_to_34@2025", + "relative_error": -0.09738917504276319, + "target": 2499000000.0 + }, + { + "name": "hmrc.cgt.gains_by_age_band.age_35_to_44@2025", + "relative_error": -0.09099713369538649, + "target": 11775000000.0 + }, + { + "name": "hmrc.cgt.gains_by_age_band.age_45_to_54@2025", + "relative_error": -0.0008617503223738624, + "target": 28422000000.0 + }, + { + "name": "hmrc.cgt.gains_by_age_band.age_55_to_64@2025", + "relative_error": -0.0034757721661273916, + "target": 38118000000.0 + }, + { + "name": "hmrc.cgt.gains_by_age_band.age_65_to_74@2025", + "relative_error": -0.00990910706059691, + "target": 24260000000.0 + }, + { + "name": "hmrc.cgt.gains_by_age_band.age_75_to_84@2025", + "relative_error": -0.004451043791979125, + "target": 11736000000.0 + }, + { + "name": "hmrc.cgt.gains_by_age_band.age_85_plus@2025", + "relative_error": -0.023987992892009674, + "target": 2076000000.0 + }, + { + "name": "hmrc.cgt.gains_by_region@E12000001@2025", + "relative_error": 0.003842304995347341, + "target": 1497797150.3974364 + }, + { + "name": "hmrc.cgt.gains_by_region@E12000002@2025", + "relative_error": -0.003152726125602141, + "target": 8790074083.38308 + }, + { + "name": "hmrc.cgt.gains_by_region@E12000003@2025", + "relative_error": -0.007599366066480678, + "target": 6970985861.949795 + }, + { + "name": "hmrc.cgt.gains_by_region@E12000004@2025", + "relative_error": 0.00035016228352850243, + "target": 6299365751.358824 + }, + { + "name": "hmrc.cgt.gains_by_region@E12000005@2025", + "relative_error": -6.896364713284721e-05, + "target": 5890960774.765152 + }, + { + "name": "hmrc.cgt.gains_by_region@E12000006@2025", + "relative_error": -0.003906488915886132, + "target": 11545870966.728456 + }, + { + "name": "hmrc.cgt.gains_by_region@E12000007@2025", + "relative_error": -0.00041746302406262177, + "target": 32519714615.602127 + }, + { + "name": "hmrc.cgt.gains_by_region@E12000008@2025", + "relative_error": -0.06881818489001876, + "target": 25606804690.69088 + }, + { + "name": "hmrc.cgt.gains_by_region@E12000009@2025", + "relative_error": 0.00021772594318457847, + "target": 10341263627.509504 + }, + { + "name": "hmrc.cgt.gains_by_region@N92000002@2025", + "relative_error": -0.001283718506117194, + "target": 1757265449.747086 + }, + { + "name": "hmrc.cgt.gains_by_region@S92000003@2025", + "relative_error": 0.0015267828000581117, + "target": 5680201325.83493 + }, + { + "name": "hmrc.cgt.gains_by_region@W92000004@2025", + "relative_error": 0.0010724509903778682, + "target": 2358632410.6946497 + }, + { + "name": "hmrc.cgt.gains_total@2025", + "relative_error": -0.01578000860139608, + "target": 119258000000.0 + }, + { + "name": "hmrc.cgt.liability_total@2025", + "relative_error": 0.10047426810067275, + "target": 22503000000.0 + }, + { + "name": "hmrc.cgt.residential_property_gains.ty2024@2025", + "relative_error": -0.0034046629656876827, + "target": 12242481474.688189 + }, + { + "name": "hmrc.cgt.residential_property_taxpayers.ty2024@2025", + "relative_error": 0.0011599993561563118, + "target": 202630.0578034682 + }, + { + "name": "hmrc.cgt.tax_by_age_band.age_16_to_24@2025", + "relative_error": 0.0012943696615683325, + "target": 59000000.0 + }, + { + "name": "hmrc.cgt.tax_by_age_band.age_25_to_34@2025", + "relative_error": 0.004459362435789485, + "target": 456000000.0 + }, + { + "name": "hmrc.cgt.tax_by_age_band.age_35_to_44@2025", + "relative_error": 0.0046053139139499814, + "target": 2246000000.0 + }, + { + "name": "hmrc.cgt.tax_by_age_band.age_45_to_54@2025", + "relative_error": 0.10033828618580723, + "target": 5522000000.0 + }, + { + "name": "hmrc.cgt.tax_by_age_band.age_55_to_64@2025", + "relative_error": 0.12875680993479263, + "target": 7152000000.0 + }, + { + "name": "hmrc.cgt.tax_by_age_band.age_65_to_74@2025", + "relative_error": 0.1402865291014784, + "target": 4409000000.0 + }, + { + "name": "hmrc.cgt.tax_by_age_band.age_75_to_84@2025", + "relative_error": 0.0718388402250349, + "target": 2260000000.0 + }, + { + "name": "hmrc.cgt.tax_by_age_band.age_85_plus@2025", + "relative_error": 0.004367374998154549, + "target": 390000000.0 + }, + { + "name": "hmrc.cgt.taxpayers_by_age_band.age_16_to_24@2025", + "relative_error": 0.0010654802322387696, + "target": 4000.0 + }, + { + "name": "hmrc.cgt.taxpayers_by_age_band.age_25_to_34@2025", + "relative_error": -0.0012174074031688548, + "target": 27000.0 + }, + { + "name": "hmrc.cgt.taxpayers_by_age_band.age_35_to_44@2025", + "relative_error": 0.0016256333864652194, + "target": 65000.0 + }, + { + "name": "hmrc.cgt.taxpayers_by_age_band.age_45_to_54@2025", + "relative_error": 0.0011383698789440857, + "target": 98000.0 + }, + { + "name": "hmrc.cgt.taxpayers_by_age_band.age_55_to_64@2025", + "relative_error": -0.001191873570283254, + "target": 144000.0 + }, + { + "name": "hmrc.cgt.taxpayers_by_age_band.age_65_to_74@2025", + "relative_error": 0.0012695675392150878, + "target": 125000.0 + }, + { + "name": "hmrc.cgt.taxpayers_by_age_band.age_75_to_84@2025", + "relative_error": -0.0007139132934458115, + "target": 68000.0 + }, + { + "name": "hmrc.cgt.taxpayers_by_age_band.age_85_plus@2025", + "relative_error": 0.00394276950233861, + "target": 19000.0 + }, + { + "name": "hmrc.cgt.taxpayers_by_region@E12000001@2025", + "relative_error": 0.004040462902366685, + "target": 11321.91780821918 + }, + { + "name": "hmrc.cgt.taxpayers_by_region@E12000002@2025", + "relative_error": 0.0003893649628837522, + "target": 41513.69863013699 + }, + { + "name": "hmrc.cgt.taxpayers_by_region@E12000003@2025", + "relative_error": -0.005455274756008045, + "target": 32078.767123287675 + }, + { + "name": "hmrc.cgt.taxpayers_by_region@E12000004@2025", + "relative_error": -0.0009455790794284241, + "target": 31135.273972602743 + }, + { + "name": "hmrc.cgt.taxpayers_by_region@E12000005@2025", + "relative_error": 0.0015949360996568693, + "target": 33022.260273972606 + }, + { + "name": "hmrc.cgt.taxpayers_by_region@E12000006@2025", + "relative_error": 0.0031836662339378677, + "target": 58496.57534246576 + }, + { + "name": "hmrc.cgt.taxpayers_by_region@E12000007@2025", + "relative_error": -2.3031829771038796e-05, + "target": 106614.72602739726 + }, + { + "name": "hmrc.cgt.taxpayers_by_region@E12000008@2025", + "relative_error": -0.00013545902162542825, + "target": 114162.67123287672 + }, + { + "name": "hmrc.cgt.taxpayers_by_region@E12000009@2025", + "relative_error": 0.000157512225386346, + "target": 60383.56164383562 + }, + { + "name": "hmrc.cgt.taxpayers_by_region@N92000002@2025", + "relative_error": 0.003830817990726981, + "target": 9434.931506849316 + }, + { + "name": "hmrc.cgt.taxpayers_by_region@S92000003@2025", + "relative_error": -0.002124418984950878, + "target": 34909.24657534247 + }, + { + "name": "hmrc.cgt.taxpayers_by_region@W92000004@2025", + "relative_error": 0.005629133447184286, + "target": 16982.87671232877 + }, + { + "name": "hmrc.cgt.taxpayers_total@2025", + "relative_error": -0.0014481832955147524, + "target": 551000.0 + }, + { + "name": "hmrc/cgt_taxpayers_band_1000000@2025", + "relative_error": 0.0026854631106058755, + "target": 9000.0 + }, + { + "name": "hmrc/cgt_taxpayers_band_100000@2025", + "relative_error": -0.0004961358680099738, + "target": 61000.0 + }, + { + "name": "hmrc/cgt_taxpayers_band_10000@2025", + "relative_error": 8.21837645310622e-05, + "target": 26000.0 + }, + { + "name": "hmrc/cgt_taxpayers_band_12300@2025", + "relative_error": 0.0033558100670883335, + "target": 97000.0 + }, + { + "name": "hmrc/cgt_taxpayers_band_2000000@2025", + "relative_error": 0.0011826237201690674, + "target": 5000.0 + }, + { + "name": "hmrc/cgt_taxpayers_band_250000@2025", + "relative_error": 0.0008463912200927735, + "target": 25000.0 + }, + { + "name": "hmrc/cgt_taxpayers_band_25000@2025", + "relative_error": 0.011272606732893964, + "target": 98000.0 + }, + { + "name": "hmrc/cgt_taxpayers_band_3000@2025", + "relative_error": 0.005316558027920658, + "target": 73000.0 + }, + { + "name": "hmrc/cgt_taxpayers_band_5000000@2025", + "relative_error": 0.0036348886489868163, + "target": 3000.0 + }, + { + "name": "hmrc/cgt_taxpayers_band_500000@2025", + "relative_error": -0.003026472747325897, + "target": 16000.0 + }, + { + "name": "hmrc/cgt_taxpayers_band_50000@2025", + "relative_error": 0.003198197242541191, + "target": 78000.0 + }, + { + "name": "hmrc/cgt_taxpayers_band_6000@2025", + "relative_error": 0.0026080383836177357, + "target": 57000.0 + } + ], + "demoted_donors_by_band_pre_calibration": [ + { + "band_lower": 12300, + "demoted": 30, + "donors": 30 + }, + { + "band_lower": 25000, + "demoted": 29, + "donors": 30 + }, + { + "band_lower": 50000, + "demoted": 27, + "donors": 30 + }, + { + "band_lower": 100000, + "demoted": 27, + "donors": 30 + }, + { + "band_lower": 250000, + "demoted": 5, + "donors": 30 + }, + { + "band_lower": 500000, + "demoted": 0, + "donors": 30 + }, + { + "band_lower": 1000000, + "demoted": 1, + "donors": 30 + }, + { + "band_lower": 2000000, + "demoted": 0, + "donors": 30 + }, + { + "band_lower": 5000000, + "demoted": 0, + "donors": 30 + } + ], + "fence": { + "base_year": 2024, + "bound": 73000.0, + "bound_source": "hmrc_cgt_conditioning_facts.json: HMRC Table 2.1a tax year 2024, taxpayers with gains from 3,000 to 6,000", + "cumulative_gains_factor_by_year": { + "2025": 1.0438, + "2026": 1.07427896, + "2027": 1.108978170408, + "2028": 1.143356493690648, + "2029": 1.1771998459038913, + "2030": 1.214281641049864 + }, + "entrants_by_year": { + "2025": 12710.522127628326, + "2026": 22331.494442284107, + "2027": 43377.05143140629, + "2028": 50415.786320358515, + "2029": 54320.654136411846, + "2030": 67610.8267680183 + }, + "exempt_amount_by_year": { + "2024": 3000.0, + "2025": 3000.0, + "2026": 3000.0, + "2027": 3000.0, + "2028": 3000.0, + "2029": 3000.0, + "2030": 3000.0 + }, + "exempt_amount_parameter": "gov.hmrc.cgt.annual_exempt_amount", + "gains_growth_parameter": "gov.economic_assumptions.yoy_growth.obr.per_capita.gdp", + "horizon_year": 2030, + "max_entrants": 67610.8267680183, + "projection_engine": "policyengine-uk==2.98.0", + "sub_exempt": { + "p10": 1428.2915602712712, + "p50": 2116.0751632950937, + "p90": 2734.29109226299, + "rows": 20642, + "weighted_at_exempt_amount": 0.0, + "weighted_persons": 326532.0945219784 + }, + "worst_year": 2030 + }, + "gate_statuses": { + "uk_aggregate_admin": "passed", + "uk_calibration_reference_coverage": "passed", + "uk_cgt_projection_entrants": "passed", + "uk_target_fit": "passed", + "uk_weight_ess": "passed", + "uk_weight_ratio": "passed", + "uk_zero_weight_strata": "passed" + }, + "pass2_score": { + "candidate_full_loss": 0.01029854759850209, + "candidate_target_wins": 500, + "incumbent_full_loss": 0.21712115556212358, + "incumbent_target_wins": 24, + "pruned": { + "n_pruned": 120, + "n_scored": 524, + "n_surface": 644, + "note": "Rows pruned from BOTH arms because their measures cannot be computed on the incumbent (admin-basis inputs the enhanced FRS does not carry). The candidate's fit on these rows is measured by the calibration diagnostics and terminal gates instead.", + "rows": { + "dwp.uc.households_children_1": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_child_count: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_child_count" + }, + "dwp.uc.households_children_2": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_child_count: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_child_count" + }, + "dwp.uc.households_children_3": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_child_count: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_child_count" + }, + "dwp.uc.households_children_4": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_child_count: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_child_count" + }, + "dwp.uc.households_children_5_or_more": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_child_count: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_child_count" + }, + "dwp.uc.households_couple_no_children": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp.uc.households_couple_with_children": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp.uc.households_single_no_children": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp.uc.households_single_with_children": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp.uc.scotland_households_child_under_1": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_has_child_under_one: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_has_child_under_one" + }, + "dwp.uc.two_child_limit.children_affected": { + "family": "dwp_two_child_limit", + "reason": "provider failed computing benunit.uc_tcl_affected_benunit_proxy: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_tcl_affected_benunit_proxy" + }, + "dwp.uc.two_child_limit.children_claimant_pip": { + "family": "dwp_two_child_limit", + "reason": "provider failed computing benunit.uc_tcl_affected_benunit_proxy: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_tcl_affected_benunit_proxy" + }, + "dwp.uc.two_child_limit.children_disabled_child_element": { + "family": "dwp_two_child_limit", + "reason": "provider failed computing benunit.uc_tcl_affected_benunit_proxy: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_tcl_affected_benunit_proxy" + }, + "dwp.uc.two_child_limit.children_in_3_children_households": { + "family": "dwp_two_child_limit", + "reason": "provider failed computing benunit.uc_tcl_affected_benunit_proxy: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_tcl_affected_benunit_proxy" + }, + "dwp.uc.two_child_limit.children_in_4_children_households": { + "family": "dwp_two_child_limit", + "reason": "provider failed computing benunit.uc_tcl_affected_benunit_proxy: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_tcl_affected_benunit_proxy" + }, + "dwp.uc.two_child_limit.children_in_5_children_households": { + "family": "dwp_two_child_limit", + "reason": "provider failed computing benunit.uc_tcl_affected_benunit_proxy: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_tcl_affected_benunit_proxy" + }, + "dwp.uc.two_child_limit.children_in_6_plus_children_households": { + "family": "dwp_two_child_limit", + "reason": "provider failed computing benunit.uc_tcl_affected_benunit_proxy: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_tcl_affected_benunit_proxy" + }, + "dwp.uc.two_child_limit.children_in_affected_households": { + "family": "dwp_two_child_limit", + "reason": "provider failed computing benunit.uc_tcl_affected_benunit_proxy: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_tcl_affected_benunit_proxy" + }, + "dwp.uc.two_child_limit.households_3_children": { + "family": "dwp_two_child_limit", + "reason": "provider failed computing benunit.uc_tcl_affected_benunit_proxy: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_tcl_affected_benunit_proxy" + }, + "dwp.uc.two_child_limit.households_4_children": { + "family": "dwp_two_child_limit", + "reason": "provider failed computing benunit.uc_tcl_affected_benunit_proxy: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_tcl_affected_benunit_proxy" + }, + "dwp.uc.two_child_limit.households_5_children": { + "family": "dwp_two_child_limit", + "reason": "provider failed computing benunit.uc_tcl_affected_benunit_proxy: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_tcl_affected_benunit_proxy" + }, + "dwp.uc.two_child_limit.households_6_plus_children": { + "family": "dwp_two_child_limit", + "reason": "provider failed computing benunit.uc_tcl_affected_benunit_proxy: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_tcl_affected_benunit_proxy" + }, + "dwp.uc.two_child_limit.households_affected": { + "family": "dwp_two_child_limit", + "reason": "provider failed computing benunit.uc_tcl_affected_benunit_proxy: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_tcl_affected_benunit_proxy" + }, + "dwp.uc.two_child_limit.households_claimant_pip": { + "family": "dwp_two_child_limit", + "reason": "provider failed computing benunit.uc_tcl_affected_benunit_proxy: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_tcl_affected_benunit_proxy" + }, + "dwp.uc.two_child_limit.households_disabled_child_element": { + "family": "dwp_two_child_limit", + "reason": "provider failed computing benunit.uc_tcl_affected_benunit_proxy: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_tcl_affected_benunit_proxy" + }, + "dwp/uc_payment_dist/COUPLE_NO_CHILDREN_annual_payment_0_to_1_200": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/COUPLE_NO_CHILDREN_annual_payment_10_800_to_12_000": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/COUPLE_NO_CHILDREN_annual_payment_12_000_to_13_200": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/COUPLE_NO_CHILDREN_annual_payment_13_200_to_14_400": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/COUPLE_NO_CHILDREN_annual_payment_14_400_to_15_600": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/COUPLE_NO_CHILDREN_annual_payment_15_600_to_16_800": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/COUPLE_NO_CHILDREN_annual_payment_16_800_to_18_000": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/COUPLE_NO_CHILDREN_annual_payment_18_000_to_19_200": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/COUPLE_NO_CHILDREN_annual_payment_19_200_to_20_400": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/COUPLE_NO_CHILDREN_annual_payment_1_200_to_2_400": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/COUPLE_NO_CHILDREN_annual_payment_20_400_to_21_600": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/COUPLE_NO_CHILDREN_annual_payment_21_600_to_22_800": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/COUPLE_NO_CHILDREN_annual_payment_22_800_to_24_000": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/COUPLE_NO_CHILDREN_annual_payment_24_000_to_25_200": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/COUPLE_NO_CHILDREN_annual_payment_25_200_to_26_400": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/COUPLE_NO_CHILDREN_annual_payment_2_400_to_3_600": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/COUPLE_NO_CHILDREN_annual_payment_3_600_to_4_800": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/COUPLE_NO_CHILDREN_annual_payment_4_800_to_6_000": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/COUPLE_NO_CHILDREN_annual_payment_6_000_to_7_200": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/COUPLE_NO_CHILDREN_annual_payment_7_200_to_8_400": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/COUPLE_NO_CHILDREN_annual_payment_8_400_to_9_600": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/COUPLE_NO_CHILDREN_annual_payment_9_600_to_10_800": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/COUPLE_WITH_CHILDREN_annual_payment_0_to_1_200": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/COUPLE_WITH_CHILDREN_annual_payment_10_800_to_12_000": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/COUPLE_WITH_CHILDREN_annual_payment_12_000_to_13_200": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/COUPLE_WITH_CHILDREN_annual_payment_13_200_to_14_400": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/COUPLE_WITH_CHILDREN_annual_payment_14_400_to_15_600": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/COUPLE_WITH_CHILDREN_annual_payment_15_600_to_16_800": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/COUPLE_WITH_CHILDREN_annual_payment_16_800_to_18_000": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/COUPLE_WITH_CHILDREN_annual_payment_18_000_to_19_200": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/COUPLE_WITH_CHILDREN_annual_payment_19_200_to_20_400": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/COUPLE_WITH_CHILDREN_annual_payment_1_200_to_2_400": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/COUPLE_WITH_CHILDREN_annual_payment_20_400_to_21_600": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/COUPLE_WITH_CHILDREN_annual_payment_21_600_to_22_800": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/COUPLE_WITH_CHILDREN_annual_payment_22_800_to_24_000": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/COUPLE_WITH_CHILDREN_annual_payment_24_000_to_25_200": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/COUPLE_WITH_CHILDREN_annual_payment_25_200_to_26_400": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/COUPLE_WITH_CHILDREN_annual_payment_26_400_to_27_600": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/COUPLE_WITH_CHILDREN_annual_payment_27_600_to_28_800": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/COUPLE_WITH_CHILDREN_annual_payment_28_800_to_30_000": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/COUPLE_WITH_CHILDREN_annual_payment_2_400_to_3_600": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/COUPLE_WITH_CHILDREN_annual_payment_3_600_to_4_800": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/COUPLE_WITH_CHILDREN_annual_payment_4_800_to_6_000": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/COUPLE_WITH_CHILDREN_annual_payment_6_000_to_7_200": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/COUPLE_WITH_CHILDREN_annual_payment_7_200_to_8_400": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/COUPLE_WITH_CHILDREN_annual_payment_8_400_to_9_600": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/COUPLE_WITH_CHILDREN_annual_payment_9_600_to_10_800": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/LONE_PARENT_annual_payment_0_to_1_200": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/LONE_PARENT_annual_payment_10_800_to_12_000": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/LONE_PARENT_annual_payment_1_200_to_2_400": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/LONE_PARENT_annual_payment_20_400_to_21_600": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/LONE_PARENT_annual_payment_22_800_to_24_000": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/LONE_PARENT_annual_payment_24_000_to_25_200": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/LONE_PARENT_annual_payment_25_200_to_26_400": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/LONE_PARENT_annual_payment_26_400_to_27_600": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/LONE_PARENT_annual_payment_27_600_to_28_800": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/LONE_PARENT_annual_payment_28_800_to_30_000": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/LONE_PARENT_annual_payment_2_400_to_3_600": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/LONE_PARENT_annual_payment_3_600_to_4_800": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/LONE_PARENT_annual_payment_4_800_to_6_000": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/LONE_PARENT_annual_payment_6_000_to_7_200": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/LONE_PARENT_annual_payment_7_200_to_8_400": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/LONE_PARENT_annual_payment_8_400_to_9_600": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/LONE_PARENT_annual_payment_9_600_to_10_800": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/SINGLE_annual_payment_0_to_1_200": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/SINGLE_annual_payment_10_800_to_12_000": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/SINGLE_annual_payment_12_000_to_13_200": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/SINGLE_annual_payment_15_600_to_16_800": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/SINGLE_annual_payment_16_800_to_18_000": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/SINGLE_annual_payment_18_000_to_19_200": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/SINGLE_annual_payment_19_200_to_20_400": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/SINGLE_annual_payment_1_200_to_2_400": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/SINGLE_annual_payment_20_400_to_21_600": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/SINGLE_annual_payment_22_800_to_24_000": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/SINGLE_annual_payment_24_000_to_25_200": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/SINGLE_annual_payment_25_200_to_26_400": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/SINGLE_annual_payment_26_400_to_27_600": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/SINGLE_annual_payment_28_800_to_30_000": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/SINGLE_annual_payment_2_400_to_3_600": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/SINGLE_annual_payment_3_600_to_4_800": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/SINGLE_annual_payment_4_800_to_6_000": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/SINGLE_annual_payment_6_000_to_7_200": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "dwp/uc_payment_dist/SINGLE_annual_payment_7_200_to_8_400": { + "family": "dwp_universal_credit", + "reason": "provider failed computing benunit.uc_calibration_administrative_family_type: \"UC relationship benunit inputs missing: ['dependent_children']\"", + "unresolvable_measure": "benunit.uc_calibration_administrative_family_type" + }, + "hmrc.cgt.residential_property_gains.ty2024": { + "family": "hmrc_cgt", + "reason": "provider does not know person.capital_gains_asset_type", + "unresolvable_measure": "person.capital_gains_asset_type" + }, + "hmrc.cgt.residential_property_taxpayers.ty2024": { + "family": "hmrc_cgt", + "reason": "provider does not know person.capital_gains_asset_type", + "unresolvable_measure": "person.capital_gains_asset_type" + }, + "ons.household_composition.couple_3_plus_children_households": { + "family": "ons_household_composition", + "reason": "provider does not know household.ons_household_type", + "unresolvable_measure": "household.ons_household_type" + }, + "ons.household_composition.couple_no_children_households": { + "family": "ons_household_composition", + "reason": "provider does not know household.ons_household_type", + "unresolvable_measure": "household.ons_household_type" + }, + "ons.household_composition.couple_non_dependent_children_only_households": { + "family": "ons_household_composition", + "reason": "provider does not know household.ons_household_type", + "unresolvable_measure": "household.ons_household_type" + }, + "ons.household_composition.couple_under_3_children_households": { + "family": "ons_household_composition", + "reason": "provider does not know household.ons_household_type", + "unresolvable_measure": "household.ons_household_type" + }, + "ons.household_composition.lone_households_over_65": { + "family": "ons_household_composition", + "reason": "provider does not know household.ons_household_type", + "unresolvable_measure": "household.ons_household_type" + }, + "ons.household_composition.lone_households_under_65": { + "family": "ons_household_composition", + "reason": "provider does not know household.ons_household_type", + "unresolvable_measure": "household.ons_household_type" + }, + "ons.household_composition.lone_parent_dependent_children_households": { + "family": "ons_household_composition", + "reason": "provider does not know household.ons_household_type", + "unresolvable_measure": "household.ons_household_type" + }, + "ons.household_composition.lone_parent_non_dependent_children_households": { + "family": "ons_household_composition", + "reason": "provider does not know household.ons_household_type", + "unresolvable_measure": "household.ons_household_type" + }, + "ons.household_composition.multi_family_households": { + "family": "ons_household_composition", + "reason": "provider does not know household.ons_household_type", + "unresolvable_measure": "household.ons_household_type" + }, + "ons.household_composition.unrelated_adult_households": { + "family": "ons_household_composition", + "reason": "provider does not know household.ons_household_type", + "unresolvable_measure": "household.ons_household_type" + } + } + }, + "register": { + "country": "uk", + "n_specs": 524, + "version": "2331a9386a2e" + }, + "target_wins_by_family": { + "council_tax_stock": { + "candidate_target_wins": 97, + "incumbent_target_wins": 0, + "ties": 0 + }, + "dfe_funded_childcare": { + "candidate_target_wins": 3, + "incumbent_target_wins": 0, + "ties": 0 + }, + "dft_local_bus": { + "candidate_target_wins": 9, + "incumbent_target_wins": 0, + "ties": 0 + }, + "dwp_benefit_cap": { + "candidate_target_wins": 4, + "incumbent_target_wins": 0, + "ties": 0 + }, + "dwp_legacy_benefits": { + "candidate_target_wins": 2, + "incumbent_target_wins": 1, + "ties": 0 + }, + "dwp_universal_credit": { + "candidate_target_wins": 6, + "incumbent_target_wins": 2, + "ties": 0 + }, + "hmrc_cgt": { + "candidate_target_wins": 73, + "incumbent_target_wins": 2, + "ties": 0 + }, + "hmrc_salary_sacrifice": { + "candidate_target_wins": 3, + "incumbent_target_wins": 0, + "ties": 0 + }, + "hmrc_spi": { + "candidate_target_wins": 121, + "incumbent_target_wins": 10, + "ties": 0 + }, + "hmrc_tfc": { + "candidate_target_wins": 2, + "incumbent_target_wins": 0, + "ties": 0 + }, + "isc": { + "candidate_target_wins": 1, + "incumbent_target_wins": 0, + "ties": 0 + }, + "obr": { + "candidate_target_wins": 16, + "incumbent_target_wins": 5, + "ties": 0 + }, + "ons_employment": { + "candidate_target_wins": 1, + "incumbent_target_wins": 0, + "ties": 0 + }, + "ons_land": { + "candidate_target_wins": 1, + "incumbent_target_wins": 2, + "ties": 0 + }, + "ons_national_accounts": { + "candidate_target_wins": 2, + "incumbent_target_wins": 0, + "ties": 0 + }, + "ons_population": { + "candidate_target_wins": 148, + "incumbent_target_wins": 1, + "ties": 0 + }, + "orr_rail": { + "candidate_target_wins": 1, + "incumbent_target_wins": 0, + "ties": 0 + }, + "scotgov_social_security": { + "candidate_target_wins": 1, + "incumbent_target_wins": 0, + "ties": 0 + }, + "slc_borrowers": { + "candidate_target_wins": 1, + "incumbent_target_wins": 0, + "ties": 0 + }, + "slc_repayments": { + "candidate_target_wins": 3, + "incumbent_target_wins": 0, + "ties": 0 + }, + "slc_student_support": { + "candidate_target_wins": 5, + "incumbent_target_wins": 1, + "ties": 0 + } + } + }, + "release_id": "uk-spine-assessment-v22", + "sub_exempt_split_after_calibration": { + "clones": { + "entrants_by_year": [ + 1788, + 2751, + 4001, + 4905, + 5662, + 6392 + ], + "liable": 486007, + "losses": 115955, + "sub_exempt": 35433 + }, + "donors": { + "demoted_rows": 119, + "entrants_by_year": [ + 10923, + 19580, + 39376, + 45510, + 48659, + 61219 + ], + "liable": 64195, + "sub_exempt": 291099 + }, + "years": [ + 2025, + 2026, + 2027, + 2028, + 2029, + 2030 + ] + }, + "target_fit_reviewed_exclusions": [ + "hmrc/self_employment_income_income_band_20_000_to_30_000@2025" + ], + "what": "The v22 calibration of spine-970 through the branch's seam (v20 recipe: frozen register pin, 1,500 epochs, family_equal; Mar\u00eda's signed SE-band deferral 90bdb809 cherry-picked onto measurement branch 2acbb9dc because it is not on main); aggregates only." +} diff --git a/docs/gate-battery-contract.md b/docs/gate-battery-contract.md index ff9cf097e6..59f0298576 100644 --- a/docs/gate-battery-contract.md +++ b/docs/gate-battery-contract.md @@ -130,6 +130,7 @@ Canonical artifact keys: | `reviewed_input_mass_exclusions` (optional override) | the UK national build | `input_mass_parity` (UK override) | | `reviewed_qrf_tail_exclusions` (optional override) | the UK national build | `tail_concentration` (UK override) | | `uk_area_support_summary`, `exclusions_evaluated_on` | the UK local rowwise build | `area_support` (UK binding; reviewed exclusions carry measured shortfalls and stale entries fail) | +| `cgt_projection` | the UK calibration seam: read from the installed engine's growth path and exempt amounts, or stated from the manifest pins when no engine is installed (the receipt's `projection_engine` names the source, and the release certifier refuses a seam part whose fence read the pins) | `cgt_projection_entrants` (UK binding; the count is a cumulative stock by year, the bound is the vendored HMRC thin-band taxpayer count as a plausibility ceiling, and the growth path and exempt amount are pinned in the manifest and drift-checked, microcosm#970) | The UK registry (`microcosm.build.uk_runtime.battery_bindings.UK_GATE_REGISTRY`) is the diff --git a/experiments/970-cgt-sub-exempt-remainder.md b/experiments/970-cgt-sub-exempt-remainder.md new file mode 100644 index 0000000000..5a67fcc7a3 --- /dev/null +++ b/experiments/970-cgt-sub-exempt-remainder.md @@ -0,0 +1,92 @@ +# UK CGT sub-exempt gainers: remainder amounts, incidence anchor and projection fence (microcosm#970) + +*2026-09-22. Branch `uk-970-cgt-sub-exempt`. The licensed receipts live outside the tree, under +`data/ukds/acceptance/970-cgt-sub-exempt/` and `runs/uk-623-first-calibrated/spine-assessment-v22/`; aggregate-only +extracts of every number quoted below are committed under `docs/evidence/uk-cgt-970/`. The v20 numbers come from the +`uk-equalising-cgt` dashboard run of 2026-09-21 (PolicyEngine/uk-equalising-cgt PR #3) and the HMRC Capital Gains Tax +statistics 2026 release.* + +## Question + +The staged v20 candidate (`staged/uk-spine-assessment-v20-calibration`) carried 11.66 million weighted people with +capital gains at or below the £3,000 annual exempt amount, 10.82 million of them at exactly £3,000.00: the amounts +stage capped every gainer beyond HMRC's published taxpayer mass at the exempt amount, and the equal-mass incidence +clone left half of all household mass on gainer households. policyengine-uk freezes the exempt amount and uprates gains +by per-capita GDP growth, so every one of them became a CGT taxpayer in the first projected year: 11.54 million +taxpayers in 2026-27 against 597 thousand on the incumbent Enhanced FRS and 551 thousand in HMRC Table 1 for 2024-25. +Issue #970 fixes this in three parts, and this note records what each changed on the licensed rebuild. + +## What changed + +1. **Amounts** (`hmrc_cgt_gains_spine`): the remainder takes amounts from the Advani-Summers Table A1 within-band + gains distribution restricted to the quantiles between each band's zero crossing and its crossing of the exempt + amount, rank-preserving within the band and deterministic (no seed consumed). The table is used as published + (2017-18 nominal), with no uprating; for the first income band the crossings sit at quantiles 0.1805 and 0.2354. +2. **Incidence** (`cgt_incidence_anchor`, new 31st spine stage, weights only): the sub-exempt and loss-making clone + households are trimmed to the reporter composition implied by the redrawn liable mass and the Advani-Summers + crossings (sub-exempt = liable × (q_AEA − q_0) / (1 − q_AEA), losses = liable × q_0 / (1 − q_AEA)), with a factor + rising in the band's incidence and capped at one; every unit of mass a clone loses goes to its paired original, so + pair mass, household mass and every non-CGT aggregate are conserved to rounding. Liable clones, zero-gain clone + carriers (no reporters) and band donors are untouched. The groups, targets and receipt are clone-side quantities, + not population counts: band donors and originals are outside the anchor. Gate: + `uk_stage_cgt_incidence_anchor_composition`, which certifies the anchor's arithmetic against its declared + composition. The first licensed build was blocked by that gate: + a global exact-total correction borrowed from the clone stage had moved the summed rounding of 26,288 transfers + onto one household (1.6e-13 relative on its pair); the fix conserves pairs arithmetically and records the total as + realised. +3. **Fence** (`uk_cgt_projection_entrants`, calibration seam): the cumulative stock of build-period sub-exempt gainers + whose uprated gains exceed each year's exempt amount is counted to 2030; the largest count must not exceed the + vendored HMRC Table 2.1a taxpayers in the £3,000–£5,999 band (73,000 in 2024-25), people already above the exempt + amount, so a plausibility ceiling rather than an entrant count. 2030 is the last year of the OBR per-capita growth + path in the engine's parameter tree (later years repeat the 2030 rate); the path and the exempt amount are pinned in + the gate's manifest entry and drift-checked, so an engine bump fails the gate visibly instead of moving its verdict. + +## Measurements + +Before: the fence evaluated on the calibrated v20 artifact, and the seam receipt of calibrating the v20 spine through +this branch (signed report, blocked at terminal, no H5). + +- Entrants by uprating: 10,832,804 in 2025 rising to 10,978,624 in 2030, against the 73,000 bound. Sub-exempt weighted + persons 11,664,957, of which 10,815,254 at exactly £3,000; the 10th, 50th and 90th percentiles of sub-exempt gains + are all £3,000. + +After: the 31-stage spine rebuilt on 24509d78 from the same licensed inputs as spine-s (all 21 spine gates pass), then +calibrated as v22 with the v20 recipe (frozen register pin, 1,500 epochs, family_equal). María's signed deferral of +the self-employment £20k–£30k band (90bdb809, not on main) was cherry-picked onto a measurement branch so the recipe +matched v20; without it main's `uk_target_fit` fails that band at +25.3%. + +- Anchor receipt (spine, clone side only): clone sub-exempt 11,291,667 → 40,729; loss-making 2,707,829 → 150,250; liable clone mass + 486,720 untouched; liable mass 557,420; reporter composition q_0 = 0.2008, q_AEA = 0.2552 (implied reporters + 748,399); 25,436 of 26,288 clone households trimmed, none capped; largest pair error 2.2e-16. +- Remainder receipt (spine): 20,642 persons placed in (£0.24, £2,999.76], all 61 Advani-Summers bands represented. +- Calibration: 7/7 seam gates pass. Loss 0.3015 → 0.01038 (v20 0.01041); 96.6% of 638 targets within 10% (v20 + 96.4%); CGT targets 61 of 65 within 10% (worst: tax by age 65–74 +14.0%, liability total +10.0%). ESS 4,907 (v20 + 9,305). Calibrated liable mass 550,202 carrying £117.4bn of gains. +- Fence (calibrated frame): PASSED. The stock of crossers is 12,711 by 2025, 22,331 by 2026, 43,377 by 2027, 50,416 + by 2028, 54,321 by 2029 and 67,611 by 2030, against 73,000: about 13k a year, so the margin is one year of trend, and + the demoted-donor decision below is what would restore it. Sub-exempt weighted persons 326,532, median £2,116, none + at exactly £3,000. +- Pass-2 head-to-head against the incumbent (scored from the deferral tree, whose scoring-prepare fix 1f45bdf3 the + eval script needs): 500–24 on 524 common targets, candidate loss 0.0103 against the incumbent's 0.2171; the CGT family + 73–2 (v20: 493–26 on 519, loss 0.0096 vs 0.211, CGT 73–2). + +## What the fence found next + +Of the 67,611 entrants in 2030, 61,219 are band donors and 6,392 are clones. The Table 3 redraw ranks every gainer in +an income band by its prior and fills the published cells from the top; it does not distinguish the band donors, so +119 of the 270 donors fall into the sub-exempt remainder, almost all in the £12.3k–£250k bands where the clones' +Advani-Summers tails outrank the band means (12.3k band 30 of 30, 25k band 29, 50k band 27, 100k band 27, 250k band +5, 1m band 1, none above). They carry 321,300 weighted persons before calibration and 291,099 after. The same +demotion existed on v20, where those donors sat at the £3,000 cap inside the 10.82 million. The donor stage's notes +describe the donors as support households and exclude bands below £12.3k because "the spline body already supplies +that support", so a demoted donor is redundant support that has kept its mass as a sub-exempt gainer. How to treat +them (protect donors in the redraw's cell selection, drop their mass, or stack donors only where the clone pool is +short) is a separate decision this branch does not take. + +## Status + +Code, manifests, fixtures and pins are on the branch; the licensed receipts stay outside the tree, with the +aggregate-only extracts under `docs/evidence/uk-cgt-970/`. The full `microcosm-build` shard on the review-round tree +passed (8,459 tests, 0 failures, 0 errors, 48 licensed or network-gated skips, 3 h 14 min); the shard on each later +head is reported in the PR thread. Still open: the dashboard measurement (after the staged upload) and María's ruling +on demoted donors. diff --git a/packages/microcosm-build/src/microcosm/build/country_spec.py b/packages/microcosm-build/src/microcosm/build/country_spec.py index de1b149e74..8672706891 100644 --- a/packages/microcosm-build/src/microcosm/build/country_spec.py +++ b/packages/microcosm-build/src/microcosm/build/country_spec.py @@ -114,6 +114,7 @@ "aggregate_admin", "area_support", "calibration_reference_coverage", + "cgt_projection_entrants", "column_implication", "degenerate_release_surface", "enum_domain", diff --git a/packages/microcosm-build/src/microcosm/build/source_manifest.py b/packages/microcosm-build/src/microcosm/build/source_manifest.py index c8476167d1..fe76832ff5 100644 --- a/packages/microcosm-build/src/microcosm/build/source_manifest.py +++ b/packages/microcosm-build/src/microcosm/build/source_manifest.py @@ -92,6 +92,7 @@ "derive_ons_household_composition", "derive_pregnancy", "derive_relationship_inputs", + "derive_reporter_composition_from_banded_quantiles", "derive_retirement_distributions", "derive_retirement_contributions", "derive_workers_compensation", @@ -126,6 +127,7 @@ "impute_workers_compensation_to_puf_support", "impute_weeks_unemployed_to_puf_support", "iterative_proportional_fit", + "pair_clone_households_to_originals", "price_domestic_energy", "rake_to_vendored_facts", "map_columns", @@ -155,6 +157,7 @@ "sub_aea_remainder", "taxable_income_proxy", "top_up_to_stock", + "transfer_clone_mass_to_originals", "uprate", "uprate_donor_columns", "uprate_to_regional_reference", diff --git a/packages/microcosm-build/src/microcosm/build/spec_engine/schema/sources.schema.json b/packages/microcosm-build/src/microcosm/build/spec_engine/schema/sources.schema.json index 11abb21818..360bb05cc5 100644 --- a/packages/microcosm-build/src/microcosm/build/spec_engine/schema/sources.schema.json +++ b/packages/microcosm-build/src/microcosm/build/spec_engine/schema/sources.schema.json @@ -5326,12 +5326,55 @@ }, "rationale": { "type": "string" + }, + "resource": { + "type": "string" + }, + "resource_vintage": { + "type": "string" + }, + "banding": { + "type": "string" + }, + "range": { + "type": "string" + }, + "mapping": { + "type": "string" + }, + "ordering": { + "type": "string" + }, + "quantile_points": { + "type": "array", + "items": { + "type": "number" + } + }, + "spline_degree": { + "type": "integer" + }, + "extrapolation": { + "type": "string" + }, + "deterministic": { + "type": "boolean" } }, "required": [ + "banding", + "deterministic", + "extrapolation", "kind", + "mapping", + "ordering", "policy", - "rationale" + "quantile_points", + "range", + "rationale", + "resource", + "resource_vintage", + "spline_degree" ] }, { @@ -7798,6 +7841,168 @@ } } }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "kind", + "clone_flag_column", + "donor_flag_column", + "id_remapping", + "pairing", + "requirement" + ], + "properties": { + "kind": { + "const": "pair_clone_households_to_originals" + }, + "clone_flag_column": { + "type": "string" + }, + "donor_flag_column": { + "type": "string" + }, + "id_remapping": { + "type": "string" + }, + "pairing": { + "type": "string" + }, + "requirement": { + "type": "string" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "kind", + "resource", + "income_proxy_components", + "allowance_subtraction", + "carrier", + "adult_minimum_age", + "quantile_points", + "spline_degree", + "extrapolation", + "crossings", + "aggregation", + "liable_mass", + "targets" + ], + "properties": { + "kind": { + "const": "derive_reporter_composition_from_banded_quantiles" + }, + "resource": { + "type": "string" + }, + "income_proxy_components": { + "type": "array", + "items": { + "type": "string" + } + }, + "allowance_subtraction": { + "type": "boolean" + }, + "carrier": { + "type": "string" + }, + "adult_minimum_age": { + "type": "integer" + }, + "quantile_points": { + "type": "array", + "items": { + "type": "number" + } + }, + "spline_degree": { + "type": "integer" + }, + "extrapolation": { + "type": "string" + }, + "crossings": { + "type": "string" + }, + "aggregation": { + "type": "string" + }, + "liable_mass": { + "type": "string" + }, + "targets": { + "type": "string" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "kind", + "population", + "sub_exempt_group", + "loss_group", + "liable_clones", + "donors", + "factor", + "maximum_factor", + "transfer", + "weight_kind_out", + "conservation", + "pair_conservation", + "declared_factor", + "reason" + ], + "properties": { + "kind": { + "const": "transfer_clone_mass_to_originals" + }, + "population": { + "type": "string" + }, + "sub_exempt_group": { + "type": "string" + }, + "loss_group": { + "type": "string" + }, + "liable_clones": { + "type": "string" + }, + "donors": { + "type": "string" + }, + "factor": { + "type": "string" + }, + "maximum_factor": { + "type": "number" + }, + "transfer": { + "type": "string" + }, + "weight_kind_out": { + "type": "string" + }, + "conservation": { + "type": "string" + }, + "pair_conservation": { + "type": "string" + }, + "declared_factor": { + "type": "number" + }, + "reason": { + "type": "string" + } + } + }, { "type": "object", "additionalProperties": false, diff --git a/packages/microcosm-build/src/microcosm/build/uk/gates.json b/packages/microcosm-build/src/microcosm/build/uk/gates.json index 05f8abbf51..4c6e1321be 100644 --- a/packages/microcosm-build/src/microcosm/build/uk/gates.json +++ b/packages/microcosm-build/src/microcosm/build/uk/gates.json @@ -397,6 +397,21 @@ }, "notes": "Stage-time gate over the CGT asset-type receipt (microcosm#725): the logistic solve reproduces the Table 8a 2024-25 totals on the individuals basis in expectation, the realised residential taxpayer count sits within one carrier row's weight of the expectation (the weighted systematic walk's own bound, max_liable_weight, about 3,300 people at full scale), and the realised residential gains sit within the wider of the reviewed relative band and three Bernoulli sigmas of the draw (an envelope that overstates a systematic draw's noise, so conservative), so a small frame is judged by its noise floor and a production frame by the band. Every liable gainer carries an asset type and the Table 7 composition receipt is finite; the calibration binds the same two totals." }, + { + "id": "uk_stage_cgt_incidence_anchor_composition", + "gate": "stage_health", + "phase": "transferred", + "criticality": "release_blocking", + "evidence_absent_blocks": true, + "parameters": { + "stage": "cgt_incidence_anchor", + "check": "cgt_incidence_anchor", + "maximum_relative_composition_error": 1e-06, + "maximum_pair_relative_error": 0.0, + "minimum_pair_count": 1 + }, + "notes": "Stage-time gate over the CGT incidence anchor receipt (microcosm#970). Clone-side receipt: the groups, targets and masses are clone households paired to originals; band donors and originals are outside the anchor, so this gate certifies the anchor's own arithmetic against its declared composition, not a population count. The sub-exempt and loss-making clone groups sit at the reporter-composition targets derived from the redrawn liable mass whenever they were trimmed, never overshoot or gain mass, and stay untouched when already at or below target; zero-gain clone carriers are no reporters and are left untouched; liable clone mass is byte-identical before and after; every clone/original pair conserves its mass to floating-point rounding; the clone side never exceeds the original side; and at least one pair exists. The level is a modelling assumption (HMRC publishes no count below the exempt amount), so the gate fixes the anchor to its own declared composition rather than to a published target." + }, { "id": "uk_stage_salary_sacrifice_realization", "gate": "stage_health", @@ -779,6 +794,42 @@ }, "notes": "Every calibration target within twenty-five percent absolute relative error, the certified June acceptance bound." }, + { + "id": "uk_cgt_projection_entrants", + "gate": "cgt_projection_entrants", + "phase": "terminal", + "criticality": "release_blocking", + "evidence_absent_blocks": true, + "parameters": { + "horizon_year": 2030, + "gains_growth_parameter": "gov.economic_assumptions.yoy_growth.obr.per_capita.gdp", + "exempt_amount_parameter": "gov.hmrc.cgt.annual_exempt_amount", + "expected_yoy_growth_by_year": { + "2023": 0.0532, + "2024": 0.0372, + "2025": 0.0438, + "2026": 0.0292, + "2027": 0.0323, + "2028": 0.031, + "2029": 0.0296, + "2030": 0.0315 + }, + "expected_exempt_amount_by_year": { + "2023": 6000.0, + "2024": 3000.0, + "2025": 3000.0, + "2026": 3000.0, + "2027": 3000.0, + "2028": 3000.0, + "2029": 3000.0, + "2030": 3000.0 + }, + "maximum_growth_drift": 0.0005, + "bound_resource": "hmrc_cgt_conditioning_facts.json", + "bound_size_band_lower_bound": 3000 + }, + "notes": "Projection fence over the calibrated frame's sub-exempt gainers (microcosm#970). The engine freezes the annual exempt amount at its nominal value and uprates capital_gains by OBR per-capita GDP growth, so a gainer at or just below the exempt amount in the build period becomes a CGT taxpayer in a later projected year without any change in behaviour. For every year to the horizon the gate counts the cumulative stock of build-period sub-exempt gainers (gains in (0, exempt amount]) whose gains, uprated by the cumulative growth factor to that year, exceed that year's exempt amount; the stock is non-decreasing in the year, so the horizon year carries the largest count. The bound is the vendored HMRC Table 2.1a count of 2024-25 taxpayers with gains from 3,000 to 5,999 (73,000): people already above the exempt amount, not entrants, so the bound is a plausibility ceiling (the population of the thinnest liable band), not a published entrant count. The horizon 2030 is the last year of the OBR per-capita growth path in the engine's parameter tree; later years repeat the 2030 rate, so a longer horizon would only compound a constant. The growth path and the exempt amount by year the seam reads from the installed engine are pinned here and drift-checked (the exempt amount was 6,000 in 2023-24 and 3,000 from 2024-25, so a 2023-period frame's base year reads 6,000), so an engine bump cannot move the verdict silently and re-pinning is a reviewed change; without an installed engine the seam builds the projection from these pins and the receipt says so. The margin is thin by construction: on the first rebuilt candidate the stock ran from 12.7k in 2025 to 67.6k in 2030, about 13k a year, and 61k of the 67.6k were band donors the redraw had demoted below the exempt amount, a separate decision; the anchored clones contributed 6.4k. The v20 candidate would have failed with 10.8 million in the first projected year. The bound comes from the same vendored facts the amounts stage is pinned to, never from a literal." + }, { "id": "uk_input_mass_parity", "gate": "input_mass_parity", diff --git a/packages/microcosm-build/src/microcosm/build/uk/release_input_coverage_manifest.json b/packages/microcosm-build/src/microcosm/build/uk/release_input_coverage_manifest.json index 83da82f429..53597aed13 100644 --- a/packages/microcosm-build/src/microcosm/build/uk/release_input_coverage_manifest.json +++ b/packages/microcosm-build/src/microcosm/build/uk/release_input_coverage_manifest.json @@ -473,7 +473,7 @@ "capital_gains" ], "source_manifest": "source_stages.json", - "source_manifest_sha256": "19e51cf5f4b11b1fb4780acfafb2ac79103f399624a94e1b08be53e92a891f00", + "source_manifest_sha256": "7675ceed6aaca930fb7856d37edf8b99d1b8a317b30775125fe20837e1119ec8", "source_vintages": { "source": "HMRC Capital Gains Tax statistics, July 2026 release, Table 2.1a for 2024-25, via the vendored conditioning facts", "survey": "HMRC Capital Gains Tax statistics Table 2.1a (2024-25 individuals) and Advani-Summers capital-gains incidence" @@ -481,6 +481,24 @@ "stage": "cgt_band_donors", "status": "required_at_build" }, + "cgt_incidence_anchor": { + "base_candidate_sha256": "f17306ccb2aad7ff0130be3589b560afb2e2a12a943570911cd0c77f07934833", + "base_candidate_tier": "frs", + "effective_mass_requirements": {}, + "mass_change_semantics": "mass_conserving", + "output_weight_kind": "importance", + "outputs": [], + "required_mass_change_reason": "Capital-gains incidence anchor moves the mass of non-liable clone households back to their paired originals until the sub-exempt and loss-making clone mass match the Advani-Summers reporter composition at the redrawn liable mass; every pair's mass and the total household mass are conserved.", + "rewrites": [], + "source_manifest": "source_stages.json", + "source_manifest_sha256": "7675ceed6aaca930fb7856d37edf8b99d1b8a317b30775125fe20837e1119ec8", + "source_vintages": { + "source": "Advani and Summers (2020), Capital Gains and UK Inequality, CAGE Working Paper 465", + "survey": "Family Resources Survey 2024-25, SPI synthetic support, the HMRC Table 3 redrawn gains on the spine and Advani-Summers capital-gains incidence" + }, + "stage": "cgt_incidence_anchor", + "status": "required_at_build" + }, "cgt_incidence_clone": { "base_candidate_sha256": "f17306ccb2aad7ff0130be3589b560afb2e2a12a943570911cd0c77f07934833", "base_candidate_tier": "frs", @@ -496,7 +514,7 @@ "capital_gains" ], "source_manifest": "source_stages.json", - "source_manifest_sha256": "19e51cf5f4b11b1fb4780acfafb2ac79103f399624a94e1b08be53e92a891f00", + "source_manifest_sha256": "7675ceed6aaca930fb7856d37edf8b99d1b8a317b30775125fe20837e1119ec8", "source_vintages": { "source": "Advani and Summers (2020), Capital Gains and UK Inequality, CAGE Working Paper 465", "survey": "Family Resources Survey 2024-25, SPI synthetic support, and Advani-Summers capital-gains incidence" @@ -525,7 +543,7 @@ "required_mass_change_reason": "ETB public-services imputation on the source spine: household weights pass through unchanged and total household mass is conserved.", "rewrites": [], "source_manifest": "source_stages.json", - "source_manifest_sha256": "19e51cf5f4b11b1fb4780acfafb2ac79103f399624a94e1b08be53e92a891f00", + "source_manifest_sha256": "7675ceed6aaca930fb7856d37edf8b99d1b8a317b30775125fe20837e1119ec8", "source_vintages": { "source": "UK Data Service SN 8856 Effects of Taxes and Benefits household tab, DfT rail fare index, and public NHS activity/cost table.", "survey": "Effects of Taxes and Benefits 1977-2024 and NHS age-gender public table" @@ -545,7 +563,7 @@ "required_mass_change_reason": "ETB VAT expenditure-rate imputation on the source spine: household weights pass through unchanged and total household mass is conserved.", "rewrites": [], "source_manifest": "source_stages.json", - "source_manifest_sha256": "19e51cf5f4b11b1fb4780acfafb2ac79103f399624a94e1b08be53e92a891f00", + "source_manifest_sha256": "7675ceed6aaca930fb7856d37edf8b99d1b8a317b30775125fe20837e1119ec8", "source_vintages": { "source": "UK Data Service SN 8856 Effects of Taxes and Benefits household tab and cited VAT anchor resource.", "survey": "Effects of Taxes and Benefits 1977-2024" @@ -566,7 +584,7 @@ "required_mass_change_reason": "CGT asset-type assignment on the source spine: household weights pass through unchanged and total household mass is conserved.", "rewrites": [], "source_manifest": "source_stages.json", - "source_manifest_sha256": "19e51cf5f4b11b1fb4780acfafb2ac79103f399624a94e1b08be53e92a891f00", + "source_manifest_sha256": "7675ceed6aaca930fb7856d37edf8b99d1b8a317b30775125fe20837e1119ec8", "source_vintages": { "source": "https://www.gov.uk/government/statistics/capital-gains-tax-statistics", "survey": "HMRC Capital Gains Tax statistics 2026 release, Table 8 (UK residential property disposals, 2024-25, administrative) and Table 7 (disposals, proceeds and gains by asset type, 2023-24, sample-based), vendored from the pinned Chronicle feed" @@ -590,7 +608,7 @@ "capital_gains" ], "source_manifest": "source_stages.json", - "source_manifest_sha256": "19e51cf5f4b11b1fb4780acfafb2ac79103f399624a94e1b08be53e92a891f00", + "source_manifest_sha256": "7675ceed6aaca930fb7856d37edf8b99d1b8a317b30775125fe20837e1119ec8", "source_vintages": { "conditioning_resource": "hmrc_cgt_conditioning_facts.json", "conditioning_resource_sha256": "f5bb0ad93fff3332b7ae8fb580f002d13207b3f95e44a3103c50108279d94797", @@ -607,7 +625,7 @@ "base_candidate_tier": "frs", "calibration_permitted": false, "canonical_source_manifest": "source_stages.json", - "canonical_source_manifest_sha256": "19e51cf5f4b11b1fb4780acfafb2ac79103f399624a94e1b08be53e92a891f00", + "canonical_source_manifest_sha256": "7675ceed6aaca930fb7856d37edf8b99d1b8a317b30775125fe20837e1119ec8", "effective_mass_requirements": { "charitable_investment_gifts": { "mass_share_denominator": "all_person_effective_mass", @@ -690,7 +708,7 @@ "superseded_by": { "reason": "The FRS spine build executes hmrc_spi_income_spine, which supersedes the June retained-leaves/hmrc_spi_income pair inside source_stages.json.", "source_manifest": "source_stages.json", - "source_manifest_sha256": "19e51cf5f4b11b1fb4780acfafb2ac79103f399624a94e1b08be53e92a891f00", + "source_manifest_sha256": "7675ceed6aaca930fb7856d37edf8b99d1b8a317b30775125fe20837e1119ec8", "stage": "hmrc_spi_income_spine" } }, @@ -724,7 +742,7 @@ "required_mass_change_reason": "LCFS consumption imputation on the source spine: household weights pass through unchanged and total household mass is conserved.", "rewrites": [], "source_manifest": "source_stages.json", - "source_manifest_sha256": "19e51cf5f4b11b1fb4780acfafb2ac79103f399624a94e1b08be53e92a891f00", + "source_manifest_sha256": "7675ceed6aaca930fb7856d37edf8b99d1b8a317b30775125fe20837e1119ec8", "source_vintages": { "source": "UK Data Service SN 9468 Living Costs and Food Survey 2023-24 household/person tabs and the vendored Chronicle facts for NEED 2023 mean kWh, the FY2024-25 Ofgem cap levels, road-fuel outturn (DESNZ, HMRC, ONS), the DfT VEH1103 licensed-car stock, NTS bus use and the DfT and devolved bus finance tables.", "survey": "Living Costs and Food Survey 2023-24" @@ -745,7 +763,7 @@ "property_wealth" ], "source_manifest": "source_stages.json", - "source_manifest_sha256": "19e51cf5f4b11b1fb4780acfafb2ac79103f399624a94e1b08be53e92a891f00", + "source_manifest_sha256": "7675ceed6aaca930fb7856d37edf8b99d1b8a317b30775125fe20837e1119ec8", "source_vintages": { "source": "MHCLG dwellings and ONS UK House Price Index December 2025 regional average prices.", "survey": "Public regional property reference" @@ -769,7 +787,7 @@ "employee_pension_contributions" ], "source_manifest": "source_stages.json", - "source_manifest_sha256": "19e51cf5f4b11b1fb4780acfafb2ac79103f399624a94e1b08be53e92a891f00", + "source_manifest_sha256": "7675ceed6aaca930fb7856d37edf8b99d1b8a317b30775125fe20837e1119ec8", "source_vintages": { "source": "HMRC, Salary sacrifice reform for pension contributions effective from 6 April 2029", "survey": "Family Resources Survey 2024-25 salary-sacrifice respondents and HMRC salary-sacrifice reform analysis" @@ -791,7 +809,7 @@ "student_loan_plan" ], "source_manifest": "source_stages.json", - "source_manifest_sha256": "19e51cf5f4b11b1fb4780acfafb2ac79103f399624a94e1b08be53e92a891f00", + "source_manifest_sha256": "7675ceed6aaca930fb7856d37edf8b99d1b8a317b30775125fe20837e1119ec8", "source_vintages": { "source": "Explore Education Statistics Table 6a, Higher education total", "survey": "Family Resources Survey 2024-25 and Student Loans Company borrower forecasts for England" @@ -826,7 +844,7 @@ "required_mass_change_reason": "WAS wealth imputation on the source spine: household weights pass through unchanged and total household mass is conserved.", "rewrites": [], "source_manifest": "source_stages.json", - "source_manifest_sha256": "19e51cf5f4b11b1fb4780acfafb2ac79103f399624a94e1b08be53e92a891f00", + "source_manifest_sha256": "7675ceed6aaca930fb7856d37edf8b99d1b8a317b30775125fe20837e1119ec8", "source_vintages": { "source": "Office for National Statistics Wealth and Assets Survey, UK Data Service SN 7215, DOI 10.5255/UKDA-SN-7215-20; local licensed 2006-22 household tab.", "survey": "Wealth and Assets Survey round 8" diff --git a/packages/microcosm-build/src/microcosm/build/uk/source_stages.json b/packages/microcosm-build/src/microcosm/build/uk/source_stages.json index 204c82b151..eb2fad94ff 100644 --- a/packages/microcosm-build/src/microcosm/build/uk/source_stages.json +++ b/packages/microcosm-build/src/microcosm/build/uk/source_stages.json @@ -3611,6 +3611,13 @@ "resource": "hmrc_cgt_conditioning_facts.json", "format": "json", "runtime_sha256_required": true + }, + { + "role": "capital_gains_within_band_distribution", + "kind": "public_aggregate_reference", + "resource": "advani_summers_capital_gains_distribution.json", + "format": "json", + "runtime_sha256_required": true } ], "operations": [ @@ -3725,8 +3732,26 @@ }, { "kind": "sub_aea_remainder", - "policy": "gainers beyond the published taxpayer mass keep their existing amounts capped at the annual exempt amount", - "rationale": "Table 3 covers only individuals with a CGT liability; remaining gainers are treated as sub-AEA gainers rather than invented into the liability distribution or deleted." + "policy": "gainers beyond the published taxpayer mass take amounts from the Advani-Summers within-band gains distribution restricted to (0, annual exempt amount], rank-preserving within each Advani-Summers total-income band", + "rationale": "Table 3 covers only individuals with a CGT liability; the remaining gainers are sub-AEA gainers, so their amounts come from the only published within-band distribution that reaches below the exempt amount, the surface the spine's gainer set was drawn from, rather than from capping the candidate's amounts, and none is invented into the liability distribution or deleted (microcosm#970).", + "resource": "advani_summers_capital_gains_distribution.json", + "resource_vintage": "2017-18 nominal pounds as published (Advani and Summers 2020, CAGE WP 465, Table A1); no uprating is applied", + "banding": "Advani-Summers total-income bands at the person's component-sum income without allowance subtraction, the banding the prior draw uses", + "range": "quantiles strictly between the band's degree-1 quantile spline zero crossing and its crossing of the build period's annual exempt amount, so every amount lies in (0, AEA]", + "mapping": "remainder gainers take the midpoints of the weight-proportional quantile strata that partition the range, evaluated on the band's spline; zero-weight persons take a nominal stratum of the band's smallest positive weight and carry no mass; no seeded draw is consumed, so the walk and plan draw order is unchanged", + "ordering": "existing gains ascending, person_id descending on ties (the allocation ranking reversed, as the within-band draws order their strata)", + "quantile_points": [ + 0.05, + 0.1, + 0.25, + 0.5, + 0.75, + 0.9, + 0.95 + ], + "spline_degree": 1, + "extrapolation": "ext=0", + "deterministic": true }, { "kind": "record_mass_conservation_receipt", @@ -3876,6 +3901,93 @@ ], "notes": "Spine stage after the amounts redraw: every liable gainer carries one main asset type and, where flagged residential, its net gain restated in capital_gains_residential_property. The residential flag is solved to the Table 8a 2024-25 totals on the individuals basis, which the hmrc.cgt.residential_property_taxpayers and hmrc.cgt.residential_property_gains targets bind; the non-residential composition follows the fenced Table 7 2023-24 prior and is diagnostic only. Carried interest (Table 9) is not modelled (microcosm#725)." }, + { + "stage": "cgt_incidence_anchor", + "survey": "Family Resources Survey 2024-25, SPI synthetic support, the HMRC Table 3 redrawn gains on the spine and Advani-Summers capital-gains incidence", + "source": "Advani and Summers (2020), Capital Gains and UK Inequality, CAGE Working Paper 465", + "grain": "household", + "artifacts": [ + { + "role": "capital_gains_incidence_and_quantiles", + "kind": "public_aggregate_reference", + "resource": "advani_summers_capital_gains_distribution.json", + "format": "json", + "runtime_sha256_required": true + }, + { + "role": "policy_parameters", + "kind": "versioned_parameter_tree", + "dependency": "policyengine-uk>=2.98.0 via microcosm-build[uk]", + "parameters": [ + "gov.hmrc.cgt.annual_exempt_amount" + ], + "instant_rule": "raw dated parameter files evaluated at 1 June of the build period's tax year", + "runtime_sha256_required": false, + "dependency_discipline": "deferred inside uk_cgt_policy_parameters; the base package never imports policyengine-uk at import time" + } + ], + "operations": [ + { + "kind": "pair_clone_households_to_originals", + "clone_flag_column": "household_is_capital_gains_clone", + "donor_flag_column": "household_is_cgt_band_donor", + "id_remapping": "id_multiplier_for_values", + "pairing": "clone household_id minus the clone stage's id multiplier, recomputed from the pre-clone rows (clone flag false and not a band donor)", + "requirement": "bijection between clone and original households; an unpaired clone or original fails the build" + }, + { + "kind": "derive_reporter_composition_from_banded_quantiles", + "resource": "advani_summers_capital_gains_distribution.json", + "income_proxy_components": [ + "employment_income", + "self_employment_income", + "state_pension_reported", + "private_pension_income", + "property_income", + "savings_interest_income", + "dividend_income", + "miscellaneous_income" + ], + "allowance_subtraction": false, + "carrier": "oldest adult; person_id ascending breaks age ties", + "adult_minimum_age": 16, + "quantile_points": [ + 0.05, + 0.1, + 0.25, + 0.5, + 0.75, + 0.9, + 0.95 + ], + "spline_degree": 1, + "extrapolation": "ext=0", + "crossings": "each band's degree-1 quantile spline inverted at zero and at the build period's annual exempt amount", + "aggregation": "crossings averaged over clone carriers weighted by household weight times the band's percent_with_gains", + "liable_mass": "weighted persons with capital_gains above the annual exempt amount on every household after the Table 3 redraw, clones and band donors alike", + "targets": "sub_exempt = liable_mass * (exempt_quantile - zero_quantile) / (1 - exempt_quantile); loss = liable_mass * zero_quantile / (1 - exempt_quantile)" + }, + { + "kind": "transfer_clone_mass_to_originals", + "population": "clone households paired to their originals; band donors and originals are outside the anchor, so the groups, targets and masses are clone-side quantities, not population counts", + "sub_exempt_group": "clone households whose carrier's capital_gains lie in (0, annual exempt amount]; a carrier with exactly zero gains is no reporter and is left untouched", + "loss_group": "clone households whose carrier's capital_gains are negative", + "liable_clones": "untouched", + "donors": "untouched", + "factor": "min(maximum_factor, scale * percent_with_gains) with scale solved per group by bisection so the group's mass equals its target; a group at or below its target is untouched", + "maximum_factor": 1.0, + "transfer": "each clone's removed mass is added to its paired original, so every pair's mass is conserved to rounding; no global exact-total correction is applied, so no household outside a trimmed pair moves", + "weight_kind_out": "importance", + "conservation": "household_total_to_rounding", + "pair_conservation": "to_rounding", + "declared_factor": 1.0, + "reason": "Capital-gains incidence anchor moves the mass of non-liable clone households back to their paired originals until the sub-exempt and loss-making clone mass match the Advani-Summers reporter composition at the redrawn liable mass; every pair's mass and the total household mass are conserved." + } + ], + "outputs": [], + "rewrites": [], + "notes": "Spine-only weight anchor after the amounts and asset-type stages (microcosm#970). The equal-mass clone leaves half of all household mass on gainer households, so after the Table 3 redraw the clones below the exempt amount carry millions of weighted persons that HMRC publishes no count for; the anchor derives their level from the redrawn liable mass and the Advani-Summers reporter composition and moves the excess back to each clone's paired original, which is identical in every cell but the gains. No cell is written: amounts, flags, liable clones, zero-gain clone carriers and band donors are untouched; the groups, targets and receipt are clone-side quantities (band donors and originals are outside the anchor), every pair's mass and the household total are conserved, and the receipt is gated at stage time." + }, { "stage": "salary_sacrifice", "survey": "Family Resources Survey 2024-25 salary-sacrifice respondents and HMRC salary-sacrifice reform analysis", diff --git a/packages/microcosm-build/src/microcosm/build/uk/spec/sources.yaml b/packages/microcosm-build/src/microcosm/build/uk/spec/sources.yaml index 4b3e655fbf..3d6b0c9ae5 100644 --- a/packages/microcosm-build/src/microcosm/build/uk/spec/sources.yaml +++ b/packages/microcosm-build/src/microcosm/build/uk/spec/sources.yaml @@ -2777,6 +2777,11 @@ stages: resource: hmrc_cgt_conditioning_facts.json format: json runtime_sha256_required: true + - role: capital_gains_within_band_distribution + kind: public_aggregate_reference + resource: advani_summers_capital_gains_distribution.json + format: json + runtime_sha256_required: true operations: - kind: verify_vendored_fact_resource artifact_role: cgt_conditioning_facts @@ -2873,8 +2878,25 @@ stages: deterministic: true cell_means: ratios of the published Table 3 2024-25 cell gains to cell taxpayers, both rounded by the publisher (counts to the nearest thousand, amounts to the nearest million), so derived rather than published numbers; suppressed-count cells take the band-total mean; every mean is repaired into its band - kind: sub_aea_remainder - policy: gainers beyond the published taxpayer mass keep their existing amounts capped at the annual exempt amount - rationale: Table 3 covers only individuals with a CGT liability; remaining gainers are treated as sub-AEA gainers rather than invented into the liability distribution or deleted. + policy: gainers beyond the published taxpayer mass take amounts from the Advani-Summers within-band gains distribution restricted to (0, annual exempt amount], rank-preserving within each Advani-Summers total-income band + rationale: Table 3 covers only individuals with a CGT liability; the remaining gainers are sub-AEA gainers, so their amounts come from the only published within-band distribution that reaches below the exempt amount, the surface the spine's gainer set was drawn from, rather than from capping the candidate's amounts, and none is invented into the liability distribution or deleted (microcosm#970). + resource: advani_summers_capital_gains_distribution.json + resource_vintage: 2017-18 nominal pounds as published (Advani and Summers 2020, CAGE WP 465, Table A1); no uprating is applied + banding: Advani-Summers total-income bands at the person's component-sum income without allowance subtraction, the banding the prior draw uses + range: quantiles strictly between the band's degree-1 quantile spline zero crossing and its crossing of the build period's annual exempt amount, so every amount lies in (0, AEA] + mapping: remainder gainers take the midpoints of the weight-proportional quantile strata that partition the range, evaluated on the band's spline; zero-weight persons take a nominal stratum of the band's smallest positive weight and carry no mass; no seeded draw is consumed, so the walk and plan draw order is unchanged + ordering: existing gains ascending, person_id descending on ties (the allocation ranking reversed, as the within-band draws order their strata) + quantile_points: + - 0.05 + - 0.1 + - 0.25 + - 0.5 + - 0.75 + - 0.9 + - 0.95 + spline_degree: 1 + extrapolation: ext=0 + deterministic: true - kind: record_mass_conservation_receipt entity: household reason: 'Amounts-only capital gains redraw on the source spine: household weights pass through unchanged and total household mass is conserved.' @@ -2991,6 +3013,76 @@ stages: nonnegative_outputs: - capital_gains_residential_property notes: 'Spine stage after the amounts redraw: every liable gainer carries one main asset type and, where flagged residential, its net gain restated in capital_gains_residential_property. The residential flag is solved to the Table 8a 2024-25 totals on the individuals basis, which the hmrc.cgt.residential_property_taxpayers and hmrc.cgt.residential_property_gains targets bind; the non-residential composition follows the fenced Table 7 2023-24 prior and is diagnostic only. Carried interest (Table 9) is not modelled (microcosm#725).' +- stage: cgt_incidence_anchor + survey: Family Resources Survey 2024-25, SPI synthetic support, the HMRC Table 3 redrawn gains on the spine and Advani-Summers capital-gains incidence + source: Advani and Summers (2020), Capital Gains and UK Inequality, CAGE Working Paper 465 + grain: household + artifacts: + - role: capital_gains_incidence_and_quantiles + kind: public_aggregate_reference + resource: advani_summers_capital_gains_distribution.json + format: json + runtime_sha256_required: true + - role: policy_parameters + kind: versioned_parameter_tree + dependency: policyengine-uk>=2.98.0 via microcosm-build[uk] + parameters: + - gov.hmrc.cgt.annual_exempt_amount + instant_rule: raw dated parameter files evaluated at 1 June of the build period's tax year + runtime_sha256_required: false + dependency_discipline: deferred inside uk_cgt_policy_parameters; the base package never imports policyengine-uk at import time + operations: + - kind: pair_clone_households_to_originals + clone_flag_column: household_is_capital_gains_clone + donor_flag_column: household_is_cgt_band_donor + id_remapping: id_multiplier_for_values + pairing: clone household_id minus the clone stage's id multiplier, recomputed from the pre-clone rows (clone flag false and not a band donor) + requirement: bijection between clone and original households; an unpaired clone or original fails the build + - kind: derive_reporter_composition_from_banded_quantiles + resource: advani_summers_capital_gains_distribution.json + income_proxy_components: + - employment_income + - self_employment_income + - state_pension_reported + - private_pension_income + - property_income + - savings_interest_income + - dividend_income + - miscellaneous_income + allowance_subtraction: false + carrier: oldest adult; person_id ascending breaks age ties + adult_minimum_age: 16 + quantile_points: + - 0.05 + - 0.1 + - 0.25 + - 0.5 + - 0.75 + - 0.9 + - 0.95 + spline_degree: 1 + extrapolation: ext=0 + crossings: each band's degree-1 quantile spline inverted at zero and at the build period's annual exempt amount + aggregation: crossings averaged over clone carriers weighted by household weight times the band's percent_with_gains + liable_mass: weighted persons with capital_gains above the annual exempt amount on every household after the Table 3 redraw, clones and band donors alike + targets: sub_exempt = liable_mass * (exempt_quantile - zero_quantile) / (1 - exempt_quantile); loss = liable_mass * zero_quantile / (1 - exempt_quantile) + - kind: transfer_clone_mass_to_originals + population: clone households paired to their originals; band donors and originals are outside the anchor, so the groups, targets and masses are clone-side quantities, not population counts + sub_exempt_group: clone households whose carrier's capital_gains lie in (0, annual exempt amount]; a carrier with exactly zero gains is no reporter and is left untouched + loss_group: clone households whose carrier's capital_gains are negative + liable_clones: untouched + donors: untouched + factor: min(maximum_factor, scale * percent_with_gains) with scale solved per group by bisection so the group's mass equals its target; a group at or below its target is untouched + maximum_factor: 1.0 + transfer: each clone's removed mass is added to its paired original, so every pair's mass is conserved to rounding; no global exact-total correction is applied, so no household outside a trimmed pair moves + weight_kind_out: importance + conservation: household_total_to_rounding + pair_conservation: to_rounding + declared_factor: 1.0 + reason: Capital-gains incidence anchor moves the mass of non-liable clone households back to their paired originals until the sub-exempt and loss-making clone mass match the Advani-Summers reporter composition at the redrawn liable mass; every pair's mass and the total household mass are conserved. + outputs: [] + rewrites: [] + notes: 'Spine-only weight anchor after the amounts and asset-type stages (microcosm#970). The equal-mass clone leaves half of all household mass on gainer households, so after the Table 3 redraw the clones below the exempt amount carry millions of weighted persons that HMRC publishes no count for; the anchor derives their level from the redrawn liable mass and the Advani-Summers reporter composition and moves the excess back to each clone''s paired original, which is identical in every cell but the gains. No cell is written: amounts, flags, liable clones, zero-gain clone carriers and band donors are untouched; the groups, targets and receipt are clone-side quantities (band donors and originals are outside the anchor), every pair''s mass and the household total are conserved, and the receipt is gated at stage time.' - stage: salary_sacrifice survey: Family Resources Survey 2024-25 salary-sacrifice respondents and HMRC salary-sacrifice reform analysis source: HMRC, Salary sacrifice reform for pension contributions effective from 6 April 2029 diff --git a/packages/microcosm-build/src/microcosm/build/uk_runtime/advani_summers.py b/packages/microcosm-build/src/microcosm/build/uk_runtime/advani_summers.py new file mode 100644 index 0000000000..63ce1d21da --- /dev/null +++ b/packages/microcosm-build/src/microcosm/build/uk_runtime/advani_summers.py @@ -0,0 +1,238 @@ +"""The Advani and Summers capital-gains surface, shared by the UK CGT stages. + +Advani and Summers (2020, CAGE Working Paper 465, Table A1) publish, for +sixty-one total-income bands, the share of individuals reporting any gain +and the 5th to 95th percentiles of gains among those who do. The surface +comes from all individuals who reported taxable gains on the SA108 form for +tax year 2017-18, net of in-year losses and excluding trusts; the amounts +are nominal pounds of that year and are used here as published, without +uprating (microcosm#970). + +Three stages read it: the incidence clone draws each carrier's prior gain +from the band's quantile function, the band-donor stage samples donors by +the band's incidence, and the Table 3 amounts stage places the sub-exempt +remainder on the quantiles between the band's zero crossing and its +crossing of the annual exempt amount. This module is the one register of +the resource, its validation, the band lookup and the degree-1 quantile +function so the three stages cannot drift apart. It imports nothing from +the stage modules, which is what lets the amounts stage use it without a +cycle. +""" + +from __future__ import annotations + +import hashlib +import json +from collections.abc import Mapping, Sequence +from importlib.resources import files +from typing import Any + +import numpy as np +from scipy.interpolate import UnivariateSpline + +__all__ = [ + "ADVANI_SUMMERS_RESOURCE", + "ADVANI_SUMMERS_VINTAGE", + "CGT_PRIOR_PERCENTILE_COLUMNS", + "CGT_QUANTILE_POINTS", + "advani_summers_band_index", + "advani_summers_knots", + "advani_summers_quantile_function", + "advani_summers_resource_sha256", + "advani_summers_rows", + "draw_banded_priors", + "exempt_range_quantiles", + "load_advani_summers_distribution", + "quantile_for_amount", + "stratified_amounts_within_range", +] + +ADVANI_SUMMERS_RESOURCE = "advani_summers_capital_gains_distribution.json" +#: Tax year of the HMRC data behind CAGE WP 465 Table A1. Amounts are nominal +#: pounds of that year and are used as published; no uprating is applied. +ADVANI_SUMMERS_VINTAGE = "2017-18" +CGT_QUANTILE_POINTS = (0.05, 0.1, 0.25, 0.5, 0.75, 0.9, 0.95) +CGT_PRIOR_PERCENTILE_COLUMNS = ("p05", "p10", "p25", "p50", "p75", "p90", "p95") + + +def load_advani_summers_distribution() -> Mapping[str, Any]: + """Load the committed Advani-Summers incidence and quantile surface.""" + + return json.loads( + files("microcosm.build.uk") + .joinpath(ADVANI_SUMMERS_RESOURCE) + .read_text(encoding="utf-8") + ) + + +def advani_summers_resource_sha256() -> str: + """Digest of the committed resource bytes, for receipts.""" + + return hashlib.sha256( + files("microcosm.build.uk").joinpath(ADVANI_SUMMERS_RESOURCE).read_bytes() + ).hexdigest() + + +def advani_summers_rows(resource: Mapping[str, Any]) -> list[Mapping[str, Any]]: + """The validated band rows of the surface, in ascending income order.""" + + rows = resource.get("rows") + if not isinstance(rows, list) or not rows: + raise ValueError("Advani-Summers resource must contain a non-empty rows list.") + minimums = [float(row["minimum_total_income"]) for row in rows] + if minimums != sorted(minimums) or minimums[0] != 0.0: + raise ValueError("Advani-Summers income bands must be sorted and start at 0.") + # Fail closed on non-monotone quantile rows: the prior draw interpolates + # and extrapolates these values as a quantile function, so a malformed + # row (the class the corrected percentile-69 dropped digit belonged to) + # would silently fabricate loss-makers instead of failing the build. + for row in rows: + knots = [float(row[column]) for column in CGT_PRIOR_PERCENTILE_COLUMNS] + if any(late < early for early, late in zip(knots, knots[1:], strict=False)): + raise ValueError( + "Advani-Summers quantile columns must be non-decreasing; " + f"row with minimum_total_income {row['minimum_total_income']!r} " + "is not a valid quantile function." + ) + return rows + + +def advani_summers_band_index( + rows: Sequence[Mapping[str, Any]], income: np.ndarray +) -> np.ndarray: + """Row index of each income's total-income band (last minimum not above it).""" + + minimums = np.asarray([row["minimum_total_income"] for row in rows], dtype=float) + return np.clip( + np.searchsorted(minimums, np.asarray(income, dtype=float), side="right") - 1, + 0, + len(rows) - 1, + ) + + +def advani_summers_knots(row: Mapping[str, Any]) -> np.ndarray: + """The row's published percentiles as the knots of its quantile function.""" + + return np.asarray( + [row[column] for column in CGT_PRIOR_PERCENTILE_COLUMNS], dtype=float + ) + + +def advani_summers_quantile_function(knots: np.ndarray) -> UnivariateSpline: + """The degree-1 interpolating spline through the published percentiles. + + ``ext=0`` extrapolates linearly below the 5th and above the 95th + percentile, which is how the incidence clone's prior draw has always + read the surface. + """ + + return UnivariateSpline(CGT_QUANTILE_POINTS, knots, k=1, s=0, ext=0) + + +def quantile_for_amount(knots: np.ndarray, amount: float) -> float: + """Invert the degree-1 quantile spline at ``amount``. + + Linear between knots and linear on the end segments beyond them, which + is what ``UnivariateSpline(k=1, s=0, ext=0)`` evaluates, so + ``spline(quantile_for_amount(knots, a)) == a`` to rounding. The segment + holding ``amount`` must rise strictly: a flat one has no unique + crossing, and the caller must not pretend it does. + """ + + points = np.asarray(CGT_QUANTILE_POINTS, dtype=float) + knots = np.asarray(knots, dtype=float) + if knots.shape != points.shape: + raise ValueError( + "Quantile knots must align with the published quantile points." + ) + if amount <= knots[0]: + segment = 0 + elif amount >= knots[-1]: + segment = len(knots) - 2 + else: + segment = int( + np.clip(np.searchsorted(knots, amount, side="right") - 1, 0, len(knots) - 2) + ) + y0, y1 = float(knots[segment]), float(knots[segment + 1]) + if not y1 > y0: + raise ValueError( + f"Quantile knots are flat on the segment holding {amount}; " + "no unique quantile." + ) + x0, x1 = float(points[segment]), float(points[segment + 1]) + return x0 + (float(amount) - y0) * (x1 - x0) / (y1 - y0) + + +def exempt_range_quantiles( + knots: np.ndarray, annual_exempt_amount: float +) -> tuple[float, float]: + """``(q0, q_aea)``: the spline's zero crossing and its crossing of the AEA.""" + + if not annual_exempt_amount > 0.0: + raise ValueError("The annual exempt amount must be positive.") + lower = quantile_for_amount(knots, 0.0) + upper = quantile_for_amount(knots, float(annual_exempt_amount)) + if not lower < upper: + raise ValueError( + "The band's quantile function does not rise between zero and the " + "annual exempt amount." + ) + return lower, upper + + +def stratified_amounts_within_range( + knots: np.ndarray, + *, + weights: np.ndarray, + lower_quantile: float, + upper_quantile: float, +) -> np.ndarray: + """Amounts at the midpoints of weight-proportional strata of the range. + + The persons arrive in the order their amounts must rise; the strata + partition ``(lower_quantile, upper_quantile)`` in proportion to the + household weights, and each person takes the spline at their stratum's + midpoint, so the weighted set reproduces the published conditional shape + on the range rather than an independent draw per person. Zero weights + take a nominal stratum of the smallest positive weight (or one when no + weight is positive): they carry no mass, and the nominal width only keeps + them strictly inside the range. + """ + + weights = np.asarray(weights, dtype=float) + if weights.size == 0: + return np.zeros(0, dtype=float) + if not np.isfinite(weights).all() or (weights < 0.0).any(): + raise ValueError("Stratum weights must be finite and non-negative.") + if not lower_quantile < upper_quantile: + raise ValueError("The stratified range must have a positive width.") + positive = weights[weights > 0.0] + nominal = float(positive.min()) if positive.size else 1.0 + stratum = np.where(weights > 0.0, weights, nominal) + cumulative = np.cumsum(stratum) + upper = cumulative / float(cumulative[-1]) + lower = np.concatenate(([0.0], upper[:-1])) + quantiles = lower_quantile + 0.5 * (lower + upper) * ( + upper_quantile - lower_quantile + ) + return np.asarray(advani_summers_quantile_function(knots)(quantiles), dtype=float) + + +def draw_banded_priors( + income: np.ndarray, + draws: np.ndarray, + *, + distribution: Mapping[str, Any], +) -> np.ndarray: + """Evaluate each person's band quantile function at their seeded draw.""" + + rows = advani_summers_rows(distribution) + indexes = advani_summers_band_index(rows, income) + values = np.zeros(len(income), dtype=float) + for index, row in enumerate(rows): + mask = indexes == index + if not mask.any(): + continue + spline = advani_summers_quantile_function(advani_summers_knots(row)) + values[mask] = spline(draws[mask]) + return values diff --git a/packages/microcosm-build/src/microcosm/build/uk_runtime/battery_bindings.py b/packages/microcosm-build/src/microcosm/build/uk_runtime/battery_bindings.py index 5cc4d9d9a5..9b006cbd8a 100644 --- a/packages/microcosm-build/src/microcosm/build/uk_runtime/battery_bindings.py +++ b/packages/microcosm-build/src/microcosm/build/uk_runtime/battery_bindings.py @@ -58,8 +58,16 @@ target_surface_gate, weights_audit_gate, ) +from microcosm.build.uk_runtime.cgt_projection import ( + UK_CGT_PROJECTION_ARTIFACT_KEY, + UKCGTProjection, +) from microcosm.build.uk_runtime.frs_take_up import uk_take_up_signal_gate from microcosm.build.uk_runtime.geography_ladder import uk_geography_ladder_gate +from microcosm.build.uk_runtime.hmrc_capital_gains import ( + HMRC_CGT_CONDITIONING_RESOURCE, + load_hmrc_cgt_conditioning_facts, +) from microcosm.build.uk_runtime.ledger_targets import ( LOCAL_REGISTRY_PARITY_FIXTURE_RESOURCE, align_uk_local_registry_parity_fixture, @@ -81,6 +89,7 @@ UKZeroWeightStratumDeclaration, _household_weights, _missing_fit_weight_evidence_gate, + uk_cgt_projection_entrants_gate, uk_default_degenerate_reviewed_exclusions, uk_default_target_fit_reviewed_exclusions, uk_degenerate_release_surface_gate, @@ -594,6 +603,144 @@ def _evaluate_aggregate_admin( ) +_CGT_PROJECTION_PARAMETER_KEYS = frozenset( + { + "horizon_year", + "gains_growth_parameter", + "exempt_amount_parameter", + "expected_yoy_growth_by_year", + "expected_exempt_amount_by_year", + "maximum_growth_drift", + "bound_resource", + "bound_size_band_lower_bound", + } +) + + +def _evaluate_cgt_projection_entrants( + context: EvidenceContext, parameters: Mapping[str, Any] +) -> GateResult: + """Fence projected sub-exempt entrants against the vendored HMRC band. + + The seam supplies the projection artifact; the binding refuses one that + does not match the declared base year (the frame's period), horizon and + parameter paths, and takes the bound from the vendored conditioning + facts the amounts stage itself is pinned to, never from a literal. + """ + + unrouted = sorted(set(parameters) - _CGT_PROJECTION_PARAMETER_KEYS) + if unrouted: + raise ValueError(f"cgt_projection_entrants cannot route {unrouted}.") + projection = context.artifacts[UK_CGT_PROJECTION_ARTIFACT_KEY] + if not isinstance(projection, UKCGTProjection): + raise TypeError( + f"{UK_CGT_PROJECTION_ARTIFACT_KEY} artifact must be a UKCGTProjection." + ) + horizon_year = parameters["horizon_year"] + if not isinstance(horizon_year, int) or isinstance(horizon_year, bool): + raise ValueError("cgt_projection_entrants horizon_year must be an integer.") + lower_bound = parameters["bound_size_band_lower_bound"] + if not isinstance(lower_bound, int) or isinstance(lower_bound, bool): + raise ValueError( + "cgt_projection_entrants bound_size_band_lower_bound must be an integer." + ) + bound_resource = str(parameters["bound_resource"]) + if bound_resource != HMRC_CGT_CONDITIONING_RESOURCE: + raise ValueError( + f"cgt_projection_entrants bound_resource {bound_resource!r} is not the " + f"vendored conditioning resource {HMRC_CGT_CONDITIONING_RESOURCE!r}." + ) + surface = _uk_gate_surface(context.frame) + base_year = int(surface.time_period) + declared = { + "base_year": base_year, + "horizon_year": horizon_year, + "growth_parameter": str(parameters["gains_growth_parameter"]), + "exempt_amount_parameter": str(parameters["exempt_amount_parameter"]), + } + actual = { + "base_year": projection.base_year, + "horizon_year": projection.horizon_year, + "growth_parameter": projection.growth_parameter, + "exempt_amount_parameter": projection.exempt_amount_parameter, + } + if actual != declared: + raise ValueError( + "cgt_projection_entrants artifact disagrees with the declared " + f"projection: declared {declared}, supplied {actual}." + ) + # The engine's growth path and exempt amount are pinned in the manifest: + # an engine bump that moves either fails the gate visibly instead of + # moving the verdict silently, and re-pinning is a reviewed change. + expected_growth = parameters["expected_yoy_growth_by_year"] + if not isinstance(expected_growth, Mapping): + raise ValueError( + "cgt_projection_entrants expected_yoy_growth_by_year must map years " + "to rates." + ) + drift_tolerance = float(parameters["maximum_growth_drift"]) + expected_exempt = parameters["expected_exempt_amount_by_year"] + if not isinstance(expected_exempt, Mapping): + raise ValueError( + "cgt_projection_entrants expected_exempt_amount_by_year must map years " + "to amounts." + ) + drifts: list[str] = [] + for year in projection.projected_years: + pinned = expected_growth.get(str(year)) + if pinned is None: + drifts.append(f"no pinned growth rate for {year}") + continue + actual_rate = float(projection.yoy_growth_by_year[str(year)]) + if abs(actual_rate - float(pinned)) > drift_tolerance: + drifts.append(f"growth {year}: engine {actual_rate} vs pinned {pinned}") + for year, amount in projection.exempt_amount_by_year.items(): + pinned_amount = expected_exempt.get(str(year)) + if pinned_amount is None: + drifts.append(f"no pinned exempt amount for {year}") + elif abs(float(amount) - float(pinned_amount)) > 0.5: + drifts.append( + f"exempt amount {year}: engine {amount} vs pinned {pinned_amount}" + ) + if drifts: + raise ValueError( + "cgt_projection_entrants projection drifted from the pinned path " + f"({projection.engine}): " + "; ".join(drifts) + "." + ) + facts = load_hmrc_cgt_conditioning_facts(bound_resource) + band = facts.size_band(lower_bound) + bound_source = ( + f"{bound_resource}: HMRC Table 2.1a tax year {facts.tax_year}, taxpayers " + f"with gains from {band.lower_bound:,} to {band.upper_bound:,}" + ) + household = surface.household + household_weights = _household_weights(household) + positions = pd.Series( + np.arange(len(household)), index=household["household_id"].to_numpy() + ).reindex(surface.person["person_household_id"].to_numpy()) + if positions.isna().any(): + raise ValueError("cgt_projection_entrants found a person without a household.") + person_weights = household_weights[positions.to_numpy(dtype=np.int64)] + return uk_cgt_projection_entrants_gate( + surface.person, + person_weights, + projection, + bound=float(band.taxpayers), + bound_source=bound_source, + ) + + +def _cgt_projection_evidence( + context: EvidenceContext, parameters: Mapping[str, Any] +) -> object: + projection = context.artifacts[UK_CGT_PROJECTION_ARTIFACT_KEY] + if not isinstance(projection, UKCGTProjection): + raise TypeError( + f"{UK_CGT_PROJECTION_ARTIFACT_KEY} artifact must be a UKCGTProjection." + ) + return {UK_CGT_PROJECTION_ARTIFACT_KEY: projection.payload()} + + def _stage_names_evidence( context: EvidenceContext, parameters: Mapping[str, Any] ) -> object: @@ -1469,6 +1616,10 @@ def _ledger_compile_parity_required_artifacts( "maximum_solve_relative_error", "support_bounds_resource", "minimum_band_rows", + # #970 cgt_incidence_anchor stage-health check. + "maximum_relative_composition_error", + "maximum_pair_relative_error", + "minimum_pair_count", # #890 energy_rake check: NEED shape at the DESNZ level at # design weights, with the published gas-connected share. "margins", @@ -1520,6 +1671,13 @@ def _ledger_compile_parity_required_artifacts( {"support_bounds_resource", "support_bounds_resources"} ), ), + "cgt_projection_entrants": UKGateBinding( + name="cgt_projection_entrants", + evaluator=_evaluate_cgt_projection_entrants, + parameter_keys=_CGT_PROJECTION_PARAMETER_KEYS, + artifact_keys=frozenset({UK_CGT_PROJECTION_ARTIFACT_KEY}), + evidence=_cgt_projection_evidence, + ), "aggregate_admin": UKGateBinding( name="aggregate_admin", evaluator=_evaluate_aggregate_admin, diff --git a/packages/microcosm-build/src/microcosm/build/uk_runtime/calibration_run.py b/packages/microcosm-build/src/microcosm/build/uk_runtime/calibration_run.py index 6698adb163..2e591a690e 100644 --- a/packages/microcosm-build/src/microcosm/build/uk_runtime/calibration_run.py +++ b/packages/microcosm-build/src/microcosm/build/uk_runtime/calibration_run.py @@ -47,6 +47,13 @@ from microcosm.build.staging_v2 import validate_staging_delivery from microcosm.build.target_materialization import assert_calibration_input_finite from microcosm.build.uk_runtime.battery_bindings import UK_GATE_REGISTRY +from microcosm.build.uk_runtime.cgt_projection import ( + UK_CGT_PROJECTION_ARTIFACT_KEY, + UKCGTProjection, + uk_cgt_projection, + uk_cgt_projection_from_pins, + uk_engine_installed, +) from microcosm.build.uk_runtime.diagnostics import ( uk_target_geography_levels, write_uk_calibration_diagnostics, @@ -58,6 +65,7 @@ from microcosm.build.uk_runtime.national_frame import ( load_uk_national_frame, uk_household_weight_kind, + uk_time_period, write_uk_national_frame, ) from microcosm.calibrate import TargetRegistry @@ -98,6 +106,10 @@ class UKCalibrationRunResult: UK_CALIBRATION_GATE_SCOPE = ( "uk_target_fit", + # The projection fence over sub-exempt gainers is a property of the + # calibrated frame under the engine's uprating; the seam owns it and the + # release cut inherits the seam's verdict (microcosm#970). + "uk_cgt_projection_entrants", "uk_weight_ratio", "uk_weight_ess", "uk_zero_weight_strata", @@ -128,6 +140,7 @@ class UKCalibrationRunResult: "uk_stage_cgt_band_donors_support", "uk_stage_hmrc_cgt_gains_spine_summary", "uk_stage_hmrc_cgt_asset_type_spine_summary", + "uk_stage_cgt_incidence_anchor_composition", "uk_stage_salary_sacrifice_realization", "uk_stage_student_loans_realization", "uk_stage_age_tail_targets", @@ -743,6 +756,7 @@ def _run_calibration_gate_battery( ) -> dict[str, object]: manifest = _calibration_gate_manifest() admin_totals, admin_receipt = uk_aggregate_admin_totals(frame, manifest) + projection = uk_cgt_projection_artifact(frame, manifest) artifacts = { "national_calibration": stage.manifest, "parity_evidence": SimpleNamespace( @@ -752,6 +766,7 @@ def _run_calibration_gate_battery( } ), "aggregate_admin": admin_totals, + UK_CGT_PROJECTION_ARTIFACT_KEY: projection, # The target-fit deferral register is evaluated against the run # clock (schema-2 approval windows); the seam supplies today's date # exactly as the rowwise candidate build supplies its start date. @@ -961,6 +976,61 @@ def uk_scoped_gate_manifest( } +def uk_cgt_projection_artifact( + frame: Frame, manifest: GatesManifest +) -> UKCGTProjection: + """Read the projection the declared entrants fence needs, fail-loud. + + The declared entry names the horizon and the two parameter paths; the + base year is the calibrated frame's period. Reading from the installed + engine keeps the receipt reproducible from the parameter tree alone; + without an engine (a data-only build, the secrets-free fast lane) the + entry's pinned path is used and the receipt names that source. Either + way the binding drift-checks the projection against the pins; the pins + label satisfies the gate but the release certifier refuses a seam part + that carries it, so an engine-free seam evaluates the fence and never + certifies a cut. Availability is decided by ``find_spec``: an engine that + is installed but fails to import raises here instead of being mislabelled + unavailable. + """ + + for entry in manifest.gates: + if entry.id == "uk_cgt_projection_entrants": + parameters = entry.parameters + break + else: + raise ValueError( + "The calibration seam declares no uk_cgt_projection_entrants entry; " + "refusing to fabricate a projection." + ) + base_year = int(uk_time_period(frame)) + horizon_year = int(parameters["horizon_year"]) + growth_parameter = str(parameters["gains_growth_parameter"]) + exempt_amount_parameter = str(parameters["exempt_amount_parameter"]) + if not uk_engine_installed(): + # No engine in this environment (a data-only build, the secrets-free + # fast lane): the pins are the reviewed statement of the engine's + # path and the receipt names that source. The release certifier + # refuses a seam part whose fence carries that label, so this branch + # evaluates the gate but can never certify a cut. + return uk_cgt_projection_from_pins( + base_year, + horizon_year, + growth_by_year=parameters["expected_yoy_growth_by_year"], + exempt_amount_by_year=parameters["expected_exempt_amount_by_year"], + growth_parameter=growth_parameter, + exempt_amount_parameter=exempt_amount_parameter, + ) + # An installed engine is read; one that is installed but fails to import + # raises here rather than being mislabelled unavailable. + return uk_cgt_projection( + base_year, + horizon_year, + growth_parameter=growth_parameter, + exempt_amount_parameter=exempt_amount_parameter, + ) + + def uk_aggregate_admin_totals( frame: Frame, manifest: GatesManifest ) -> tuple[dict[str, float], list[dict[str, object]]]: diff --git a/packages/microcosm-build/src/microcosm/build/uk_runtime/cgt_imputation.py b/packages/microcosm-build/src/microcosm/build/uk_runtime/cgt_imputation.py index 6694c7b929..bb607d131c 100644 --- a/packages/microcosm-build/src/microcosm/build/uk_runtime/cgt_imputation.py +++ b/packages/microcosm-build/src/microcosm/build/uk_runtime/cgt_imputation.py @@ -45,11 +45,22 @@ allocation reconciles to published numbers by construction. The bottom band's support is floored at the annual exempt amount, since every allocated person is a taxpayer with a liability. -4. **Gainers beyond the published taxpayer mass keep their existing amounts, - capped at the annual exempt amount.** Table 3 covers only individuals - with a CGT liability, so the candidate's remaining gainers are treated as - sub-AEA gainers rather than being invented into the liability - distribution or deleted. +4. **Gainers beyond the published taxpayer mass take Advani and Summers + within-band amounts restricted to (0, AEA].** Table 3 covers only + individuals with a CGT liability, so the candidate's remaining gainers + are sub-AEA gainers. Their amounts come from the Advani and Summers + (2020) quantile surface of gains by total-income band, the surface the + spine's gainer set was drawn from, restricted to the quantiles between + the band spline's zero crossing and its crossing of the build period's + annual exempt amount and placed rank-preservingly within each band, + rather than from a cap on the candidate's amounts (microcosm#970). A cap + left every such gainer at exactly the exempt amount, which the engine's + uprating then carried past the frozen threshold in the first projected + year. The table is used as published, 2017-18 nominal pounds without + uprating: between the 10th and 25th percentiles the spline is one + straight segment, so the shape on (0, AEA] is close to uniform whatever + the scale. Nothing is invented into the liability distribution or + deleted. 5. **The joint is the published 2024-25 surface, not an aged one.** The 2026 release publishes Table 3 for 2024-25, the tax year of the Table 1 observations the calibration fits, and its rows are vendored verbatim @@ -88,6 +99,18 @@ import pandas as pd from microcosm.build.source_manifest import SourceStageSpec +from microcosm.build.uk_runtime.advani_summers import ( + ADVANI_SUMMERS_RESOURCE, + ADVANI_SUMMERS_VINTAGE, + CGT_QUANTILE_POINTS, + advani_summers_band_index, + advani_summers_knots, + advani_summers_resource_sha256, + advani_summers_rows, + exempt_range_quantiles, + load_advani_summers_distribution, + stratified_amounts_within_range, +) from microcosm.build.uk_runtime.hmrc_capital_gains import ( HMRC_CGT_BUILD_PERIOD, HMRC_CGT_CONDITIONING_RECORD_SETS, @@ -123,11 +146,13 @@ "UK_CGT_RAKE_TOLERANCE", "UK_CGT_REGION_GROUP_LABELS", "UK_CGT_REGION_GROUPS", + "UK_CGT_REMAINDER_POLICY", "UKCGTAllocationReport", "impute_uk_capital_gains", "impute_uk_capital_gains_with_report", "summarize_uk_cgt_imputation", "uk_cgt_spine_stage_transform", + "uk_cgt_component_sum_income", "uk_cgt_policy_parameters", "uk_cgt_taxable_income_proxy", ] @@ -141,6 +166,11 @@ #: periods draw differently while each build is reproducible. UK_CGT_IMPUTATION_SEED = 552 +#: How the sub-AEA remainder takes its amounts (microcosm#970): the +#: Advani-Summers within-band quantile function restricted to the exempt +#: range, rank-preserving within each total-income band, no seeded draw. +UK_CGT_REMAINDER_POLICY = "advani_summers_within_band_exempt_range" + #: The reviewed mass-conservation receipt the spine stage records. The #: terminal family gate requires a valid mass-conserving MassChangeRecord #: carrying exactly this reason, so a build whose CGT stage silently moved @@ -252,7 +282,7 @@ @dataclass(frozen=True) class UKCGTPolicyParameters: - """Policy amounts the proxy and the sub-AEA cap depend on. + """Policy amounts the proxy and the sub-AEA remainder range depend on. Read from the policyengine-uk parameter tree at a stated instant by :func:`uk_cgt_policy_parameters`, or constructed directly in tests. @@ -306,10 +336,13 @@ def uk_cgt_policy_parameters(build_period: int | str) -> UKCGTPolicyParameters: ) -def uk_cgt_taxable_income_proxy( - person: pd.DataFrame, parameters: UKCGTPolicyParameters -) -> np.ndarray: - """Approximate taxable income after reliefs and the Personal Allowance.""" +def uk_cgt_component_sum_income(person: pd.DataFrame) -> np.ndarray: + """Sum the persisted total_income components before any allowance. + + The Advani-Summers surface conditions on total income, so the prior + draw and the sub-AEA remainder band persons on this sum; the Table 3 + proxy subtracts the tapered Personal Allowance from it. + """ missing = [ column for column in UK_CGT_TAXABLE_INCOME_PROXY_COMPONENTS @@ -322,6 +355,14 @@ def uk_cgt_taxable_income_proxy( total = np.zeros(len(person)) for column in UK_CGT_TAXABLE_INCOME_PROXY_COMPONENTS: total += pd.to_numeric(person[column], errors="raise").to_numpy(dtype=float) + return total + + +def uk_cgt_taxable_income_proxy( + person: pd.DataFrame, parameters: UKCGTPolicyParameters +) -> np.ndarray: + """Approximate taxable income after reliefs and the Personal Allowance.""" + total = uk_cgt_component_sum_income(person) taper = parameters.personal_allowance_taper_rate * np.maximum( 0.0, total - parameters.personal_allowance_taper_threshold @@ -561,9 +602,11 @@ class UKCGTAllocationReport: fallback_share_by_band: Mapping[int, float] conditioning: Mapping[str, object] rounding_carry_out: Mapping[str, Mapping[str, float]] = field(default_factory=dict) + remainder: Mapping[str, object] = field(default_factory=dict) def evidence(self) -> dict[str, object]: return { + "remainder": dict(self.remainder), "band_rows": [dict(row) for row in self.band_rows], "joint_rows": [dict(row) for row in self.joint_rows], "rake": dict(self.rake), @@ -1203,6 +1246,77 @@ def _ranked( return indices[order] +def _map_remainder_amounts( + *, + remainder: np.ndarray, + existing: np.ndarray, + person_id: np.ndarray, + person_weight: np.ndarray, + band_index: np.ndarray, + rows: Sequence[Mapping[str, object]], + annual_exempt_amount: float, + new_gains: np.ndarray, +) -> dict[str, object]: + """Place the sub-AEA remainder on the A&S within-band surface (microcosm#970). + + ``remainder`` holds the row positions of the gainers the allocation left + unassigned. Within each Advani-Summers total-income band they take, in + ascending existing-gain order (the allocation ranking reversed, as the + plan draws order their strata), the midpoints of the weight-proportional + quantile strata of ``(q0, q_aea)``, the band spline's zero crossing and + its crossing of the annual exempt amount, so the band's weighted + remainder reproduces the published conditional shape on (0, AEA]. The + mapping is a monotone transform of the existing gains inside each band, + consumes no seeded draw, and fails closed if any amount leaves the + range. Returns the receipt for the allocation report. + """ + + band_rows: list[dict[str, object]] = [] + for band in np.unique(band_index[remainder]): + members = remainder[band_index[remainder] == band] + ordered = _ranked(members, person_id=person_id, existing=existing)[::-1] + knots = advani_summers_knots(rows[int(band)]) + lower_quantile, upper_quantile = exempt_range_quantiles( + knots, annual_exempt_amount + ) + amounts = stratified_amounts_within_range( + knots, + weights=person_weight[ordered], + lower_quantile=lower_quantile, + upper_quantile=upper_quantile, + ) + if not ((amounts > 0.0).all() and (amounts <= annual_exempt_amount).all()): + raise ValueError( + "Sub-AEA remainder amounts left (0, annual exempt amount]." + ) + new_gains[ordered] = amounts + band_rows.append( + { + "minimum_total_income": float(rows[int(band)]["minimum_total_income"]), + "zero_quantile": lower_quantile, + "exempt_quantile": upper_quantile, + "persons": int(ordered.size), + "mass": float(person_weight[ordered].sum()), + "min_amount": float(amounts.min()), + "max_amount": float(amounts.max()), + } + ) + return { + "policy": UK_CGT_REMAINDER_POLICY, + "resource": ADVANI_SUMMERS_RESOURCE, + "resource_sha256": advani_summers_resource_sha256(), + "resource_vintage": ADVANI_SUMMERS_VINTAGE, + "income_measure": "component_sum_without_allowance", + "annual_exempt_amount": float(annual_exempt_amount), + "persons": int(remainder.size), + "mass": float(person_weight[remainder].sum()), + "min_amount": float(new_gains[remainder].min()) if remainder.size else 0.0, + "max_amount": float(new_gains[remainder].max()) if remainder.size else 0.0, + "bands_with_remainder": len(band_rows), + "band_rows": band_rows, + } + + def impute_uk_capital_gains_with_report( frame: Frame, distribution: HMRCCapitalGainsJointDistribution, @@ -1211,10 +1325,20 @@ def impute_uk_capital_gains_with_report( conditioning: HMRCCGTConditioningFacts, seed: int = UK_CGT_IMPUTATION_SEED, mass_change_reason: str = UK_CGT_SPINE_MASS_CONSERVATION_REASON, + remainder_distribution: Mapping[str, object] | None = None, ) -> tuple[Frame, UKCGTAllocationReport]: - """Redraw gainers' amounts, conditioned on income, age and region.""" + """Redraw gainers' amounts, conditioned on income, age and region. + + ``remainder_distribution`` is the Advani-Summers surface the sub-AEA + remainder maps onto; it defaults to the committed resource. + """ validate_uk_national_frame(frame) + remainder_rows = advani_summers_rows( + remainder_distribution + if remainder_distribution is not None + else load_advani_summers_distribution() + ) time_period = uk_time_period(frame) person = frame.table("person").reset_index(drop=True) if "capital_gains" not in person.columns: @@ -1401,11 +1525,21 @@ def impute_uk_capital_gains_with_report( new_gains=new_gains, ) - # Below the published taxpayer mass: sub-AEA gainers keep their - # existing amounts, capped at the annual exempt amount. - remainder = is_gainer & ~assigned - new_gains[remainder] = np.minimum( - existing[remainder], parameters.annual_exempt_amount + # Below the published taxpayer mass: sub-AEA gainers take Advani-Summers + # within-band amounts restricted to (0, AEA], rank-preserving within each + # A&S total-income band (microcosm#970). Nothing here touches rng, so the + # walk and plan draw order above is unchanged. + remainder_receipt = _map_remainder_amounts( + remainder=np.flatnonzero(is_gainer & ~assigned), + existing=existing, + person_id=person_id, + person_weight=person_weight, + band_index=advani_summers_band_index( + remainder_rows, uk_cgt_component_sum_income(person) + ), + rows=remainder_rows, + annual_exempt_amount=parameters.annual_exempt_amount, + new_gains=new_gains, ) if not np.isfinite(new_gains).all(): @@ -1448,6 +1582,7 @@ def impute_uk_capital_gains_with_report( for gi, gain_lower in enumerate(gains) } report = UKCGTAllocationReport( + remainder=remainder_receipt, band_rows=band_rows, joint_rows=tuple(joint_rows), rake=rake_report, @@ -1513,10 +1648,12 @@ def impute_uk_capital_gains( conditioning: HMRCCGTConditioningFacts | None = None, seed: int = UK_CGT_IMPUTATION_SEED, mass_change_reason: str = UK_CGT_SPINE_MASS_CONSERVATION_REASON, + remainder_distribution: Mapping[str, object] | None = None, ) -> Frame: """Redraw gainers' amounts from the published joint distribution. - ``conditioning`` defaults to the committed vendored 2024-25 resource. + ``conditioning`` defaults to the committed vendored 2024-25 resource and + ``remainder_distribution`` to the committed Advani-Summers surface. """ result, _ = impute_uk_capital_gains_with_report( @@ -1526,6 +1663,7 @@ def impute_uk_capital_gains( conditioning=conditioning or load_hmrc_cgt_conditioning_facts(), seed=seed, mass_change_reason=mass_change_reason, + remainder_distribution=remainder_distribution, ) return result @@ -1639,6 +1777,7 @@ def uk_cgt_spine_stage_transform( distribution: HMRCCapitalGainsJointDistribution | None = None, parameters: UKCGTPolicyParameters | None = None, conditioning: HMRCCGTConditioningFacts | None = None, + remainder_distribution: Mapping[str, object] | None = None, ): """Bind the spine manifest, then reuse the reviewed merged CGT runtime. @@ -1652,6 +1791,7 @@ def uk_cgt_spine_stage_transform( distribution=distribution, parameters=parameters, conditioning=conditioning, + remainder_distribution=remainder_distribution, ) @@ -1663,6 +1803,7 @@ class UKCGTSpineStageTransform: distribution: HMRCCapitalGainsJointDistribution | None = None parameters: UKCGTPolicyParameters | None = None conditioning: HMRCCGTConditioningFacts | None = None + remainder_distribution: Mapping[str, object] | None = None last_result: UKCGTImputationSummary | None = field(default=None, init=False) def __call__(self, frame: Frame) -> Frame: @@ -1683,6 +1824,7 @@ def __call__(self, frame: Frame) -> Frame: conditioning=conditioning, seed=UK_CGT_IMPUTATION_SEED, mass_change_reason=UK_CGT_SPINE_MASS_CONSERVATION_REASON, + remainder_distribution=self.remainder_distribution, ) summary = summarize_uk_cgt_imputation( frame, @@ -1928,14 +2070,52 @@ def _assert_cgt_spine_stage_parameters(stage: SourceStageSpec) -> None: }, "sub_aea_remainder": { "policy": ( - "gainers beyond the published taxpayer mass keep their " - "existing amounts capped at the annual exempt amount" + "gainers beyond the published taxpayer mass take amounts from " + "the Advani-Summers within-band gains distribution restricted " + "to (0, annual exempt amount], rank-preserving within each " + "Advani-Summers total-income band" ), "rationale": ( - "Table 3 covers only individuals with a CGT liability; " - "remaining gainers are treated as sub-AEA gainers rather " - "than invented into the liability distribution or deleted." + "Table 3 covers only individuals with a CGT liability; the " + "remaining gainers are sub-AEA gainers, so their amounts come " + "from the only published within-band distribution that reaches " + "below the exempt amount, the surface the spine's gainer set " + "was drawn from, rather than from capping the candidate's " + "amounts, and none is invented into the liability distribution " + "or deleted (microcosm#970)." + ), + "resource": ADVANI_SUMMERS_RESOURCE, + "resource_vintage": ( + "2017-18 nominal pounds as published (Advani and Summers 2020, " + "CAGE WP 465, Table A1); no uprating is applied" + ), + "banding": ( + "Advani-Summers total-income bands at the person's component-sum " + "income without allowance subtraction, the banding the prior " + "draw uses" + ), + "range": ( + "quantiles strictly between the band's degree-1 quantile spline " + "zero crossing and its crossing of the build period's annual " + "exempt amount, so every amount lies in (0, AEA]" + ), + "mapping": ( + "remainder gainers take the midpoints of the weight-proportional " + "quantile strata that partition the range, evaluated on the " + "band's spline; zero-weight persons take a nominal stratum of " + "the band's smallest positive weight and carry no mass; no " + "seeded draw is consumed, so the walk and plan draw order is " + "unchanged" ), + "ordering": ( + "existing gains ascending, person_id descending on ties (the " + "allocation ranking reversed, as the within-band draws order " + "their strata)" + ), + "quantile_points": list(CGT_QUANTILE_POINTS), + "spline_degree": 1, + "extrapolation": "ext=0", + "deterministic": True, }, "record_mass_conservation_receipt": { "entity": "household", @@ -1995,3 +2175,21 @@ def _assert_cgt_spine_stage_parameters(stage: SourceStageSpec) -> None: f"CGT spine {kind} declaration drifted from the reviewed " f"mapping on parameter(s) {drifted}." ) + # The sub-AEA remainder reads the Advani-Summers surface (microcosm#970), + # so the stage must declare it as a hash-covered artifact alongside the + # conditioning facts; an undeclared read would sit outside the manifest. + artifacts = { + str(artifact.get("role")): dict(artifact) for artifact in stage.artifacts + } + expected_artifact = { + "role": "capital_gains_within_band_distribution", + "kind": "public_aggregate_reference", + "resource": ADVANI_SUMMERS_RESOURCE, + "format": "json", + "runtime_sha256_required": True, + } + if artifacts.get("capital_gains_within_band_distribution") != expected_artifact: + raise ValueError( + "CGT spine must declare the Advani-Summers within-band distribution " + "artifact exactly as reviewed." + ) diff --git a/packages/microcosm-build/src/microcosm/build/uk_runtime/cgt_projection.py b/packages/microcosm-build/src/microcosm/build/uk_runtime/cgt_projection.py new file mode 100644 index 0000000000..1f77ae028b --- /dev/null +++ b/packages/microcosm-build/src/microcosm/build/uk_runtime/cgt_projection.py @@ -0,0 +1,290 @@ +"""Project the frame's capital gains against the frozen exempt amount (#970). + +policyengine-uk carries ``capital_gains`` forward from the build period by +the OBR per-capita GDP growth path while the annual exempt amount stays at +its legislated nominal value, so a gainer at or just below the exempt amount +in the build period becomes a CGT taxpayer in a later projected year. The +calibration seam's ``cgt_projection_entrants`` gate counts those entrants +year by year and fences them against a published band count; this module +supplies the projection the gate needs, read from the installed engine at +January-first instants (the donor-uprating precedent) and reported in full +so the receipt is reproducible from the parameter tree alone. Without an +installed engine the seam states the projection from the manifest pins and +labels the receipt accordingly; that label satisfies the gate but the release +certifier refuses it, so an engine-free seam can never certify a cut. +""" + +from __future__ import annotations + +import math +from collections.abc import Callable, Mapping +from dataclasses import dataclass +from importlib import metadata +from importlib.util import find_spec +from types import MappingProxyType + +__all__ = [ + "UK_CGT_EXEMPT_AMOUNT_PARAMETER", + "UK_CGT_GAINS_GROWTH_PARAMETER", + "UK_CGT_PROJECTION_ARTIFACT_KEY", + "UK_CGT_PROJECTION_ENGINE_LABEL_PREFIX", + "UK_CGT_PROJECTION_INSTANT_RULE", + "UK_CGT_PROJECTION_PINS_ENGINE", + "UKCGTProjection", + "uk_cgt_projection", + "uk_cgt_projection_from_pins", + "uk_cgt_projection_read_from_engine", + "uk_engine_installed", +] + +#: The engine's uprating index for ``capital_gains`` (per-capita GDP growth). +UK_CGT_GAINS_GROWTH_PARAMETER = "gov.economic_assumptions.yoy_growth.obr.per_capita.gdp" +#: The legislated annual exempt amount, flat in nominal terms from 2024. +UK_CGT_EXEMPT_AMOUNT_PARAMETER = "gov.hmrc.cgt.annual_exempt_amount" +#: The evidence-context artifact key the seam supplies and the binding reads. +UK_CGT_PROJECTION_ARTIFACT_KEY = "cgt_projection" +#: Parameters are read at 1 January of each year, as the donor uprating does. +UK_CGT_PROJECTION_INSTANT_RULE = "january_first" +#: The engine label of a projection built from the manifest pins because no +#: engine is installed; the binding still drift-checks it against the pins. +UK_CGT_PROJECTION_PINS_ENGINE = "manifest_pins (policyengine-uk unavailable)" +#: The label of a projection read from an installed policyengine-uk with a +#: resolvable version, ``policyengine-uk==``: the only source the +#: release certifier accepts for the fence. +UK_CGT_PROJECTION_ENGINE_LABEL_PREFIX = "policyengine-uk==" + +ParameterReader = Callable[[str, int], float] + + +@dataclass(frozen=True) +class UKCGTProjection: + """Growth factors and exempt amounts from the build period to a horizon. + + ``cumulative_gains_factor_by_year[y]`` multiplies build-period gains to + reach year ``y``: the product of ``1 + g`` over the years after the base + up to and including ``y``. ``exempt_amount_by_year`` covers the base year + and every projected year. Keys are years as strings so the payload is + JSON-stable. + """ + + base_year: int + horizon_year: int + growth_parameter: str + exempt_amount_parameter: str + yoy_growth_by_year: Mapping[str, float] + cumulative_gains_factor_by_year: Mapping[str, float] + exempt_amount_by_year: Mapping[str, float] + engine: str + instant_rule: str = UK_CGT_PROJECTION_INSTANT_RULE + + def __post_init__(self) -> None: + if self.horizon_year <= self.base_year: + raise ValueError("The CGT projection horizon must follow the base year.") + years = [str(year) for year in range(self.base_year + 1, self.horizon_year + 1)] + if list(self.yoy_growth_by_year) != years: + raise ValueError("yoy_growth_by_year must cover every projected year.") + if list(self.cumulative_gains_factor_by_year) != years: + raise ValueError( + "cumulative_gains_factor_by_year must cover every projected year." + ) + if list(self.exempt_amount_by_year) != [str(self.base_year), *years]: + raise ValueError( + "exempt_amount_by_year must cover the base year and every " + "projected year." + ) + for mapping, label in ( + (self.yoy_growth_by_year, "growth"), + (self.cumulative_gains_factor_by_year, "cumulative factor"), + (self.exempt_amount_by_year, "exempt amount"), + ): + for year, value in mapping.items(): + if not isinstance(value, int | float) or not math.isfinite( + float(value) + ): + raise ValueError( + f"CGT projection {label} for {year} is not finite." + ) + if any(float(value) <= -1.0 for value in self.yoy_growth_by_year.values()): + raise ValueError("CGT projection growth must exceed -100 percent.") + if any(float(value) <= 0.0 for value in self.exempt_amount_by_year.values()): + raise ValueError("CGT projection exempt amounts must be positive.") + object.__setattr__( + self, "yoy_growth_by_year", MappingProxyType(dict(self.yoy_growth_by_year)) + ) + object.__setattr__( + self, + "cumulative_gains_factor_by_year", + MappingProxyType(dict(self.cumulative_gains_factor_by_year)), + ) + object.__setattr__( + self, + "exempt_amount_by_year", + MappingProxyType(dict(self.exempt_amount_by_year)), + ) + + @property + def projected_years(self) -> tuple[int, ...]: + return tuple(range(self.base_year + 1, self.horizon_year + 1)) + + def payload(self) -> dict[str, object]: + """A JSON-stable description of the projection for receipts.""" + + return { + "base_year": self.base_year, + "horizon_year": self.horizon_year, + "growth_parameter": self.growth_parameter, + "exempt_amount_parameter": self.exempt_amount_parameter, + "instant_rule": self.instant_rule, + "engine": self.engine, + "yoy_growth_by_year": dict(self.yoy_growth_by_year), + "cumulative_gains_factor_by_year": dict( + self.cumulative_gains_factor_by_year + ), + "exempt_amount_by_year": dict(self.exempt_amount_by_year), + } + + +def _installed_engine() -> str: + try: + version = metadata.version("policyengine-uk") + except metadata.PackageNotFoundError: # pragma: no cover - engine extra absent + return "policyengine-uk (version unavailable)" + return f"{UK_CGT_PROJECTION_ENGINE_LABEL_PREFIX}{version}" + + +def uk_engine_installed() -> bool: + """Whether policyengine-uk is installed in this environment. + + Decided by ``find_spec`` and nothing else: an engine that is installed + but fails to import (a broken extension, a missing shared library) must + raise where it is read, not be mislabelled as unavailable and quietly + replaced by the manifest pins. + """ + + return find_spec("policyengine_uk") is not None + + +def uk_cgt_projection_read_from_engine(engine: object) -> bool: + """Whether ``engine`` labels a projection read from an installed engine. + + True only for ``policyengine-uk==``. The pins label, a supplied + reader's label and an engine whose distribution metadata is missing are + all refused: none of them ties the receipt to a runtime version, and the + release certifier requires that tie. + """ + + return ( + isinstance(engine, str) + and engine.startswith(UK_CGT_PROJECTION_ENGINE_LABEL_PREFIX) + and len(engine) > len(UK_CGT_PROJECTION_ENGINE_LABEL_PREFIX) + ) + + +def uk_cgt_projection( + base_year: int, + horizon_year: int, + *, + growth_parameter: str = UK_CGT_GAINS_GROWTH_PARAMETER, + exempt_amount_parameter: str = UK_CGT_EXEMPT_AMOUNT_PARAMETER, + parameter_reader: ParameterReader | None = None, + engine_label: str | None = None, +) -> UKCGTProjection: + """Read the growth path and exempt amounts from the engine (or a reader). + + Without ``parameter_reader`` the installed policyengine-uk tree is read + through :func:`microcosm.build.uk_runtime.donor_uprating.engine_parameter_reader` + at 1 January of each year, which is how the donor uprating reads the same + tree. A non-finite growth rate, a rate at or below -100 percent or a + non-positive exempt amount fails closed. + """ + + base_year = int(base_year) + horizon_year = int(horizon_year) + if horizon_year <= base_year: + raise ValueError( + f"The CGT projection horizon {horizon_year} must follow the base " + f"year {base_year}." + ) + if parameter_reader is None: + from microcosm.build.uk_runtime.donor_uprating import engine_parameter_reader + + reader = engine_parameter_reader() + engine = _installed_engine() + else: + reader = parameter_reader + engine = engine_label or "supplied_parameter_reader" + growth: dict[str, float] = {} + cumulative: dict[str, float] = {} + exempt: dict[str, float] = { + str(base_year): float(reader(exempt_amount_parameter, base_year)) + } + factor = 1.0 + for year in range(base_year + 1, horizon_year + 1): + rate = float(reader(growth_parameter, year)) + if not math.isfinite(rate) or rate <= -1.0: + raise ValueError( + f"CGT projection growth {growth_parameter!r} for {year} is " + f"unusable: {rate!r}." + ) + factor *= 1.0 + rate + growth[str(year)] = rate + cumulative[str(year)] = factor + exempt[str(year)] = float(reader(exempt_amount_parameter, year)) + for year, amount in exempt.items(): + if not math.isfinite(amount) or amount <= 0.0: + raise ValueError( + f"CGT projection exempt amount {exempt_amount_parameter!r} for " + f"{year} is unusable: {amount!r}." + ) + return UKCGTProjection( + base_year=base_year, + horizon_year=horizon_year, + growth_parameter=growth_parameter, + exempt_amount_parameter=exempt_amount_parameter, + yoy_growth_by_year=growth, + cumulative_gains_factor_by_year=cumulative, + exempt_amount_by_year=exempt, + engine=engine, + ) + + +def uk_cgt_projection_from_pins( + base_year: int, + horizon_year: int, + *, + growth_by_year: Mapping[str, float], + exempt_amount_by_year: Mapping[str, float], + growth_parameter: str = UK_CGT_GAINS_GROWTH_PARAMETER, + exempt_amount_parameter: str = UK_CGT_EXEMPT_AMOUNT_PARAMETER, +) -> UKCGTProjection: + """The projection the manifest pins describe, for an engine-free build. + + The seam reads the installed engine when there is one and drift-checks it + against the pins; without an engine (the secrets-free fast lane, a + data-only build) the pins themselves are the reviewed statement of the + engine's uprating, and the receipt names that source. A year the pins do + not cover fails closed here rather than at the gate. + """ + + def read(path: str, year: int) -> float: + table = ( + growth_by_year + if path == growth_parameter + else exempt_amount_by_year + if path == exempt_amount_parameter + else None + ) + if table is None or str(year) not in table: + raise ValueError( + f"The CGT projection pins carry no {path!r} value for {year}." + ) + return float(table[str(year)]) + + return uk_cgt_projection( + base_year, + horizon_year, + growth_parameter=growth_parameter, + exempt_amount_parameter=exempt_amount_parameter, + parameter_reader=read, + engine_label=UK_CGT_PROJECTION_PINS_ENGINE, + ) diff --git a/packages/microcosm-build/src/microcosm/build/uk_runtime/cgt_structure.py b/packages/microcosm-build/src/microcosm/build/uk_runtime/cgt_structure.py index be574dca66..c4a1105c45 100644 --- a/packages/microcosm-build/src/microcosm/build/uk_runtime/cgt_structure.py +++ b/packages/microcosm-build/src/microcosm/build/uk_runtime/cgt_structure.py @@ -1,24 +1,40 @@ -"""UK capital-gains incidence cloning and HMRC size-band support.""" +"""UK capital-gains incidence cloning, HMRC size-band support and the +post-redraw incidence anchor (microcosm#970). +""" from __future__ import annotations -import json from collections.abc import Mapping from dataclasses import dataclass, field -from importlib.resources import files from typing import Any import numpy as np import pandas as pd -from scipy.interpolate import UnivariateSpline from microcosm.build.source_manifest import SourceOperationSpec, SourceStageSpec from microcosm.build.stochastic_assignment import stable_identity_uniforms +from microcosm.build.uk_runtime.advani_summers import ( + ADVANI_SUMMERS_RESOURCE, + CGT_QUANTILE_POINTS, + advani_summers_band_index, + advani_summers_knots, + exempt_range_quantiles, + load_advani_summers_distribution, +) +from microcosm.build.uk_runtime.advani_summers import ( + advani_summers_rows as _distribution_rows, +) +from microcosm.build.uk_runtime.advani_summers import ( + draw_banded_priors as _draw_banded_priors, +) from microcosm.build.uk_runtime.cgt_imputation import ( UK_CGT_TAXABLE_INCOME_PROXY_COMPONENTS, + UKCGTPolicyParameters, + uk_cgt_policy_parameters, ) from microcosm.build.uk_runtime.hmrc_capital_gains import ( HMRC_CGT_CONDITIONING_RESOURCE, + HMRC_CGT_INCOME_BAND_LOWER_BOUNDS, load_hmrc_cgt_conditioning_facts, ) from microcosm.build.uk_runtime.national_frame import ( @@ -38,8 +54,6 @@ CGT_CLONE_MASS_SPLIT = 0.5 CGT_PRIOR_SEED = 0 CGT_PRIOR_SALT = "cgt_prior_amount" -CGT_QUANTILE_POINTS = (0.05, 0.1, 0.25, 0.5, 0.75, 0.9, 0.95) -CGT_PRIOR_PERCENTILE_COLUMNS = ("p05", "p10", "p25", "p50", "p75", "p90", "p95") CGT_ADULT_MINIMUM_AGE = 16 DONORS_PER_BAND = 30 DONOR_BAND_COUNT = 9 @@ -63,16 +77,29 @@ "Stack 30 positive-weight HMRC Table 2.1a support households per retained " "gain band; published donor mass is added explicitly." ) - - -def load_advani_summers_distribution() -> Mapping[str, Any]: - """Load the committed Advani-Summers incidence and quantile surface.""" - - return json.loads( - files("microcosm.build.uk") - .joinpath("advani_summers_capital_gains_distribution.json") - .read_text(encoding="utf-8") - ) +CGT_INCIDENCE_ANCHOR_STAGE_NAME = "cgt_incidence_anchor" +#: A clone never gains mass at the anchor: its factor is capped at one. +CGT_ANCHOR_MAXIMUM_FACTOR = 1.0 +#: The non-liable clone groups the anchor moves mass out of. +CGT_ANCHOR_GROUPS = ("sub_exempt", "loss") +#: What the anchor's groups, targets and receipt describe: the clone side of +#: the pairs. Band donors and originals are outside its reach, so the receipt +#: certifies the anchor's own arithmetic, not a population count. +CGT_ANCHOR_SCOPE = ( + "clone households paired to their originals; band donors and originals are " + "outside the anchor, so the groups, targets and masses are clone-side " + "quantities, not population counts" +) +CGT_ANCHOR_MASS_CHANGE_REASON = ( + "Capital-gains incidence anchor moves the mass of non-liable clone " + "households back to their paired originals until the sub-exempt and " + "loss-making clone mass match the Advani-Summers reporter composition at " + "the redrawn liable mass; every pair's mass and the total household mass " + "are conserved." +) +#: Bisection steps for the per-group scale; float64 brackets collapse long +#: before this, so the solve is exact to rounding and deterministic. +_ANCHOR_BISECTION_STEPS = 200 def load_hmrc_cgt_size_bands() -> Mapping[str, Any]: @@ -206,6 +233,110 @@ def checkpoint_metadata(self) -> dict[str, object]: return {"evidence": self.last_result.evidence()} +@dataclass(frozen=True) +class UKCGTIncidenceAnchorResult: + """Anchored frame and the executed-effect receipt of the incidence anchor. + + Group masses are household weights over clone households; each clone + household carries exactly one gainer, so they equal the weighted gainer + persons of the group and are commensurate with ``liable_mass``. They are + clone-side quantities: band donors and originals are outside the anchor, + so ``after`` is what the anchor realised, not the population's sub-exempt + or loss-making mass. + """ + + frame: Frame + annual_exempt_amount: float + liable_mass: float + liable_persons: int + zero_quantile: float + exempt_quantile: float + targets: Mapping[str, float] + before: Mapping[str, float] + after: Mapping[str, float] + scale: Mapping[str, float | None] + transferred_mass: float + pair_count: int + trimmed_households: int + capped_households: int + zero_gain_clone_households: int + zero_gain_clone_mass: float + max_pair_relative_error: float + original_mass: float + clone_mass: float + donor_mass: float + by_income_band: tuple[Mapping[str, object], ...] + + def evidence(self) -> dict[str, object]: + liable_share = 1.0 - self.exempt_quantile + return { + "stage": CGT_INCIDENCE_ANCHOR_STAGE_NAME, + "annual_exempt_amount": self.annual_exempt_amount, + "liable_mass": self.liable_mass, + "liable_persons": self.liable_persons, + "composition": { + "scope": CGT_ANCHOR_SCOPE, + "zero_quantile": self.zero_quantile, + "exempt_quantile": self.exempt_quantile, + "liable_share": liable_share, + "implied_reporter_mass": self.liable_mass / liable_share, + }, + "targets": dict(self.targets), + "before": dict(self.before), + "after": dict(self.after), + "scale": dict(self.scale), + "transferred_mass": self.transferred_mass, + "pair_count": self.pair_count, + "trimmed_households": self.trimmed_households, + "capped_households": self.capped_households, + "zero_gain_clone_households": self.zero_gain_clone_households, + "zero_gain_clone_mass": self.zero_gain_clone_mass, + "max_pair_relative_error": self.max_pair_relative_error, + "mass_by_clone_flag": { + "false": self.original_mass, + "true": self.clone_mass, + }, + "donor_mass": self.donor_mass, + "by_income_band": [dict(row) for row in self.by_income_band], + } + + +@dataclass(frozen=True) +class UKCGTIncidenceAnchorStageTransform: + """Whole-stage transform for the post-redraw incidence anchor. + + Weights only: no cell is written, no row is added or removed. The annual + exempt amount is read from the policyengine-uk parameter tree at the + frame's build period unless ``parameters`` is supplied. + """ + + stage: SourceStageSpec + distribution: Mapping[str, Any] | None = None + parameters: UKCGTPolicyParameters | None = None + last_result: UKCGTIncidenceAnchorResult | None = field(default=None, init=False) + + def __call__(self, frame: Frame) -> Frame: + _assert_cgt_incidence_anchor_stage_parameters(self.stage) + distribution = self.distribution or load_advani_summers_distribution() + parameters = self.parameters + if parameters is None: + parameters = uk_cgt_policy_parameters(uk_time_period(frame)) + result = anchor_cgt_incidence( + frame, distribution=distribution, parameters=parameters + ) + object.__setattr__(self, "last_result", result) + return result.frame + + @staticmethod + def output_columns() -> tuple[str, ...]: + return () + + def checkpoint_metadata(self) -> dict[str, object]: + if self.last_result is None: + raise RuntimeError("checkpoint metadata requires a completed stage run.") + return {"evidence": self.last_result.evidence()} + + def clone_cgt_incidence( frame: Frame, *, @@ -450,6 +581,354 @@ def stack_cgt_band_donors( ) +def pair_clone_households( + person: pd.DataFrame, + benunit: pd.DataFrame, + household: pd.DataFrame, +) -> tuple[np.ndarray, np.ndarray, int]: + """Positions of every clone household and of its paired original. + + The clone stage offsets ids by ``id_multiplier_for_values`` over the rows + it saw; those rows are exactly the households flagged neither clone nor + band donor (band donors copy clones and originals alike and carry their + source's clone flag, so the donor flag is checked first). Recomputing the + multiplier from them reproduces the executor's lineage rule, and the + pairing must be a bijection: an unpaired clone or original fails closed. + """ + + for column in (HOUSEHOLD_IS_CGT_CLONE, HOUSEHOLD_IS_CGT_BAND_DONOR): + if column not in household.columns: + raise ValueError(f"CGT incidence anchor requires household.{column}.") + is_clone = household[HOUSEHOLD_IS_CGT_CLONE].to_numpy(dtype=bool) + is_donor = household[HOUSEHOLD_IS_CGT_BAND_DONOR].to_numpy(dtype=bool) + original = ~is_clone & ~is_donor + clone = is_clone & ~is_donor + household_ids = ( + pd.to_numeric(household["household_id"], errors="raise") + .astype("int64") + .to_numpy() + ) + if len(np.unique(household_ids)) != len(household_ids): + raise ValueError("CGT incidence anchor requires unique household ids.") + original_ids = set(household_ids[original].tolist()) + person_original = person["person_household_id"].isin(original_ids).to_numpy() + benunit_original = ( + benunit["benunit_id"] + .isin(set(person.loc[person_original, "person_benunit_id"].tolist())) + .to_numpy() + ) + multiplier = id_multiplier_for_values( + person.loc[person_original, "person_id"], + person.loc[person_original, "person_household_id"], + person.loc[person_original, "person_benunit_id"], + benunit.loc[benunit_original, "benunit_id"], + household_ids[original], + ) + position_by_id = {int(value): index for index, value in enumerate(household_ids)} + clone_positions = np.flatnonzero(clone) + original_positions = np.empty(len(clone_positions), dtype=np.int64) + for slot, position in enumerate(clone_positions): + source = position_by_id.get(int(household_ids[position]) - multiplier) + if source is None or not original[source]: + raise ValueError( + "CGT incidence anchor found a clone household without a paired " + f"original: household_id {int(household_ids[position])}." + ) + original_positions[slot] = source + if len(np.unique(original_positions)) != len(original_positions): + raise ValueError( + "CGT incidence anchor found two clone households paired to one original." + ) + if len(clone_positions) != int(original.sum()): + raise ValueError( + "CGT incidence anchor requires exactly one clone per original " + f"household; found {len(clone_positions)} clones for " + f"{int(original.sum())} originals." + ) + return clone_positions, original_positions, multiplier + + +def reporter_composition_quantiles( + income: np.ndarray, + *, + distribution: Mapping[str, Any], + annual_exempt_amount: float, +) -> tuple[np.ndarray, np.ndarray]: + """Per-carrier ``(zero_quantile, exempt_quantile)`` of their A&S band.""" + + rows = _distribution_rows(distribution) + indexes = advani_summers_band_index(rows, income) + lower = np.empty(len(rows), dtype=float) + upper = np.empty(len(rows), dtype=float) + for index, row in enumerate(rows): + lower[index], upper[index] = exempt_range_quantiles( + advani_summers_knots(row), annual_exempt_amount + ) + return lower[indexes], upper[indexes] + + +def _solve_group_factors( + weights: np.ndarray, + propensity: np.ndarray, + target: float, +) -> tuple[float, np.ndarray]: + """The scale ``s`` with ``sum(w * min(1, s * p)) == target`` and its factors. + + The left side rises continuously from zero to the group's mass as ``s`` + grows, so for a target below that mass the bracket ``[0, 1 / min(p)]`` + holds exactly one solution; bisection to bracket collapse finds it to + rounding, and the upper end of the collapsed bracket is kept so the + achieved mass never falls short of the target by more than rounding. + """ + + lower, upper = 0.0, 1.0 / float(propensity.min()) + for _ in range(_ANCHOR_BISECTION_STEPS): + middle = 0.5 * (lower + upper) + if middle <= lower or middle >= upper: + break + achieved = float( + (weights * np.minimum(CGT_ANCHOR_MAXIMUM_FACTOR, middle * propensity)).sum() + ) + if achieved < target: + lower = middle + else: + upper = middle + scale = upper + return scale, np.minimum(CGT_ANCHOR_MAXIMUM_FACTOR, scale * propensity) + + +def anchor_cgt_incidence( + frame: Frame, + *, + distribution: Mapping[str, Any], + parameters: UKCGTPolicyParameters, +) -> UKCGTIncidenceAnchorResult: + """Move non-liable clone mass back to the paired originals (microcosm#970). + + The equal-mass clone leaves half of all household mass on gainer + households, far more than the reporter population the Table 3 redraw can + place above the exempt amount, so after the redraw the sub-exempt and + loss-making clones carry millions of weighted persons. HMRC publishes no + count for them, so their level is derived: the liable mass the redraw + realised, scaled by the Advani-Summers composition of reporters (the + share of a band's reporters below zero and below the exempt amount, + averaged over the clone carriers by weight times incidence). Each + non-liable group is scaled down to its target with a factor rising in the + band's incidence and capped at one; every unit of mass a clone loses goes + to its paired original, which is identical in every cell but the gains, + so pair mass, household mass and every non-CGT aggregate are conserved. + Liable clones and band donors are untouched. + """ + + validate_uk_national_frame(frame) + person = frame.table("person").copy().reset_index(drop=True) + benunit = frame.table("benunit").copy().reset_index(drop=True) + household = frame.table("household").copy().reset_index(drop=True) + annual_exempt_amount = float(parameters.annual_exempt_amount) + if not np.isfinite(annual_exempt_amount) or annual_exempt_amount <= 0.0: + raise ValueError( + "CGT incidence anchor requires a positive annual exempt amount." + ) + if "capital_gains" not in person.columns: + raise ValueError("CGT incidence anchor requires person.capital_gains.") + clone_positions, original_positions, _ = pair_clone_households( + person, benunit, household + ) + if len(clone_positions) == 0: + raise ValueError("CGT incidence anchor found no clone households.") + old_weights = frame.weights_for("household") + weights = np.asarray(old_weights.values, dtype=float).copy() + gains = pd.to_numeric(person["capital_gains"], errors="raise").to_numpy(dtype=float) + if not np.isfinite(gains).all(): + raise ValueError("CGT incidence anchor requires finite capital gains.") + household_ids = household["household_id"].to_numpy() + position_by_id = pd.Series(np.arange(len(household)), index=household_ids) + person_positions = position_by_id.reindex( + person["person_household_id"].to_numpy() + ).to_numpy() + if np.isnan(person_positions.astype(float)).any(): + raise ValueError("CGT incidence anchor found a person without a household.") + person_positions = person_positions.astype(np.int64) + liable = gains > annual_exempt_amount + liable_mass = float(weights[person_positions][liable].sum()) + if not liable_mass > 0.0: + raise ValueError( + "CGT incidence anchor requires positive liable mass above the annual " + "exempt amount after the redraw." + ) + + clone_ids = household_ids[clone_positions] + carriers = _oldest_adult_indices(person, household_ids=set(clone_ids.tolist())) + carrier_by_household = dict( + zip(person.loc[carriers, "person_household_id"].tolist(), carriers, strict=True) + ) + carrier_rows = np.asarray( + [carrier_by_household[household_id] for household_id in clone_ids.tolist()], + dtype=np.int64, + ) + in_clone = person["person_household_id"].isin(set(clone_ids.tolist())).to_numpy() + non_carrier = in_clone.copy() + non_carrier[carrier_rows] = False + if (gains[non_carrier] != 0.0).any(): + raise ValueError( + "CGT incidence anchor requires the clone carrier to be the only " + "person with capital gains in a clone household." + ) + carrier_gains = gains[carrier_rows] + income = _component_sum_income(person.loc[carrier_rows]) + propensity = _incidence_propensity(income, distribution=distribution) + if not np.isfinite(propensity).all() or (propensity <= 0.0).any(): + raise ValueError( + "CGT incidence anchor requires a finite, positive Advani-Summers " + "incidence for every clone carrier." + ) + zero_quantiles, exempt_quantiles = reporter_composition_quantiles( + income, distribution=distribution, annual_exempt_amount=annual_exempt_amount + ) + clone_weights = weights[clone_positions] + mix = clone_weights * propensity + if not mix.sum() > 0.0: + raise ValueError("CGT incidence anchor found no clone mass to compose.") + zero_quantile = float((mix * zero_quantiles).sum() / mix.sum()) + exempt_quantile = float((mix * exempt_quantiles).sum() / mix.sum()) + if not 0.0 <= zero_quantile < exempt_quantile < 1.0: + raise ValueError( + "CGT incidence anchor derived an unusable reporter composition: " + f"zero quantile {zero_quantile}, exempt quantile {exempt_quantile}." + ) + liable_share = 1.0 - exempt_quantile + targets = { + "sub_exempt": liable_mass * (exempt_quantile - zero_quantile) / liable_share, + "loss": liable_mass * zero_quantile / liable_share, + } + if not all(value > 0.0 for value in targets.values()): + raise ValueError("CGT incidence anchor derived a non-positive group target.") + groups = { + # A carrier with exactly zero gains is no reporter: it is neither in the + # (0, AEA] slice the sub-exempt target derives from nor a loss-maker, so + # it is left untouched and reported separately. + "sub_exempt": (carrier_gains > 0.0) & (carrier_gains <= annual_exempt_amount), + "loss": carrier_gains < 0.0, + } + liable_clone = carrier_gains > annual_exempt_amount + before = {name: float(clone_weights[mask].sum()) for name, mask in groups.items()} + before["liable"] = float(clone_weights[liable_clone].sum()) + factors = np.ones(len(clone_positions), dtype=float) + scale: dict[str, float | None] = {} + capped = 0 + for name in CGT_ANCHOR_GROUPS: + mask = groups[name] + if not mask.any() or before[name] <= targets[name]: + scale[name] = None + continue + group_scale, group_factors = _solve_group_factors( + clone_weights[mask], propensity[mask], targets[name] + ) + factors[mask] = group_factors + scale[name] = group_scale + capped += int((group_factors >= CGT_ANCHOR_MAXIMUM_FACTOR).sum()) + new_clone_weights = clone_weights * factors + delta = clone_weights - new_clone_weights + new_weights = weights.copy() + new_weights[clone_positions] = new_clone_weights + new_weights[original_positions] = weights[original_positions] + delta + # Pairs are conserved arithmetically (three roundings per pair, so a few + # ulps); no global exact-total correction is applied, because it would move + # the summed rounding of every transfer onto one household, breaking that + # household's pair (1.6e-13 relative on the licensed spine) and possibly + # touching a liable clone or a donor. The total is recorded as realised. + values = new_weights + if (values < 0.0).any() or not np.array_equal(values > 0.0, weights > 0.0): + raise ValueError( + "CGT incidence anchor must keep every household weight non-negative " + "and leave the zero-weight pattern unchanged." + ) + pair_before = weights[clone_positions] + weights[original_positions] + pair_after = values[clone_positions] + values[original_positions] + pair_gap = np.abs(pair_after - pair_before) + positive = pair_before > 0.0 + pair_error = np.where( + positive, pair_gap / np.where(positive, pair_before, 1.0), pair_gap + ) + after = { + name: float(values[clone_positions][mask].sum()) + for name, mask in groups.items() + } + after["liable"] = float(values[clone_positions][liable_clone].sum()) + is_donor = household[HOUSEHOLD_IS_CGT_BAND_DONOR].to_numpy(dtype=bool) + band_index = np.clip( + np.searchsorted( + np.asarray(HMRC_CGT_INCOME_BAND_LOWER_BOUNDS, dtype=float), + income, + side="right", + ) + - 1, + 0, + len(HMRC_CGT_INCOME_BAND_LOWER_BOUNDS) - 1, + ) + by_income_band = [] + for index, lower_limit in enumerate(HMRC_CGT_INCOME_BAND_LOWER_BOUNDS): + in_band = band_index == index + by_income_band.append( + { + "lower_limit": int(lower_limit), + "income_measure": "component_sum_without_allowance", + "clone_households": int(in_band.sum()), + "sub_exempt_before": float( + clone_weights[in_band & groups["sub_exempt"]].sum() + ), + "sub_exempt_after": float( + values[clone_positions][in_band & groups["sub_exempt"]].sum() + ), + "loss_before": float(clone_weights[in_band & groups["loss"]].sum()), + "loss_after": float( + values[clone_positions][in_band & groups["loss"]].sum() + ), + "liable": float(clone_weights[in_band & liable_clone].sum()), + } + ) + receipt = MassChangeRecord( + entity="household", + old_total=old_weights.total, + new_total=float(values.sum()), + declared_factor=1.0, + reason=CGT_ANCHOR_MASS_CHANGE_REASON, + ) + result = uk_national_frame( + person=person, + benunit=benunit, + household=household, + time_period=uk_time_period(frame), + weight_kind=WeightKind.IMPORTANCE, + household_weights=values, + mass_log=(*frame.mass_log, receipt), + ) + validate_uk_national_frame(result) + return UKCGTIncidenceAnchorResult( + frame=result, + annual_exempt_amount=annual_exempt_amount, + liable_mass=liable_mass, + liable_persons=int(liable.sum()), + zero_quantile=zero_quantile, + exempt_quantile=exempt_quantile, + targets=targets, + before=before, + after=after, + scale=scale, + transferred_mass=float(delta.sum()), + pair_count=int(len(clone_positions)), + trimmed_households=int((factors < CGT_ANCHOR_MAXIMUM_FACTOR).sum()), + capped_households=capped, + zero_gain_clone_households=int((carrier_gains == 0.0).sum()), + zero_gain_clone_mass=float(clone_weights[carrier_gains == 0.0].sum()), + max_pair_relative_error=float(pair_error.max()), + original_mass=float(values[original_positions].sum()), + clone_mass=float(values[clone_positions].sum()), + donor_mass=float(values[is_donor].sum()), + by_income_band=tuple(by_income_band), + ) + + def _oldest_adult_indices( person: pd.DataFrame, *, @@ -492,62 +971,13 @@ def _component_sum_income(person: pd.DataFrame) -> np.ndarray: return numeric.sum(axis=1).to_numpy(dtype=float) -def _distribution_rows(resource: Mapping[str, Any]) -> list[Mapping[str, Any]]: - rows = resource.get("rows") - if not isinstance(rows, list) or not rows: - raise ValueError("Advani-Summers resource must contain a non-empty rows list.") - minimums = [float(row["minimum_total_income"]) for row in rows] - if minimums != sorted(minimums) or minimums[0] != 0.0: - raise ValueError("Advani-Summers income bands must be sorted and start at 0.") - # Fail closed on non-monotone quantile rows: the prior draw interpolates - # and extrapolates these values as a quantile function, so a malformed - # row (the class the corrected percentile-69 dropped digit belonged to) - # would silently fabricate loss-makers instead of failing the build. - for row in rows: - knots = [float(row[column]) for column in CGT_PRIOR_PERCENTILE_COLUMNS] - if any(late < early for early, late in zip(knots, knots[1:], strict=False)): - raise ValueError( - "Advani-Summers quantile columns must be non-decreasing; " - f"row with minimum_total_income {row['minimum_total_income']!r} " - "is not a valid quantile function." - ) - return rows - - -def _draw_banded_priors( - income: np.ndarray, - draws: np.ndarray, - *, - distribution: Mapping[str, Any], -) -> np.ndarray: - rows = _distribution_rows(distribution) - minimums = np.asarray([row["minimum_total_income"] for row in rows], dtype=float) - indexes = np.clip( - np.searchsorted(minimums, income, side="right") - 1, 0, len(rows) - 1 - ) - values = np.zeros(len(income), dtype=float) - for index, row in enumerate(rows): - mask = indexes == index - if not mask.any(): - continue - knots = np.asarray( - [row[column] for column in CGT_PRIOR_PERCENTILE_COLUMNS], dtype=float - ) - spline = UnivariateSpline(CGT_QUANTILE_POINTS, knots, k=1, s=0, ext=0) - values[mask] = spline(draws[mask]) - return values - - def _incidence_propensity( income: np.ndarray, *, distribution: Mapping[str, Any], ) -> np.ndarray: rows = _distribution_rows(distribution) - minimums = np.asarray([row["minimum_total_income"] for row in rows], dtype=float) - indexes = np.clip( - np.searchsorted(minimums, income, side="right") - 1, 0, len(rows) - 1 - ) + indexes = advani_summers_band_index(rows, income) rates = np.asarray([row["percent_with_gains"] for row in rows], dtype=float) return rates[indexes] @@ -665,7 +1095,7 @@ def _assert_cgt_incidence_stage_parameters(stage: SourceStageSpec) -> None: ( "draw_capital_gains_prior_from_banded_quantiles", { - "resource": "advani_summers_capital_gains_distribution.json", + "resource": ADVANI_SUMMERS_RESOURCE, "income_proxy_components": list( UK_CGT_TAXABLE_INCOME_PROXY_COMPONENTS ), @@ -701,9 +1131,7 @@ def _assert_cgt_donor_stage_parameters( "size_band_vintage": DONOR_SIZE_BAND_VINTAGE, "retained_taxpayers": DONOR_RETAINED_TAXPAYERS, "retained_gains_gbp": DONOR_RETAINED_GAINS_GBP, - "incidence_resource": ( - "advani_summers_capital_gains_distribution.json" - ), + "incidence_resource": ADVANI_SUMMERS_RESOURCE, "minimum_band_lower": MIN_DONOR_BAND_LOWER, "donors_per_band": DONORS_PER_BAND, "expected_band_count": DONOR_BAND_COUNT, @@ -747,3 +1175,132 @@ def _assert_cgt_donor_stage_parameters( f"{DONOR_RETAINED_TAXPAYERS}, gains {retained_gains} vs " f"{DONOR_RETAINED_GAINS_GBP}." ) + + +def cgt_incidence_anchor_operation_parameters() -> tuple[ + tuple[str, dict[str, object]], ... +]: + """The reviewed anchor operations the manifests must carry verbatim.""" + + return ( + ( + "pair_clone_households_to_originals", + { + "clone_flag_column": HOUSEHOLD_IS_CGT_CLONE, + "donor_flag_column": HOUSEHOLD_IS_CGT_BAND_DONOR, + "id_remapping": "id_multiplier_for_values", + "pairing": ( + "clone household_id minus the clone stage's id multiplier, " + "recomputed from the pre-clone rows (clone flag false and not " + "a band donor)" + ), + "requirement": ( + "bijection between clone and original households; an " + "unpaired clone or original fails the build" + ), + }, + ), + ( + "derive_reporter_composition_from_banded_quantiles", + { + "resource": ADVANI_SUMMERS_RESOURCE, + "income_proxy_components": list(UK_CGT_TAXABLE_INCOME_PROXY_COMPONENTS), + "allowance_subtraction": False, + "carrier": "oldest adult; person_id ascending breaks age ties", + "adult_minimum_age": CGT_ADULT_MINIMUM_AGE, + "quantile_points": list(CGT_QUANTILE_POINTS), + "spline_degree": 1, + "extrapolation": "ext=0", + "crossings": ( + "each band's degree-1 quantile spline inverted at zero and at " + "the build period's annual exempt amount" + ), + "aggregation": ( + "crossings averaged over clone carriers weighted by household " + "weight times the band's percent_with_gains" + ), + "liable_mass": ( + "weighted persons with capital_gains above the annual exempt " + "amount on every household after the Table 3 redraw, clones " + "and band donors alike" + ), + "targets": ( + "sub_exempt = liable_mass * (exempt_quantile - zero_quantile) " + "/ (1 - exempt_quantile); loss = liable_mass * zero_quantile " + "/ (1 - exempt_quantile)" + ), + }, + ), + ( + "transfer_clone_mass_to_originals", + { + "population": CGT_ANCHOR_SCOPE, + "sub_exempt_group": ( + "clone households whose carrier's capital_gains lie in " + "(0, annual exempt amount]; a carrier with exactly zero gains is " + "no reporter and is left untouched" + ), + "loss_group": ( + "clone households whose carrier's capital_gains are negative" + ), + "liable_clones": "untouched", + "donors": "untouched", + "factor": ( + "min(maximum_factor, scale * percent_with_gains) with scale " + "solved per group by bisection so the group's mass equals its " + "target; a group at or below its target is untouched" + ), + "maximum_factor": CGT_ANCHOR_MAXIMUM_FACTOR, + "transfer": ( + "each clone's removed mass is added to its paired original, " + "so every pair's mass is conserved to rounding; no global " + "exact-total correction is applied, so no household outside " + "a trimmed pair moves" + ), + "weight_kind_out": WeightKind.IMPORTANCE.value, + "conservation": "household_total_to_rounding", + "pair_conservation": "to_rounding", + "declared_factor": 1.0, + "reason": CGT_ANCHOR_MASS_CHANGE_REASON, + }, + ), + ) + + +def _assert_cgt_incidence_anchor_stage_parameters(stage: SourceStageSpec) -> None: + """Bind every anchor manifest parameter to reviewed code constants.""" + + _assert_closed_world_operations(stage, cgt_incidence_anchor_operation_parameters()) + if stage.stage != CGT_INCIDENCE_ANCHOR_STAGE_NAME: + raise ValueError( + f"Expected stage {CGT_INCIDENCE_ANCHOR_STAGE_NAME!r}, got {stage.stage!r}." + ) + if tuple(stage.outputs) or tuple(stage.rewrites): + raise ValueError( + "The CGT incidence anchor writes no cell; the manifest must declare " + f"no outputs or rewrites, got {stage.outputs} / {stage.rewrites}." + ) + if stage.grain != "household": + raise ValueError( + f"The CGT incidence anchor moves household mass; grain must be " + f"'household', got {stage.grain!r}." + ) + artifacts = {str(artifact.get("role")): artifact for artifact in stage.artifacts} + distribution = artifacts.get("capital_gains_incidence_and_quantiles") + if ( + distribution is None + or distribution.get("resource") != ADVANI_SUMMERS_RESOURCE + or distribution.get("runtime_sha256_required") is not True + ): + raise ValueError( + "The CGT incidence anchor must declare the Advani-Summers resource " + "artifact with runtime_sha256_required: true." + ) + policy = artifacts.get("policy_parameters") + if policy is None or "gov.hmrc.cgt.annual_exempt_amount" not in tuple( + policy.get("parameters", ()) + ): + raise ValueError( + "The CGT incidence anchor must declare the annual exempt amount " + "policy-parameter artifact." + ) diff --git a/packages/microcosm-build/src/microcosm/build/uk_runtime/graph.py b/packages/microcosm-build/src/microcosm/build/uk_runtime/graph.py index 1cfdde6e1c..93e71afd1f 100644 --- a/packages/microcosm-build/src/microcosm/build/uk_runtime/graph.py +++ b/packages/microcosm-build/src/microcosm/build/uk_runtime/graph.py @@ -50,7 +50,12 @@ ) UK_SPINE_STRUCTURAL_STAGES = frozenset( - {"spi_support_channel", "cgt_incidence_clone", "cgt_band_donors"} + { + "spi_support_channel", + "cgt_incidence_clone", + "cgt_band_donors", + "cgt_incidence_anchor", + } ) # The executor's mass ledger is weighted *person* mass per stratum @@ -70,12 +75,14 @@ "spi_support_channel": "declared", "cgt_incidence_clone": "conserve", "cgt_band_donors": "free", + "cgt_incidence_anchor": "conserve", } _STRUCTURAL_WEIGHT_KIND = { "spi_support_channel": "importance", "cgt_incidence_clone": "importance", "cgt_band_donors": "importance", + "cgt_incidence_anchor": "importance", } # ``hmrc_spi_income_spine`` has an intentionally conservative open input @@ -242,6 +249,29 @@ # The asset-type stage classifies the redrawn net gains; the AEA it # gates on is a policy parameter, not a frame column (microcosm#725). "hmrc_cgt_asset_type_spine": frozenset({("person", "capital_gains")}), + # The incidence anchor reads the redrawn gains, the carrier income proxy + # and the clone/donor flags; it writes no cell and only moves household + # weight between paired rows (microcosm#970). + "cgt_incidence_anchor": frozenset( + { + *( + ("person", column) + for column in ( + "capital_gains", + "employment_income", + "self_employment_income", + "state_pension_reported", + "private_pension_income", + "property_income", + "savings_interest_income", + "dividend_income", + "miscellaneous_income", + ) + ), + ("household", "household_is_capital_gains_clone"), + ("household", "household_is_cgt_band_donor"), + } + ), "salary_sacrifice": None, "student_loans": frozenset( { @@ -599,6 +629,8 @@ def _cells( _Cell("person", "capital_gains_asset_type", "string"), _Cell("person", "capital_gains_residential_property", "float64"), ), + # Weights only: the anchor owns no cell (microcosm#970). + "cgt_incidence_anchor": (), "salary_sacrifice": _cells( "person", ( @@ -936,30 +968,33 @@ def uk_spine_graph( description=f"Run structural UK stage {stage_name}.", ) ) - nodes.append( - Node( - id=f"{stage_name}.owned", - kernel="uk.claim@1", - outputs=tuple( - cell.owned(rewrite=cell.coordinate in incumbent) - for cell in cells - ), - population=stage_name, - params={ - # Amendment 8 projects declared rewrites directly. - # These are only the new cells installed physically by - # the preceding EXPAND node; structural declarations - # still cannot own them (the one remaining interface - # gap from lane F). - "materialized_expand_outputs": tuple( - f"{cell.entity}.{cell.column}" + # A weights-only structural stage (the #970 incidence anchor) + # materializes no cell, so it has nothing to claim. + if cells: + nodes.append( + Node( + id=f"{stage_name}.owned", + kernel="uk.claim@1", + outputs=tuple( + cell.owned(rewrite=cell.coordinate in incumbent) for cell in cells - if cell.coordinate not in incumbent - ) - }, - description=f"Own the cells materialized by {stage_name}.", + ), + population=stage_name, + params={ + # Amendment 8 projects declared rewrites directly. + # These are only the new cells installed physically by + # the preceding EXPAND node; structural declarations + # still cannot own them (the one remaining interface + # gap from lane F). + "materialized_expand_outputs": tuple( + f"{cell.entity}.{cell.column}" + for cell in cells + if cell.coordinate not in incumbent + ) + }, + description=f"Own the cells materialized by {stage_name}.", + ) ) - ) current_population = stage_name version_owned = set(coordinates) else: diff --git a/packages/microcosm-build/src/microcosm/build/uk_runtime/graph_kernels.py b/packages/microcosm-build/src/microcosm/build/uk_runtime/graph_kernels.py index fb45f74ccd..04bb336ac9 100644 --- a/packages/microcosm-build/src/microcosm/build/uk_runtime/graph_kernels.py +++ b/packages/microcosm-build/src/microcosm/build/uk_runtime/graph_kernels.py @@ -88,6 +88,7 @@ "uc_deduction_attributes": "uc_deduction_attributes", "cgt_incidence_clone": "cgt_structure", "cgt_band_donors": "cgt_structure", + "cgt_incidence_anchor": "cgt_structure", "hmrc_cgt_gains_spine": "cgt_imputation", "hmrc_cgt_asset_type_spine": "cgt_asset_type", "salary_sacrifice": "salary_sacrifice", @@ -351,6 +352,7 @@ def _fixture_implementations(source: Path) -> Mapping[str, object]: from .cgt_imputation import UKCGTPolicyParameters, uk_cgt_spine_stage_transform from .cgt_structure import ( UKCGTBandDonorStageTransform, + UKCGTIncidenceAnchorStageTransform, UKCGTIncidenceCloneStageTransform, ) from .etb_services import UKETBServicesStageTransform @@ -517,6 +519,10 @@ def _fixture_implementations(source: Path) -> Mapping[str, object]: facts=cgt_asset_type_facts, parameters=cgt_parameters, ), + "cgt_incidence_anchor": UKCGTIncidenceAnchorStageTransform( + stage=stages["cgt_incidence_anchor"], + parameters=cgt_parameters, + ), "salary_sacrifice": UKSalarySacrificeStageTransform( stage=stages["salary_sacrifice"] ), @@ -808,6 +814,16 @@ def _source_lineage( before_table = before.table(entity) after_table = after.table(entity) before_ids = pd.Index(before_table[id_column]) + # A weights-only structural stage (the #970 incidence anchor) adds no + # row: every target is an incumbent, so the lineage is empty without + # walking the table row by row. + if pd.Index(after_table[id_column]).isin(before_ids).all(): + return pd.Series( + [], + index=pd.Index([], name=id_column, dtype=before_table[id_column].dtype), + dtype=before_table[id_column].dtype, + name=id_column, + ) targets: list[object] = [] values: list[object] = [] source_column = f"{entity}_source_id" @@ -1007,6 +1023,7 @@ def build_uk_registry( "spi_support_channel", "cgt_incidence_clone", "cgt_band_donors", + "cgt_incidence_anchor", }: registry.register(UKExpandStageKernel(stage, transform, fixture_resolver)) else: diff --git a/packages/microcosm-build/src/microcosm/build/uk_runtime/release_certification.py b/packages/microcosm-build/src/microcosm/build/uk_runtime/release_certification.py index 6f25764581..537e698d9a 100644 --- a/packages/microcosm-build/src/microcosm/build/uk_runtime/release_certification.py +++ b/packages/microcosm-build/src/microcosm/build/uk_runtime/release_certification.py @@ -55,6 +55,10 @@ uk_aggregate_admin_totals, uk_scoped_gate_manifest, ) +from microcosm.build.uk_runtime.cgt_projection import ( + UK_CGT_PROJECTION_ENGINE_LABEL_PREFIX, + uk_cgt_projection_read_from_engine, +) __all__ = [ "UK_RELEASE_CERTIFICATION_KIND", @@ -100,6 +104,14 @@ } +#: The seam's projection fence records where its projection came from. The +#: seam accepts a projection stated from the manifest pins when no engine is +#: installed (the secrets-free fast lane, a data-only build) and labels the +#: receipt; certification requires the fence to have read an installed +#: engine, so that label can never certify a cut. +_PROJECTION_GATE_ID = "uk_cgt_projection_entrants" + + class UKReleaseCertificationError(ValueError): """A certification refusal: the parts do not certify the candidate.""" @@ -573,6 +585,7 @@ def _verify_part( raise UKReleaseCertificationError( f"{part_name}: release-blocking entries not passed: {failing}." ) + _verify_projection_source(part_name, gates) expected = _scoped_digests(scope, phases=phases, policy_suffix=policy_suffix) for field in ("gates_manifest_sha256", "policy_sha256"): if payload.get(field) != expected[field]: @@ -585,6 +598,30 @@ def _verify_part( _verify_part_signature(part_name, payload, signing_key) +def _verify_projection_source(part_name: str, gates: Mapping[str, Any]) -> None: + """Refuse a part whose projection fence did not read an installed engine. + + Only the calibration seam carries the fence. Its ``details`` record the + projection's source: ``policyengine-uk==`` when the seam read + the installed engine, the pins label when it had none. A pinned + projection passes the gate (the binding drift-checks it against itself), + which is why the refusal lives here, on the only path to a certified cut. + """ + + entry = gates.get(_PROJECTION_GATE_ID) + if entry is None: + return + details = entry.get("details") + engine = details.get("projection_engine") if isinstance(details, Mapping) else None + if not uk_cgt_projection_read_from_engine(engine): + raise UKReleaseCertificationError( + f"{part_name}: {_PROJECTION_GATE_ID}.details.projection_engine must " + f"name the installed engine ({UK_CGT_PROJECTION_ENGINE_LABEL_PREFIX}" + f"), got {engine!r}; a fence projected from the manifest " + "pins evaluates the gate but cannot certify." + ) + + def _verify_part_signature( part_name: str, payload: Mapping[str, Any], signing_key: bytes ) -> None: diff --git a/packages/microcosm-build/src/microcosm/build/uk_runtime/source_runtime.py b/packages/microcosm-build/src/microcosm/build/uk_runtime/source_runtime.py index 8ace9e9713..0170b29340 100644 --- a/packages/microcosm-build/src/microcosm/build/uk_runtime/source_runtime.py +++ b/packages/microcosm-build/src/microcosm/build/uk_runtime/source_runtime.py @@ -76,6 +76,7 @@ def uk_stage_implementations( cgt_band_donors_transform: Callable[[Frame], Frame] | None = None, hmrc_cgt_gains_spine_transform: Callable[[Frame], Frame] | None = None, hmrc_cgt_asset_type_spine_transform: Callable[[Frame], Frame] | None = None, + cgt_incidence_anchor_transform: Callable[[Frame], Frame] | None = None, salary_sacrifice_transform: Callable[[Frame], Frame] | None = None, student_loans_transform: Callable[[Frame], Frame] | None = None, ) -> dict[str, Callable[[Frame], Frame]]: @@ -111,6 +112,7 @@ def uk_stage_implementations( "cgt_band_donors": cgt_band_donors_transform, "hmrc_cgt_gains_spine": hmrc_cgt_gains_spine_transform, "hmrc_cgt_asset_type_spine": hmrc_cgt_asset_type_spine_transform, + "cgt_incidence_anchor": cgt_incidence_anchor_transform, "salary_sacrifice": salary_sacrifice_transform, "student_loans": student_loans_transform, } diff --git a/packages/microcosm-build/src/microcosm/build/uk_runtime/stage_health.py b/packages/microcosm-build/src/microcosm/build/uk_runtime/stage_health.py index 169a6c8ff4..d8247249ad 100644 --- a/packages/microcosm-build/src/microcosm/build/uk_runtime/stage_health.py +++ b/packages/microcosm-build/src/microcosm/build/uk_runtime/stage_health.py @@ -55,6 +55,8 @@ def uk_stage_health_gate( return _cgt_imputation_summary_gate(stage, evidence, parameters) if check == "cgt_asset_type_summary": return _cgt_asset_type_summary_gate(stage, evidence, parameters) + if check == "cgt_incidence_anchor": + return _cgt_incidence_anchor_gate(stage, evidence, parameters) if check == "latent_attribute_realization": return _latent_attribute_realization_gate(stage, evidence) if check == "household_composition": @@ -817,6 +819,47 @@ def _cgt_imputation_summary_gate( details["ipf_max_abs_margin_error"] = rake.get("ipf_max_abs_margin_error") details["gains_margin_max_abs_error"] = rake.get("gains_margin_max_abs_error") details["fallback_released_mass"] = released + # The sub-AEA remainder receipt (microcosm#970): every remainder + # amount must sit strictly above zero and at or below the exempt + # amount, or the projection fence downstream measures the wrong + # population. Optional so receipts predating the mapping still read. + remainder = allocation.get("remainder") + if remainder is not None: + if not isinstance(remainder, Mapping): + raise ValueError(f"{stage}.allocation.remainder must be a mapping.") + persons = _finite_number( + remainder.get("persons"), label=f"{stage}.allocation.remainder.persons" + ) + remainder_mass = _finite_number( + remainder.get("mass"), label=f"{stage}.allocation.remainder.mass" + ) + if persons < 0.0 or remainder_mass < 0.0: + failures.append( + f"{stage}: allocation.remainder count or mass is negative." + ) + if persons > 0.0: + exempt = _finite_number( + remainder.get("annual_exempt_amount"), + label=f"{stage}.allocation.remainder.annual_exempt_amount", + ) + low = _finite_number( + remainder.get("min_amount"), + label=f"{stage}.allocation.remainder.min_amount", + ) + high = _finite_number( + remainder.get("max_amount"), + label=f"{stage}.allocation.remainder.max_amount", + ) + if not low > 0.0: + failures.append( + f"{stage}: allocation.remainder.min_amount is not positive." + ) + if high > exempt: + failures.append( + f"{stage}: allocation.remainder.max_amount exceeds the " + "annual exempt amount." + ) + details["remainder_mass"] = remainder_mass return ( _fail(stage, check, failures, details) if failures @@ -918,6 +961,132 @@ def number(key: str) -> float: ) +def _cgt_incidence_anchor_gate( + stage: str, + evidence: Mapping[str, object], + parameters: Mapping[str, object], +) -> GateResult: + """The anchor realised its reporter composition without touching anyone else. + + The stage derives a target for the sub-exempt and loss-making clone mass + from the redrawn liable mass and the Advani-Summers composition, then + moves the excess to the paired originals. This gate holds each group to + its target when it was trimmed (never overshooting, never gaining mass, + untouched when it was already at or below target), the liable clone mass + to exactly its pre-anchor value, every pair's mass to rounding, and the + clone side to no more than the original side; the pairing must cover at + least ``minimum_pair_count`` households (microcosm#970). + """ + + check = "cgt_incidence_anchor" + failures: list[str] = [] + max_relative = _finite_number( + parameters["maximum_relative_composition_error"], + label=f"{stage}.maximum_relative_composition_error", + ) + max_pair_error = max( + _finite_number( + parameters["maximum_pair_relative_error"], + label=f"{stage}.maximum_pair_relative_error", + ), + _FLOAT_RELATIVE_TOLERANCE, + ) + minimum_pairs = parameters["minimum_pair_count"] + if not isinstance(minimum_pairs, int) or isinstance(minimum_pairs, bool): + raise ValueError(f"{stage}.minimum_pair_count must be an integer.") + liable_mass = _finite_number( + evidence.get("liable_mass"), label=f"{stage}.liable_mass" + ) + transferred = _finite_number( + evidence.get("transferred_mass"), label=f"{stage}.transferred_mass" + ) + pair_error = _finite_number( + evidence.get("max_pair_relative_error"), + label=f"{stage}.max_pair_relative_error", + ) + pair_count = evidence.get("pair_count") + if not isinstance(pair_count, int) or isinstance(pair_count, bool): + raise ValueError(f"{stage}.pair_count must be an integer.") + targets = _mapping(evidence.get("targets"), label=f"{stage}.targets") + before = _mapping(evidence.get("before"), label=f"{stage}.before") + after = _mapping(evidence.get("after"), label=f"{stage}.after") + mass = _mapping( + evidence.get("mass_by_clone_flag"), label=f"{stage}.mass_by_clone_flag" + ) + details: dict[str, object] = { + "liable_mass": liable_mass, + "transferred_mass": transferred, + "pair_count": pair_count, + "max_pair_relative_error": pair_error, + "effective_pair_relative_tolerance": max_pair_error, + } + if liable_mass <= 0.0: + failures.append(f"{stage}: liable mass must be positive.") + if transferred < 0.0: + failures.append(f"{stage}: transferred mass must be non-negative.") + if pair_count < minimum_pairs: + failures.append( + f"{stage}: {pair_count} clone/original pairs is below the required " + f"{minimum_pairs}." + ) + if pair_error > max_pair_error: + failures.append( + f"{stage}: pair mass error {pair_error} exceeds {max_pair_error}." + ) + removed = 0.0 + for group in ("sub_exempt", "loss"): + target = _finite_number(targets.get(group), label=f"{stage}.targets.{group}") + was = _finite_number(before.get(group), label=f"{stage}.before.{group}") + now = _finite_number(after.get(group), label=f"{stage}.after.{group}") + removed += was - now + details[f"{group}_target"] = target + details[f"{group}_before"] = was + details[f"{group}_after"] = now + if target <= 0.0: + failures.append(f"{stage}: {group} target must be positive.") + continue + if now > was * (1.0 + _FLOAT_RELATIVE_TOLERANCE): + failures.append(f"{stage}: {group} clone mass rose from {was} to {now}.") + if was > target: + error = abs(now - target) / target + details[f"{group}_relative_error"] = error + if error > max(max_relative, _FLOAT_RELATIVE_TOLERANCE): + failures.append( + f"{stage}: {group} clone mass {now} misses its target {target} " + f"by {error} (allowed {max_relative})." + ) + elif not np.isclose(now, was, rtol=_FLOAT_RELATIVE_TOLERANCE, atol=0.0): + failures.append( + f"{stage}: {group} clone mass {was} was already at or below its " + f"target {target} but moved to {now}." + ) + liable_before = _finite_number(before.get("liable"), label=f"{stage}.before.liable") + liable_after = _finite_number(after.get("liable"), label=f"{stage}.after.liable") + details["liable_clone_mass"] = liable_after + if liable_after != liable_before: + failures.append( + f"{stage}: liable clone mass moved from {liable_before} to {liable_after}." + ) + if not np.isclose(removed, transferred, rtol=1e-9, atol=0.0): + failures.append( + f"{stage}: removed group mass {removed} disagrees with the transferred " + f"mass {transferred}." + ) + original = _finite_number(mass.get("false"), label=f"{stage}.mass.false") + clone = _finite_number(mass.get("true"), label=f"{stage}.mass.true") + details["original_mass"] = original + details["clone_mass"] = clone + if clone > original * (1.0 + _FLOAT_RELATIVE_TOLERANCE): + failures.append( + f"{stage}: clone mass {clone} exceeds original mass {original}." + ) + return ( + _fail(stage, check, failures, details) + if failures + else _pass(stage, check, details) + ) + + def _latent_attribute_realization_gate( stage: str, evidence: Mapping[str, object], diff --git a/packages/microcosm-build/src/microcosm/build/uk_runtime/terminal_gates.py b/packages/microcosm-build/src/microcosm/build/uk_runtime/terminal_gates.py index 377af985d5..c4d310eb31 100644 --- a/packages/microcosm-build/src/microcosm/build/uk_runtime/terminal_gates.py +++ b/packages/microcosm-build/src/microcosm/build/uk_runtime/terminal_gates.py @@ -29,6 +29,7 @@ from microcosm.build.gates import ( target_surface_gate as _target_surface_gate, ) +from microcosm.build.uk_runtime.cgt_projection import UKCGTProjection from microcosm.build.uk_runtime.diagnostics import uk_weight_summary from microcosm.build.uk_runtime.weighted_integrity import ( UK_DEGENERATE_EXCLUSION_REGISTER_RESOURCE, @@ -69,6 +70,7 @@ "uk_weight_ess_gate", "uk_weight_ratio_gate", "uk_zero_weight_strata_gate", + "uk_cgt_projection_entrants_gate", ] UK_CANDIDATE_DATASET_NAME = "microcosm_uk_2024" @@ -888,3 +890,114 @@ def _missing_fit_weight_evidence_gate() -> GateResult: ), details={"fits_checked": 0, "evidence_missing": True}, ) + + +def _weighted_quantiles( + values: np.ndarray, weights: np.ndarray, probabilities: Sequence[float] +) -> dict[str, float | None]: + order = np.argsort(values, kind="stable") + ordered = values[order] + mass = weights[order] + total = float(mass.sum()) + if total <= 0.0 or ordered.size == 0: + return {f"p{int(round(p * 100))}": None for p in probabilities} + cumulative = np.cumsum(mass) / total + return { + f"p{int(round(p * 100))}": float( + ordered[ + min(int(np.searchsorted(cumulative, p, side="left")), ordered.size - 1) + ] + ) + for p in probabilities + } + + +def uk_cgt_projection_entrants_gate( + person: pd.DataFrame, + person_weights: np.ndarray, + projection: UKCGTProjection, + *, + bound: float, + bound_source: str, +) -> GateResult: + """Fence the gainers the engine would tip into liability by uprating. + + The engine freezes the annual exempt amount at its nominal value and + uprates ``capital_gains`` by per-capita GDP growth, so a gainer at or + just below the exempt amount in the build period becomes a taxpayer in + a later projected year without any change in behaviour. For every year + to the horizon the gate counts the cumulative stock of build-period + sub-exempt gainers whose uprated gains exceed that year's exempt amount + (non-decreasing in the year), and the largest count must not exceed + ``bound``: the published count of the thinnest liable band, people + already above the exempt amount, so a plausibility ceiling rather than + an entrant count (microcosm#970). A frame without ``capital_gains`` + cannot be fenced and fails closed. + """ + + if "capital_gains" not in person.columns: + raise ValueError( + "cgt_projection_entrants requires person.capital_gains; a frame " + "without it cannot be fenced." + ) + gains = pd.to_numeric(person["capital_gains"], errors="raise").to_numpy(dtype=float) + weights = np.asarray(person_weights, dtype=float) + if weights.shape != gains.shape: + raise ValueError("cgt_projection_entrants needs one weight per person.") + if not np.isfinite(gains).all() or not np.isfinite(weights).all(): + raise ValueError("cgt_projection_entrants requires finite gains and weights.") + if not math.isfinite(bound) or bound <= 0.0: + raise ValueError("cgt_projection_entrants requires a positive finite bound.") + base_exempt = float(projection.exempt_amount_by_year[str(projection.base_year)]) + sub_exempt = (gains > 0.0) & (gains <= base_exempt) + entrants_by_year: dict[str, float] = {} + for year in projection.projected_years: + factor = float(projection.cumulative_gains_factor_by_year[str(year)]) + exempt = float(projection.exempt_amount_by_year[str(year)]) + crossing = sub_exempt & (gains * factor > exempt) + entrants_by_year[str(year)] = float(weights[crossing].sum()) + worst_year = max( + entrants_by_year, key=lambda year: (entrants_by_year[year], -int(year)) + ) + max_entrants = entrants_by_year[worst_year] + sub_exempt_weights = weights[sub_exempt] + details: dict[str, object] = { + "base_year": projection.base_year, + "horizon_year": projection.horizon_year, + "entrants_by_year": entrants_by_year, + "worst_year": int(worst_year), + "max_entrants": max_entrants, + "bound": float(bound), + "bound_source": bound_source, + "cumulative_gains_factor_by_year": dict( + projection.cumulative_gains_factor_by_year + ), + "exempt_amount_by_year": dict(projection.exempt_amount_by_year), + "gains_growth_parameter": projection.growth_parameter, + "exempt_amount_parameter": projection.exempt_amount_parameter, + "projection_engine": projection.engine, + "sub_exempt": { + "rows": int(sub_exempt.sum()), + "weighted_persons": float(sub_exempt_weights.sum()), + "weighted_at_exempt_amount": float( + weights[sub_exempt & (gains == base_exempt)].sum() + ), + **_weighted_quantiles( + gains[sub_exempt], sub_exempt_weights, (0.1, 0.5, 0.9) + ), + }, + } + failures: tuple[str, ...] = () + if max_entrants > bound: + failures = ( + f"{UK_CANDIDATE_DATASET_NAME}: {max_entrants:,.0f} weighted sub-exempt " + f"gainers cross the frozen annual exempt amount by {worst_year} under " + f"the engine's uprating, above the bound of {bound:,.0f} " + f"({bound_source}).", + ) + return GateResult( + name="cgt_projection_entrants", + passed=not failures, + failures=failures, + details=details, + ) diff --git a/packages/microcosm-build/tests/test_country_spec.py b/packages/microcosm-build/tests/test_country_spec.py index bacd4e5396..873bf00261 100644 --- a/packages/microcosm-build/tests/test_country_spec.py +++ b/packages/microcosm-build/tests/test_country_spec.py @@ -994,11 +994,11 @@ def test_uk_source_manifest_loads_thirty_stages(self) -> None: spec = load_country_spec("uk") assert spec.sources is not None - # 30 spine stages (uc_reporter_redraw #832, uc_deduction_attributes - # #685, frs_relationships #791, then hmrc_cgt_asset_type_spine #725 as - # the newest) plus the - # two certified-pair stages the June path still uses. - assert len(spec.sources.stages) == 32 + # 31 spine stages (uc_reporter_redraw #832, uc_deduction_attributes + # #685, frs_relationships #791, hmrc_cgt_asset_type_spine #725, then + # cgt_incidence_anchor #970 as the newest) plus the two certified-pair + # stages the June path still uses. + assert len(spec.sources.stages) == 33 class TestExistingPackagesGeneralize: @@ -1354,6 +1354,7 @@ def test_declares_the_full_june_battery(self, manifest) -> None: "uk_stage_cgt_band_donors_support", "uk_stage_hmrc_cgt_gains_spine_summary", "uk_stage_hmrc_cgt_asset_type_spine_summary", + "uk_stage_cgt_incidence_anchor_composition", "uk_stage_salary_sacrifice_realization", "uk_stage_student_loans_realization", "uk_stage_age_tail_targets", @@ -1378,6 +1379,7 @@ def test_declares_the_full_june_battery(self, manifest) -> None: "uk_calibration_reference_coverage", "uk_target_surface", "uk_target_fit", + "uk_cgt_projection_entrants", "uk_input_mass_parity", "uk_qrf_tail_concentration", "uk_local_geography_ladder_post_calibration", @@ -1448,11 +1450,15 @@ def test_strict_absent_evidence_entries_are_declared(self, manifest) -> None: "uk_stage_cgt_band_donors_support", "uk_stage_hmrc_cgt_gains_spine_summary", "uk_stage_hmrc_cgt_asset_type_spine_summary", + "uk_stage_cgt_incidence_anchor_composition", "uk_stage_salary_sacrifice_realization", "uk_stage_student_loans_realization", "uk_stage_age_tail_targets", "uk_stage_frs_relationships_composition", "uk_weights_audit", + # The #970 projection fence: a seam that cannot project cannot + # certify the candidate's sub-exempt gainers. + "uk_cgt_projection_entrants", ] assert all(g.not_applicable is None for g in manifest.gates) diff --git a/packages/microcosm-build/tests/test_uk_advani_summers.py b/packages/microcosm-build/tests/test_uk_advani_summers.py new file mode 100644 index 0000000000..20bf8cdd1a --- /dev/null +++ b/packages/microcosm-build/tests/test_uk_advani_summers.py @@ -0,0 +1,183 @@ +"""The shared Advani-Summers surface: loader, inverse and stratified mapping.""" + +from __future__ import annotations + +import hashlib +from importlib.resources import files + +import numpy as np +import pytest + +from microcosm.build.uk_runtime.advani_summers import ( + ADVANI_SUMMERS_RESOURCE, + CGT_PRIOR_PERCENTILE_COLUMNS, + CGT_QUANTILE_POINTS, + advani_summers_band_index, + advani_summers_knots, + advani_summers_quantile_function, + advani_summers_resource_sha256, + advani_summers_rows, + draw_banded_priors, + exempt_range_quantiles, + load_advani_summers_distribution, + quantile_for_amount, + stratified_amounts_within_range, +) + +# One synthetic band whose spline crosses zero at q = 0.1 and 50 at q = 0.5. +SYNTHETIC_KNOTS = np.asarray([-10.0, 0.0, 25.0, 50.0, 75.0, 100.0, 125.0]) + + +def _synthetic_distribution() -> dict: + return { + "rows": [ + { + "percentile": "<40", + "minimum_total_income": 0, + "percent_with_gains": 0.01, + **dict(zip(CGT_PRIOR_PERCENTILE_COLUMNS, SYNTHETIC_KNOTS, strict=True)), + }, + { + "percentile": 40, + "minimum_total_income": 10_000, + "percent_with_gains": 0.02, + **dict( + zip(CGT_PRIOR_PERCENTILE_COLUMNS, SYNTHETIC_KNOTS * 2, strict=True) + ), + }, + ] + } + + +def test_committed_rows_validate_and_place_zero_on_the_p10_p25_segment() -> None: + rows = advani_summers_rows(load_advani_summers_distribution()) + + assert len(rows) == 61 + for row in rows: + assert row["p10"] < 0 < row["p25"], row["minimum_total_income"] + + +def test_quantile_for_amount_inverts_the_prior_spline_on_every_row() -> None: + rows = advani_summers_rows(load_advani_summers_distribution()) + for row in rows: + knots = advani_summers_knots(row) + spline = advani_summers_quantile_function(knots) + for amount in ( + knots[0] - 5_000.0, + 0.0, + 3_000.0, + 6_000.0, + 12_300.0, + knots[-1] + 50_000.0, + ): + quantile = quantile_for_amount(knots, amount) + assert float(spline(quantile)) == pytest.approx(amount, abs=1e-6) + + +def test_quantile_for_amount_refuses_a_flat_segment_and_misaligned_knots() -> None: + with pytest.raises(ValueError, match="flat"): + quantile_for_amount(np.asarray([0.0, 0.0, 0.0, 10.0, 20.0, 30.0, 40.0]), 0.0) + with pytest.raises(ValueError, match="align"): + quantile_for_amount(np.asarray([0.0, 1.0, 2.0]), 1.0) + + +def test_exempt_range_quantiles_are_ordered_and_inside_the_body() -> None: + rows = advani_summers_rows(load_advani_summers_distribution()) + for exempt_amount, upper_bounds in ( + (3_000.0, (0.22, 0.31)), + (6_000.0, (0.30, 0.36)), + ): + for row in rows: + lower, upper = exempt_range_quantiles( + advani_summers_knots(row), exempt_amount + ) + assert 0.05 < lower < upper < 0.95 + assert 0.16 <= lower <= 0.25 + assert upper_bounds[0] <= upper <= upper_bounds[1] + with pytest.raises(ValueError, match="positive"): + exempt_range_quantiles(SYNTHETIC_KNOTS, 0.0) + + +def test_synthetic_crossings_are_exact() -> None: + assert exempt_range_quantiles(SYNTHETIC_KNOTS, 50.0) == pytest.approx((0.1, 0.5)) + + +def test_stratified_amounts_take_stratum_midpoints_in_order() -> None: + equal = stratified_amounts_within_range( + SYNTHETIC_KNOTS, weights=np.ones(4), lower_quantile=0.1, upper_quantile=0.5 + ) + # Strata midpoints at q = 0.15, 0.25, 0.35, 0.45 on the two segments. + assert equal == pytest.approx([25.0 / 3.0, 25.0, 35.0, 45.0]) + unequal = stratified_amounts_within_range( + SYNTHETIC_KNOTS, + weights=np.asarray([1.0, 3.0]), + lower_quantile=0.1, + upper_quantile=0.5, + ) + # Strata (0.1, 0.2) and (0.2, 0.5): midpoints q = 0.15 and 0.35. + assert unequal == pytest.approx([25.0 / 3.0, 35.0]) + assert ( + stratified_amounts_within_range( + SYNTHETIC_KNOTS, weights=np.zeros(0), lower_quantile=0.1, upper_quantile=0.5 + ).size + == 0 + ) + + +def test_stratified_amounts_keep_zero_weight_persons_strictly_inside() -> None: + amounts = stratified_amounts_within_range( + SYNTHETIC_KNOTS, + weights=np.asarray([0.0, 0.0, 4.0]), + lower_quantile=0.1, + upper_quantile=0.5, + ) + assert (amounts > 0.0).all() and (amounts < 50.0).all() + assert (np.diff(amounts) >= 0.0).all() + with pytest.raises(ValueError, match="finite"): + stratified_amounts_within_range( + SYNTHETIC_KNOTS, + weights=np.asarray([1.0, -1.0]), + lower_quantile=0.1, + upper_quantile=0.5, + ) + with pytest.raises(ValueError, match="width"): + stratified_amounts_within_range( + SYNTHETIC_KNOTS, weights=np.ones(2), lower_quantile=0.5, upper_quantile=0.5 + ) + + +def test_band_index_matches_the_prior_draw_banding() -> None: + rows = advani_summers_rows(_synthetic_distribution()) + index = advani_summers_band_index( + rows, np.asarray([-1.0, 0.0, 9_999.0, 10_000.0, 1e9]) + ) + assert index.tolist() == [0, 0, 0, 1, 1] + + +def test_draw_banded_priors_evaluates_each_band_spline() -> None: + distribution = _synthetic_distribution() + draws = draw_banded_priors( + np.asarray([5_000.0, 20_000.0]), + np.asarray([0.05, 0.05]), + distribution=distribution, + ) + assert draws == pytest.approx([-10.0, -20.0]) + + +def test_rows_validation_fails_closed() -> None: + with pytest.raises(ValueError, match="non-empty"): + advani_summers_rows({"rows": []}) + bad = _synthetic_distribution() + bad["rows"][1]["p50"] = -1.0 + with pytest.raises(ValueError, match="non-decreasing"): + advani_summers_rows(bad) + + +def test_resource_sha256_is_the_committed_bytes() -> None: + digest = advani_summers_resource_sha256() + expected = hashlib.sha256( + files("microcosm.build.uk").joinpath(ADVANI_SUMMERS_RESOURCE).read_bytes() + ).hexdigest() + assert digest == expected + assert len(digest) == 64 and digest == digest.lower() + assert CGT_QUANTILE_POINTS == (0.05, 0.1, 0.25, 0.5, 0.75, 0.9, 0.95) diff --git a/packages/microcosm-build/tests/test_uk_battery_bindings.py b/packages/microcosm-build/tests/test_uk_battery_bindings.py index 88cdc41b9a..ab3de953f5 100644 --- a/packages/microcosm-build/tests/test_uk_battery_bindings.py +++ b/packages/microcosm-build/tests/test_uk_battery_bindings.py @@ -105,6 +105,9 @@ def _tables(*, n: int = 4, weights=None): "person_benunit_id": np.arange(201, 201 + n, dtype=np.int64), "employment_income": np.arange(1, n + 1, dtype=float), "universal_credit_reported": np.asarray([10.0, 0.0] * n)[:n], + # Distinct sub-exempt gains: not degenerate, and none crosses the + # frozen 3,000 under the fake projection's pinned growth path. + "capital_gains": np.linspace(500.0, 2_000.0, n), } ) benunit = pd.DataFrame( @@ -188,6 +191,49 @@ def _fixture_coverage_registry(): } +def _fake_cgt_projection(base_year: int = 2023, horizon_year: int = 2030): + """A projection on the manifest's pinned growth path and exempt amounts. + + The seam reads the engine; tests supply the pins themselves, which is + also what the seam falls back to when no engine is installed. + """ + + from microcosm.build.uk_runtime.cgt_projection import ( + UK_CGT_EXEMPT_AMOUNT_PARAMETER, + UK_CGT_GAINS_GROWTH_PARAMETER, + UKCGTProjection, + ) + + parameters = next( + entry + for entry in load_country_spec("uk").gates.gates + if entry.id == "uk_cgt_projection_entrants" + ).parameters + pinned_growth = parameters["expected_yoy_growth_by_year"] + pinned_exempt = parameters["expected_exempt_amount_by_year"] + growth: dict[str, float] = {} + cumulative: dict[str, float] = {} + factor = 1.0 + for year in range(base_year + 1, horizon_year + 1): + rate = float(pinned_growth[str(year)]) + factor *= 1.0 + rate + growth[str(year)] = rate + cumulative[str(year)] = factor + return UKCGTProjection( + base_year=base_year, + horizon_year=horizon_year, + growth_parameter=UK_CGT_GAINS_GROWTH_PARAMETER, + exempt_amount_parameter=UK_CGT_EXEMPT_AMOUNT_PARAMETER, + yoy_growth_by_year=growth, + cumulative_gains_factor_by_year=cumulative, + exempt_amount_by_year={ + str(year): float(pinned_exempt[str(year)]) + for year in range(base_year, horizon_year + 1) + }, + engine="test", + ) + + def _run_battery( tables, *, @@ -217,6 +263,7 @@ def _run_battery( artifacts["aggregate_admin"] = { "nhs_spending_total": 202_000_000_000, } + artifacts["cgt_projection"] = _fake_cgt_projection() if extra_artifacts: artifacts.update(extra_artifacts) # Small synthetic totals exercise battery behavior without disclosing @@ -629,7 +676,10 @@ def test_fully_armed_battery_evaluates_gate_for_gate(self) -> None: # unscoped compatibility probe. The local ladder gate fails because # this national fixture deliberately carries no ladder columns; the # three evidence-backed local arms are named gaps below. - assert len(passed) == 19 + # Plus the #970 projection fence, armed here with the pinned growth + # path over distinct sub-exempt gains that never cross the exempt + # amount. + assert len(passed) == 20 qrf = by_id["uk_qrf_tail_concentration"] assert qrf.status is GateStatus.FAILED assert "declared QRF output is absent" in qrf.result.failures[0] @@ -684,6 +734,7 @@ def test_battery_records_evidence_absent(self, uk_gates) -> None: "uk_calibration_reference_coverage", "uk_target_surface", "uk_target_fit", + "uk_cgt_projection_entrants", "uk_input_mass_parity", "uk_aggregate_admin", "uk_local_area_support", @@ -702,6 +753,8 @@ def test_battery_records_evidence_absent(self, uk_gates) -> None: "uk_local_geography_ladder_post_calibration", "uk_qrf_tail_concentration", "uk_weights_audit", + # The #970 projection fence cannot be skipped in any posture. + "uk_cgt_projection_entrants", } blocked = { o.entry.id for o in battery.blocking_outcomes(release_candidate=True) diff --git a/packages/microcosm-build/tests/test_uk_calibration_run.py b/packages/microcosm-build/tests/test_uk_calibration_run.py index f38ec430a0..d5f05a0252 100644 --- a/packages/microcosm-build/tests/test_uk_calibration_run.py +++ b/packages/microcosm-build/tests/test_uk_calibration_run.py @@ -58,6 +58,58 @@ def _signing_key(monkeypatch): monkeypatch.setenv("MICROCOSM_UK_TERMINAL_GATE_SIGNING_KEY", SIGNING_KEY) +def _fake_cgt_projection(base_year: int = 2023, horizon_year: int = 2030): + """A flat 3 percent growth path against a frozen 3,000 exempt amount.""" + + from microcosm.build.uk_runtime.cgt_projection import ( + UK_CGT_EXEMPT_AMOUNT_PARAMETER, + UK_CGT_GAINS_GROWTH_PARAMETER, + UKCGTProjection, + ) + + # The manifest pins the engine's growth path; the fake must sit on it or + # the binding's drift check fails the gate. + parameters = next( + entry + for entry in load_country_spec("uk").gates.gates + if entry.id == "uk_cgt_projection_entrants" + ).parameters + pinned = parameters["expected_yoy_growth_by_year"] + pinned_exempt = parameters["expected_exempt_amount_by_year"] + growth: dict[str, float] = {} + cumulative: dict[str, float] = {} + factor = 1.0 + for year in range(base_year + 1, horizon_year + 1): + rate = float(pinned[str(year)]) + factor *= 1.0 + rate + growth[str(year)] = rate + cumulative[str(year)] = factor + return UKCGTProjection( + base_year=base_year, + horizon_year=horizon_year, + growth_parameter=UK_CGT_GAINS_GROWTH_PARAMETER, + exempt_amount_parameter=UK_CGT_EXEMPT_AMOUNT_PARAMETER, + yoy_growth_by_year=growth, + cumulative_gains_factor_by_year=cumulative, + exempt_amount_by_year={ + str(year): float(pinned_exempt[str(year)]) + for year in range(base_year, horizon_year + 1) + }, + engine="test", + ) + + +@pytest.fixture(autouse=True) +def _cgt_projection(monkeypatch): + """The seam reads the projection from the engine; tests supply a flat one.""" + + monkeypatch.setattr( + calibration_run, + "uk_cgt_projection_artifact", + lambda frame, manifest: _fake_cgt_projection(), + ) + + def _frame(): ids = np.arange(4, dtype="int64") return uk_national_frame( @@ -67,6 +119,7 @@ def _frame(): "person_benunit_id": ids, "person_household_id": ids, "nhs_spending": [50.0, 50.0, 50.0, 50.0], + "capital_gains": [0.0, 0.0, 0.0, 0.0], } ), benunit=pd.DataFrame( diff --git a/packages/microcosm-build/tests/test_uk_cgt_imputation.py b/packages/microcosm-build/tests/test_uk_cgt_imputation.py index 6103a583a0..f568225292 100644 --- a/packages/microcosm-build/tests/test_uk_cgt_imputation.py +++ b/packages/microcosm-build/tests/test_uk_cgt_imputation.py @@ -6,10 +6,22 @@ from microcosm.build.country_spec import load_country_spec from microcosm.build.uk_runtime import cgt_imputation +from microcosm.build.uk_runtime.advani_summers import ( + ADVANI_SUMMERS_RESOURCE, + ADVANI_SUMMERS_VINTAGE, + CGT_PRIOR_PERCENTILE_COLUMNS, + advani_summers_band_index, + advani_summers_knots, + advani_summers_rows, + exempt_range_quantiles, + load_advani_summers_distribution, + quantile_for_amount, +) from microcosm.build.uk_runtime.cgt_imputation import ( UK_CGT_AGE_GROUP_LOWER_BOUNDS, UK_CGT_REGION_GROUP_LABELS, UK_CGT_REGION_GROUPS, + UK_CGT_REMAINDER_POLICY, UK_CGT_SPINE_MASS_CONSERVATION_REASON, UK_CGT_SPINE_STAGE_NAME, UK_CGT_TAXABLE_INCOME_PROXY_COMPONENTS, @@ -18,6 +30,7 @@ _CellPlan, _draw_plan_amounts, _joint_plans, + _map_remainder_amounts, _pareto_quantile, _pareto_stratum_means, _rake_allocation_targets, @@ -26,6 +39,7 @@ impute_uk_capital_gains, impute_uk_capital_gains_with_report, summarize_uk_cgt_imputation, + uk_cgt_component_sum_income, uk_cgt_spine_stage_transform, uk_cgt_taxable_income_proxy, ) @@ -377,22 +391,160 @@ def test_loss_makers_pass_through_byte_identical(self) -> None: receipt = result.mass_log[-1] assert receipt.reason == UK_CGT_SPINE_MASS_CONSERVATION_REASON - def test_remainder_keeps_existing_amounts_capped_at_the_aea(self) -> None: + def test_remainder_takes_advani_summers_amounts_inside_the_exempt_range( + self, + ) -> None: # One income band holds far more gainer mass than the published - # taxpayers, so the ranking's tail lands in the sub-AEA remainder. + # taxpayers, so the ranking's tail lands in the sub-AEA remainder, + # which the mapping places on the A&S surface (microcosm#970). distribution = _distribution(cell_people=10.0) rows = 3_000 rng = np.random.default_rng(2) gains = rng.lognormal(9, 1.5, rows) frame = _frame(rows, gains=gains, incomes=np.full(rows, 20_000.0)) - result = impute_uk_capital_gains(frame, distribution, PARAMETERS) + result, report = impute_uk_capital_gains_with_report( + frame, + distribution, + PARAMETERS, + conditioning=load_hmrc_cgt_conditioning_facts(), + ) drawn = result.table("person")["capital_gains"].to_numpy() - remainder = drawn <= PARAMETERS.annual_exempt_amount + exempt = PARAMETERS.annual_exempt_amount + remainder = drawn <= exempt assert remainder.any() - expected = np.minimum(gains, PARAMETERS.annual_exempt_amount) - assert drawn[remainder] == pytest.approx(expected[remainder]) + assert (drawn[remainder] > 0.0).all() + # The cap is gone: the remainder no longer sits at min(existing, AEA). + capped = np.minimum(gains, exempt) + assert not np.allclose(drawn[remainder], capped[remainder]) + # Rank-preserving within the (single) A&S band: a larger existing + # gain never maps lower. + order = np.argsort(gains[remainder], kind="stable") + assert (np.diff(drawn[remainder][order]) >= 0.0).all() + # Every amount's inverse quantile lies strictly inside (q0, q_aea). + surface = advani_summers_rows(load_advani_summers_distribution()) + band = int(advani_summers_band_index(surface, np.asarray([20_000.0]))[0]) + knots = advani_summers_knots(surface[band]) + lower, upper = exempt_range_quantiles(knots, exempt) + for amount in drawn[remainder]: + assert lower < quantile_for_amount(knots, float(amount)) < upper + # The receipt describes exactly the mapped population. + receipt = report.remainder + assert receipt["policy"] == UK_CGT_REMAINDER_POLICY + assert receipt["resource"] == ADVANI_SUMMERS_RESOURCE + assert receipt["resource_vintage"] == ADVANI_SUMMERS_VINTAGE + assert receipt["income_measure"] == "component_sum_without_allowance" + assert receipt["persons"] == int(remainder.sum()) + assert receipt["mass"] == pytest.approx(100.0 * remainder.sum()) + assert 0.0 < receipt["min_amount"] <= receipt["max_amount"] <= exempt + assert receipt["bands_with_remainder"] == 1 + assert receipt["band_rows"][0]["zero_quantile"] == pytest.approx(lower) + assert receipt["band_rows"][0]["exempt_quantile"] == pytest.approx(upper) + assert len(receipt["resource_sha256"]) == 64 + assert report.evidence()["remainder"] == receipt + + def test_remainder_mapping_leaves_the_liable_set_and_amounts_untouched( + self, + ) -> None: + # Review of PR #979: the remainder mapping must never reach the + # published cells. Two different within-band surfaces change the + # remainder amounts and nothing else. + distribution = _distribution(cell_people=10.0) + rows = 3_000 + rng = np.random.default_rng(2) + gains = rng.lognormal(9, 1.5, rows) + frame = _frame(rows, gains=gains, incomes=np.full(rows, 20_000.0)) + published = load_advani_summers_distribution() + halved = { + "rows": [ + { + **row, + **{ + column: 0.5 * float(row[column]) + for column in CGT_PRIOR_PERCENTILE_COLUMNS + }, + } + for row in published["rows"] + ] + } + + first, _ = impute_uk_capital_gains_with_report( + frame, + distribution, + PARAMETERS, + conditioning=load_hmrc_cgt_conditioning_facts(), + ) + second, _ = impute_uk_capital_gains_with_report( + frame, + distribution, + PARAMETERS, + conditioning=load_hmrc_cgt_conditioning_facts(), + remainder_distribution=halved, + ) + + one = first.table("person")["capital_gains"].to_numpy() + two = second.table("person")["capital_gains"].to_numpy() + exempt = PARAMETERS.annual_exempt_amount + liable = one > exempt + np.testing.assert_array_equal(liable, two > exempt) + np.testing.assert_array_equal(one[liable], two[liable]) + np.testing.assert_array_equal(one <= 0.0, two <= 0.0) + np.testing.assert_array_equal(one[one <= 0.0], two[two <= 0.0]) + remainder = (one > 0.0) & (one <= exempt) + assert remainder.any() + assert not np.array_equal(one[remainder], two[remainder]) + assert (two[remainder] > 0.0).all() and (two[remainder] <= exempt).all() + + def test_remainder_mapping_is_stable_under_row_permutation(self) -> None: + surface = advani_summers_rows(load_advani_summers_distribution()) + rng = np.random.default_rng(11) + rows = 40 + existing = rng.lognormal(8, 1.0, rows) + person_id = np.arange(100, 100 + rows) + weight = rng.uniform(10.0, 500.0, rows) + weight[:3] = 0.0 # zero-weight rows keep a nominal stratum + income = rng.choice([5_000.0, 30_000.0, 150_000.0], rows) + band_index = advani_summers_band_index(surface, income) + + def mapped(order: np.ndarray) -> np.ndarray: + gains = existing[order].copy() + _map_remainder_amounts( + remainder=np.arange(rows), + existing=existing[order], + person_id=person_id[order], + person_weight=weight[order], + band_index=band_index[order], + rows=surface, + annual_exempt_amount=3_000.0, + new_gains=gains, + ) + result = np.empty(rows) + result[order] = gains + return result + + forward = mapped(np.arange(rows)) + shuffled = mapped(rng.permutation(rows)) + assert forward == pytest.approx(shuffled) + assert (forward > 0.0).all() and (forward <= 3_000.0).all() + # Within a band the map is monotone in the existing gain. + for band in np.unique(band_index): + members = np.flatnonzero(band_index == band) + order = members[np.argsort(existing[members], kind="stable")] + assert (np.diff(forward[order]) >= 0.0).all() + + def test_component_sum_income_is_the_pre_allowance_proxy(self) -> None: + person = pd.DataFrame( + { + column: [1_000.0, 2_000.0] + for column in UK_CGT_TAXABLE_INCOME_PROXY_COMPONENTS + } + ) + total = uk_cgt_component_sum_income(person) + components = len(UK_CGT_TAXABLE_INCOME_PROXY_COMPONENTS) + assert total == pytest.approx([1_000.0 * components, 2_000.0 * components]) + with pytest.raises(ValueError, match="missing"): + uk_cgt_component_sum_income(person.drop(columns=["dividend_income"])) def test_allocation_preserves_the_existing_ranking(self) -> None: # Everyone sits in one income band, whose cells hold 100 people each @@ -571,6 +723,34 @@ def unexpected_loader(*_args, **_kwargs): ) +def test_spine_checkpoint_carries_the_remainder_receipt() -> None: + """The spine receipt names the surface the sub-AEA remainder maps onto.""" + spec = load_country_spec("uk") + assert spec.sources is not None + stage = spec.sources.stage_map()["hmrc_cgt_gains_spine"] + rows = 3_000 + rng = np.random.default_rng(12) + frame = _frame( + rows, gains=rng.lognormal(9, 1.5, rows), incomes=np.full(rows, 20_000.0) + ) + transform = uk_cgt_spine_stage_transform( + stage, distribution=_distribution(cell_people=10.0), parameters=PARAMETERS + ) + + result = transform(frame) + + drawn = result.table("person")["capital_gains"].to_numpy() + remainder = (drawn > 0.0) & (drawn <= PARAMETERS.annual_exempt_amount) + evidence = transform.checkpoint_metadata()["evidence"] + receipt = evidence["allocation"]["remainder"] + assert receipt["persons"] == int(remainder.sum()) > 0 + assert receipt["resource"] == ADVANI_SUMMERS_RESOURCE + assert receipt["resource_vintage"] == ADVANI_SUMMERS_VINTAGE + assert receipt["annual_exempt_amount"] == PARAMETERS.annual_exempt_amount + assert evidence["remainder_mass"] == pytest.approx(receipt["mass"]) + assert evidence["stage"] == "hmrc_cgt_gains_spine" + + def _real_distribution() -> HMRCCapitalGainsJointDistribution: """The published 2024-25 joint, from the committed vendored rows. @@ -660,8 +840,10 @@ def test_imputation_runs_across_every_income_band(self) -> None: # Every draw allocated to the liability distribution clears the AEA. assert liable.size > 0 below = redrawn[(redrawn > 0) & (redrawn <= PARAMETERS.annual_exempt_amount)] - # The remainder keeps capped existing amounts, never band draws. + # The remainder takes A&S within-band amounts in (0, AEA], never + # band draws (microcosm#970). assert below.size == 0 or below.max() <= PARAMETERS.annual_exempt_amount + assert below.size == 0 or below.min() > 0.0 assert drawn.max() >= 5_000_000.0 @@ -1028,4 +1210,5 @@ def test_report_records_the_conditioning_resource(self) -> None: "fallback_share_by_band", "conditioning", "rounding_carry_out", + "remainder", } diff --git a/packages/microcosm-build/tests/test_uk_cgt_projection.py b/packages/microcosm-build/tests/test_uk_cgt_projection.py new file mode 100644 index 0000000000..9b7a2f2e4f --- /dev/null +++ b/packages/microcosm-build/tests/test_uk_cgt_projection.py @@ -0,0 +1,470 @@ +"""The #970 projection fence: the engine path, the evaluator, the binding.""" + +from __future__ import annotations + +import importlib.util +import math + +import numpy as np +import pandas as pd +import pytest + +from microcosm.build.country_spec import load_country_spec +from microcosm.build.gate_battery import EvidenceContext, GateStatus, evaluate_phase +from microcosm.build.uk_runtime import calibration_run +from microcosm.build.uk_runtime.battery_bindings import UK_GATE_REGISTRY +from microcosm.build.uk_runtime.calibration_run import UK_CALIBRATION_GATE_SCOPE +from microcosm.build.uk_runtime.cgt_projection import ( + UK_CGT_EXEMPT_AMOUNT_PARAMETER, + UK_CGT_GAINS_GROWTH_PARAMETER, + UK_CGT_PROJECTION_ARTIFACT_KEY, + UK_CGT_PROJECTION_PINS_ENGINE, + UKCGTProjection, + uk_cgt_projection, + uk_cgt_projection_from_pins, + uk_cgt_projection_read_from_engine, + uk_engine_installed, +) +from microcosm.build.uk_runtime.hmrc_capital_gains import ( + HMRC_CGT_CONDITIONING_RESOURCE, + load_hmrc_cgt_conditioning_facts, +) +from microcosm.build.uk_runtime.national_frame import uk_national_frame +from microcosm.build.uk_runtime.terminal_gates import uk_cgt_projection_entrants_gate + +GATE_ID = "uk_cgt_projection_entrants" + + +class _Reader: + """A parameter reader that records every (path, year) it is asked for.""" + + def __init__(self, *, growth: float = 0.03, exempt: float = 3_000.0) -> None: + self.growth = growth + self.exempt = exempt + self.calls: list[tuple[str, int]] = [] + + def __call__(self, path: str, year: int) -> float: + self.calls.append((path, year)) + if path == UK_CGT_GAINS_GROWTH_PARAMETER: + return self.growth + if path == UK_CGT_EXEMPT_AMOUNT_PARAMETER: + return self.exempt + raise AssertionError(path) + + +def _projection(base_year: int = 2024, horizon_year: int = 2030, **reader_kwargs): + return uk_cgt_projection( + base_year, horizon_year, parameter_reader=_Reader(**reader_kwargs) + ) + + +def _manifest_entry(): + return next(g for g in load_country_spec("uk").gates.gates if g.id == GATE_ID) + + +class _PinnedReader: + """A reader that returns the manifest's pinned growth path and exempt amount.""" + + def __init__(self, *, growth_offset: float = 0.0) -> None: + parameters = _manifest_entry().parameters + self.growth = { + int(year): float(rate) + growth_offset + for year, rate in parameters["expected_yoy_growth_by_year"].items() + } + self.exempt = { + int(year): float(amount) + for year, amount in parameters["expected_exempt_amount_by_year"].items() + } + + def __call__(self, path: str, year: int) -> float: + if path == UK_CGT_GAINS_GROWTH_PARAMETER: + return self.growth[year] + if path == UK_CGT_EXEMPT_AMOUNT_PARAMETER: + return self.exempt[year] + raise AssertionError(path) + + +def _pinned_projection(base_year: int = 2024, horizon_year: int = 2030, **kwargs): + return uk_cgt_projection( + base_year, horizon_year, parameter_reader=_PinnedReader(**kwargs) + ) + + +def _frame(gains: list[float], weights: list[float], *, time_period: str = "2024"): + ids = np.arange(1, len(gains) + 1, dtype="int64") + return uk_national_frame( + person=pd.DataFrame( + { + "person_id": ids, + "person_benunit_id": ids, + "person_household_id": ids, + "capital_gains": np.asarray(gains, dtype=float), + } + ), + benunit=pd.DataFrame({"benunit_id": ids}), + household=pd.DataFrame( + {"household_id": ids, "household_weight": np.asarray(weights, dtype=float)} + ), + time_period=time_period, + ) + + +def test_projection_reads_exactly_the_declared_paths_and_compounds() -> None: + reader = _Reader() + projection = uk_cgt_projection(2024, 2027, parameter_reader=reader) + + assert reader.calls == [ + (UK_CGT_EXEMPT_AMOUNT_PARAMETER, 2024), + (UK_CGT_GAINS_GROWTH_PARAMETER, 2025), + (UK_CGT_EXEMPT_AMOUNT_PARAMETER, 2025), + (UK_CGT_GAINS_GROWTH_PARAMETER, 2026), + (UK_CGT_EXEMPT_AMOUNT_PARAMETER, 2026), + (UK_CGT_GAINS_GROWTH_PARAMETER, 2027), + (UK_CGT_EXEMPT_AMOUNT_PARAMETER, 2027), + ] + assert projection.projected_years == (2025, 2026, 2027) + assert list(projection.cumulative_gains_factor_by_year.values()) == pytest.approx( + [1.03, 1.03**2, 1.03**3] + ) + assert set(projection.exempt_amount_by_year) == {"2024", "2025", "2026", "2027"} + assert set(projection.exempt_amount_by_year.values()) == {3_000.0} + assert projection.engine == "supplied_parameter_reader" + payload = projection.payload() + assert set(payload) == { + "base_year", + "horizon_year", + "growth_parameter", + "exempt_amount_parameter", + "instant_rule", + "engine", + "yoy_growth_by_year", + "cumulative_gains_factor_by_year", + "exempt_amount_by_year", + } + assert payload["instant_rule"] == "january_first" + + +def test_projection_refuses_unusable_inputs() -> None: + with pytest.raises(ValueError, match="must follow the base year"): + _projection(2024, 2024) + with pytest.raises(ValueError, match="growth"): + _projection(growth=-1.5) + with pytest.raises(ValueError, match="growth"): + _projection(growth=math.nan) + with pytest.raises(ValueError, match="exempt amount"): + _projection(exempt=0.0) + with pytest.raises(ValueError, match="every projected year"): + UKCGTProjection( + base_year=2024, + horizon_year=2026, + growth_parameter=UK_CGT_GAINS_GROWTH_PARAMETER, + exempt_amount_parameter=UK_CGT_EXEMPT_AMOUNT_PARAMETER, + yoy_growth_by_year={"2025": 0.03}, + cumulative_gains_factor_by_year={"2025": 1.03}, + exempt_amount_by_year={"2024": 3_000.0, "2025": 3_000.0}, + engine="test", + ) + + +def test_gate_counts_entrants_year_by_year_against_the_bound() -> None: + projection = _projection(growth=0.05) + # At the exempt amount: crosses in the first year. 2,800 needs a factor + # above 1.0714, so the second year. 2,000 needs 1.5, beyond the horizon. + # Losses, zeros and liable gains are never entrants. + person = pd.DataFrame( + {"capital_gains": [3_000.0, 3_000.0, 2_000.0, 2_800.0, -50.0, 0.0, 10_000.0]} + ) + weights = np.asarray([10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0]) + + passed = uk_cgt_projection_entrants_gate( + person, weights, projection, bound=73_000.0, bound_source="test" + ) + assert passed.passed + assert passed.name == "cgt_projection_entrants" + entrants = passed.details["entrants_by_year"] + assert entrants["2025"] == 30.0 + assert all(entrants[str(year)] == 70.0 for year in range(2026, 2031)) + assert passed.details["worst_year"] == 2026 + assert passed.details["max_entrants"] == 70.0 + sub_exempt = passed.details["sub_exempt"] + assert sub_exempt["rows"] == 4 + assert sub_exempt["weighted_persons"] == 100.0 + assert sub_exempt["weighted_at_exempt_amount"] == 30.0 + assert sub_exempt["p10"] == 2_000.0 + assert sub_exempt["p50"] == 2_800.0 + assert sub_exempt["p90"] == 3_000.0 + assert "min" not in sub_exempt and "max" not in sub_exempt + + # Equality to the bound passes; one person more fails. + assert uk_cgt_projection_entrants_gate( + person, weights, projection, bound=70.0, bound_source="test" + ).passed + failed = uk_cgt_projection_entrants_gate( + person, weights, projection, bound=69.0, bound_source="the thin band" + ) + assert not failed.passed + assert "by 2026" in failed.failures[0] + assert "the thin band" in failed.failures[0] + + +def test_gate_fails_closed_on_missing_or_malformed_evidence() -> None: + projection = _projection() + with pytest.raises(ValueError, match="capital_gains"): + uk_cgt_projection_entrants_gate( + pd.DataFrame({"age": [1.0]}), + np.ones(1), + projection, + bound=1.0, + bound_source="test", + ) + with pytest.raises(ValueError, match="one weight per person"): + uk_cgt_projection_entrants_gate( + pd.DataFrame({"capital_gains": [1.0, 2.0]}), + np.ones(1), + projection, + bound=1.0, + bound_source="test", + ) + with pytest.raises(ValueError, match="positive finite bound"): + uk_cgt_projection_entrants_gate( + pd.DataFrame({"capital_gains": [1.0]}), + np.ones(1), + projection, + bound=0.0, + bound_source="test", + ) + + +def _outcome(frame, artifacts): + battery = evaluate_phase( + load_country_spec("uk").gates, + "terminal", + EvidenceContext(frame=frame, artifacts=artifacts), + registry=UK_GATE_REGISTRY, + ) + return battery, next(o for o in battery.outcomes if o.entry.id == GATE_ID) + + +def test_binding_reads_the_vendored_bound_and_cross_checks_the_artifact() -> None: + projection = _pinned_projection() + heavy = _frame([3_000.0, 2_000.0], [80_000.0, 10.0]) + battery, outcome = _outcome(heavy, {UK_CGT_PROJECTION_ARTIFACT_KEY: projection}) + assert outcome.status is GateStatus.FAILED + assert outcome.result.details["bound"] == 73_000.0 + assert outcome.result.details["max_entrants"] == 80_000.0 + assert "tax year 2024" in outcome.result.details["bound_source"] + assert HMRC_CGT_CONDITIONING_RESOURCE in outcome.result.details["bound_source"] + + light = _frame([3_000.0, 2_000.0], [72_999.0, 10.0]) + _, outcome = _outcome(light, {UK_CGT_PROJECTION_ARTIFACT_KEY: projection}) + assert outcome.status is GateStatus.PASSED + + mismatched = _pinned_projection(horizon_year=2029) + _, outcome = _outcome(light, {UK_CGT_PROJECTION_ARTIFACT_KEY: mismatched}) + assert outcome.status is GateStatus.FAILED + assert "disagrees with the declared projection" in "".join(outcome.result.failures) + + wrong_base = _frame([3_000.0], [1.0], time_period="2023") + _, outcome = _outcome(wrong_base, {UK_CGT_PROJECTION_ARTIFACT_KEY: projection}) + assert outcome.status is GateStatus.FAILED + + # An engine whose growth path moved off the pinned one fails visibly. + drifted = _pinned_projection(growth_offset=0.001) + _, outcome = _outcome(light, {UK_CGT_PROJECTION_ARTIFACT_KEY: drifted}) + assert outcome.status is GateStatus.FAILED + assert "drifted from the pinned path" in "".join(outcome.result.failures) + # A projection past the declared horizon is refused as a disagreement + # with the manifest before any drift check runs; the unpinned-year branch + # of the drift check has its own test below. + beyond_horizon = _projection(growth=0.05, horizon_year=2031) + _, outcome = _outcome(light, {UK_CGT_PROJECTION_ARTIFACT_KEY: beyond_horizon}) + assert outcome.status is GateStatus.FAILED + assert "disagrees with the declared projection" in "".join(outcome.result.failures) + + battery, outcome = _outcome(light, {}) + assert outcome.status is GateStatus.EVIDENCE_ABSENT + assert GATE_ID in { + o.entry.id for o in battery.blocking_outcomes(release_candidate=False) + } + + +def test_binding_names_the_year_the_pins_do_not_cover() -> None: + """A projection that reaches a year the manifest does not pin fails the + drift check, and the failure names the year for both the growth rate and + the exempt amount, so the re-pin a horizon change needs is spelled out.""" + + parameters = dict(_manifest_entry().parameters) + parameters["expected_yoy_growth_by_year"] = { + year: rate + for year, rate in parameters["expected_yoy_growth_by_year"].items() + if year != "2030" + } + parameters["expected_exempt_amount_by_year"] = { + year: amount + for year, amount in parameters["expected_exempt_amount_by_year"].items() + if year != "2030" + } + binding = UK_GATE_REGISTRY["cgt_projection_entrants"] + context = EvidenceContext( + frame=_frame([2_000.0], [1.0]), + artifacts={UK_CGT_PROJECTION_ARTIFACT_KEY: _pinned_projection()}, + ) + with pytest.raises(ValueError, match="drifted from the pinned path") as excinfo: + binding.evaluator(context, parameters) + assert "no pinned growth rate for 2030" in str(excinfo.value) + assert "no pinned exempt amount for 2030" in str(excinfo.value) + + +def test_manifest_entry_and_bound_are_the_reviewed_ones() -> None: + entry = next(g for g in load_country_spec("uk").gates.gates if g.id == GATE_ID) + assert entry.gate == "cgt_projection_entrants" + assert entry.phase == "terminal" + assert entry.criticality == "release_blocking" + assert entry.evidence_absent_blocks is True + assert dict(entry.parameters) == { + "horizon_year": 2030, + "gains_growth_parameter": UK_CGT_GAINS_GROWTH_PARAMETER, + "exempt_amount_parameter": UK_CGT_EXEMPT_AMOUNT_PARAMETER, + # The OBR per-capita path as the engine carries it; 2030 is its last + # published year and later years repeat the 2030 rate. + "expected_yoy_growth_by_year": { + "2023": 0.0532, + "2024": 0.0372, + "2025": 0.0438, + "2026": 0.0292, + "2027": 0.0323, + "2028": 0.031, + "2029": 0.0296, + "2030": 0.0315, + }, + # 6,000 in 2023-24, 3,000 from 2024-25: a 2023-period frame's base + # year reads the older amount. + "expected_exempt_amount_by_year": { + "2023": 6_000.0, + **{str(year): 3_000.0 for year in range(2024, 2031)}, + }, + "maximum_growth_drift": 0.0005, + "bound_resource": HMRC_CGT_CONDITIONING_RESOURCE, + "bound_size_band_lower_bound": 3_000, + } + assert GATE_ID in UK_CALIBRATION_GATE_SCOPE + binding = UK_GATE_REGISTRY["cgt_projection_entrants"] + assert binding.artifact_keys == frozenset({UK_CGT_PROJECTION_ARTIFACT_KEY}) + assert binding.parameter_keys == frozenset(entry.parameters) + facts = load_hmrc_cgt_conditioning_facts(HMRC_CGT_CONDITIONING_RESOURCE) + band = facts.size_band(3_000) + assert facts.tax_year == 2024 + assert (band.lower_bound, band.upper_bound, band.taxpayers) == ( + 3_000, + 6_000, + 73_000.0, + ) + + +@pytest.mark.requires_uk +def test_engine_projection_matches_the_published_growth_path() -> None: + projection = uk_cgt_projection(2024, 2030) + + assert projection.engine.startswith("policyengine-uk==") + # The label the release certifier accepts on the seam's fence. + assert uk_cgt_projection_read_from_engine(projection.engine) + assert set(projection.exempt_amount_by_year.values()) == {3_000.0} + assert list(projection.yoy_growth_by_year.values()) == pytest.approx( + [0.0438, 0.0292, 0.0323, 0.0310, 0.0296, 0.0315], abs=5e-4 + ) + assert projection.cumulative_gains_factor_by_year["2030"] == pytest.approx( + 1.2143, abs=5e-4 + ) + pinned = _manifest_entry().parameters + for year, rate in projection.yoy_growth_by_year.items(): + assert rate == pytest.approx( + float(pinned["expected_yoy_growth_by_year"][year]), + abs=float(pinned["maximum_growth_drift"]), + ) + # Beyond the pinned horizon the engine repeats the 2030 rate. + beyond = uk_cgt_projection(2024, 2033) + assert beyond.yoy_growth_by_year["2033"] == projection.yoy_growth_by_year["2030"] + + +def test_projection_from_pins_is_the_engine_free_statement_of_the_path() -> None: + parameters = _manifest_entry().parameters + pinned = uk_cgt_projection_from_pins( + 2023, + 2030, + growth_by_year=parameters["expected_yoy_growth_by_year"], + exempt_amount_by_year=parameters["expected_exempt_amount_by_year"], + ) + assert pinned.engine == UK_CGT_PROJECTION_PINS_ENGINE + assert pinned.exempt_amount_by_year["2023"] == 6_000.0 + assert pinned.exempt_amount_by_year["2024"] == 3_000.0 + assert pinned.yoy_growth_by_year["2024"] == pytest.approx(0.0372) + with pytest.raises(ValueError, match="no .* value for 2031"): + uk_cgt_projection_from_pins( + 2024, + 2031, + growth_by_year=parameters["expected_yoy_growth_by_year"], + exempt_amount_by_year=parameters["expected_exempt_amount_by_year"], + ) + + +def test_seam_artifact_states_the_pins_only_when_no_engine_is_installed( + monkeypatch, +) -> None: + """The secrets-free fast lane and data-only builds have no engine: the + seam still produces the projection, from the pins, without touching the + engine path, and the receipt says so. The release certifier refuses that + receipt (``test_uk_release_certification``).""" + + monkeypatch.setattr(calibration_run, "uk_engine_installed", lambda: False) + + def never(*args, **kwargs): + raise AssertionError("an absent engine must not be read") + + monkeypatch.setattr(calibration_run, "uk_cgt_projection", never) + frame = _frame([3_000.0, 2_000.0], [1.0, 1.0], time_period="2023") + projection = calibration_run.uk_cgt_projection_artifact( + frame, load_country_spec("uk").gates + ) + assert projection.engine == UK_CGT_PROJECTION_PINS_ENGINE + assert not uk_cgt_projection_read_from_engine(projection.engine) + assert projection.base_year == 2023 and projection.horizon_year == 2030 + assert projection.exempt_amount_by_year["2023"] == 6_000.0 + # The binding accepts a pinned projection (it drift-checks against + # itself); only the certifier refuses it. + battery, outcome = _outcome(frame, {UK_CGT_PROJECTION_ARTIFACT_KEY: projection}) + assert outcome.status is GateStatus.PASSED + assert outcome.result.details["projection_engine"] == UK_CGT_PROJECTION_PINS_ENGINE + + +def test_seam_artifact_lets_an_installed_engine_that_will_not_import_raise( + monkeypatch, +) -> None: + """An installed but broken engine is not "unavailable": the import error + surfaces instead of the pins quietly standing in for the engine.""" + + monkeypatch.setattr(calibration_run, "uk_engine_installed", lambda: True) + + def broken(*args, **kwargs): + raise ImportError("libomp.dylib not found") + + monkeypatch.setattr(calibration_run, "uk_cgt_projection", broken) + frame = _frame([3_000.0], [1.0], time_period="2023") + with pytest.raises(ImportError, match="libomp"): + calibration_run.uk_cgt_projection_artifact(frame, load_country_spec("uk").gates) + + +def test_engine_availability_is_decided_by_find_spec() -> None: + assert uk_engine_installed() is ( + importlib.util.find_spec("policyengine_uk") is not None + ) + + +def test_only_a_versioned_engine_label_reads_as_the_engine() -> None: + assert uk_cgt_projection_read_from_engine("policyengine-uk==2.98.0") + assert not uk_cgt_projection_read_from_engine(UK_CGT_PROJECTION_PINS_ENGINE) + assert not uk_cgt_projection_read_from_engine( + "policyengine-uk (version unavailable)" + ) + assert not uk_cgt_projection_read_from_engine("supplied_parameter_reader") + assert not uk_cgt_projection_read_from_engine("policyengine-uk==") + assert not uk_cgt_projection_read_from_engine(None) diff --git a/packages/microcosm-build/tests/test_uk_cgt_source_manifest.py b/packages/microcosm-build/tests/test_uk_cgt_source_manifest.py index 3c0fbdfdc2..9fc56a829e 100644 --- a/packages/microcosm-build/tests/test_uk_cgt_source_manifest.py +++ b/packages/microcosm-build/tests/test_uk_cgt_source_manifest.py @@ -3,12 +3,22 @@ from __future__ import annotations from microcosm.build.country_spec import load_country_spec +from microcosm.build.uk_runtime.advani_summers import ( + ADVANI_SUMMERS_RESOURCE, + CGT_QUANTILE_POINTS, +) from microcosm.build.uk_runtime.cgt_imputation import ( UK_CGT_IMPUTATION_SEED, + UK_CGT_REMAINDER_POLICY, UK_CGT_SPINE_MASS_CONSERVATION_REASON, UK_CGT_SPINE_STAGE_NAME, UK_CGT_TAXABLE_INCOME_PROXY_COMPONENTS, ) +from microcosm.build.uk_runtime.cgt_structure import ( + CGT_ANCHOR_MASS_CHANGE_REASON, + CGT_INCIDENCE_ANCHOR_STAGE_NAME, + cgt_incidence_anchor_operation_parameters, +) from microcosm.build.uk_runtime.hmrc_capital_gains import ( HMRC_CGT_BUILD_PERIOD, HMRC_CGT_CONDITIONING_RECORD_SETS, @@ -137,3 +147,63 @@ def test_every_source_stage_family_requires_its_declared_receipt() -> None: assert family["output_weight_kind"] == "importance", name for name, family in manifest.family_coverage.items(): assert str(family["required_mass_change_reason"]).strip(), name + + +def test_manifest_declares_the_advani_summers_remainder_surface() -> None: + """The sub-AEA remainder reads the within-band surface the manifest pins + (microcosm#970); the artifact and the operation name the same resource.""" + artifacts = {artifact["role"]: artifact for artifact in _stage().artifacts} + surface = artifacts["capital_gains_within_band_distribution"] + remainder = _operations()["sub_aea_remainder"] + + assert surface["kind"] == "public_aggregate_reference" + assert surface["resource"] == ADVANI_SUMMERS_RESOURCE + assert surface["format"] == "json" + assert surface["runtime_sha256_required"] is True + # The manifest carries the reviewed sentence; the receipt carries the + # short policy name the code pins. + assert "Advani-Summers within-band" in remainder["policy"] + assert "(0, annual exempt amount]" in remainder["policy"] + assert UK_CGT_REMAINDER_POLICY == "advani_summers_within_band_exempt_range" + assert remainder["resource"] == ADVANI_SUMMERS_RESOURCE + assert remainder["quantile_points"] == list(CGT_QUANTILE_POINTS) + assert remainder["spline_degree"] == 1 + assert remainder["extrapolation"] == "ext=0" + assert remainder["deterministic"] is True + assert "no uprating" in remainder["resource_vintage"] + + +def test_incidence_anchor_family_requires_its_conserving_receipt() -> None: + """The #970 anchor is a weights-only spine stage the terminal gate demands.""" + manifest = load_uk_release_input_coverage_manifest() + family = manifest.family_coverage[CGT_INCIDENCE_ANCHOR_STAGE_NAME] + + assert family["status"] == "required_at_build" + assert family["outputs"] == [] + assert family["rewrites"] == [] + assert family["output_weight_kind"] == "importance" + assert family["mass_change_semantics"] == "mass_conserving" + assert family["required_mass_change_reason"] == CGT_ANCHOR_MASS_CHANGE_REASON + assert CGT_INCIDENCE_ANCHOR_STAGE_NAME in manifest.required_build_stages + + +def test_incidence_anchor_manifest_operations_match_the_stage_implementation() -> None: + stage = load_country_spec("uk").sources.stage_map()[CGT_INCIDENCE_ANCHOR_STAGE_NAME] + declared = [ + (operation.kind, dict(operation.parameters)) for operation in stage.operations + ] + + assert declared == [ + (kind, dict(parameters)) + for kind, parameters in cgt_incidence_anchor_operation_parameters() + ] + assert declared[-1][1]["reason"] == CGT_ANCHOR_MASS_CHANGE_REASON + assert declared[-1][1]["declared_factor"] == 1.0 + assert stage.outputs == () and stage.rewrites == () + roles = {artifact["role"]: artifact for artifact in stage.artifacts} + assert roles["capital_gains_incidence_and_quantiles"]["resource"] == ( + ADVANI_SUMMERS_RESOURCE + ) + assert ( + "gov.hmrc.cgt.annual_exempt_amount" in roles["policy_parameters"]["parameters"] + ) diff --git a/packages/microcosm-build/tests/test_uk_cgt_structure.py b/packages/microcosm-build/tests/test_uk_cgt_structure.py index f2c2f25f64..fcf59f8f0a 100644 --- a/packages/microcosm-build/tests/test_uk_cgt_structure.py +++ b/packages/microcosm-build/tests/test_uk_cgt_structure.py @@ -10,26 +10,42 @@ from microcosm.build.country_spec import load_country_spec from microcosm.build.source_manifest import SourceOperationSpec +from microcosm.build.uk_runtime.advani_summers import ( + advani_summers_knots, + advani_summers_rows, + exempt_range_quantiles, + load_advani_summers_distribution, +) from microcosm.build.uk_runtime.cgt_imputation import ( UK_CGT_TAXABLE_INCOME_PROXY_COMPONENTS, + UKCGTPolicyParameters, ) from microcosm.build.uk_runtime.cgt_structure import ( + CGT_ANCHOR_MASS_CHANGE_REASON, CGT_CLONE_MASS_CHANGE_REASON, + CGT_INCIDENCE_ANCHOR_STAGE_NAME, DONOR_BAND_COUNT, DONOR_TOTAL, DONORS_PER_BAND, HOUSEHOLD_IS_CGT_BAND_DONOR, HOUSEHOLD_IS_CGT_CLONE, MIN_DONOR_BAND_LOWER, + UKCGTIncidenceAnchorStageTransform, _assert_cgt_donor_stage_parameters, + _assert_cgt_incidence_anchor_stage_parameters, _assert_cgt_incidence_stage_parameters, _draw_banded_priors, + _solve_group_factors, + anchor_cgt_incidence, + cgt_incidence_anchor_operation_parameters, clone_cgt_incidence, load_hmrc_cgt_size_bands, + pair_clone_households, + reporter_composition_quantiles, stack_cgt_band_donors, ) from microcosm.build.uk_runtime.national_frame import uk_national_frame -from microcosm.frame import WeightKind +from microcosm.frame import MassChangeRecord, WeightKind def _distribution(*, negative: bool = False) -> dict[str, object]: @@ -313,3 +329,477 @@ def test_drift_asserts_reject_extra_operations() -> None: ) with pytest.raises(ValueError, match="operation order drifted"): check(extra) + + +# --- #970 incidence anchor --------------------------------------------------- + +ANCHOR_KNOTS = [-10.0, 0.0, 25.0, 50.0, 75.0, 100.0, 125.0] +#: Per clone household: liable, sub-exempt, loss-making, zero-gain carriers. +ANCHOR_PATTERN = [60.0, 20.0, -5.0, 0.0] * 3 + + +def _anchor_distribution() -> dict[str, object]: + """Two bands: zero at q=0.1 in both, the AEA of 50 at q=0.5 and q=0.25.""" + + columns = ("p05", "p10", "p25", "p50", "p75", "p90", "p95") + return { + "rows": [ + { + "minimum_total_income": 0, + "percent_with_gains": 0.2, + **dict(zip(columns, ANCHOR_KNOTS, strict=True)), + }, + { + "minimum_total_income": 50_000, + "percent_with_gains": 0.5, + **dict( + zip(columns, [2.0 * knot for knot in ANCHOR_KNOTS], strict=True) + ), + }, + ] + } + + +def _parameters(annual_exempt_amount: float = 50.0) -> UKCGTPolicyParameters: + return UKCGTPolicyParameters( + personal_allowance=12_570.0, + personal_allowance_taper_threshold=100_000.0, + personal_allowance_taper_rate=0.5, + annual_exempt_amount=annual_exempt_amount, + instant="2024-06-01", + source="test", + ) + + +def _anchor_input( + pattern: list[float], + *, + weights: np.ndarray | None = None, + donors: bool = True, + permutation: np.ndarray | None = None, +): + """Two-person households cloned at equal mass with carrier gains set. + + Each source household holds a 40-year-old carrier and a 30-year-old; the + clone stage draws the prior on the carrier and the pattern overwrites it. + Two band donors copied from the first clone are appended with the donor + flag set and the inherited clone flag left true, as the donor stage + leaves them. + """ + + n = len(pattern) + household_ids = np.arange(1, n + 1, dtype="int64") + person = pd.DataFrame( + { + "person_id": np.arange(1, 2 * n + 1, dtype="int64"), + "person_benunit_id": np.repeat(household_ids, 2), + "person_household_id": np.repeat(household_ids, 2), + "age": np.tile([40, 30], n), + "capital_gains": 0.0, + "employment_income": np.repeat(np.linspace(10_000.0, 120_000.0, n), 2), + } + ) + for column in UK_CGT_TAXABLE_INCOME_PROXY_COMPONENTS: + if column not in person: + person[column] = 0.0 + frame = uk_national_frame( + person=person, + benunit=pd.DataFrame({"benunit_id": household_ids}), + household=pd.DataFrame({"household_id": household_ids}), + household_weights=np.linspace(10.0, 20.0, n) if weights is None else weights, + time_period="2024", + ) + cloned = clone_cgt_incidence(frame, distribution=_anchor_distribution()).frame + person = cloned.table("person").copy() + benunit = cloned.table("benunit").copy() + household = cloned.table("household").copy() + household[HOUSEHOLD_IS_CGT_BAND_DONOR] = False + clone_ids = household.loc[ + household[HOUSEHOLD_IS_CGT_CLONE], "household_id" + ].tolist() + carriers = person["age"].eq(40).to_numpy() + for household_id, gain in zip(clone_ids, pattern, strict=True): + person.loc[ + carriers & person["person_household_id"].eq(household_id).to_numpy(), + "capital_gains", + ] = gain + household_weights = np.asarray(cloned.weights_for("household").values, dtype=float) + mass_log = cloned.mass_log + if donors: + source = clone_ids[0] + for offset in (1_000, 2_000): + donor_person = person.loc[person.person_household_id == source].copy() + for column in ("person_id", "person_benunit_id", "person_household_id"): + donor_person[column] = donor_person[column] + offset + donor_person["capital_gains"] = np.where( + donor_person["age"] == 40, 500.0, 0.0 + ) + donor_benunit = benunit.loc[benunit.benunit_id == source].copy() + donor_benunit["benunit_id"] = donor_benunit["benunit_id"] + offset + donor_household = household.loc[household.household_id == source].copy() + donor_household["household_id"] = donor_household["household_id"] + offset + donor_household[HOUSEHOLD_IS_CGT_BAND_DONOR] = True + person = pd.concat([person, donor_person], ignore_index=True) + benunit = pd.concat([benunit, donor_benunit], ignore_index=True) + household = pd.concat([household, donor_household], ignore_index=True) + household_weights = np.r_[household_weights, 7.0] + mass_log = ( + *mass_log, + MassChangeRecord( + entity="household", + old_total=cloned.weights_for("household").total, + new_total=float(household_weights.sum()), + declared_factor=None, + reason="test donors", + ), + ) + if permutation is not None: + # Group ids must stay sorted; person order is free. + person = person.iloc[permutation].reset_index(drop=True) + return uk_national_frame( + person=person, + benunit=benunit, + household=household, + time_period="2024", + weight_kind=WeightKind.IMPORTANCE, + household_weights=household_weights, + mass_log=mass_log, + ) + + +def _weights_by_id(frame) -> pd.Series: + return pd.Series( + frame.weights_for("household").values, + index=frame.table("household")["household_id"].to_numpy(), + ) + + +def test_pairing_is_a_bijection_and_ignores_donors() -> None: + frame = _anchor_input(ANCHOR_PATTERN) + person, benunit, household = ( + frame.table("person"), + frame.table("benunit"), + frame.table("household"), + ) + clone_positions, original_positions, multiplier = pair_clone_households( + person, benunit, household + ) + + assert multiplier == 100 + assert len(clone_positions) == len(ANCHOR_PATTERN) + ids = household["household_id"].to_numpy() + assert (ids[clone_positions] - multiplier == ids[original_positions]).all() + donors = household[HOUSEHOLD_IS_CGT_BAND_DONOR].to_numpy() + assert donors.sum() == 2 + assert household[HOUSEHOLD_IS_CGT_CLONE].to_numpy()[donors].all() + assert not donors[clone_positions].any() and not donors[original_positions].any() + + orphaned = household.loc[household.household_id != 3] + with pytest.raises(ValueError, match="without a paired original"): + pair_clone_households(person, benunit, orphaned) + extra = pd.concat( + [household, household.iloc[[0]].assign(household_id=13)], ignore_index=True + ) + with pytest.raises(ValueError, match="exactly one clone per original"): + pair_clone_households(person, benunit, extra) + with pytest.raises(ValueError, match=HOUSEHOLD_IS_CGT_BAND_DONOR): + pair_clone_households( + person, benunit, household.drop(columns=[HOUSEHOLD_IS_CGT_BAND_DONOR]) + ) + + +def test_reporter_composition_crossings_match_the_published_first_band() -> None: + distribution = load_advani_summers_distribution() + rows = advani_summers_rows(distribution) + lower, upper = reporter_composition_quantiles( + np.asarray([0.0, 1e9]), distribution=distribution, annual_exempt_amount=3_000.0 + ) + # The <40 row: p10 = -4,400 and p25 = 3,800, so zero sits 4,400/8,200 of + # the way along the 0.15-wide segment and 3,000 sits 7,400/8,200 along it. + assert lower[0] == pytest.approx(0.10 + 0.15 * 4_400 / 8_200, abs=1e-12) + assert upper[0] == pytest.approx(0.10 + 0.15 * 7_400 / 8_200, abs=1e-12) + assert (lower[1], upper[1]) == pytest.approx( + exempt_range_quantiles(advani_summers_knots(rows[-1]), 3_000.0) + ) + synthetic_lower, synthetic_upper = reporter_composition_quantiles( + np.asarray([10_000.0, 60_000.0]), + distribution=_anchor_distribution(), + annual_exempt_amount=50.0, + ) + assert synthetic_lower == pytest.approx([0.1, 0.1]) + assert synthetic_upper == pytest.approx([0.5, 0.25]) + + +def test_solve_group_factors_meets_the_target_and_caps_at_one() -> None: + scale, factors = _solve_group_factors(np.ones(3), np.asarray([0.01, 0.5, 1.0]), 2.0) + # One capped household leaves 1.0 for the other two: s = 1 / 0.51. + assert scale == pytest.approx(1.0 / 0.51, rel=1e-12) + assert factors == pytest.approx([0.01 / 0.51, 0.5 / 0.51, 1.0], rel=1e-12) + assert factors.sum() == pytest.approx(2.0, rel=1e-12) + uncapped_scale, uncapped = _solve_group_factors( + np.asarray([2.0, 3.0]), np.asarray([0.2, 0.4]), 1.0 + ) + assert uncapped_scale == pytest.approx(1.0 / 1.6, rel=1e-12) + assert (uncapped < 1.0).all() + assert (np.asarray([2.0, 3.0]) * uncapped).sum() == pytest.approx(1.0, rel=1e-12) + + +def test_anchor_moves_non_liable_clone_mass_and_conserves_every_pair() -> None: + frame = _anchor_input(ANCHOR_PATTERN) + result = anchor_cgt_incidence( + frame, distribution=_anchor_distribution(), parameters=_parameters() + ) + person = frame.table("person") + household = frame.table("household") + before = np.asarray(frame.weights_for("household").values) + after = np.asarray(result.frame.weights_for("household").values) + clone_positions, original_positions, _ = pair_clone_households( + person, frame.table("benunit"), household + ) + evidence = result.evidence() + + position = pd.Series(np.arange(len(household)), index=household["household_id"]) + person_weight = before[position.reindex(person["person_household_id"]).to_numpy()] + liable = person["capital_gains"].to_numpy() > 50.0 + assert result.liable_mass == pytest.approx(person_weight[liable].sum()) + assert result.liable_persons == 5 # three liable clones and two donors + pattern = np.asarray(ANCHOR_PATTERN) + for group, mask in ( + ("sub_exempt", (pattern > 0.0) & (pattern <= 50.0)), + ("loss", pattern < 0.0), + ): + assert result.before[group] == pytest.approx( + before[clone_positions][mask].sum() + ) + assert result.after[group] == pytest.approx(result.targets[group], rel=1e-9) + assert result.after[group] < result.before[group] + assert result.after[group] == pytest.approx(after[clone_positions][mask].sum()) + assert result.after["liable"] == result.before["liable"] + liable_clones = clone_positions[pattern > 50.0] + np.testing.assert_array_equal(after[liable_clones], before[liable_clones]) + donors = household[HOUSEHOLD_IS_CGT_BAND_DONOR].to_numpy() + np.testing.assert_array_equal(after[donors], before[donors]) + np.testing.assert_allclose( + after[clone_positions] + after[original_positions], + before[clone_positions] + before[original_positions], + rtol=1e-12, + ) + assert (after[original_positions] >= before[original_positions]).all() + assert (after > 0.0).all() + assert result.frame.weights_for("household").total == pytest.approx( + frame.weights_for("household").total, rel=1e-12 + ) + assert result.frame.weights_for("household").kind is WeightKind.IMPORTANCE + for entity in ("person", "benunit", "household"): + pd.testing.assert_frame_equal( + result.frame.table(entity).reset_index(drop=True), + frame.table(entity).reset_index(drop=True), + ) + record = result.frame.mass_log[-1] + assert record.reason == CGT_ANCHOR_MASS_CHANGE_REASON + assert record.declared_factor == 1.0 + assert record.new_total == pytest.approx(record.old_total, rel=1e-12) + assert record.new_total == result.frame.weights_for("household").total + assert result.frame.mass_log[:-1] == frame.mass_log + + assert evidence["stage"] == CGT_INCIDENCE_ANCHOR_STAGE_NAME + assert set(evidence) == { + "stage", + "annual_exempt_amount", + "liable_mass", + "liable_persons", + "composition", + "targets", + "before", + "after", + "scale", + "transferred_mass", + "pair_count", + "trimmed_households", + "capped_households", + "zero_gain_clone_households", + "zero_gain_clone_mass", + "max_pair_relative_error", + "mass_by_clone_flag", + "donor_mass", + "by_income_band", + } + composition = evidence["composition"] + assert 0.0 < composition["zero_quantile"] < composition["exempt_quantile"] < 1.0 + assert composition["implied_reporter_mass"] == pytest.approx( + result.liable_mass / (1.0 - composition["exempt_quantile"]) + ) + assert evidence["pair_count"] == len(ANCHOR_PATTERN) + assert evidence["zero_gain_clone_households"] == 3 + zero_gain = clone_positions[pattern == 0.0] + assert evidence["zero_gain_clone_mass"] == pytest.approx(before[zero_gain].sum()) + # A zero-gain carrier is no reporter: untouched, outside both groups. + np.testing.assert_array_equal(after[zero_gain], before[zero_gain]) + assert evidence["trimmed_households"] == 6 + assert evidence["composition"]["scope"].startswith("clone households paired") + assert evidence["capped_households"] == 0 + assert evidence["transferred_mass"] == pytest.approx( + (result.before["sub_exempt"] - result.after["sub_exempt"]) + + (result.before["loss"] - result.after["loss"]) + ) + assert evidence["max_pair_relative_error"] <= 1e-12 + assert ( + evidence["mass_by_clone_flag"]["true"] < evidence["mass_by_clone_flag"]["false"] + ) + assert evidence["mass_by_clone_flag"]["true"] == pytest.approx( + after[clone_positions].sum() + ) + assert evidence["donor_mass"] == 14.0 + assert sum(row["sub_exempt_after"] for row in evidence["by_income_band"]) == ( + pytest.approx(result.after["sub_exempt"]) + ) + assert sum(row["clone_households"] for row in evidence["by_income_band"]) == ( + len(ANCHOR_PATTERN) + ) + + +def test_anchor_factor_rises_with_band_incidence() -> None: + frame = _anchor_input(ANCHOR_PATTERN) + result = anchor_cgt_incidence( + frame, distribution=_anchor_distribution(), parameters=_parameters() + ) + before = np.asarray(frame.weights_for("household").values) + after = np.asarray(result.frame.weights_for("household").values) + clone_positions, _, _ = pair_clone_households( + frame.table("person"), frame.table("benunit"), frame.table("household") + ) + factors = after[clone_positions] / before[clone_positions] + income = np.linspace(10_000.0, 120_000.0, len(ANCHOR_PATTERN)) + pattern = np.asarray(ANCHOR_PATTERN) + for mask in ((pattern > 0.0) & (pattern <= 50.0), pattern < 0.0): + low_band = factors[mask & (income < 50_000.0)] + high_band = factors[mask & (income >= 50_000.0)] + assert low_band.size and high_band.size + # Incidence 0.5 against 0.2: the same scale, so factors sit 2.5x apart. + assert np.unique(np.round(low_band, 12)).size == 1 + assert np.unique(np.round(high_band, 12)).size == 1 + assert high_band[0] / low_band[0] == pytest.approx(2.5, rel=1e-9) + assert (factors[mask] < 1.0).all() + assert (factors[pattern > 50.0] == 1.0).all() + assert (factors[pattern == 0.0] == 1.0).all() + + +def test_anchor_leaves_a_group_already_below_its_target_untouched() -> None: + pattern = [60.0] * 10 + [20.0, -5.0] + weights = np.full(len(pattern), 10.0) + # A negligible sub-exempt clone sits under its target; a heavy loss-maker + # sits far above its own. + weights[10] = 1e-3 + weights[11] = 200.0 + frame = _anchor_input(pattern, weights=weights) + result = anchor_cgt_incidence( + frame, distribution=_anchor_distribution(), parameters=_parameters() + ) + + assert result.before["sub_exempt"] < result.targets["sub_exempt"] + assert result.after["sub_exempt"] == result.before["sub_exempt"] + assert result.scale["sub_exempt"] is None + assert result.before["loss"] > result.targets["loss"] + assert result.after["loss"] == pytest.approx(result.targets["loss"], rel=1e-9) + assert result.scale["loss"] is not None + assert result.trimmed_households == 1 + + +def test_anchor_is_deterministic_under_person_permutation() -> None: + permutation = np.random.default_rng(5).permutation(len(ANCHOR_PATTERN) * 4 + 4) + straight = anchor_cgt_incidence( + _anchor_input(ANCHOR_PATTERN), + distribution=_anchor_distribution(), + parameters=_parameters(), + ) + permuted = anchor_cgt_incidence( + _anchor_input(ANCHOR_PATTERN, permutation=permutation), + distribution=_anchor_distribution(), + parameters=_parameters(), + ) + pd.testing.assert_series_equal( + _weights_by_id(straight.frame).sort_index(), + _weights_by_id(permuted.frame).sort_index(), + rtol=0.0, + atol=1e-12, + ) + assert straight.after == pytest.approx(permuted.after, rel=1e-12) + assert straight.evidence()["targets"] == pytest.approx( + permuted.evidence()["targets"], rel=1e-12 + ) + + +def test_anchor_refuses_missing_liable_mass_and_stray_gains() -> None: + no_liable = _anchor_input([20.0, -5.0, 0.0, 20.0], donors=False) + with pytest.raises(ValueError, match="positive liable mass"): + anchor_cgt_incidence( + no_liable, distribution=_anchor_distribution(), parameters=_parameters() + ) + + frame = _anchor_input(ANCHOR_PATTERN) + person = frame.table("person").copy() + household = frame.table("household") + clone_id = household.loc[ + household[HOUSEHOLD_IS_CGT_CLONE] & ~household[HOUSEHOLD_IS_CGT_BAND_DONOR], + "household_id", + ].iloc[0] + stray = person["person_household_id"].eq(clone_id) & person["age"].eq(30) + person.loc[stray, "capital_gains"] = 1.0 + tainted = uk_national_frame( + person=person, + benunit=frame.table("benunit").copy(), + household=household.copy(), + time_period="2024", + weight_kind=WeightKind.IMPORTANCE, + household_weights=frame.weights_for("household").values, + mass_log=frame.mass_log, + ) + with pytest.raises(ValueError, match="only person with capital gains"): + anchor_cgt_incidence( + tainted, distribution=_anchor_distribution(), parameters=_parameters() + ) + with pytest.raises(ValueError, match="positive annual exempt amount"): + anchor_cgt_incidence( + frame, distribution=_anchor_distribution(), parameters=_parameters(0.0) + ) + + +def test_anchor_transform_locks_the_manifest_and_reports_evidence() -> None: + stage = _stage("cgt_incidence_anchor") + _assert_cgt_incidence_anchor_stage_parameters(stage) + operations = cgt_incidence_anchor_operation_parameters() + assert [operation.kind for operation in stage.operations] == [ + kind for kind, _ in operations + ] + for index, (_, parameters) in enumerate(operations): + for parameter in parameters: + with pytest.raises(ValueError, match="drifted"): + _assert_cgt_incidence_anchor_stage_parameters( + _drift(stage, index, parameter) + ) + with pytest.raises(ValueError, match="drifted"): + _assert_cgt_incidence_anchor_stage_parameters( + replace(stage, operations=(*stage.operations, stage.operations[0])) + ) + with pytest.raises(ValueError, match="no outputs"): + _assert_cgt_incidence_anchor_stage_parameters( + replace(stage, outputs=("capital_gains",)) + ) + with pytest.raises(ValueError, match="grain"): + _assert_cgt_incidence_anchor_stage_parameters(replace(stage, grain="person")) + assert stage.outputs == () and stage.rewrites == () + + transform = UKCGTIncidenceAnchorStageTransform( + stage=stage, distribution=_anchor_distribution(), parameters=_parameters() + ) + assert transform.output_columns() == () + with pytest.raises(RuntimeError, match="completed stage run"): + transform.checkpoint_metadata() + frame = _anchor_input(ANCHOR_PATTERN) + anchored = transform(frame) + assert anchored.weights_for("household").total == pytest.approx( + frame.weights_for("household").total, rel=1e-12 + ) + evidence = transform.checkpoint_metadata()["evidence"] + assert evidence["stage"] == CGT_INCIDENCE_ANCHOR_STAGE_NAME + assert evidence["annual_exempt_amount"] == 50.0 diff --git a/packages/microcosm-build/tests/test_uk_frs_spine.py b/packages/microcosm-build/tests/test_uk_frs_spine.py index 3cf68d376d..a81aa1afc9 100644 --- a/packages/microcosm-build/tests/test_uk_frs_spine.py +++ b/packages/microcosm-build/tests/test_uk_frs_spine.py @@ -2116,6 +2116,8 @@ def test_e8_manifest_seeds_all_reach_the_build_sidecar_harvester() -> None: "assign_residential_property_flag": 553, "assign_main_asset_type": 554, } + # The #970 incidence anchor is deterministic and consumes no seed. + assert "cgt_incidence_anchor" not in declared assert declared["salary_sacrifice"] == { "salary_sacrifice": 42, "salary_sacrifice_conversion": 2024, diff --git a/packages/microcosm-build/tests/test_uk_graph.py b/packages/microcosm-build/tests/test_uk_graph.py index 7ccdf43e9e..a7faa91ec9 100644 --- a/packages/microcosm-build/tests/test_uk_graph.py +++ b/packages/microcosm-build/tests/test_uk_graph.py @@ -163,11 +163,11 @@ def test_uk_spine_graph_contains_manifest_stages_and_named_exclusions() -> None: graph = uk_spine_graph(spec) ids = {node.id for node in graph.nodes} - # 30 with the #832 uc_reporter_redraw, #685 uc_deduction_attributes, - # #791 frs_relationships and #725 hmrc_cgt_asset_type_spine stages; the - # two named exclusions are the certified-pair alternatives, not steps of - # this pipeline. - assert len(expected) == 30 + # 31 with the #832 uc_reporter_redraw, #685 uc_deduction_attributes, + # #791 frs_relationships, #725 hmrc_cgt_asset_type_spine and #970 + # cgt_incidence_anchor stages; the two named exclusions are the + # certified-pair alternatives, not steps of this pipeline. + assert len(expected) == 31 assert UK_SPINE_EXCLUSIONS == { "frs_hmrc_retained_leaves", "hmrc_spi_income", @@ -477,6 +477,9 @@ def test_spi_support_channel_declares_its_mass_change_and_cgt_clones_conserve() assert graph.node("spi_support_channel").mass == "declared" assert graph.node("cgt_incidence_clone").mass == "conserve" assert graph.node("cgt_band_donors").mass == "free" + assert graph.node("cgt_incidence_anchor").mass == "conserve" + assert graph.node("cgt_incidence_anchor").params["expand_cells"] == () + assert "cgt_incidence_anchor.owned" not in {node.id for node in graph.nodes} @pytest.mark.requires_uk @@ -579,3 +582,94 @@ def test_driver_projects_a_stage_record_for_every_graph_stage_on_the_fixture( "household_id", ): assert root_shares[column] == 1.0 + + +def _zero_row_expand_node(node_id: str = "anchor") -> Node: + return Node( + id=node_id, + kernel="uk.stage.expand.test@1", + structural=StructuralDelta.EXPAND, + base="root", + params={ + "expand_cells": (), + "expand_weight_entity": "household", + "expand_weight_kind": "importance", + }, + mass="conserve", + ) + + +def _zero_row_expand_result(weights: list[float], *, total: float) -> KernelResult: + def empty(id_column: str) -> pd.Series: + return pd.Series( + [], + index=pd.Index([], name=id_column, dtype="int64"), + dtype="int64", + name=id_column, + ) + + return KernelResult( + expand={ + "person": empty("person_id"), + "benunit": empty("benunit_id"), + "household": empty("household_id"), + }, + columns={}, + weights=Weights(np.asarray(weights, dtype=np.float64), WeightKind.IMPORTANCE), + receipt={ + "frame_mass_log_append": [ + { + "entity": "household", + "old_total": total, + "new_total": total, + "declared_factor": 1.0, + "reason": "test anchor moves mass between paired households", + } + ] + }, + ) + + +def test_zero_row_expand_conserves_mass_through_the_executor() -> None: + """An EXPAND may add no rows and only move weight between incumbent rows. + + The #970 incidence anchor is such a stage: empty lineage, no owned cells, + and importance weights rescaled between a clone household and its paired + original. The executor's ``conserve`` ledger is stratum person mass, so + the pair must share a stratum and a size, which paired clones do because + they copy the same persons. + """ + + expanded = patch( + _expand_population(), + _zero_row_expand_node(), + _zero_row_expand_result([0.25, 2.75], total=3.0), + ) + + assert expanded.version == "anchor" + assert expanded.frame.table("person")["person_id"].tolist() == [1, 2] + assert expanded.frame.table("household")["household_id"].tolist() == [10, 20] + weights = expanded.frame.weights_for("household") + assert weights.kind is WeightKind.IMPORTANCE + np.testing.assert_array_equal(weights.values, np.array([0.25, 2.75])) + assert weights.total == 3.0 + record = expanded.mass_ledger[-1] + assert record.operation == "expand" + assert record.before_total == 3.0 + assert record.after_total == 3.0 + np.testing.assert_array_equal( + expanded.design_weights["household"], np.array([1.0, 2.0]) + ) + assert expanded.frame.mass_log[-1].declared_factor == 1.0 + + +def test_zero_row_expand_still_refuses_a_shift_across_household_sizes() -> None: + # Conservation stays on person mass: a zero-row EXPAND that moves weight + # between households of different size changes the stratum ledger and is + # rejected, so the anchor can only move mass within same-size pairs. + with pytest.raises(PopulationError, match="changed stratum"): + patch( + _mixed_size_population(), + _zero_row_expand_node(), + _zero_row_expand_result([0.5, 2.5], total=3.0), + ) diff --git a/packages/microcosm-build/tests/test_uk_release_assembler.py b/packages/microcosm-build/tests/test_uk_release_assembler.py index c20a96df04..cfe48ea321 100644 --- a/packages/microcosm-build/tests/test_uk_release_assembler.py +++ b/packages/microcosm-build/tests/test_uk_release_assembler.py @@ -91,7 +91,7 @@ def _frame(weights: list[float], *, weight_kind: WeightKind): ) -def _diagnostics(spine_sha256: str) -> dict: +def _diagnostics(spine_sha256: str, *, runtime: dict | None = None) -> dict: return { "schema_version": 6, "weight_entity": "household", @@ -178,7 +178,7 @@ def _diagnostics(spine_sha256: str) -> dict: "code_pin": _CODE_PIN, "build_id": _ATTEMPT_ID, "input_posture": {"tier": "staging_candidate", "sha256": spine_sha256}, - "runtime": dict(_SIGNED_RUNTIME), + "runtime": dict(_SIGNED_RUNTIME if runtime is None else runtime), }, } @@ -203,6 +203,7 @@ def _build_assembler_inputs( tmp_path: Path, *, spine_frame=None, + runtime=None, ): pytest.importorskip("tables") # pandas HDF backend pytest.importorskip("h5py") @@ -217,7 +218,7 @@ def _build_assembler_inputs( diagnostics_path = tmp_path / "calibration_diagnostics.json" diagnostics_path.write_text( - json.dumps(_diagnostics(sha256(spine))), encoding="utf-8" + json.dumps(_diagnostics(sha256(spine), runtime=runtime)), encoding="utf-8" ) diagnostics_sha = sha256(diagnostics_path) for report_name in ("seam_report_path", "release_cut_report_path"): @@ -588,6 +589,23 @@ def test_assemble_refuses_runtime_override_contradicting_provenance( ) +def test_assemble_refuses_unresolved_runtime_provenance( + green_certification_inputs, tmp_path: Path +) -> None: + """A seam that could not resolve the engine's version signs + ``unavailable``; the assembler refuses to pin a runtime it cannot + authenticate, the packaging backstop behind the certifier's own refusal + of a fence projected without an engine.""" + + inputs = _build_assembler_inputs( + green_certification_inputs, + tmp_path, + runtime={**_SIGNED_RUNTIME, "policyengine-uk": "unavailable"}, + ) + with pytest.raises(SystemExit, match="missing or unresolved"): + _load_driver_module().main(inputs["argv"]) + + def test_assemble_refuses_existing_destination(assembler_inputs, capsys) -> None: driver = _load_driver_module() assert driver.main(assembler_inputs["argv"]) == 0 diff --git a/packages/microcosm-build/tests/test_uk_release_certification.py b/packages/microcosm-build/tests/test_uk_release_certification.py index 6746d76e16..439e4ec843 100644 --- a/packages/microcosm-build/tests/test_uk_release_certification.py +++ b/packages/microcosm-build/tests/test_uk_release_certification.py @@ -20,7 +20,9 @@ UK_NATIONAL_GATE_SCOPE, UK_SHARED_GATE_IDS, UK_SPINE_GATE_SCOPE, + resign_uk_gate_report, ) +from microcosm.build.uk_runtime.cgt_projection import UK_CGT_PROJECTION_PINS_ENGINE from microcosm.build.uk_runtime.release_certification import ( UKReleaseCertificationError, compose_uk_release_certification, @@ -225,6 +227,52 @@ def test_compose_refuses_failing_release_blocking_entry(green_certification_inpu compose_uk_release_certification(**green_certification_inputs) +def _rewrite_seam_part(inputs, mutate): + """Mutate the seam part, re-sign it and keep the build record's byte pin + in step, so the refusal under test is the one that fires.""" + + seam_path = inputs["seam_report_path"] + payload = json.loads(seam_path.read_text(encoding="utf-8")) + mutate(payload) + resign_uk_gate_report(payload) + seam_path.write_text( + json.dumps(payload, indent=2, sort_keys=True), encoding="utf-8" + ) + inputs["build_record"]["artifacts"]["terminal_gate_json"]["sha256"] = _sha( + seam_path + ) + + +def test_compose_refuses_a_seam_fence_projected_from_the_pins( + green_certification_inputs, +): + """Without an installed engine the seam states the projection from the + manifest pins and the fence passes, drift-checked against itself; the + signed receipt says so, and that receipt can never certify a cut.""" + + def to_pins(payload): + payload["gates"]["uk_cgt_projection_entrants"]["details"][ + "projection_engine" + ] = UK_CGT_PROJECTION_PINS_ENGINE + + _rewrite_seam_part(green_certification_inputs, to_pins) + with pytest.raises( + UKReleaseCertificationError, match="projection_engine.*cannot certify" + ): + compose_uk_release_certification(**green_certification_inputs) + + +def test_compose_refuses_a_seam_fence_without_a_projection_source( + green_certification_inputs, +): + def drop_source(payload): + payload["gates"]["uk_cgt_projection_entrants"]["details"] = {} + + _rewrite_seam_part(green_certification_inputs, drop_source) + with pytest.raises(UKReleaseCertificationError, match="projection_engine"): + compose_uk_release_certification(**green_certification_inputs) + + def test_compose_refuses_sidecar_report_mismatch(green_certification_inputs): green_certification_inputs["spine_sidecar"]["spine_gate_report"]["sha256"] = ( "0" * 64 diff --git a/packages/microcosm-build/tests/test_uk_release_input_coverage.py b/packages/microcosm-build/tests/test_uk_release_input_coverage.py index e678211ceb..22c5b9cc10 100644 --- a/packages/microcosm-build/tests/test_uk_release_input_coverage.py +++ b/packages/microcosm-build/tests/test_uk_release_input_coverage.py @@ -620,6 +620,7 @@ def test_shipped_manifest_is_current(self) -> None: "cgt_band_donors", "hmrc_cgt_gains_spine", "hmrc_cgt_asset_type_spine", + "cgt_incidence_anchor", "salary_sacrifice", "student_loans", "was_wealth", diff --git a/packages/microcosm-build/tests/test_uk_source_stages.py b/packages/microcosm-build/tests/test_uk_source_stages.py index 0ce3817f51..6e81575c88 100644 --- a/packages/microcosm-build/tests/test_uk_source_stages.py +++ b/packages/microcosm-build/tests/test_uk_source_stages.py @@ -67,6 +67,7 @@ "cgt_band_donors", "hmrc_cgt_gains_spine", "hmrc_cgt_asset_type_spine", + "cgt_incidence_anchor", "salary_sacrifice", "student_loans", ] @@ -324,6 +325,7 @@ def test_country_stage_plan_assembles_spine_plan(self) -> None: "cgt_band_donors": _identity, "hmrc_cgt_gains_spine": _identity, "hmrc_cgt_asset_type_spine": _identity, + "cgt_incidence_anchor": _identity, "salary_sacrifice": _identity, "student_loans": _identity, "age_tail": _identity, diff --git a/packages/microcosm-build/tests/test_uk_spine_acceptance_receipt.py b/packages/microcosm-build/tests/test_uk_spine_acceptance_receipt.py index 11ba3c2d67..0cc86a3fbe 100644 --- a/packages/microcosm-build/tests/test_uk_spine_acceptance_receipt.py +++ b/packages/microcosm-build/tests/test_uk_spine_acceptance_receipt.py @@ -74,6 +74,11 @@ def _apply_pending_roster_transformations( # gains right after the amounts stage. assert "hmrc_cgt_asset_type_spine" not in roster roster.insert(roster.index("hmrc_cgt_gains_spine") + 1, "hmrc_cgt_asset_type_spine") + + # #970 re-mint pending: the incidence anchor moves non-liable clone mass + # back to the originals right after the asset-type stage. + assert "cgt_incidence_anchor" not in roster + roster.insert(roster.index("hmrc_cgt_asset_type_spine") + 1, "cgt_incidence_anchor") return tuple(roster) diff --git a/packages/microcosm-build/tests/test_uk_stage_health.py b/packages/microcosm-build/tests/test_uk_stage_health.py index eff64ec883..77120a409b 100644 --- a/packages/microcosm-build/tests/test_uk_stage_health.py +++ b/packages/microcosm-build/tests/test_uk_stage_health.py @@ -366,6 +366,13 @@ def evidence(error: float, released: float) -> dict: "ipf_zero_seed_cells": 0, }, "fallback_released_mass": released, + "remainder": { + "persons": 3, + "mass": 300.0, + "annual_exempt_amount": 3000.0, + "min_amount": 12.5, + "max_amount": 2990.0, + }, }, } @@ -392,6 +399,67 @@ def evidence(error: float, released: float) -> dict: ) +def test_cgt_summary_remainder_must_stay_inside_the_exempt_range() -> None: + """The sub-AEA remainder receipt (microcosm#970) is fenced on its range.""" + parameters = { + "stage": "hmrc_cgt_gains_spine", + "check": "cgt_imputation_summary", + "minimum_band_rows": 1, + } + + def evidence(remainder: dict) -> dict: + return { + "stage": "hmrc_cgt_gains_spine", + "rows": [{"gain_lower_bound": 12300.0}], + "taxpayer_mass": 1.0, + "published_taxpayer_mass": 1.0, + "remainder_mass": 0.0, + "allocation": { + "rake": { + "ipf_max_abs_margin_error": 0.0, + "gains_margin_max_abs_error": 0.0, + "ipf_zero_seed_cells": 0, + }, + "fallback_released_mass": 0.0, + "remainder": remainder, + }, + } + + def verdict(remainder: dict): + return uk_stage_health_gate( + evidence=evidence(remainder), + stage="hmrc_cgt_gains_spine", + check="cgt_imputation_summary", + parameters=parameters, + ) + + inside = { + "persons": 2, + "mass": 200.0, + "annual_exempt_amount": 3000.0, + "min_amount": 1.0, + "max_amount": 3000.0, + } + assert _passed(verdict(inside)) + assert not verdict({**inside, "max_amount": 3000.5}).passed + assert not verdict({**inside, "min_amount": 0.0}).passed + assert not verdict({**inside, "mass": -1.0}).passed + # An empty remainder carries zero amounts and passes. + assert _passed( + verdict( + { + "persons": 0, + "mass": 0.0, + "annual_exempt_amount": 3000.0, + "min_amount": 0.0, + "max_amount": 0.0, + } + ) + ) + with pytest.raises(ValueError): + verdict({**inside, "mass": float("nan")}) + + def test_cgt_summary_minimum_rows_parameter_is_live() -> None: evidence = { "stage": "hmrc_cgt_gains_spine", @@ -653,3 +721,101 @@ def test_cgt_asset_type_summary_holds_the_residential_realisation() -> None: evidence={"stage": "hmrc_cgt_asset_type_spine"}, parameters=parameters, ) + + +def _anchor_evidence(**overrides: object) -> dict[str, object]: + evidence: dict[str, object] = { + "stage": "cgt_incidence_anchor", + "liable_mass": 551_600.0, + "transferred_mass": 11_921_000.0, + "pair_count": 60_000, + "max_pair_relative_error": 0.0, + "targets": {"sub_exempt": 43_000.0, "loss": 136_000.0}, + "before": { + "sub_exempt": 10_800_000.0, + "loss": 1_300_000.0, + "liable": 159_000.0, + }, + "after": {"sub_exempt": 43_000.0, "loss": 136_000.0, "liable": 159_000.0}, + "mass_by_clone_flag": {"false": 40_000_000.0, "true": 338_000.0}, + } + evidence.update(overrides) + return evidence + + +_ANCHOR_PARAMETERS = { + "stage": "cgt_incidence_anchor", + "check": "cgt_incidence_anchor", + "maximum_relative_composition_error": 1e-6, + "maximum_pair_relative_error": 0.0, + "minimum_pair_count": 1, +} + + +def _anchor_gate(evidence: dict[str, object]): + return uk_stage_health_gate( + stage="cgt_incidence_anchor", + check="cgt_incidence_anchor", + evidence=evidence, + parameters=_ANCHOR_PARAMETERS, + ) + + +def test_cgt_incidence_anchor_gate_holds_the_composition_and_the_pairs() -> None: + passed = _anchor_gate(_anchor_evidence()) + assert passed.passed + assert passed.details["sub_exempt_relative_error"] == 0.0 + assert passed.details["liable_clone_mass"] == 159_000.0 + assert passed.details["pair_count"] == 60_000 + + # A group already at or below its target stays where it was. + untouched = _anchor_evidence( + targets={"sub_exempt": 43_000.0, "loss": 2_000_000.0}, + after={"sub_exempt": 43_000.0, "loss": 1_300_000.0, "liable": 159_000.0}, + transferred_mass=10_757_000.0, + ) + assert _anchor_gate(untouched).passed + + def fails(match: str, **overrides: object) -> None: + result = _anchor_gate(_anchor_evidence(**overrides)) + assert not result.passed + assert any(match in failure for failure in result.failures), result.failures + + fails( + "misses its target", + after={"sub_exempt": 43_100.0, "loss": 136_000.0, "liable": 159_000.0}, + transferred_mass=11_920_900.0, + ) + fails( + "liable clone mass moved", + after={"sub_exempt": 43_000.0, "loss": 136_000.0, "liable": 158_000.0}, + ) + fails("pair mass error", max_pair_relative_error=1e-9) + fails("pairs is below", pair_count=0) + fails( + "but moved to", + targets={"sub_exempt": 43_000.0, "loss": 2_000_000.0}, + after={"sub_exempt": 43_000.0, "loss": 1_200_000.0, "liable": 159_000.0}, + transferred_mass=10_857_000.0, + ) + fails( + "clone mass rose", + before={"sub_exempt": 40_000.0, "loss": 1_300_000.0, "liable": 159_000.0}, + transferred_mass=1_161_000.0, + ) + fails("disagrees with the transferred mass", transferred_mass=1.0) + fails("exceeds original mass", mass_by_clone_flag={"false": 1.0, "true": 2.0}) + fails("liable mass must be positive", liable_mass=0.0) + + with pytest.raises(ValueError, match="targets"): + _anchor_gate( + { + "stage": "cgt_incidence_anchor", + "liable_mass": 1.0, + "transferred_mass": 0.0, + "max_pair_relative_error": 0.0, + "pair_count": 1, + } + ) + with pytest.raises(ValueError, match="pair_count"): + _anchor_gate(_anchor_evidence(pair_count=1.5)) diff --git a/packages/microcosm-build/tests/uk_certification_fixtures.py b/packages/microcosm-build/tests/uk_certification_fixtures.py index d3d47c232e..8f7124d826 100644 --- a/packages/microcosm-build/tests/uk_certification_fixtures.py +++ b/packages/microcosm-build/tests/uk_certification_fixtures.py @@ -31,6 +31,10 @@ ) TEST_KEY = base64.b64encode(bytes(range(32))).decode("ascii") +#: The projection source the stub seam records on its fence: the certifier +#: requires an installed engine there. The version matches the runtime the +#: assembler fixture signs into its diagnostics. +STUB_PROJECTION_ENGINE = "policyengine-uk==2.89.0" @pytest.fixture(name="uk_certification_signing_key", autouse=True) @@ -46,9 +50,15 @@ def stub_registry(): for entry in spec.gates: parameter_keys.setdefault(entry.gate, set()).update(entry.parameters) + details_by_gate = { + "cgt_projection_entrants": {"projection_engine": STUB_PROJECTION_ENGINE}, + } + def passing(name): def gate(**_kwargs): - return GateResult(name=name, passed=True) + return GateResult( + name=name, passed=True, details=details_by_gate.get(name, {}) + ) return gate diff --git a/packages/microcosm-data/src/microcosm/data/contract.py b/packages/microcosm-data/src/microcosm/data/contract.py index a579dca158..c0b644c0ed 100644 --- a/packages/microcosm-data/src/microcosm/data/contract.py +++ b/packages/microcosm-data/src/microcosm/data/contract.py @@ -416,13 +416,13 @@ def compatibility_claim_declarer_error(declared_by: object) -> str | None: # fingerprint derives from the manifest digest. Editing the spec moves all # three here in the same reviewed change. _UK_GATE_BATTERY_POLICY_SHA256 = ( - "62bc37def863b68d5595dcd5ec8b6483d1fad5f0874b80073f5752e0a1a5ebff" + "1f3980db96a922ced81c89b63daae829f180984b1157a0a73cdc768c1ade7017" ) _UK_GATE_BATTERY_GATES_MANIFEST_SHA256 = ( - "4f2be1f12ebdbebd57b00dee355e27de0cac280be2b09e177cc82cc6afaf552e" + "c9141c748dac31d4a0f37d746a8a7639080cf9a80f8cc296aae8e7ef6bafbf79" ) _UK_GATE_BATTERY_SPEC_FINGERPRINT = ( - "775ba91346d29ce958cd82c0e459140f77d0ba00f695254f93ca02d8d7e52981" + "4e88c60ff7688a8528b793bee666ec669b871899f9d8eae7a6d19eb0b659ddd2" ) #: Spec entry id -> the legacy gate name whose observable detail checks #: apply unchanged (the battery re-keys the report by entry id; the gate @@ -491,6 +491,10 @@ def compatibility_claim_declarer_error(declared_by: object) -> str | None: "stage_health", "transferred", ), + "uk_stage_cgt_incidence_anchor_composition": ( + "stage_health", + "transferred", + ), "uk_stage_salary_sacrifice_realization": ( "stage_health", "transferred", @@ -526,6 +530,10 @@ def compatibility_claim_declarer_error(declared_by: object) -> str | None: ), "uk_target_surface": ("target_surface", "terminal"), "uk_target_fit": ("target_fit", "terminal"), + "uk_cgt_projection_entrants": ( + "cgt_projection_entrants", + "terminal", + ), "uk_input_mass_parity": ("input_mass_parity", "terminal"), "uk_qrf_tail_concentration": ("tail_concentration", "terminal"), "uk_local_geography_ladder_post_calibration": ( @@ -565,10 +573,12 @@ def compatibility_claim_declarer_error(declared_by: object) -> str | None: "uk_stage_cgt_band_donors_support", "uk_stage_hmrc_cgt_gains_spine_summary", "uk_stage_hmrc_cgt_asset_type_spine_summary", + "uk_stage_cgt_incidence_anchor_composition", "uk_stage_salary_sacrifice_realization", "uk_stage_student_loans_realization", "uk_stage_age_tail_targets", "uk_stage_frs_relationships_composition", + "uk_cgt_projection_entrants", } ) # The input-mass binding's evidence payload wraps the reviewed reference @@ -708,6 +718,7 @@ def compatibility_claim_declarer_error(declared_by: object) -> str | None: "uk_ons_household_type_enum_domain", "uk_stage_age_tail_targets", "uk_stage_cgt_band_donors_support", + "uk_stage_cgt_incidence_anchor_composition", "uk_stage_cgt_incidence_clone_mass", "uk_stage_etb_services_support", "uk_stage_etb_vat_support", @@ -729,6 +740,7 @@ def compatibility_claim_declarer_error(declared_by: object) -> str | None: { "uk_aggregate_admin", "uk_calibration_reference_coverage", + "uk_cgt_projection_entrants", "uk_target_fit", "uk_weight_ess", "uk_weight_ratio", @@ -763,18 +775,18 @@ def compatibility_claim_declarer_error(declared_by: object) -> str | None: _UK_CERTIFICATION_PART_DIGESTS: Mapping[str, Mapping[str, str]] = { "spine": { "gates_manifest_sha256": ( - "778f5d32d421c4fb2cc8c37ef4232093070d2606ec17bf9d4f7ee1c1e6de8b8d" + "59b636f960c2a9e4e6cfb7f989949b89c64330bf9f3784d897926e0a9c8dcea3" ), "policy_sha256": ( - "c59f645c51ec234e91bd582df2a1576116f2c67183c2cb015f51d3f1a2be9ea7" + "a638c43c4b486962fba6bb6a6ffab557765dce26981b06f066ffdee36616482c" ), }, "calibration_seam": { "gates_manifest_sha256": ( - "9ed1529c9c6e9ecaca6469d1fe0570930f0598b10002629ff89429334b13a726" + "35ab5136b190ea514e6d0b346d0d27e3c202576f74e3c7e2725abdea33f95cf9" ), "policy_sha256": ( - "eaaaacace07b4d282e1b7497f82f027daa9bb31aa7a6268063b05a069588e385" + "5a78ad115f958eb20247534ff9fd52e5d5d4f277b608d55ad4251d48a7232049" ), }, "release_cut": { diff --git a/packages/microcosm-data/tests/test_contract.py b/packages/microcosm-data/tests/test_contract.py index 0f25398c2f..9daf01574b 100644 --- a/packages/microcosm-data/tests/test_contract.py +++ b/packages/microcosm-data/tests/test_contract.py @@ -116,13 +116,13 @@ def _trusted_terminal_gate_signing_key(monkeypatch) -> None: UK_GATE_BATTERY_PRODUCER = "microcosm.build.gate_battery" UK_GATE_BATTERY_SIGNING_KEY_ENV = "MICROCOSM_UK_TERMINAL_GATE_SIGNING_KEY" UK_GATE_BATTERY_POLICY_SHA256 = ( - "62bc37def863b68d5595dcd5ec8b6483d1fad5f0874b80073f5752e0a1a5ebff" + "1f3980db96a922ced81c89b63daae829f180984b1157a0a73cdc768c1ade7017" ) UK_GATE_BATTERY_GATES_MANIFEST_SHA256 = ( - "4f2be1f12ebdbebd57b00dee355e27de0cac280be2b09e177cc82cc6afaf552e" + "c9141c748dac31d4a0f37d746a8a7639080cf9a80f8cc296aae8e7ef6bafbf79" ) UK_GATE_BATTERY_SPEC_FINGERPRINT = ( - "775ba91346d29ce958cd82c0e459140f77d0ba00f695254f93ca02d8d7e52981" + "4e88c60ff7688a8528b793bee666ec669b871899f9d8eae7a6d19eb0b659ddd2" ) UK_GATE_BATTERY_DEGENERATE_EVIDENCE_SHA256 = ( "6f0243bcda09dad26945376230c44ec3cf55d4e417c3a25e29bae8c59bc1a69d" @@ -203,6 +203,11 @@ def _trusted_terminal_gate_signing_key(monkeypatch) -> None: "transferred", None, ), + "uk_stage_cgt_incidence_anchor_composition": ( + "stage_health", + "transferred", + None, + ), "uk_stage_age_tail_targets": ("stage_health", "assembled", None), "uk_stage_frs_relationships_composition": ("stage_health", "assembled", None), "uk_ledger_compile_parity_local_incumbent_2025": ( @@ -267,6 +272,7 @@ def _trusted_terminal_gate_signing_key(monkeypatch) -> None: ), "uk_target_surface": ("target_surface", "terminal", "target_surface"), "uk_target_fit": ("target_fit", "terminal", "target_fit"), + "uk_cgt_projection_entrants": ("cgt_projection_entrants", "terminal", None), "uk_input_mass_parity": ("input_mass_parity", "terminal", "input_mass_parity"), "uk_qrf_tail_concentration": ( "tail_concentration", @@ -1181,6 +1187,7 @@ def _gate_battery_payload( "uk_stage_cgt_band_donors_support": "cgt_band_donors", "uk_stage_hmrc_cgt_gains_spine_summary": "hmrc_cgt_gains_spine", "uk_stage_hmrc_cgt_asset_type_spine_summary": "hmrc_cgt_asset_type_spine", + "uk_stage_cgt_incidence_anchor_composition": "cgt_incidence_anchor", "uk_stage_salary_sacrifice_realization": "salary_sacrifice", "uk_stage_student_loans_realization": "student_loans", "uk_stage_age_tail_targets": "age_tail", @@ -1226,6 +1233,12 @@ def _gate_battery_payload( } elif entry_id == "uk_calibration_reference_coverage": details = {"activated": 388, "resolved": 388, "matrix": 388} + elif entry_id == "uk_cgt_projection_entrants": + details = { + "max_entrants": 41_000.0, + "worst_year": 2030, + "bound": 73_000.0, + } elif entry_id.startswith("uk_local_"): # Local candidate gates are explicitly excluded from national # certification; this full-report fixture needs only their @@ -1294,6 +1307,9 @@ def _gate_battery_payload( ), "uk_degenerate_release_surface": UK_GATE_BATTERY_DEGENERATE_EVIDENCE_SHA256, "uk_input_mass_parity": UK_GATE_BATTERY_INPUT_MASS_EVIDENCE_SHA256, + "uk_cgt_projection_entrants": _canonical_sha256( + {"cgt_projection": {"base_year": 2023, "horizon_year": 2030}} + ), } for entry_id, stage in stage_health_stages.items(): evidence[entry_id] = _canonical_sha256({stage: {"stage": stage}}) diff --git a/packages/microcosm-graph/tests/fixtures/parity/uk_spine/PRODUCED_BY.txt b/packages/microcosm-graph/tests/fixtures/parity/uk_spine/PRODUCED_BY.txt index b7efdb8490..3a24f4b36b 100644 --- a/packages/microcosm-graph/tests/fixtures/parity/uk_spine/PRODUCED_BY.txt +++ b/packages/microcosm-graph/tests/fixtures/parity/uk_spine/PRODUCED_BY.txt @@ -1 +1 @@ -tools/graph_uk_spine_fixture.py; current 30-transform legacy StagePlan oracle with parsed private-source seams. The acceptance test runs both sides from frs_raw in-process (root weights differ by one ulp between machines); the captured root tables serve the unbound registry's data-only CREATE path. +tools/graph_uk_spine_fixture.py; current 31-transform legacy StagePlan oracle with parsed private-source seams. The acceptance test runs both sides from frs_raw in-process (root weights differ by one ulp between machines); the captured root tables serve the unbound registry's data-only CREATE path. diff --git a/packages/microcosm-graph/tests/fixtures/parity/uk_spine/sources/fixture.json b/packages/microcosm-graph/tests/fixtures/parity/uk_spine/sources/fixture.json index d6199ba23a..f5a0abf093 100644 --- a/packages/microcosm-graph/tests/fixtures/parity/uk_spine/sources/fixture.json +++ b/packages/microcosm-graph/tests/fixtures/parity/uk_spine/sources/fixture.json @@ -135,6 +135,94 @@ "stage": "cgt_band_donors", "survey": "HMRC Capital Gains Tax statistics Table 2.1a (2024-25 individuals) and Advani-Summers capital-gains incidence" }, + "cgt_incidence_anchor": { + "artifacts": [ + { + "format": "json", + "kind": "public_aggregate_reference", + "resource": "advani_summers_capital_gains_distribution.json", + "role": "capital_gains_incidence_and_quantiles", + "runtime_sha256_required": true + }, + { + "dependency": "policyengine-uk>=2.98.0 via microcosm-build[uk]", + "dependency_discipline": "deferred inside uk_cgt_policy_parameters; the base package never imports policyengine-uk at import time", + "instant_rule": "raw dated parameter files evaluated at 1 June of the build period's tax year", + "kind": "versioned_parameter_tree", + "parameters": [ + "gov.hmrc.cgt.annual_exempt_amount" + ], + "role": "policy_parameters", + "runtime_sha256_required": false + } + ], + "grain": "household", + "nonnegative_outputs": [], + "notes": "Spine-only weight anchor after the amounts and asset-type stages (microcosm#970). The equal-mass clone leaves half of all household mass on gainer households, so after the Table 3 redraw the clones below the exempt amount carry millions of weighted persons that HMRC publishes no count for; the anchor derives their level from the redrawn liable mass and the Advani-Summers reporter composition and moves the excess back to each clone's paired original, which is identical in every cell but the gains. No cell is written: amounts, flags, liable clones, zero-gain clone carriers and band donors are untouched; the groups, targets and receipt are clone-side quantities (band donors and originals are outside the anchor), every pair's mass and the household total are conserved, and the receipt is gated at stage time.", + "operations": [ + { + "clone_flag_column": "household_is_capital_gains_clone", + "donor_flag_column": "household_is_cgt_band_donor", + "id_remapping": "id_multiplier_for_values", + "kind": "pair_clone_households_to_originals", + "pairing": "clone household_id minus the clone stage's id multiplier, recomputed from the pre-clone rows (clone flag false and not a band donor)", + "requirement": "bijection between clone and original households; an unpaired clone or original fails the build" + }, + { + "adult_minimum_age": 16, + "aggregation": "crossings averaged over clone carriers weighted by household weight times the band's percent_with_gains", + "allowance_subtraction": false, + "carrier": "oldest adult; person_id ascending breaks age ties", + "crossings": "each band's degree-1 quantile spline inverted at zero and at the build period's annual exempt amount", + "extrapolation": "ext=0", + "income_proxy_components": [ + "employment_income", + "self_employment_income", + "state_pension_reported", + "private_pension_income", + "property_income", + "savings_interest_income", + "dividend_income", + "miscellaneous_income" + ], + "kind": "derive_reporter_composition_from_banded_quantiles", + "liable_mass": "weighted persons with capital_gains above the annual exempt amount on every household after the Table 3 redraw, clones and band donors alike", + "quantile_points": [ + 0.05, + 0.1, + 0.25, + 0.5, + 0.75, + 0.9, + 0.95 + ], + "resource": "advani_summers_capital_gains_distribution.json", + "spline_degree": 1, + "targets": "sub_exempt = liable_mass * (exempt_quantile - zero_quantile) / (1 - exempt_quantile); loss = liable_mass * zero_quantile / (1 - exempt_quantile)" + }, + { + "conservation": "household_total_to_rounding", + "declared_factor": 1, + "donors": "untouched", + "factor": "min(maximum_factor, scale * percent_with_gains) with scale solved per group by bisection so the group's mass equals its target; a group at or below its target is untouched", + "kind": "transfer_clone_mass_to_originals", + "liable_clones": "untouched", + "loss_group": "clone households whose carrier's capital_gains are negative", + "maximum_factor": 1, + "pair_conservation": "to_rounding", + "population": "clone households paired to their originals; band donors and originals are outside the anchor, so the groups, targets and masses are clone-side quantities, not population counts", + "reason": "Capital-gains incidence anchor moves the mass of non-liable clone households back to their paired originals until the sub-exempt and loss-making clone mass match the Advani-Summers reporter composition at the redrawn liable mass; every pair's mass and the total household mass are conserved.", + "sub_exempt_group": "clone households whose carrier's capital_gains lie in (0, annual exempt amount]; a carrier with exactly zero gains is no reporter and is left untouched", + "transfer": "each clone's removed mass is added to its paired original, so every pair's mass is conserved to rounding; no global exact-total correction is applied, so no household outside a trimmed pair moves", + "weight_kind_out": "importance" + } + ], + "outputs": [], + "rewrites": [], + "source": "Advani and Summers (2020), Capital Gains and UK Inequality, CAGE Working Paper 465", + "stage": "cgt_incidence_anchor", + "survey": "Family Resources Survey 2024-25, SPI synthetic support, the HMRC Table 3 redrawn gains on the spine and Advani-Summers capital-gains incidence" + }, "cgt_incidence_clone": { "artifacts": [ { @@ -1934,6 +2022,13 @@ "resource": "hmrc_cgt_conditioning_facts.json", "role": "cgt_conditioning_facts", "runtime_sha256_required": true + }, + { + "format": "json", + "kind": "public_aggregate_reference", + "resource": "advani_summers_capital_gains_distribution.json", + "role": "capital_gains_within_band_distribution", + "runtime_sha256_required": true } ], "grain": "person", @@ -2050,9 +2145,27 @@ "seed_mixing": "seed combined with the build period; each walk draws its rounding offset in cell order then pooled-fallback order, then each plan draws its quantile jitter, income bands ascending and gain bands highest first" }, { + "banding": "Advani-Summers total-income bands at the person's component-sum income without allowance subtraction, the banding the prior draw uses", + "deterministic": true, + "extrapolation": "ext=0", "kind": "sub_aea_remainder", - "policy": "gainers beyond the published taxpayer mass keep their existing amounts capped at the annual exempt amount", - "rationale": "Table 3 covers only individuals with a CGT liability; remaining gainers are treated as sub-AEA gainers rather than invented into the liability distribution or deleted." + "mapping": "remainder gainers take the midpoints of the weight-proportional quantile strata that partition the range, evaluated on the band's spline; zero-weight persons take a nominal stratum of the band's smallest positive weight and carry no mass; no seeded draw is consumed, so the walk and plan draw order is unchanged", + "ordering": "existing gains ascending, person_id descending on ties (the allocation ranking reversed, as the within-band draws order their strata)", + "policy": "gainers beyond the published taxpayer mass take amounts from the Advani-Summers within-band gains distribution restricted to (0, annual exempt amount], rank-preserving within each Advani-Summers total-income band", + "quantile_points": [ + 0.05, + 0.1, + 0.25, + 0.5, + 0.75, + 0.9, + 0.95 + ], + "range": "quantiles strictly between the band's degree-1 quantile spline zero crossing and its crossing of the build period's annual exempt amount, so every amount lies in (0, AEA]", + "rationale": "Table 3 covers only individuals with a CGT liability; the remaining gainers are sub-AEA gainers, so their amounts come from the only published within-band distribution that reaches below the exempt amount, the surface the spine's gainer set was drawn from, rather than from capping the candidate's amounts, and none is invented into the liability distribution or deleted (microcosm#970).", + "resource": "advani_summers_capital_gains_distribution.json", + "resource_vintage": "2017-18 nominal pounds as published (Advani and Summers 2020, CAGE WP 465, Table A1); no uprating is applied", + "spline_degree": 1 }, { "declared_factor": 1, diff --git a/packages/microcosm-graph/tests/fixtures/parity/uk_spine/uk_spine.json b/packages/microcosm-graph/tests/fixtures/parity/uk_spine/uk_spine.json index bbf8718062..2b14f6ca32 100644 --- a/packages/microcosm-graph/tests/fixtures/parity/uk_spine/uk_spine.json +++ b/packages/microcosm-graph/tests/fixtures/parity/uk_spine/uk_spine.json @@ -1 +1 @@ -{"country":"uk","nodes":[{"base":null,"citation":"","description":"Load the source-bound UK FRS root population.","id":"create_uk_frs","inputs":[],"kernel":"uk.create@1","mass":"conserve","outputs":[{"column":"age","dtype":"int64","entity":"person","ownership":"produced","rows":"all"},{"column":"gender","dtype":"string","entity":"person","ownership":"produced","rows":"all"},{"column":"marital_status","dtype":"string","entity":"person","ownership":"produced","rows":"all"},{"column":"hours_worked","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"care_hours","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"is_household_head","dtype":"bool","entity":"person","ownership":"produced","rows":"all"},{"column":"is_benunit_head","dtype":"bool","entity":"person","ownership":"produced","rows":"all"},{"column":"is_parent","dtype":"bool","entity":"person","ownership":"produced","rows":"all"},{"column":"is_uc_claimant","dtype":"bool","entity":"person","ownership":"produced","rows":"all"},{"column":"employment_income","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"self_employment_income","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"private_pension_income","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"tax_free_savings_income","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"savings_interest_income","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"dividend_income","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"property_income","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"maintenance_income","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"miscellaneous_income","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"private_transfer_income","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"lump_sum_income","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"student_loan_repayments","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"statutory_sick_pay","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"statutory_maternity_pay","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"student_loans","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"access_fund","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"education_grants","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"healthy_start_vouchers","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"free_school_breakfasts","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"free_school_fruit_veg","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"free_school_meals","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"council_tax_benefit_reported","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"maintenance_expenses","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"childcare_expenses","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"personal_pension_contributions","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"employee_pension_contributions","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"pension_contributions_via_salary_sacrifice","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"salary_sacrifice_reported","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"salary_sacrifice_asked","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"child_benefit_reported","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"income_support_reported","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"housing_benefit_reported","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"attendance_allowance_reported","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"dla_sc_reported","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"dla_m_reported","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"iidb_reported","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"carers_allowance_reported","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"would_claim_carers_allowance","dtype":"bool","entity":"person","ownership":"produced","rows":"all"},{"column":"sda_reported","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"afcs_reported","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"ssmg_reported","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"pension_credit_reported","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"child_tax_credit_reported","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"working_tax_credit_reported","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"state_pension_reported","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"winter_fuel_allowance_reported","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"incapacity_benefit_reported","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"universal_credit_reported","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"pip_m_reported","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"pip_dl_reported","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"jsa_contrib_reported","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"jsa_income_reported","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"esa_contrib_reported","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"esa_income_reported","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"bsp_reported","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"frs_benunit_capital","dtype":"float64","entity":"benunit","ownership":"produced","rows":"all"},{"column":"is_married","dtype":"bool","entity":"benunit","ownership":"produced","rows":"all"},{"column":"dependent_children","dtype":"int64","entity":"benunit","ownership":"produced","rows":"all"},{"column":"region","dtype":"string","entity":"household","ownership":"produced","rows":"all"},{"column":"tenure_type","dtype":"string","entity":"household","ownership":"produced","rows":"all"},{"column":"accommodation_type","dtype":"string","entity":"household","ownership":"produced","rows":"all"},{"column":"num_bedrooms","dtype":"int64","entity":"household","ownership":"produced","rows":"all"},{"column":"council_tax_reported","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"council_tax_band","dtype":"string","entity":"household","ownership":"produced","rows":"all"},{"column":"council_tax_rebate","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"council_tax_single_adult_raw","dtype":"int64","entity":"household","ownership":"produced","rows":"all"},{"column":"water_and_sewerage_charges","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"domestic_rates","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"rent","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"subrent","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"mortgage_interest_repayment","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"mortgage_capital_repayment","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"structural_insurance_payments","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"housing_service_charges","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"external_child_payments","dtype":"float64","entity":"household","ownership":"produced","rows":"all"}],"params":{"sample_fraction":1.0,"sample_seed":578,"stage_contract_sha256":"047d30937efb21e39259fdbed5597751e4f5a0c341ea583c7446e4c9c11eba48","time_period":"2024"},"population":null,"sources":["frs"],"structural":"create","weights":null},{"base":"create_uk_frs","citation":"","description":"Ownership boundary for the source-assembling root stage.","id":"frs_spine.boundary","inputs":[{"columns":["frs_benunit_capital","is_married","dependent_children"],"entity":"benunit","rows":"all"},{"columns":["region","tenure_type","accommodation_type","num_bedrooms","council_tax_reported","council_tax_band","council_tax_rebate","council_tax_single_adult_raw","water_and_sewerage_charges","domestic_rates","rent","subrent","mortgage_interest_repayment","mortgage_capital_repayment","structural_insurance_payments","housing_service_charges","external_child_payments"],"entity":"household","rows":"all"},{"columns":["age","gender","marital_status","hours_worked","care_hours","is_household_head","is_benunit_head","is_parent","is_uc_claimant","employment_income","self_employment_income","private_pension_income","tax_free_savings_income","savings_interest_income","dividend_income","property_income","maintenance_income","miscellaneous_income","private_transfer_income","lump_sum_income","student_loan_repayments","statutory_sick_pay","statutory_maternity_pay","student_loans","access_fund","education_grants","healthy_start_vouchers","free_school_breakfasts","free_school_fruit_veg","free_school_meals","council_tax_benefit_reported","maintenance_expenses","childcare_expenses","personal_pension_contributions","employee_pension_contributions","pension_contributions_via_salary_sacrifice","salary_sacrifice_reported","salary_sacrifice_asked","child_benefit_reported","income_support_reported","housing_benefit_reported","attendance_allowance_reported","dla_sc_reported","dla_m_reported","iidb_reported","carers_allowance_reported","would_claim_carers_allowance","sda_reported","afcs_reported","ssmg_reported","pension_credit_reported","child_tax_credit_reported","working_tax_credit_reported","state_pension_reported","winter_fuel_allowance_reported","incapacity_benefit_reported","universal_credit_reported","pip_m_reported","pip_dl_reported","jsa_contrib_reported","jsa_income_reported","esa_contrib_reported","esa_income_reported","bsp_reported"],"entity":"person","rows":"all"}],"kernel":"uk.identity@1","mass":"conserve","outputs":[],"params":{},"population":null,"sources":[],"structural":"filter","weights":null},{"base":null,"citation":"","description":"Claim the cells assembled by the UK FRS root transform.","id":"frs_spine","inputs":[],"kernel":"uk.claim@1","mass":"conserve","outputs":[{"column":"age","dtype":"int64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"gender","dtype":"string","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"marital_status","dtype":"string","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"hours_worked","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"care_hours","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"is_household_head","dtype":"bool","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"is_benunit_head","dtype":"bool","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"is_parent","dtype":"bool","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"is_uc_claimant","dtype":"bool","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"employment_income","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"self_employment_income","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"private_pension_income","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"tax_free_savings_income","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"savings_interest_income","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"dividend_income","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"property_income","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"maintenance_income","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"miscellaneous_income","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"private_transfer_income","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"lump_sum_income","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"student_loan_repayments","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"statutory_sick_pay","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"statutory_maternity_pay","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"student_loans","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"access_fund","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"education_grants","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"healthy_start_vouchers","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"free_school_breakfasts","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"free_school_fruit_veg","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"free_school_meals","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"council_tax_benefit_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"maintenance_expenses","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"childcare_expenses","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"personal_pension_contributions","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"employee_pension_contributions","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"pension_contributions_via_salary_sacrifice","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"salary_sacrifice_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"salary_sacrifice_asked","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"child_benefit_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"income_support_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"housing_benefit_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"attendance_allowance_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"dla_sc_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"dla_m_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"iidb_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"carers_allowance_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"would_claim_carers_allowance","dtype":"bool","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"sda_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"afcs_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"ssmg_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"pension_credit_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"child_tax_credit_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"working_tax_credit_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"state_pension_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"winter_fuel_allowance_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"incapacity_benefit_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"universal_credit_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"pip_m_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"pip_dl_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"jsa_contrib_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"jsa_income_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"esa_contrib_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"esa_income_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"bsp_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"frs_benunit_capital","dtype":"float64","entity":"benunit","ownership":"produced","rewrite":true,"rows":"all"},{"column":"is_married","dtype":"bool","entity":"benunit","ownership":"produced","rewrite":true,"rows":"all"},{"column":"dependent_children","dtype":"int64","entity":"benunit","ownership":"produced","rewrite":true,"rows":"all"},{"column":"region","dtype":"string","entity":"household","ownership":"produced","rewrite":true,"rows":"all"},{"column":"tenure_type","dtype":"string","entity":"household","ownership":"produced","rewrite":true,"rows":"all"},{"column":"accommodation_type","dtype":"string","entity":"household","ownership":"produced","rewrite":true,"rows":"all"},{"column":"num_bedrooms","dtype":"int64","entity":"household","ownership":"produced","rewrite":true,"rows":"all"},{"column":"council_tax_reported","dtype":"float64","entity":"household","ownership":"produced","rewrite":true,"rows":"all"},{"column":"council_tax_band","dtype":"string","entity":"household","ownership":"produced","rewrite":true,"rows":"all"},{"column":"council_tax_rebate","dtype":"float64","entity":"household","ownership":"produced","rewrite":true,"rows":"all"},{"column":"council_tax_single_adult_raw","dtype":"int64","entity":"household","ownership":"produced","rewrite":true,"rows":"all"},{"column":"water_and_sewerage_charges","dtype":"float64","entity":"household","ownership":"produced","rewrite":true,"rows":"all"},{"column":"domestic_rates","dtype":"float64","entity":"household","ownership":"produced","rewrite":true,"rows":"all"},{"column":"rent","dtype":"float64","entity":"household","ownership":"produced","rewrite":true,"rows":"all"},{"column":"subrent","dtype":"float64","entity":"household","ownership":"produced","rewrite":true,"rows":"all"},{"column":"mortgage_interest_repayment","dtype":"float64","entity":"household","ownership":"produced","rewrite":true,"rows":"all"},{"column":"mortgage_capital_repayment","dtype":"float64","entity":"household","ownership":"produced","rewrite":true,"rows":"all"},{"column":"structural_insurance_payments","dtype":"float64","entity":"household","ownership":"produced","rewrite":true,"rows":"all"},{"column":"housing_service_charges","dtype":"float64","entity":"household","ownership":"produced","rewrite":true,"rows":"all"},{"column":"external_child_payments","dtype":"float64","entity":"household","ownership":"produced","rewrite":true,"rows":"all"}],"params":{},"population":"frs_spine.boundary","sources":[],"structural":"none","weights":null},{"base":"frs_spine.boundary","citation":"","description":"Ownership boundary before age_tail rewrites.","id":"age_tail.boundary","inputs":[{"columns":["frs_benunit_capital","is_married","dependent_children"],"entity":"benunit","rows":"all"},{"columns":["region","tenure_type","accommodation_type","num_bedrooms","council_tax_reported","council_tax_band","council_tax_rebate","council_tax_single_adult_raw","water_and_sewerage_charges","domestic_rates","rent","subrent","mortgage_interest_repayment","mortgage_capital_repayment","structural_insurance_payments","housing_service_charges","external_child_payments"],"entity":"household","rows":"all"},{"columns":["age","gender","marital_status","hours_worked","care_hours","is_household_head","is_benunit_head","is_parent","is_uc_claimant","employment_income","self_employment_income","private_pension_income","tax_free_savings_income","savings_interest_income","dividend_income","property_income","maintenance_income","miscellaneous_income","private_transfer_income","lump_sum_income","student_loan_repayments","statutory_sick_pay","statutory_maternity_pay","student_loans","access_fund","education_grants","healthy_start_vouchers","free_school_breakfasts","free_school_fruit_veg","free_school_meals","council_tax_benefit_reported","maintenance_expenses","childcare_expenses","personal_pension_contributions","employee_pension_contributions","pension_contributions_via_salary_sacrifice","salary_sacrifice_reported","salary_sacrifice_asked","child_benefit_reported","income_support_reported","housing_benefit_reported","attendance_allowance_reported","dla_sc_reported","dla_m_reported","iidb_reported","carers_allowance_reported","would_claim_carers_allowance","sda_reported","afcs_reported","ssmg_reported","pension_credit_reported","child_tax_credit_reported","working_tax_credit_reported","state_pension_reported","winter_fuel_allowance_reported","incapacity_benefit_reported","universal_credit_reported","pip_m_reported","pip_dl_reported","jsa_contrib_reported","jsa_income_reported","esa_contrib_reported","esa_income_reported","bsp_reported"],"entity":"person","rows":"all"}],"kernel":"uk.identity@1","mass":"conserve","outputs":[],"params":{},"population":null,"sources":[],"structural":"filter","weights":null},{"base":null,"citation":"","description":"Run UK spine stage age_tail.","id":"age_tail","inputs":[{"columns":["frs_benunit_capital"],"entity":"benunit","rows":"all"},{"columns":["external_child_payments","region"],"entity":"household","rows":"all"},{"columns":["gender"],"entity":"person","rows":"all"}],"kernel":"uk.stage.age_tail@1","mass":"conserve","outputs":[{"column":"age","dtype":"int64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"}],"params":{"stage":"age_tail","stage_contract_sha256":"963a34f38caede2525e3b8735f904bb70a1bbef5a272f0eefd4346343444484f","time_period":"2024"},"population":"age_tail.boundary","sources":["frs"],"structural":"none","weights":null},{"base":null,"citation":"","description":"Run UK spine stage frs_relationships.","id":"frs_relationships","inputs":[{"columns":["frs_benunit_capital"],"entity":"benunit","rows":"all"},{"columns":["region"],"entity":"household","rows":"all"},{"columns":["age","is_household_head"],"entity":"person","rows":"all"}],"kernel":"uk.stage.frs_relationships@1","mass":"conserve","outputs":[{"column":"relationship_to_head","dtype":"string","entity":"person","ownership":"produced","rows":"all"},{"column":"ons_family_role","dtype":"string","entity":"person","ownership":"produced","rows":"all"},{"column":"ons_family_index","dtype":"int64","entity":"person","ownership":"produced","rows":"all"},{"column":"ons_household_type","dtype":"string","entity":"household","ownership":"produced","rows":"all"}],"params":{"stage":"frs_relationships","stage_contract_sha256":"30628808d06d9829ebcbcf460cd355a01261e78c7c73e6006c75bec2e3d60c33","time_period":"2024"},"population":"age_tail.boundary","sources":["frs"],"structural":"none","weights":null},{"base":null,"citation":"","description":"Run UK spine stage frs_employment.","id":"frs_employment","inputs":[{"columns":["frs_benunit_capital"],"entity":"benunit","rows":"all"},{"columns":["ons_household_type","region"],"entity":"household","rows":"all"},{"columns":["age"],"entity":"person","rows":"all"}],"kernel":"uk.stage.frs_employment@1","mass":"conserve","outputs":[{"column":"employment_status","dtype":"string","entity":"person","ownership":"produced","rows":"all"},{"column":"employment_sector","dtype":"string","entity":"person","ownership":"produced","rows":"all"},{"column":"sic_industry_division","dtype":"int64","entity":"person","ownership":"produced","rows":"all"}],"params":{"stage":"frs_employment","stage_contract_sha256":"ddbefaf05b44788d794a6e4b0e8926c14318d66e50d2f11f50ca549538bbf60c","time_period":"2024"},"population":"age_tail.boundary","sources":["frs"],"structural":"none","weights":null},{"base":null,"citation":"","description":"Run UK spine stage frs_council_tax.","id":"frs_council_tax","inputs":[{"columns":["frs_benunit_capital"],"entity":"benunit","rows":"all"},{"columns":["region"],"entity":"household","rows":"all"},{"columns":["age","sic_industry_division"],"entity":"person","rows":"all"}],"kernel":"uk.stage.frs_council_tax@1","mass":"conserve","outputs":[{"column":"council_tax","dtype":"float64","entity":"household","ownership":"produced","rows":"all"}],"params":{"stage":"frs_council_tax","stage_contract_sha256":"3381cd9f7a736514d5073c57480e07f5098890f3ca9ed3865392043594771eb9","time_period":"2024"},"population":"age_tail.boundary","sources":["frs"],"structural":"none","weights":null},{"base":null,"citation":"","description":"Run UK spine stage frs_disability.","id":"frs_disability","inputs":[{"columns":["frs_benunit_capital"],"entity":"benunit","rows":"all"},{"columns":["council_tax","region"],"entity":"household","rows":"all"},{"columns":["afcs_reported","age","attendance_allowance_reported","dla_m_reported","dla_sc_reported","esa_contrib_reported","esa_income_reported","iidb_reported","incapacity_benefit_reported","pip_dl_reported","pip_m_reported","sda_reported"],"entity":"person","rows":"all"}],"kernel":"uk.stage.frs_disability@1","mass":"conserve","outputs":[{"column":"aa_category","dtype":"string","entity":"person","ownership":"produced","rows":"all"},{"column":"dla_sc_category","dtype":"string","entity":"person","ownership":"produced","rows":"all"},{"column":"dla_m_category","dtype":"string","entity":"person","ownership":"produced","rows":"all"},{"column":"pip_m_category","dtype":"string","entity":"person","ownership":"produced","rows":"all"},{"column":"pip_dl_category","dtype":"string","entity":"person","ownership":"produced","rows":"all"},{"column":"is_disabled_for_benefits","dtype":"bool","entity":"person","ownership":"produced","rows":"all"},{"column":"is_enhanced_disabled_for_benefits","dtype":"bool","entity":"person","ownership":"produced","rows":"all"},{"column":"is_severely_disabled_for_benefits","dtype":"bool","entity":"person","ownership":"produced","rows":"all"}],"params":{"stage":"frs_disability","stage_contract_sha256":"ac500b4cb1bc04d4fb72d198592dd376d61da8c150921a6ac53914ac18be0508","time_period":"2024"},"population":"age_tail.boundary","sources":["frs"],"structural":"none","weights":null},{"base":null,"citation":"","description":"Run UK spine stage frs_education.","id":"frs_education","inputs":[{"columns":["frs_benunit_capital"],"entity":"benunit","rows":"all"},{"columns":["region"],"entity":"household","rows":"all"},{"columns":["age","esa_contrib_reported","esa_income_reported","is_severely_disabled_for_benefits","jsa_contrib_reported","jsa_income_reported","universal_credit_reported"],"entity":"person","rows":"all"}],"kernel":"uk.stage.frs_education@1","mass":"conserve","outputs":[{"column":"current_education","dtype":"string","entity":"person","ownership":"produced","rows":"all"},{"column":"highest_education","dtype":"string","entity":"person","ownership":"produced","rows":"all"},{"column":"is_in_non_advanced_education","dtype":"bool","entity":"person","ownership":"produced","rows":"all"},{"column":"is_in_approved_training","dtype":"bool","entity":"person","ownership":"produced","rows":"all"},{"column":"age_started_or_accepted_current_education_or_training","dtype":"int64","entity":"person","ownership":"produced","rows":"all"},{"column":"is_before_universal_credit_qualifying_young_person_terminal_date","dtype":"bool","entity":"person","ownership":"produced","rows":"all"},{"column":"adult_ema","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"child_ema","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"receives_benefits_in_own_right","dtype":"bool","entity":"person","ownership":"produced","rows":"all"}],"params":{"stage":"frs_education","stage_contract_sha256":"836cb0f5a582fee1425190e96c9cb81bdef859bd236c8b6bc27660b6e3d0c2f8","time_period":"2024"},"population":"age_tail.boundary","sources":["frs"],"structural":"none","weights":null},{"base":null,"citation":"","description":"Run UK spine stage frs_legacy_proxies.","id":"frs_legacy_proxies","inputs":[{"columns":["frs_benunit_capital","is_married","dependent_children"],"entity":"benunit","rows":"all"},{"columns":["region","tenure_type","accommodation_type","num_bedrooms","council_tax_reported","council_tax_band","council_tax_rebate","council_tax_single_adult_raw","water_and_sewerage_charges","domestic_rates","rent","subrent","mortgage_interest_repayment","mortgage_capital_repayment","structural_insurance_payments","housing_service_charges","external_child_payments","ons_household_type","council_tax"],"entity":"household","rows":"all"},{"columns":["age","gender","marital_status","hours_worked","care_hours","is_household_head","is_benunit_head","is_parent","is_uc_claimant","employment_income","self_employment_income","private_pension_income","tax_free_savings_income","savings_interest_income","dividend_income","property_income","maintenance_income","miscellaneous_income","private_transfer_income","lump_sum_income","student_loan_repayments","statutory_sick_pay","statutory_maternity_pay","student_loans","access_fund","education_grants","healthy_start_vouchers","free_school_breakfasts","free_school_fruit_veg","free_school_meals","council_tax_benefit_reported","maintenance_expenses","childcare_expenses","personal_pension_contributions","employee_pension_contributions","pension_contributions_via_salary_sacrifice","salary_sacrifice_reported","salary_sacrifice_asked","child_benefit_reported","income_support_reported","housing_benefit_reported","attendance_allowance_reported","dla_sc_reported","dla_m_reported","iidb_reported","carers_allowance_reported","would_claim_carers_allowance","sda_reported","afcs_reported","ssmg_reported","pension_credit_reported","child_tax_credit_reported","working_tax_credit_reported","state_pension_reported","winter_fuel_allowance_reported","incapacity_benefit_reported","universal_credit_reported","pip_m_reported","pip_dl_reported","jsa_contrib_reported","jsa_income_reported","esa_contrib_reported","esa_income_reported","bsp_reported","relationship_to_head","ons_family_role","ons_family_index","employment_status","employment_sector","sic_industry_division","aa_category","dla_sc_category","dla_m_category","pip_m_category","pip_dl_category","is_disabled_for_benefits","is_enhanced_disabled_for_benefits","is_severely_disabled_for_benefits","current_education","highest_education","is_in_non_advanced_education","is_in_approved_training","age_started_or_accepted_current_education_or_training","is_before_universal_credit_qualifying_young_person_terminal_date","adult_ema","child_ema","receives_benefits_in_own_right"],"entity":"person","rows":"all"}],"kernel":"uk.stage.frs_legacy_proxies@1","mass":"conserve","outputs":[{"column":"legacy_jobseeker_proxy","dtype":"bool","entity":"person","ownership":"produced","rows":"all"},{"column":"esa_health_condition_proxy","dtype":"bool","entity":"person","ownership":"produced","rows":"all"},{"column":"esa_support_group_proxy","dtype":"bool","entity":"person","ownership":"produced","rows":"all"}],"params":{"stage":"frs_legacy_proxies","stage_contract_sha256":"1ecf761cf3fa7180da15659e138e67a8654e58272b4fde28344aa27a874ad0aa","time_period":"2024"},"population":"age_tail.boundary","sources":["frs"],"structural":"none","weights":null},{"base":"age_tail.boundary","citation":"","description":"Ownership boundary before frs_education_grant_split rewrites.","id":"frs_education_grant_split.boundary","inputs":[{"columns":["frs_benunit_capital","is_married","dependent_children"],"entity":"benunit","rows":"all"},{"columns":["region","tenure_type","accommodation_type","num_bedrooms","council_tax_reported","council_tax_band","council_tax_rebate","council_tax_single_adult_raw","water_and_sewerage_charges","domestic_rates","rent","subrent","mortgage_interest_repayment","mortgage_capital_repayment","structural_insurance_payments","housing_service_charges","external_child_payments","ons_household_type","council_tax"],"entity":"household","rows":"all"},{"columns":["age","gender","marital_status","hours_worked","care_hours","is_household_head","is_benunit_head","is_parent","is_uc_claimant","employment_income","self_employment_income","private_pension_income","tax_free_savings_income","savings_interest_income","dividend_income","property_income","maintenance_income","miscellaneous_income","private_transfer_income","lump_sum_income","student_loan_repayments","statutory_sick_pay","statutory_maternity_pay","student_loans","access_fund","education_grants","healthy_start_vouchers","free_school_breakfasts","free_school_fruit_veg","free_school_meals","council_tax_benefit_reported","maintenance_expenses","childcare_expenses","personal_pension_contributions","employee_pension_contributions","pension_contributions_via_salary_sacrifice","salary_sacrifice_reported","salary_sacrifice_asked","child_benefit_reported","income_support_reported","housing_benefit_reported","attendance_allowance_reported","dla_sc_reported","dla_m_reported","iidb_reported","carers_allowance_reported","would_claim_carers_allowance","sda_reported","afcs_reported","ssmg_reported","pension_credit_reported","child_tax_credit_reported","working_tax_credit_reported","state_pension_reported","winter_fuel_allowance_reported","incapacity_benefit_reported","universal_credit_reported","pip_m_reported","pip_dl_reported","jsa_contrib_reported","jsa_income_reported","esa_contrib_reported","esa_income_reported","bsp_reported","relationship_to_head","ons_family_role","ons_family_index","employment_status","employment_sector","sic_industry_division","aa_category","dla_sc_category","dla_m_category","pip_m_category","pip_dl_category","is_disabled_for_benefits","is_enhanced_disabled_for_benefits","is_severely_disabled_for_benefits","current_education","highest_education","is_in_non_advanced_education","is_in_approved_training","age_started_or_accepted_current_education_or_training","is_before_universal_credit_qualifying_young_person_terminal_date","adult_ema","child_ema","receives_benefits_in_own_right","legacy_jobseeker_proxy","esa_health_condition_proxy","esa_support_group_proxy"],"entity":"person","rows":"all"}],"kernel":"uk.identity@1","mass":"conserve","outputs":[],"params":{},"population":null,"sources":[],"structural":"filter","weights":null},{"base":null,"citation":"","description":"Run UK spine stage frs_education_grant_split.","id":"frs_education_grant_split","inputs":[{"columns":["frs_benunit_capital","is_married","dependent_children"],"entity":"benunit","rows":"all"},{"columns":["region","tenure_type","accommodation_type","num_bedrooms","council_tax_reported","council_tax_band","council_tax_rebate","council_tax_single_adult_raw","water_and_sewerage_charges","domestic_rates","rent","subrent","mortgage_interest_repayment","mortgage_capital_repayment","structural_insurance_payments","housing_service_charges","external_child_payments","ons_household_type","council_tax"],"entity":"household","rows":"all"},{"columns":["age","gender","marital_status","hours_worked","care_hours","is_household_head","is_benunit_head","is_parent","is_uc_claimant","employment_income","self_employment_income","private_pension_income","tax_free_savings_income","savings_interest_income","dividend_income","property_income","maintenance_income","miscellaneous_income","private_transfer_income","lump_sum_income","student_loan_repayments","statutory_sick_pay","statutory_maternity_pay","student_loans","access_fund","healthy_start_vouchers","free_school_breakfasts","free_school_fruit_veg","free_school_meals","council_tax_benefit_reported","maintenance_expenses","childcare_expenses","personal_pension_contributions","employee_pension_contributions","pension_contributions_via_salary_sacrifice","salary_sacrifice_reported","salary_sacrifice_asked","child_benefit_reported","income_support_reported","housing_benefit_reported","attendance_allowance_reported","dla_sc_reported","dla_m_reported","iidb_reported","carers_allowance_reported","would_claim_carers_allowance","sda_reported","afcs_reported","ssmg_reported","pension_credit_reported","child_tax_credit_reported","working_tax_credit_reported","state_pension_reported","winter_fuel_allowance_reported","incapacity_benefit_reported","universal_credit_reported","pip_m_reported","pip_dl_reported","jsa_contrib_reported","jsa_income_reported","esa_contrib_reported","esa_income_reported","bsp_reported","relationship_to_head","ons_family_role","ons_family_index","employment_status","employment_sector","sic_industry_division","aa_category","dla_sc_category","dla_m_category","pip_m_category","pip_dl_category","is_disabled_for_benefits","is_enhanced_disabled_for_benefits","is_severely_disabled_for_benefits","current_education","highest_education","is_in_non_advanced_education","is_in_approved_training","age_started_or_accepted_current_education_or_training","is_before_universal_credit_qualifying_young_person_terminal_date","adult_ema","child_ema","receives_benefits_in_own_right","legacy_jobseeker_proxy","esa_health_condition_proxy","esa_support_group_proxy"],"entity":"person","rows":"all"}],"kernel":"uk.stage.frs_education_grant_split@1","mass":"conserve","outputs":[{"column":"disabled_students_allowance_eligible_expenses","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"education_grants","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"}],"params":{"stage":"frs_education_grant_split","stage_contract_sha256":"8718f014b90498c2cc7c754289775ca4a41452d9b978d8cbc5f34b64cbdc7df2","time_period":"2024"},"population":"frs_education_grant_split.boundary","sources":["frs"],"structural":"none","weights":null},{"base":null,"citation":"","description":"Run UK spine stage frs_take_up.","id":"frs_take_up","inputs":[{"columns":["frs_benunit_capital","is_married"],"entity":"benunit","rows":"all"},{"columns":["region"],"entity":"household","rows":"all"},{"columns":["age","child_benefit_reported","education_grants","pension_credit_reported","universal_credit_reported"],"entity":"person","rows":"all"}],"kernel":"uk.stage.frs_take_up@1","mass":"conserve","outputs":[{"column":"would_claim_child_benefit","dtype":"bool","entity":"benunit","ownership":"produced","rows":"all"},{"column":"child_benefit_opts_out","dtype":"bool","entity":"benunit","ownership":"produced","rows":"all"},{"column":"would_claim_pc","dtype":"bool","entity":"benunit","ownership":"produced","rows":"all"},{"column":"would_claim_uc","dtype":"bool","entity":"benunit","ownership":"produced","rows":"all"},{"column":"would_claim_tfc","dtype":"bool","entity":"benunit","ownership":"produced","rows":"all"},{"column":"would_claim_extended_childcare","dtype":"bool","entity":"benunit","ownership":"produced","rows":"all"},{"column":"would_claim_universal_childcare","dtype":"bool","entity":"benunit","ownership":"produced","rows":"all"},{"column":"would_claim_targeted_childcare","dtype":"bool","entity":"benunit","ownership":"produced","rows":"all"},{"column":"would_claim_uc_childcare","dtype":"bool","entity":"benunit","ownership":"produced","rows":"all"},{"column":"maximum_extended_childcare_hours_usage","dtype":"float64","entity":"benunit","ownership":"produced","rows":"all"}],"params":{"stage":"frs_take_up","stage_contract_sha256":"067d31b7fbe733d1d182588705e9ce79df0c8e5f6e8f519314765af49b1b00d7","time_period":"2024"},"population":"frs_education_grant_split.boundary","sources":["frs"],"structural":"none","weights":null},{"base":null,"citation":"","description":"Run UK spine stage frs_person_draws.","id":"frs_person_draws","inputs":[{"columns":["frs_benunit_capital","maximum_extended_childcare_hours_usage"],"entity":"benunit","rows":"all"},{"columns":["region"],"entity":"household","rows":"all"},{"columns":["age"],"entity":"person","rows":"all"}],"kernel":"uk.stage.frs_person_draws@1","mass":"conserve","outputs":[{"column":"would_claim_marriage_allowance","dtype":"bool","entity":"person","ownership":"produced","rows":"all"},{"column":"would_claim_scp","dtype":"bool","entity":"person","ownership":"produced","rows":"all"},{"column":"attends_private_school_random_draw","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"tax_free_childcare_spend_routed_share","dtype":"float64","entity":"person","ownership":"produced","rows":"all"}],"params":{"stage":"frs_person_draws","stage_contract_sha256":"7e0af955541ec5669dcdf1de4e1e6ca642853e48b775ec4e25238b41491df324","time_period":"2024"},"population":"frs_education_grant_split.boundary","sources":["frs"],"structural":"none","weights":null},{"base":null,"citation":"","description":"Run UK spine stage frs_household_draws.","id":"frs_household_draws","inputs":[{"columns":["frs_benunit_capital"],"entity":"benunit","rows":"all"},{"columns":["region"],"entity":"household","rows":"all"},{"columns":["age","tax_free_childcare_spend_routed_share"],"entity":"person","rows":"all"}],"kernel":"uk.stage.frs_household_draws@1","mass":"conserve","outputs":[{"column":"household_owns_tv","dtype":"bool","entity":"household","ownership":"produced","rows":"all"},{"column":"would_evade_tv_licence_fee","dtype":"bool","entity":"household","ownership":"produced","rows":"all"},{"column":"main_residential_property_purchased_is_first_home","dtype":"bool","entity":"household","ownership":"produced","rows":"all"},{"column":"property_purchased","dtype":"bool","entity":"household","ownership":"produced","rows":"all"}],"params":{"stage":"frs_household_draws","stage_contract_sha256":"c74c63b09264319c4bf0049dabba00ecd0ce660beb7b53d6dae71518434bc949","time_period":"2024"},"population":"frs_education_grant_split.boundary","sources":["frs"],"structural":"none","weights":null},{"base":null,"citation":"","description":"Run UK spine stage frs_brma.","id":"frs_brma","inputs":[{"columns":["frs_benunit_capital","is_married","dependent_children","would_claim_child_benefit","child_benefit_opts_out","would_claim_pc","would_claim_uc","would_claim_tfc","would_claim_extended_childcare","would_claim_universal_childcare","would_claim_targeted_childcare","would_claim_uc_childcare","maximum_extended_childcare_hours_usage"],"entity":"benunit","rows":"all"},{"columns":["region","tenure_type","accommodation_type","num_bedrooms","council_tax_reported","council_tax_band","council_tax_rebate","council_tax_single_adult_raw","water_and_sewerage_charges","domestic_rates","rent","subrent","mortgage_interest_repayment","mortgage_capital_repayment","structural_insurance_payments","housing_service_charges","external_child_payments","ons_household_type","council_tax","household_owns_tv","would_evade_tv_licence_fee","main_residential_property_purchased_is_first_home","property_purchased"],"entity":"household","rows":"all"},{"columns":["age","gender","marital_status","hours_worked","care_hours","is_household_head","is_benunit_head","is_parent","is_uc_claimant","employment_income","self_employment_income","private_pension_income","tax_free_savings_income","savings_interest_income","dividend_income","property_income","maintenance_income","miscellaneous_income","private_transfer_income","lump_sum_income","student_loan_repayments","statutory_sick_pay","statutory_maternity_pay","student_loans","access_fund","education_grants","healthy_start_vouchers","free_school_breakfasts","free_school_fruit_veg","free_school_meals","council_tax_benefit_reported","maintenance_expenses","childcare_expenses","personal_pension_contributions","employee_pension_contributions","pension_contributions_via_salary_sacrifice","salary_sacrifice_reported","salary_sacrifice_asked","child_benefit_reported","income_support_reported","housing_benefit_reported","attendance_allowance_reported","dla_sc_reported","dla_m_reported","iidb_reported","carers_allowance_reported","would_claim_carers_allowance","sda_reported","afcs_reported","ssmg_reported","pension_credit_reported","child_tax_credit_reported","working_tax_credit_reported","state_pension_reported","winter_fuel_allowance_reported","incapacity_benefit_reported","universal_credit_reported","pip_m_reported","pip_dl_reported","jsa_contrib_reported","jsa_income_reported","esa_contrib_reported","esa_income_reported","bsp_reported","relationship_to_head","ons_family_role","ons_family_index","employment_status","employment_sector","sic_industry_division","aa_category","dla_sc_category","dla_m_category","pip_m_category","pip_dl_category","is_disabled_for_benefits","is_enhanced_disabled_for_benefits","is_severely_disabled_for_benefits","current_education","highest_education","is_in_non_advanced_education","is_in_approved_training","age_started_or_accepted_current_education_or_training","is_before_universal_credit_qualifying_young_person_terminal_date","adult_ema","child_ema","receives_benefits_in_own_right","legacy_jobseeker_proxy","esa_health_condition_proxy","esa_support_group_proxy","disabled_students_allowance_eligible_expenses","would_claim_marriage_allowance","would_claim_scp","attends_private_school_random_draw","tax_free_childcare_spend_routed_share"],"entity":"person","rows":"all"}],"kernel":"uk.stage.frs_brma@1","mass":"conserve","outputs":[{"column":"brma","dtype":"string","entity":"household","ownership":"produced","rows":"all"}],"params":{"stage":"frs_brma","stage_contract_sha256":"5b8f0b361310c7cd3efbaae3762b347649b7d4afb9e9943131d9bef9266fc3c8","time_period":"2024"},"population":"frs_education_grant_split.boundary","sources":["frs"],"structural":"none","weights":null},{"base":"frs_education_grant_split.boundary","citation":"","description":"Freeze the assembled-spine gate population.","id":"frs_brma.checkpoint","inputs":[{"columns":["frs_benunit_capital","is_married","dependent_children","would_claim_child_benefit","child_benefit_opts_out","would_claim_pc","would_claim_uc","would_claim_tfc","would_claim_extended_childcare","would_claim_universal_childcare","would_claim_targeted_childcare","would_claim_uc_childcare","maximum_extended_childcare_hours_usage"],"entity":"benunit","rows":"all"},{"columns":["region","tenure_type","accommodation_type","num_bedrooms","council_tax_reported","council_tax_band","council_tax_rebate","council_tax_single_adult_raw","water_and_sewerage_charges","domestic_rates","rent","subrent","mortgage_interest_repayment","mortgage_capital_repayment","structural_insurance_payments","housing_service_charges","external_child_payments","ons_household_type","council_tax","household_owns_tv","would_evade_tv_licence_fee","main_residential_property_purchased_is_first_home","property_purchased","brma"],"entity":"household","rows":"all"},{"columns":["age","gender","marital_status","hours_worked","care_hours","is_household_head","is_benunit_head","is_parent","is_uc_claimant","employment_income","self_employment_income","private_pension_income","tax_free_savings_income","savings_interest_income","dividend_income","property_income","maintenance_income","miscellaneous_income","private_transfer_income","lump_sum_income","student_loan_repayments","statutory_sick_pay","statutory_maternity_pay","student_loans","access_fund","education_grants","healthy_start_vouchers","free_school_breakfasts","free_school_fruit_veg","free_school_meals","council_tax_benefit_reported","maintenance_expenses","childcare_expenses","personal_pension_contributions","employee_pension_contributions","pension_contributions_via_salary_sacrifice","salary_sacrifice_reported","salary_sacrifice_asked","child_benefit_reported","income_support_reported","housing_benefit_reported","attendance_allowance_reported","dla_sc_reported","dla_m_reported","iidb_reported","carers_allowance_reported","would_claim_carers_allowance","sda_reported","afcs_reported","ssmg_reported","pension_credit_reported","child_tax_credit_reported","working_tax_credit_reported","state_pension_reported","winter_fuel_allowance_reported","incapacity_benefit_reported","universal_credit_reported","pip_m_reported","pip_dl_reported","jsa_contrib_reported","jsa_income_reported","esa_contrib_reported","esa_income_reported","bsp_reported","relationship_to_head","ons_family_role","ons_family_index","employment_status","employment_sector","sic_industry_division","aa_category","dla_sc_category","dla_m_category","pip_m_category","pip_dl_category","is_disabled_for_benefits","is_enhanced_disabled_for_benefits","is_severely_disabled_for_benefits","current_education","highest_education","is_in_non_advanced_education","is_in_approved_training","age_started_or_accepted_current_education_or_training","is_before_universal_credit_qualifying_young_person_terminal_date","adult_ema","child_ema","receives_benefits_in_own_right","legacy_jobseeker_proxy","esa_health_condition_proxy","esa_support_group_proxy","disabled_students_allowance_eligible_expenses","would_claim_marriage_allowance","would_claim_scp","attends_private_school_random_draw","tax_free_childcare_spend_routed_share"],"entity":"person","rows":"all"}],"kernel":"uk.identity@1","mass":"conserve","outputs":[],"params":{},"population":null,"sources":[],"structural":"filter","weights":null},{"base":null,"citation":"","description":"Run UK spine stage was_wealth.","id":"was_wealth","inputs":[{"columns":["frs_benunit_capital","is_married","dependent_children","would_claim_child_benefit","child_benefit_opts_out","would_claim_pc","would_claim_uc","would_claim_tfc","would_claim_extended_childcare","would_claim_universal_childcare","would_claim_targeted_childcare","would_claim_uc_childcare","maximum_extended_childcare_hours_usage"],"entity":"benunit","rows":"all"},{"columns":["region","tenure_type","accommodation_type","num_bedrooms","council_tax_reported","council_tax_band","council_tax_rebate","council_tax_single_adult_raw","water_and_sewerage_charges","domestic_rates","rent","subrent","mortgage_interest_repayment","mortgage_capital_repayment","structural_insurance_payments","housing_service_charges","external_child_payments","ons_household_type","council_tax","household_owns_tv","would_evade_tv_licence_fee","main_residential_property_purchased_is_first_home","property_purchased","brma"],"entity":"household","rows":"all"},{"columns":["age","gender","marital_status","hours_worked","care_hours","is_household_head","is_benunit_head","is_parent","is_uc_claimant","employment_income","self_employment_income","private_pension_income","tax_free_savings_income","savings_interest_income","dividend_income","property_income","maintenance_income","miscellaneous_income","private_transfer_income","lump_sum_income","student_loan_repayments","statutory_sick_pay","statutory_maternity_pay","student_loans","access_fund","education_grants","healthy_start_vouchers","free_school_breakfasts","free_school_fruit_veg","free_school_meals","council_tax_benefit_reported","maintenance_expenses","childcare_expenses","personal_pension_contributions","employee_pension_contributions","pension_contributions_via_salary_sacrifice","salary_sacrifice_reported","salary_sacrifice_asked","child_benefit_reported","income_support_reported","housing_benefit_reported","attendance_allowance_reported","dla_sc_reported","dla_m_reported","iidb_reported","carers_allowance_reported","would_claim_carers_allowance","sda_reported","afcs_reported","ssmg_reported","pension_credit_reported","child_tax_credit_reported","working_tax_credit_reported","state_pension_reported","winter_fuel_allowance_reported","incapacity_benefit_reported","universal_credit_reported","pip_m_reported","pip_dl_reported","jsa_contrib_reported","jsa_income_reported","esa_contrib_reported","esa_income_reported","bsp_reported","relationship_to_head","ons_family_role","ons_family_index","employment_status","employment_sector","sic_industry_division","aa_category","dla_sc_category","dla_m_category","pip_m_category","pip_dl_category","is_disabled_for_benefits","is_enhanced_disabled_for_benefits","is_severely_disabled_for_benefits","current_education","highest_education","is_in_non_advanced_education","is_in_approved_training","age_started_or_accepted_current_education_or_training","is_before_universal_credit_qualifying_young_person_terminal_date","adult_ema","child_ema","receives_benefits_in_own_right","legacy_jobseeker_proxy","esa_health_condition_proxy","esa_support_group_proxy","disabled_students_allowance_eligible_expenses","would_claim_marriage_allowance","would_claim_scp","attends_private_school_random_draw","tax_free_childcare_spend_routed_share"],"entity":"person","rows":"all"}],"kernel":"uk.stage.was_wealth@1","mass":"conserve","outputs":[{"column":"owned_land","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"property_wealth","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"corporate_wealth","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"private_pension_wealth","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"gross_financial_wealth","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"net_financial_wealth","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"main_residence_value","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"other_residential_property_value","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"non_residential_property_value","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"savings","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"num_vehicles","dtype":"int64","entity":"household","ownership":"produced","rows":"all"},{"column":"cash_isa","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"stocks_and_shares_isa","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"mortgage_debt","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"consumer_debt","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"student_loan_balance","dtype":"float64","entity":"person","ownership":"produced","rows":"all"}],"params":{"stage":"was_wealth","stage_contract_sha256":"366e019c41ee2a28bcd61bbaa62a024ed0bb4b30b3ffc4d87ab23b340b4076bf","time_period":"2024"},"population":"frs_brma.checkpoint","sources":["frs"],"structural":"none","weights":null},{"base":"frs_brma.checkpoint","citation":"","description":"Ownership boundary before regional_property_uprating rewrites.","id":"regional_property_uprating.boundary","inputs":[{"columns":["frs_benunit_capital","is_married","dependent_children","would_claim_child_benefit","child_benefit_opts_out","would_claim_pc","would_claim_uc","would_claim_tfc","would_claim_extended_childcare","would_claim_universal_childcare","would_claim_targeted_childcare","would_claim_uc_childcare","maximum_extended_childcare_hours_usage"],"entity":"benunit","rows":"all"},{"columns":["region","tenure_type","accommodation_type","num_bedrooms","council_tax_reported","council_tax_band","council_tax_rebate","council_tax_single_adult_raw","water_and_sewerage_charges","domestic_rates","rent","subrent","mortgage_interest_repayment","mortgage_capital_repayment","structural_insurance_payments","housing_service_charges","external_child_payments","ons_household_type","council_tax","household_owns_tv","would_evade_tv_licence_fee","main_residential_property_purchased_is_first_home","property_purchased","brma","owned_land","property_wealth","corporate_wealth","private_pension_wealth","gross_financial_wealth","net_financial_wealth","main_residence_value","other_residential_property_value","non_residential_property_value","savings","num_vehicles","cash_isa","stocks_and_shares_isa","mortgage_debt","consumer_debt"],"entity":"household","rows":"all"},{"columns":["age","gender","marital_status","hours_worked","care_hours","is_household_head","is_benunit_head","is_parent","is_uc_claimant","employment_income","self_employment_income","private_pension_income","tax_free_savings_income","savings_interest_income","dividend_income","property_income","maintenance_income","miscellaneous_income","private_transfer_income","lump_sum_income","student_loan_repayments","statutory_sick_pay","statutory_maternity_pay","student_loans","access_fund","education_grants","healthy_start_vouchers","free_school_breakfasts","free_school_fruit_veg","free_school_meals","council_tax_benefit_reported","maintenance_expenses","childcare_expenses","personal_pension_contributions","employee_pension_contributions","pension_contributions_via_salary_sacrifice","salary_sacrifice_reported","salary_sacrifice_asked","child_benefit_reported","income_support_reported","housing_benefit_reported","attendance_allowance_reported","dla_sc_reported","dla_m_reported","iidb_reported","carers_allowance_reported","would_claim_carers_allowance","sda_reported","afcs_reported","ssmg_reported","pension_credit_reported","child_tax_credit_reported","working_tax_credit_reported","state_pension_reported","winter_fuel_allowance_reported","incapacity_benefit_reported","universal_credit_reported","pip_m_reported","pip_dl_reported","jsa_contrib_reported","jsa_income_reported","esa_contrib_reported","esa_income_reported","bsp_reported","relationship_to_head","ons_family_role","ons_family_index","employment_status","employment_sector","sic_industry_division","aa_category","dla_sc_category","dla_m_category","pip_m_category","pip_dl_category","is_disabled_for_benefits","is_enhanced_disabled_for_benefits","is_severely_disabled_for_benefits","current_education","highest_education","is_in_non_advanced_education","is_in_approved_training","age_started_or_accepted_current_education_or_training","is_before_universal_credit_qualifying_young_person_terminal_date","adult_ema","child_ema","receives_benefits_in_own_right","legacy_jobseeker_proxy","esa_health_condition_proxy","esa_support_group_proxy","disabled_students_allowance_eligible_expenses","would_claim_marriage_allowance","would_claim_scp","attends_private_school_random_draw","tax_free_childcare_spend_routed_share","student_loan_balance"],"entity":"person","rows":"all"}],"kernel":"uk.identity@1","mass":"conserve","outputs":[],"params":{},"population":null,"sources":[],"structural":"filter","weights":null},{"base":null,"citation":"","description":"Run UK spine stage regional_property_uprating.","id":"regional_property_uprating","inputs":[{"columns":["frs_benunit_capital"],"entity":"benunit","rows":"all"},{"columns":["region"],"entity":"household","rows":"all"},{"columns":["age","student_loan_balance"],"entity":"person","rows":"all"}],"kernel":"uk.stage.regional_property_uprating@1","mass":"conserve","outputs":[{"column":"main_residence_value","dtype":"float64","entity":"household","ownership":"produced","rewrite":true,"rows":"all"},{"column":"property_wealth","dtype":"float64","entity":"household","ownership":"produced","rewrite":true,"rows":"all"}],"params":{"stage":"regional_property_uprating","stage_contract_sha256":"acb342e8ca1770556c5682a9411c5ead5d61dd1420cfe6b0305ed14a3407fb25","time_period":"2024"},"population":"regional_property_uprating.boundary","sources":["frs"],"structural":"none","weights":null},{"base":null,"citation":"","description":"Run UK spine stage lcfs_consumption.","id":"lcfs_consumption","inputs":[{"columns":["frs_benunit_capital","is_married","dependent_children","would_claim_child_benefit","child_benefit_opts_out","would_claim_pc","would_claim_uc","would_claim_tfc","would_claim_extended_childcare","would_claim_universal_childcare","would_claim_targeted_childcare","would_claim_uc_childcare","maximum_extended_childcare_hours_usage"],"entity":"benunit","rows":"all"},{"columns":["region","tenure_type","accommodation_type","num_bedrooms","council_tax_reported","council_tax_band","council_tax_rebate","council_tax_single_adult_raw","water_and_sewerage_charges","domestic_rates","rent","subrent","mortgage_interest_repayment","mortgage_capital_repayment","structural_insurance_payments","housing_service_charges","external_child_payments","ons_household_type","council_tax","household_owns_tv","would_evade_tv_licence_fee","main_residential_property_purchased_is_first_home","property_purchased","brma","owned_land","property_wealth","corporate_wealth","private_pension_wealth","gross_financial_wealth","net_financial_wealth","main_residence_value","other_residential_property_value","non_residential_property_value","savings","num_vehicles","cash_isa","stocks_and_shares_isa","mortgage_debt","consumer_debt"],"entity":"household","rows":"all"},{"columns":["age","gender","marital_status","hours_worked","care_hours","is_household_head","is_benunit_head","is_parent","is_uc_claimant","employment_income","self_employment_income","private_pension_income","tax_free_savings_income","savings_interest_income","dividend_income","property_income","maintenance_income","miscellaneous_income","private_transfer_income","lump_sum_income","student_loan_repayments","statutory_sick_pay","statutory_maternity_pay","student_loans","access_fund","education_grants","healthy_start_vouchers","free_school_breakfasts","free_school_fruit_veg","free_school_meals","council_tax_benefit_reported","maintenance_expenses","childcare_expenses","personal_pension_contributions","employee_pension_contributions","pension_contributions_via_salary_sacrifice","salary_sacrifice_reported","salary_sacrifice_asked","child_benefit_reported","income_support_reported","housing_benefit_reported","attendance_allowance_reported","dla_sc_reported","dla_m_reported","iidb_reported","carers_allowance_reported","would_claim_carers_allowance","sda_reported","afcs_reported","ssmg_reported","pension_credit_reported","child_tax_credit_reported","working_tax_credit_reported","state_pension_reported","winter_fuel_allowance_reported","incapacity_benefit_reported","universal_credit_reported","pip_m_reported","pip_dl_reported","jsa_contrib_reported","jsa_income_reported","esa_contrib_reported","esa_income_reported","bsp_reported","relationship_to_head","ons_family_role","ons_family_index","employment_status","employment_sector","sic_industry_division","aa_category","dla_sc_category","dla_m_category","pip_m_category","pip_dl_category","is_disabled_for_benefits","is_enhanced_disabled_for_benefits","is_severely_disabled_for_benefits","current_education","highest_education","is_in_non_advanced_education","is_in_approved_training","age_started_or_accepted_current_education_or_training","is_before_universal_credit_qualifying_young_person_terminal_date","adult_ema","child_ema","receives_benefits_in_own_right","legacy_jobseeker_proxy","esa_health_condition_proxy","esa_support_group_proxy","disabled_students_allowance_eligible_expenses","would_claim_marriage_allowance","would_claim_scp","attends_private_school_random_draw","tax_free_childcare_spend_routed_share","student_loan_balance"],"entity":"person","rows":"all"}],"kernel":"uk.stage.lcfs_consumption@1","mass":"conserve","outputs":[{"column":"food_and_non_alcoholic_beverages_consumption","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"alcohol_and_tobacco_consumption","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"clothing_and_footwear_consumption","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"housing_water_and_electricity_consumption","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"household_furnishings_consumption","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"health_consumption","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"transport_consumption","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"communication_consumption","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"recreation_consumption","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"education_consumption","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"restaurants_and_hotels_consumption","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"miscellaneous_consumption","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"petrol_spending","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"diesel_spending","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"bus_fare_spending","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"domestic_energy_consumption","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"electricity_consumption","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"gas_consumption","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"has_fuel_consumption","dtype":"bool","entity":"household","ownership":"produced","rows":"all"}],"params":{"stage":"lcfs_consumption","stage_contract_sha256":"e321f404b2e3a6c0c8bb29861290046db1d4987be071b5c9e3f6fca69a780daf","time_period":"2024"},"population":"regional_property_uprating.boundary","sources":["frs"],"structural":"none","weights":null},{"base":null,"citation":"","description":"Run UK spine stage etb_vat.","id":"etb_vat","inputs":[{"columns":["frs_benunit_capital","is_married","dependent_children","would_claim_child_benefit","child_benefit_opts_out","would_claim_pc","would_claim_uc","would_claim_tfc","would_claim_extended_childcare","would_claim_universal_childcare","would_claim_targeted_childcare","would_claim_uc_childcare","maximum_extended_childcare_hours_usage"],"entity":"benunit","rows":"all"},{"columns":["region","tenure_type","accommodation_type","num_bedrooms","council_tax_reported","council_tax_band","council_tax_rebate","council_tax_single_adult_raw","water_and_sewerage_charges","domestic_rates","rent","subrent","mortgage_interest_repayment","mortgage_capital_repayment","structural_insurance_payments","housing_service_charges","external_child_payments","ons_household_type","council_tax","household_owns_tv","would_evade_tv_licence_fee","main_residential_property_purchased_is_first_home","property_purchased","brma","owned_land","property_wealth","corporate_wealth","private_pension_wealth","gross_financial_wealth","net_financial_wealth","main_residence_value","other_residential_property_value","non_residential_property_value","savings","num_vehicles","cash_isa","stocks_and_shares_isa","mortgage_debt","consumer_debt","food_and_non_alcoholic_beverages_consumption","alcohol_and_tobacco_consumption","clothing_and_footwear_consumption","housing_water_and_electricity_consumption","household_furnishings_consumption","health_consumption","transport_consumption","communication_consumption","recreation_consumption","education_consumption","restaurants_and_hotels_consumption","miscellaneous_consumption","petrol_spending","diesel_spending","bus_fare_spending","domestic_energy_consumption","electricity_consumption","gas_consumption","has_fuel_consumption"],"entity":"household","rows":"all"},{"columns":["age","gender","marital_status","hours_worked","care_hours","is_household_head","is_benunit_head","is_parent","is_uc_claimant","employment_income","self_employment_income","private_pension_income","tax_free_savings_income","savings_interest_income","dividend_income","property_income","maintenance_income","miscellaneous_income","private_transfer_income","lump_sum_income","student_loan_repayments","statutory_sick_pay","statutory_maternity_pay","student_loans","access_fund","education_grants","healthy_start_vouchers","free_school_breakfasts","free_school_fruit_veg","free_school_meals","council_tax_benefit_reported","maintenance_expenses","childcare_expenses","personal_pension_contributions","employee_pension_contributions","pension_contributions_via_salary_sacrifice","salary_sacrifice_reported","salary_sacrifice_asked","child_benefit_reported","income_support_reported","housing_benefit_reported","attendance_allowance_reported","dla_sc_reported","dla_m_reported","iidb_reported","carers_allowance_reported","would_claim_carers_allowance","sda_reported","afcs_reported","ssmg_reported","pension_credit_reported","child_tax_credit_reported","working_tax_credit_reported","state_pension_reported","winter_fuel_allowance_reported","incapacity_benefit_reported","universal_credit_reported","pip_m_reported","pip_dl_reported","jsa_contrib_reported","jsa_income_reported","esa_contrib_reported","esa_income_reported","bsp_reported","relationship_to_head","ons_family_role","ons_family_index","employment_status","employment_sector","sic_industry_division","aa_category","dla_sc_category","dla_m_category","pip_m_category","pip_dl_category","is_disabled_for_benefits","is_enhanced_disabled_for_benefits","is_severely_disabled_for_benefits","current_education","highest_education","is_in_non_advanced_education","is_in_approved_training","age_started_or_accepted_current_education_or_training","is_before_universal_credit_qualifying_young_person_terminal_date","adult_ema","child_ema","receives_benefits_in_own_right","legacy_jobseeker_proxy","esa_health_condition_proxy","esa_support_group_proxy","disabled_students_allowance_eligible_expenses","would_claim_marriage_allowance","would_claim_scp","attends_private_school_random_draw","tax_free_childcare_spend_routed_share","student_loan_balance"],"entity":"person","rows":"all"}],"kernel":"uk.stage.etb_vat@1","mass":"conserve","outputs":[{"column":"full_rate_vat_expenditure_rate","dtype":"float64","entity":"household","ownership":"produced","rows":"all"}],"params":{"stage":"etb_vat","stage_contract_sha256":"5a2b777d88315bcccb72477c088a136c634131e0bc1a339b7287217ee5cfd8d3","time_period":"2024"},"population":"regional_property_uprating.boundary","sources":["frs"],"structural":"none","weights":null},{"base":null,"citation":"","description":"Run UK spine stage etb_services.","id":"etb_services","inputs":[{"columns":["frs_benunit_capital","is_married","dependent_children","would_claim_child_benefit","child_benefit_opts_out","would_claim_pc","would_claim_uc","would_claim_tfc","would_claim_extended_childcare","would_claim_universal_childcare","would_claim_targeted_childcare","would_claim_uc_childcare","maximum_extended_childcare_hours_usage"],"entity":"benunit","rows":"all"},{"columns":["region","tenure_type","accommodation_type","num_bedrooms","council_tax_reported","council_tax_band","council_tax_rebate","council_tax_single_adult_raw","water_and_sewerage_charges","domestic_rates","rent","subrent","mortgage_interest_repayment","mortgage_capital_repayment","structural_insurance_payments","housing_service_charges","external_child_payments","ons_household_type","council_tax","household_owns_tv","would_evade_tv_licence_fee","main_residential_property_purchased_is_first_home","property_purchased","brma","owned_land","property_wealth","corporate_wealth","private_pension_wealth","gross_financial_wealth","net_financial_wealth","main_residence_value","other_residential_property_value","non_residential_property_value","savings","num_vehicles","cash_isa","stocks_and_shares_isa","mortgage_debt","consumer_debt","food_and_non_alcoholic_beverages_consumption","alcohol_and_tobacco_consumption","clothing_and_footwear_consumption","housing_water_and_electricity_consumption","household_furnishings_consumption","health_consumption","transport_consumption","communication_consumption","recreation_consumption","education_consumption","restaurants_and_hotels_consumption","miscellaneous_consumption","petrol_spending","diesel_spending","bus_fare_spending","domestic_energy_consumption","electricity_consumption","gas_consumption","has_fuel_consumption","full_rate_vat_expenditure_rate"],"entity":"household","rows":"all"},{"columns":["age","gender","marital_status","hours_worked","care_hours","is_household_head","is_benunit_head","is_parent","is_uc_claimant","employment_income","self_employment_income","private_pension_income","tax_free_savings_income","savings_interest_income","dividend_income","property_income","maintenance_income","miscellaneous_income","private_transfer_income","lump_sum_income","student_loan_repayments","statutory_sick_pay","statutory_maternity_pay","student_loans","access_fund","education_grants","healthy_start_vouchers","free_school_breakfasts","free_school_fruit_veg","free_school_meals","council_tax_benefit_reported","maintenance_expenses","childcare_expenses","personal_pension_contributions","employee_pension_contributions","pension_contributions_via_salary_sacrifice","salary_sacrifice_reported","salary_sacrifice_asked","child_benefit_reported","income_support_reported","housing_benefit_reported","attendance_allowance_reported","dla_sc_reported","dla_m_reported","iidb_reported","carers_allowance_reported","would_claim_carers_allowance","sda_reported","afcs_reported","ssmg_reported","pension_credit_reported","child_tax_credit_reported","working_tax_credit_reported","state_pension_reported","winter_fuel_allowance_reported","incapacity_benefit_reported","universal_credit_reported","pip_m_reported","pip_dl_reported","jsa_contrib_reported","jsa_income_reported","esa_contrib_reported","esa_income_reported","bsp_reported","relationship_to_head","ons_family_role","ons_family_index","employment_status","employment_sector","sic_industry_division","aa_category","dla_sc_category","dla_m_category","pip_m_category","pip_dl_category","is_disabled_for_benefits","is_enhanced_disabled_for_benefits","is_severely_disabled_for_benefits","current_education","highest_education","is_in_non_advanced_education","is_in_approved_training","age_started_or_accepted_current_education_or_training","is_before_universal_credit_qualifying_young_person_terminal_date","adult_ema","child_ema","receives_benefits_in_own_right","legacy_jobseeker_proxy","esa_health_condition_proxy","esa_support_group_proxy","disabled_students_allowance_eligible_expenses","would_claim_marriage_allowance","would_claim_scp","attends_private_school_random_draw","tax_free_childcare_spend_routed_share","student_loan_balance"],"entity":"person","rows":"all"}],"kernel":"uk.stage.etb_services@1","mass":"conserve","outputs":[{"column":"dfe_education_spending","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"rail_subsidy_spending","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"bus_subsidy_spending","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"rail_usage","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"a_and_e_visits","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"admitted_patient_visits","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"outpatient_visits","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"nhs_a_and_e_spending","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"nhs_admitted_patient_spending","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"nhs_outpatient_spending","dtype":"float64","entity":"person","ownership":"produced","rows":"all"}],"params":{"stage":"etb_services","stage_contract_sha256":"a354a6bbf0be88a4001970a0879600511ff83f1194836dafd8c56fc11b31278e","time_period":"2024"},"population":"regional_property_uprating.boundary","sources":["frs"],"structural":"none","weights":null},{"base":null,"citation":"","description":"Run UK spine stage frs_hmrc_spine_leaves.","id":"frs_hmrc_spine_leaves","inputs":[{"columns":["frs_benunit_capital"],"entity":"benunit","rows":"all"},{"columns":["region"],"entity":"household","rows":"all"},{"columns":["age","employee_pension_contributions","nhs_outpatient_spending"],"entity":"person","rows":"all"}],"kernel":"uk.stage.frs_hmrc_spine_leaves@1","mass":"conserve","outputs":[{"column":"hmrc_spi_pay","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"hmrc_spi_unemployment_benefit_income","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"hmrc_spi_incapacity_benefit_income","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"ossben_identifiable_subset","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"srp_regular_code5","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"employer_pension_contributions","dtype":"float64","entity":"person","ownership":"produced","rows":"all"}],"params":{"stage":"frs_hmrc_spine_leaves","stage_contract_sha256":"2bb3d068003489b47cc8676ac26c203ce3c39a9b6e92f3ecd0a3c06acd6addc4","time_period":"2024"},"population":"regional_property_uprating.boundary","sources":["frs"],"structural":"none","weights":null},{"base":"regional_property_uprating.boundary","citation":"","description":"Run structural UK stage spi_support_channel.","id":"spi_support_channel","inputs":[{"columns":["frs_benunit_capital","is_married","dependent_children","would_claim_child_benefit","child_benefit_opts_out","would_claim_pc","would_claim_uc","would_claim_tfc","would_claim_extended_childcare","would_claim_universal_childcare","would_claim_targeted_childcare","would_claim_uc_childcare","maximum_extended_childcare_hours_usage"],"entity":"benunit","rows":"all"},{"columns":["region","tenure_type","accommodation_type","num_bedrooms","council_tax_reported","council_tax_band","council_tax_rebate","council_tax_single_adult_raw","water_and_sewerage_charges","domestic_rates","rent","subrent","mortgage_interest_repayment","mortgage_capital_repayment","structural_insurance_payments","housing_service_charges","external_child_payments","ons_household_type","council_tax","household_owns_tv","would_evade_tv_licence_fee","main_residential_property_purchased_is_first_home","property_purchased","brma","owned_land","property_wealth","corporate_wealth","private_pension_wealth","gross_financial_wealth","net_financial_wealth","main_residence_value","other_residential_property_value","non_residential_property_value","savings","num_vehicles","cash_isa","stocks_and_shares_isa","mortgage_debt","consumer_debt","food_and_non_alcoholic_beverages_consumption","alcohol_and_tobacco_consumption","clothing_and_footwear_consumption","housing_water_and_electricity_consumption","household_furnishings_consumption","health_consumption","transport_consumption","communication_consumption","recreation_consumption","education_consumption","restaurants_and_hotels_consumption","miscellaneous_consumption","petrol_spending","diesel_spending","bus_fare_spending","domestic_energy_consumption","electricity_consumption","gas_consumption","has_fuel_consumption","full_rate_vat_expenditure_rate","dfe_education_spending","rail_subsidy_spending","bus_subsidy_spending","rail_usage"],"entity":"household","rows":"all"},{"columns":["age","gender","marital_status","hours_worked","care_hours","is_household_head","is_benunit_head","is_parent","is_uc_claimant","employment_income","self_employment_income","private_pension_income","tax_free_savings_income","savings_interest_income","dividend_income","property_income","maintenance_income","miscellaneous_income","private_transfer_income","lump_sum_income","student_loan_repayments","statutory_sick_pay","statutory_maternity_pay","student_loans","access_fund","education_grants","healthy_start_vouchers","free_school_breakfasts","free_school_fruit_veg","free_school_meals","council_tax_benefit_reported","maintenance_expenses","childcare_expenses","personal_pension_contributions","employee_pension_contributions","pension_contributions_via_salary_sacrifice","salary_sacrifice_reported","salary_sacrifice_asked","child_benefit_reported","income_support_reported","housing_benefit_reported","attendance_allowance_reported","dla_sc_reported","dla_m_reported","iidb_reported","carers_allowance_reported","would_claim_carers_allowance","sda_reported","afcs_reported","ssmg_reported","pension_credit_reported","child_tax_credit_reported","working_tax_credit_reported","state_pension_reported","winter_fuel_allowance_reported","incapacity_benefit_reported","universal_credit_reported","pip_m_reported","pip_dl_reported","jsa_contrib_reported","jsa_income_reported","esa_contrib_reported","esa_income_reported","bsp_reported","relationship_to_head","ons_family_role","ons_family_index","employment_status","employment_sector","sic_industry_division","aa_category","dla_sc_category","dla_m_category","pip_m_category","pip_dl_category","is_disabled_for_benefits","is_enhanced_disabled_for_benefits","is_severely_disabled_for_benefits","current_education","highest_education","is_in_non_advanced_education","is_in_approved_training","age_started_or_accepted_current_education_or_training","is_before_universal_credit_qualifying_young_person_terminal_date","adult_ema","child_ema","receives_benefits_in_own_right","legacy_jobseeker_proxy","esa_health_condition_proxy","esa_support_group_proxy","disabled_students_allowance_eligible_expenses","would_claim_marriage_allowance","would_claim_scp","attends_private_school_random_draw","tax_free_childcare_spend_routed_share","student_loan_balance","a_and_e_visits","admitted_patient_visits","outpatient_visits","nhs_a_and_e_spending","nhs_admitted_patient_spending","nhs_outpatient_spending","hmrc_spi_pay","hmrc_spi_unemployment_benefit_income","hmrc_spi_incapacity_benefit_income","ossben_identifiable_subset","srp_regular_code5","employer_pension_contributions"],"entity":"person","rows":"all"}],"kernel":"uk.stage.expand.spi_support_channel@1","mass":"declared","outputs":[],"params":{"expand_cells":[["person","person_source_id","int64"],["person","person_support_channel","string"],["person","person_support_clone_index","int64"],["benunit","benunit_source_id","int64"],["benunit","benunit_support_channel","string"],["benunit","benunit_support_clone_index","int64"],["household","source_household_id","int64"],["household","source_year","int64"],["household","source_household_key","string"],["household","household_source_id","int64"],["household","household_support_channel","string"],["household","household_support_clone_index","int64"],["household","household_is_spi_synthetic","bool"]],"expand_weight_entity":"household","expand_weight_kind":"importance","stage":"spi_support_channel","stage_contract_sha256":"4b10f1a4a215cdf2c406c9974de2d1e580eb3cd3a25ed34973c1b63abf4a54bb","time_period":"2024"},"population":null,"sources":["frs"],"structural":"expand","weights":null},{"base":null,"citation":"","description":"Own the cells materialized by spi_support_channel.","id":"spi_support_channel.owned","inputs":[],"kernel":"uk.claim@1","mass":"conserve","outputs":[{"column":"person_source_id","dtype":"int64","entity":"person","ownership":"produced","rows":"all"},{"column":"person_support_channel","dtype":"string","entity":"person","ownership":"produced","rows":"all"},{"column":"person_support_clone_index","dtype":"int64","entity":"person","ownership":"produced","rows":"all"},{"column":"benunit_source_id","dtype":"int64","entity":"benunit","ownership":"produced","rows":"all"},{"column":"benunit_support_channel","dtype":"string","entity":"benunit","ownership":"produced","rows":"all"},{"column":"benunit_support_clone_index","dtype":"int64","entity":"benunit","ownership":"produced","rows":"all"},{"column":"source_household_id","dtype":"int64","entity":"household","ownership":"produced","rows":"all"},{"column":"source_year","dtype":"int64","entity":"household","ownership":"produced","rows":"all"},{"column":"source_household_key","dtype":"string","entity":"household","ownership":"produced","rows":"all"},{"column":"household_source_id","dtype":"int64","entity":"household","ownership":"produced","rows":"all"},{"column":"household_support_channel","dtype":"string","entity":"household","ownership":"produced","rows":"all"},{"column":"household_support_clone_index","dtype":"int64","entity":"household","ownership":"produced","rows":"all"},{"column":"household_is_spi_synthetic","dtype":"bool","entity":"household","ownership":"produced","rows":"all"}],"params":{"materialized_expand_outputs":["person.person_source_id","person.person_support_channel","person.person_support_clone_index","benunit.benunit_source_id","benunit.benunit_support_channel","benunit.benunit_support_clone_index","household.source_household_id","household.source_year","household.source_household_key","household.household_source_id","household.household_support_channel","household.household_support_clone_index","household.household_is_spi_synthetic"]},"population":"spi_support_channel","sources":[],"structural":"none","weights":null},{"base":null,"citation":"","description":"Run UK spine stage hmrc_spi_income_spine.","id":"hmrc_spi_income_spine","inputs":[{"columns":["frs_benunit_capital","is_married","dependent_children","would_claim_child_benefit","child_benefit_opts_out","would_claim_pc","would_claim_uc","would_claim_tfc","would_claim_extended_childcare","would_claim_universal_childcare","would_claim_targeted_childcare","would_claim_uc_childcare","maximum_extended_childcare_hours_usage","benunit_source_id","benunit_support_channel","benunit_support_clone_index"],"entity":"benunit","rows":"all"},{"columns":["region","tenure_type","accommodation_type","num_bedrooms","council_tax_reported","council_tax_band","council_tax_rebate","council_tax_single_adult_raw","water_and_sewerage_charges","domestic_rates","rent","subrent","mortgage_interest_repayment","mortgage_capital_repayment","structural_insurance_payments","housing_service_charges","external_child_payments","ons_household_type","council_tax","household_owns_tv","would_evade_tv_licence_fee","main_residential_property_purchased_is_first_home","property_purchased","brma","owned_land","property_wealth","corporate_wealth","private_pension_wealth","gross_financial_wealth","net_financial_wealth","main_residence_value","other_residential_property_value","non_residential_property_value","savings","num_vehicles","cash_isa","stocks_and_shares_isa","mortgage_debt","consumer_debt","food_and_non_alcoholic_beverages_consumption","alcohol_and_tobacco_consumption","clothing_and_footwear_consumption","housing_water_and_electricity_consumption","household_furnishings_consumption","health_consumption","transport_consumption","communication_consumption","recreation_consumption","education_consumption","restaurants_and_hotels_consumption","miscellaneous_consumption","petrol_spending","diesel_spending","bus_fare_spending","domestic_energy_consumption","electricity_consumption","gas_consumption","has_fuel_consumption","full_rate_vat_expenditure_rate","dfe_education_spending","rail_subsidy_spending","bus_subsidy_spending","rail_usage","source_household_id","source_year","source_household_key","household_source_id","household_support_channel","household_support_clone_index","household_is_spi_synthetic"],"entity":"household","rows":"all"},{"columns":["age","gender","marital_status","hours_worked","care_hours","is_household_head","is_benunit_head","is_parent","is_uc_claimant","maintenance_income","miscellaneous_income","private_transfer_income","lump_sum_income","student_loan_repayments","statutory_sick_pay","statutory_maternity_pay","student_loans","access_fund","education_grants","healthy_start_vouchers","free_school_breakfasts","free_school_fruit_veg","free_school_meals","maintenance_expenses","childcare_expenses","salary_sacrifice_reported","salary_sacrifice_asked","ssmg_reported","incapacity_benefit_reported","relationship_to_head","ons_family_role","ons_family_index","employment_status","employment_sector","sic_industry_division","current_education","highest_education","is_in_non_advanced_education","is_in_approved_training","age_started_or_accepted_current_education_or_training","is_before_universal_credit_qualifying_young_person_terminal_date","adult_ema","child_ema","receives_benefits_in_own_right","legacy_jobseeker_proxy","esa_health_condition_proxy","esa_support_group_proxy","disabled_students_allowance_eligible_expenses","would_claim_marriage_allowance","would_claim_scp","attends_private_school_random_draw","tax_free_childcare_spend_routed_share","student_loan_balance","a_and_e_visits","admitted_patient_visits","outpatient_visits","nhs_a_and_e_spending","nhs_admitted_patient_spending","nhs_outpatient_spending","ossben_identifiable_subset","srp_regular_code5","person_source_id","person_support_channel","person_support_clone_index"],"entity":"person","rows":"all"}],"kernel":"uk.stage.hmrc_spi_income_spine@1","mass":"conserve","outputs":[{"column":"charitable_investment_gifts","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"gift_aid","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"other_investment_income","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"hmrc_spi_employment_benefits","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"hmrc_spi_employment_expenses","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"hmrc_spi_other_social_security_income","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"hmrc_spi_taxable_termination_pay","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"hmrc_spi_miscellaneous_employment_income","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"hmrc_spi_other_income","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"hmrc_spi_state_pension_income","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"hmrc_spi_employed_income","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"hmrc_spi_total_earned_income","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"hmrc_spi_total_investment_income","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"hmrc_spi_assessable_income","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"employment_income","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"self_employment_income","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"savings_interest_income","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"dividend_income","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"private_pension_income","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"property_income","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"employee_pension_contributions","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"employer_pension_contributions","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"personal_pension_contributions","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"pension_contributions_via_salary_sacrifice","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"tax_free_savings_income","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"universal_credit_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"pension_credit_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"child_benefit_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"housing_benefit_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"income_support_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"working_tax_credit_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"child_tax_credit_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"attendance_allowance_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"state_pension_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"dla_sc_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"dla_m_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"pip_m_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"pip_dl_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"sda_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"carers_allowance_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"iidb_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"afcs_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"bsp_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"winter_fuel_allowance_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"council_tax_benefit_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"jsa_contrib_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"jsa_income_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"esa_contrib_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"esa_income_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"hmrc_spi_pay","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"hmrc_spi_unemployment_benefit_income","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"hmrc_spi_incapacity_benefit_income","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"aa_category","dtype":"string","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"dla_sc_category","dtype":"string","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"dla_m_category","dtype":"string","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"pip_m_category","dtype":"string","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"pip_dl_category","dtype":"string","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"is_disabled_for_benefits","dtype":"bool","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"is_enhanced_disabled_for_benefits","dtype":"bool","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"is_severely_disabled_for_benefits","dtype":"bool","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"would_claim_carers_allowance","dtype":"bool","entity":"person","ownership":"produced","rewrite":true,"rows":"all"}],"params":{"stage":"hmrc_spi_income_spine","stage_contract_sha256":"4c2edca1f77416a00cff10c7e9d0a9da50e332a2bb6a995ef9ac1bdd00ff97a9","time_period":"2024"},"population":"spi_support_channel","sources":["frs"],"structural":"none","weights":null},{"base":"spi_support_channel","citation":"","description":"Ownership boundary before uc_reporter_redraw rewrites.","id":"uc_reporter_redraw.boundary","inputs":[{"columns":["frs_benunit_capital","is_married","dependent_children","would_claim_child_benefit","child_benefit_opts_out","would_claim_pc","would_claim_uc","would_claim_tfc","would_claim_extended_childcare","would_claim_universal_childcare","would_claim_targeted_childcare","would_claim_uc_childcare","maximum_extended_childcare_hours_usage","benunit_source_id","benunit_support_channel","benunit_support_clone_index"],"entity":"benunit","rows":"all"},{"columns":["region","tenure_type","accommodation_type","num_bedrooms","council_tax_reported","council_tax_band","council_tax_rebate","council_tax_single_adult_raw","water_and_sewerage_charges","domestic_rates","rent","subrent","mortgage_interest_repayment","mortgage_capital_repayment","structural_insurance_payments","housing_service_charges","external_child_payments","ons_household_type","council_tax","household_owns_tv","would_evade_tv_licence_fee","main_residential_property_purchased_is_first_home","property_purchased","brma","owned_land","property_wealth","corporate_wealth","private_pension_wealth","gross_financial_wealth","net_financial_wealth","main_residence_value","other_residential_property_value","non_residential_property_value","savings","num_vehicles","cash_isa","stocks_and_shares_isa","mortgage_debt","consumer_debt","food_and_non_alcoholic_beverages_consumption","alcohol_and_tobacco_consumption","clothing_and_footwear_consumption","housing_water_and_electricity_consumption","household_furnishings_consumption","health_consumption","transport_consumption","communication_consumption","recreation_consumption","education_consumption","restaurants_and_hotels_consumption","miscellaneous_consumption","petrol_spending","diesel_spending","bus_fare_spending","domestic_energy_consumption","electricity_consumption","gas_consumption","has_fuel_consumption","full_rate_vat_expenditure_rate","dfe_education_spending","rail_subsidy_spending","bus_subsidy_spending","rail_usage","source_household_id","source_year","source_household_key","household_source_id","household_support_channel","household_support_clone_index","household_is_spi_synthetic"],"entity":"household","rows":"all"},{"columns":["age","gender","marital_status","hours_worked","care_hours","is_household_head","is_benunit_head","is_parent","is_uc_claimant","employment_income","self_employment_income","private_pension_income","tax_free_savings_income","savings_interest_income","dividend_income","property_income","maintenance_income","miscellaneous_income","private_transfer_income","lump_sum_income","student_loan_repayments","statutory_sick_pay","statutory_maternity_pay","student_loans","access_fund","education_grants","healthy_start_vouchers","free_school_breakfasts","free_school_fruit_veg","free_school_meals","council_tax_benefit_reported","maintenance_expenses","childcare_expenses","personal_pension_contributions","employee_pension_contributions","pension_contributions_via_salary_sacrifice","salary_sacrifice_reported","salary_sacrifice_asked","child_benefit_reported","income_support_reported","housing_benefit_reported","attendance_allowance_reported","dla_sc_reported","dla_m_reported","iidb_reported","carers_allowance_reported","would_claim_carers_allowance","sda_reported","afcs_reported","ssmg_reported","pension_credit_reported","child_tax_credit_reported","working_tax_credit_reported","state_pension_reported","winter_fuel_allowance_reported","incapacity_benefit_reported","universal_credit_reported","pip_m_reported","pip_dl_reported","jsa_contrib_reported","jsa_income_reported","esa_contrib_reported","esa_income_reported","bsp_reported","relationship_to_head","ons_family_role","ons_family_index","employment_status","employment_sector","sic_industry_division","aa_category","dla_sc_category","dla_m_category","pip_m_category","pip_dl_category","is_disabled_for_benefits","is_enhanced_disabled_for_benefits","is_severely_disabled_for_benefits","current_education","highest_education","is_in_non_advanced_education","is_in_approved_training","age_started_or_accepted_current_education_or_training","is_before_universal_credit_qualifying_young_person_terminal_date","adult_ema","child_ema","receives_benefits_in_own_right","legacy_jobseeker_proxy","esa_health_condition_proxy","esa_support_group_proxy","disabled_students_allowance_eligible_expenses","would_claim_marriage_allowance","would_claim_scp","attends_private_school_random_draw","tax_free_childcare_spend_routed_share","student_loan_balance","a_and_e_visits","admitted_patient_visits","outpatient_visits","nhs_a_and_e_spending","nhs_admitted_patient_spending","nhs_outpatient_spending","hmrc_spi_pay","hmrc_spi_unemployment_benefit_income","hmrc_spi_incapacity_benefit_income","ossben_identifiable_subset","srp_regular_code5","employer_pension_contributions","person_source_id","person_support_channel","person_support_clone_index","charitable_investment_gifts","gift_aid","other_investment_income","hmrc_spi_employment_benefits","hmrc_spi_employment_expenses","hmrc_spi_other_social_security_income","hmrc_spi_taxable_termination_pay","hmrc_spi_miscellaneous_employment_income","hmrc_spi_other_income","hmrc_spi_state_pension_income","hmrc_spi_employed_income","hmrc_spi_total_earned_income","hmrc_spi_total_investment_income","hmrc_spi_assessable_income"],"entity":"person","rows":"all"}],"kernel":"uk.identity@1","mass":"conserve","outputs":[],"params":{},"population":null,"sources":[],"structural":"filter","weights":null},{"base":null,"citation":"","description":"Run UK spine stage uc_reporter_redraw.","id":"uc_reporter_redraw","inputs":[{"columns":["frs_benunit_capital","is_married","dependent_children","would_claim_child_benefit","child_benefit_opts_out","would_claim_pc","would_claim_uc","would_claim_tfc","would_claim_extended_childcare","would_claim_universal_childcare","would_claim_targeted_childcare","would_claim_uc_childcare","maximum_extended_childcare_hours_usage","benunit_source_id","benunit_support_channel","benunit_support_clone_index"],"entity":"benunit","rows":"all"},{"columns":["region","tenure_type","accommodation_type","num_bedrooms","council_tax_reported","council_tax_band","council_tax_rebate","council_tax_single_adult_raw","water_and_sewerage_charges","domestic_rates","rent","subrent","mortgage_interest_repayment","mortgage_capital_repayment","structural_insurance_payments","housing_service_charges","external_child_payments","ons_household_type","council_tax","household_owns_tv","would_evade_tv_licence_fee","main_residential_property_purchased_is_first_home","property_purchased","brma","owned_land","property_wealth","corporate_wealth","private_pension_wealth","gross_financial_wealth","net_financial_wealth","main_residence_value","other_residential_property_value","non_residential_property_value","savings","num_vehicles","cash_isa","stocks_and_shares_isa","mortgage_debt","consumer_debt","food_and_non_alcoholic_beverages_consumption","alcohol_and_tobacco_consumption","clothing_and_footwear_consumption","housing_water_and_electricity_consumption","household_furnishings_consumption","health_consumption","transport_consumption","communication_consumption","recreation_consumption","education_consumption","restaurants_and_hotels_consumption","miscellaneous_consumption","petrol_spending","diesel_spending","bus_fare_spending","domestic_energy_consumption","electricity_consumption","gas_consumption","has_fuel_consumption","full_rate_vat_expenditure_rate","dfe_education_spending","rail_subsidy_spending","bus_subsidy_spending","rail_usage","source_household_id","source_year","source_household_key","household_source_id","household_support_channel","household_support_clone_index","household_is_spi_synthetic"],"entity":"household","rows":"all"},{"columns":["age","gender","marital_status","hours_worked","care_hours","is_household_head","is_benunit_head","is_parent","is_uc_claimant","employment_income","self_employment_income","private_pension_income","tax_free_savings_income","savings_interest_income","dividend_income","property_income","maintenance_income","miscellaneous_income","private_transfer_income","lump_sum_income","student_loan_repayments","statutory_sick_pay","statutory_maternity_pay","student_loans","access_fund","education_grants","healthy_start_vouchers","free_school_breakfasts","free_school_fruit_veg","free_school_meals","council_tax_benefit_reported","maintenance_expenses","childcare_expenses","personal_pension_contributions","employee_pension_contributions","pension_contributions_via_salary_sacrifice","salary_sacrifice_reported","salary_sacrifice_asked","child_benefit_reported","income_support_reported","housing_benefit_reported","attendance_allowance_reported","dla_sc_reported","dla_m_reported","iidb_reported","carers_allowance_reported","would_claim_carers_allowance","sda_reported","afcs_reported","ssmg_reported","pension_credit_reported","child_tax_credit_reported","working_tax_credit_reported","state_pension_reported","winter_fuel_allowance_reported","incapacity_benefit_reported","pip_m_reported","pip_dl_reported","jsa_contrib_reported","jsa_income_reported","esa_contrib_reported","esa_income_reported","bsp_reported","relationship_to_head","ons_family_role","ons_family_index","employment_status","employment_sector","sic_industry_division","aa_category","dla_sc_category","dla_m_category","pip_m_category","pip_dl_category","is_disabled_for_benefits","is_enhanced_disabled_for_benefits","is_severely_disabled_for_benefits","current_education","highest_education","is_in_non_advanced_education","is_in_approved_training","age_started_or_accepted_current_education_or_training","is_before_universal_credit_qualifying_young_person_terminal_date","adult_ema","child_ema","receives_benefits_in_own_right","legacy_jobseeker_proxy","esa_health_condition_proxy","esa_support_group_proxy","disabled_students_allowance_eligible_expenses","would_claim_marriage_allowance","would_claim_scp","attends_private_school_random_draw","tax_free_childcare_spend_routed_share","student_loan_balance","a_and_e_visits","admitted_patient_visits","outpatient_visits","nhs_a_and_e_spending","nhs_admitted_patient_spending","nhs_outpatient_spending","hmrc_spi_pay","hmrc_spi_unemployment_benefit_income","hmrc_spi_incapacity_benefit_income","ossben_identifiable_subset","srp_regular_code5","employer_pension_contributions","person_source_id","person_support_channel","person_support_clone_index","charitable_investment_gifts","gift_aid","other_investment_income","hmrc_spi_employment_benefits","hmrc_spi_employment_expenses","hmrc_spi_other_social_security_income","hmrc_spi_taxable_termination_pay","hmrc_spi_miscellaneous_employment_income","hmrc_spi_other_income","hmrc_spi_state_pension_income","hmrc_spi_employed_income","hmrc_spi_total_earned_income","hmrc_spi_total_investment_income","hmrc_spi_assessable_income"],"entity":"person","rows":"all"}],"kernel":"uk.stage.uc_reporter_redraw@1","mass":"conserve","outputs":[{"column":"universal_credit_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"}],"params":{"stage":"uc_reporter_redraw","stage_contract_sha256":"7e94e034cad29c5bae566ffb875c42cddf3a1025c15496907e783ceff1144f8b","time_period":"2024"},"population":"uc_reporter_redraw.boundary","sources":["frs"],"structural":"none","weights":null},{"base":"uc_reporter_redraw.boundary","citation":"","description":"Ownership boundary before uc_capital_coherence rewrites.","id":"uc_capital_coherence.boundary","inputs":[{"columns":["frs_benunit_capital","is_married","dependent_children","would_claim_child_benefit","child_benefit_opts_out","would_claim_pc","would_claim_uc","would_claim_tfc","would_claim_extended_childcare","would_claim_universal_childcare","would_claim_targeted_childcare","would_claim_uc_childcare","maximum_extended_childcare_hours_usage","benunit_source_id","benunit_support_channel","benunit_support_clone_index"],"entity":"benunit","rows":"all"},{"columns":["region","tenure_type","accommodation_type","num_bedrooms","council_tax_reported","council_tax_band","council_tax_rebate","council_tax_single_adult_raw","water_and_sewerage_charges","domestic_rates","rent","subrent","mortgage_interest_repayment","mortgage_capital_repayment","structural_insurance_payments","housing_service_charges","external_child_payments","ons_household_type","council_tax","household_owns_tv","would_evade_tv_licence_fee","main_residential_property_purchased_is_first_home","property_purchased","brma","owned_land","property_wealth","corporate_wealth","private_pension_wealth","gross_financial_wealth","net_financial_wealth","main_residence_value","other_residential_property_value","non_residential_property_value","savings","num_vehicles","cash_isa","stocks_and_shares_isa","mortgage_debt","consumer_debt","food_and_non_alcoholic_beverages_consumption","alcohol_and_tobacco_consumption","clothing_and_footwear_consumption","housing_water_and_electricity_consumption","household_furnishings_consumption","health_consumption","transport_consumption","communication_consumption","recreation_consumption","education_consumption","restaurants_and_hotels_consumption","miscellaneous_consumption","petrol_spending","diesel_spending","bus_fare_spending","domestic_energy_consumption","electricity_consumption","gas_consumption","has_fuel_consumption","full_rate_vat_expenditure_rate","dfe_education_spending","rail_subsidy_spending","bus_subsidy_spending","rail_usage","source_household_id","source_year","source_household_key","household_source_id","household_support_channel","household_support_clone_index","household_is_spi_synthetic"],"entity":"household","rows":"all"},{"columns":["age","gender","marital_status","hours_worked","care_hours","is_household_head","is_benunit_head","is_parent","is_uc_claimant","employment_income","self_employment_income","private_pension_income","tax_free_savings_income","savings_interest_income","dividend_income","property_income","maintenance_income","miscellaneous_income","private_transfer_income","lump_sum_income","student_loan_repayments","statutory_sick_pay","statutory_maternity_pay","student_loans","access_fund","education_grants","healthy_start_vouchers","free_school_breakfasts","free_school_fruit_veg","free_school_meals","council_tax_benefit_reported","maintenance_expenses","childcare_expenses","personal_pension_contributions","employee_pension_contributions","pension_contributions_via_salary_sacrifice","salary_sacrifice_reported","salary_sacrifice_asked","child_benefit_reported","income_support_reported","housing_benefit_reported","attendance_allowance_reported","dla_sc_reported","dla_m_reported","iidb_reported","carers_allowance_reported","would_claim_carers_allowance","sda_reported","afcs_reported","ssmg_reported","pension_credit_reported","child_tax_credit_reported","working_tax_credit_reported","state_pension_reported","winter_fuel_allowance_reported","incapacity_benefit_reported","universal_credit_reported","pip_m_reported","pip_dl_reported","jsa_contrib_reported","jsa_income_reported","esa_contrib_reported","esa_income_reported","bsp_reported","relationship_to_head","ons_family_role","ons_family_index","employment_status","employment_sector","sic_industry_division","aa_category","dla_sc_category","dla_m_category","pip_m_category","pip_dl_category","is_disabled_for_benefits","is_enhanced_disabled_for_benefits","is_severely_disabled_for_benefits","current_education","highest_education","is_in_non_advanced_education","is_in_approved_training","age_started_or_accepted_current_education_or_training","is_before_universal_credit_qualifying_young_person_terminal_date","adult_ema","child_ema","receives_benefits_in_own_right","legacy_jobseeker_proxy","esa_health_condition_proxy","esa_support_group_proxy","disabled_students_allowance_eligible_expenses","would_claim_marriage_allowance","would_claim_scp","attends_private_school_random_draw","tax_free_childcare_spend_routed_share","student_loan_balance","a_and_e_visits","admitted_patient_visits","outpatient_visits","nhs_a_and_e_spending","nhs_admitted_patient_spending","nhs_outpatient_spending","hmrc_spi_pay","hmrc_spi_unemployment_benefit_income","hmrc_spi_incapacity_benefit_income","ossben_identifiable_subset","srp_regular_code5","employer_pension_contributions","person_source_id","person_support_channel","person_support_clone_index","charitable_investment_gifts","gift_aid","other_investment_income","hmrc_spi_employment_benefits","hmrc_spi_employment_expenses","hmrc_spi_other_social_security_income","hmrc_spi_taxable_termination_pay","hmrc_spi_miscellaneous_employment_income","hmrc_spi_other_income","hmrc_spi_state_pension_income","hmrc_spi_employed_income","hmrc_spi_total_earned_income","hmrc_spi_total_investment_income","hmrc_spi_assessable_income"],"entity":"person","rows":"all"}],"kernel":"uk.identity@1","mass":"conserve","outputs":[],"params":{},"population":null,"sources":[],"structural":"filter","weights":null},{"base":null,"citation":"","description":"Run UK spine stage uc_capital_coherence.","id":"uc_capital_coherence","inputs":[{"columns":["benunit_support_channel","dependent_children","is_married"],"entity":"benunit","rows":"all"},{"columns":["region"],"entity":"household","rows":"all"},{"columns":["age","is_benunit_head","is_parent","person_support_channel","universal_credit_reported"],"entity":"person","rows":"all"}],"kernel":"uk.stage.uc_capital_coherence@1","mass":"conserve","outputs":[{"column":"uc_reported_capital","dtype":"float64","entity":"benunit","ownership":"produced","rows":"all"},{"column":"frs_benunit_capital","dtype":"float64","entity":"benunit","ownership":"produced","rewrite":true,"rows":"all"},{"column":"would_claim_uc","dtype":"bool","entity":"benunit","ownership":"produced","rewrite":true,"rows":"all"}],"params":{"stage":"uc_capital_coherence","stage_contract_sha256":"059f27ee687ee15385b246d43068b17aeecbe3645ccc41fd52fcad06a006a081","time_period":"2024"},"population":"uc_capital_coherence.boundary","sources":["frs"],"structural":"none","weights":null},{"base":null,"citation":"","description":"Run UK spine stage uc_deduction_attributes.","id":"uc_deduction_attributes","inputs":[{"columns":["frs_benunit_capital","would_claim_uc"],"entity":"benunit","rows":"all"},{"columns":["region"],"entity":"household","rows":"all"},{"columns":["age"],"entity":"person","rows":"all"}],"kernel":"uk.stage.uc_deduction_attributes@1","mass":"conserve","outputs":[{"column":"uc_deduction_random_draw","dtype":"float64","entity":"benunit","ownership":"produced","rows":"all"},{"column":"uc_deduction_type_random_draw","dtype":"float64","entity":"benunit","ownership":"produced","rows":"all"},{"column":"uc_latent_deduction_rate","dtype":"float64","entity":"benunit","ownership":"produced","rows":"all"},{"column":"uc_deduction_combination","dtype":"string","entity":"benunit","ownership":"produced","rows":"all"}],"params":{"stage":"uc_deduction_attributes","stage_contract_sha256":"6bc7e3ec1a5712e23ca1b7d2956e8b17f2526b94431ba3e5a9294b04658e4fe0","time_period":"2024"},"population":"uc_capital_coherence.boundary","sources":["frs"],"structural":"none","weights":null},{"base":"uc_capital_coherence.boundary","citation":"","description":"Run structural UK stage cgt_incidence_clone.","id":"cgt_incidence_clone","inputs":[{"columns":["frs_benunit_capital","is_married","dependent_children","would_claim_child_benefit","child_benefit_opts_out","would_claim_pc","would_claim_uc","would_claim_tfc","would_claim_extended_childcare","would_claim_universal_childcare","would_claim_targeted_childcare","would_claim_uc_childcare","maximum_extended_childcare_hours_usage","benunit_source_id","benunit_support_channel","benunit_support_clone_index","uc_reported_capital","uc_deduction_random_draw","uc_deduction_type_random_draw","uc_latent_deduction_rate","uc_deduction_combination"],"entity":"benunit","rows":"all"},{"columns":["region","tenure_type","accommodation_type","num_bedrooms","council_tax_reported","council_tax_band","council_tax_rebate","council_tax_single_adult_raw","water_and_sewerage_charges","domestic_rates","rent","subrent","mortgage_interest_repayment","mortgage_capital_repayment","structural_insurance_payments","housing_service_charges","external_child_payments","ons_household_type","council_tax","household_owns_tv","would_evade_tv_licence_fee","main_residential_property_purchased_is_first_home","property_purchased","brma","owned_land","property_wealth","corporate_wealth","private_pension_wealth","gross_financial_wealth","net_financial_wealth","main_residence_value","other_residential_property_value","non_residential_property_value","savings","num_vehicles","cash_isa","stocks_and_shares_isa","mortgage_debt","consumer_debt","food_and_non_alcoholic_beverages_consumption","alcohol_and_tobacco_consumption","clothing_and_footwear_consumption","housing_water_and_electricity_consumption","household_furnishings_consumption","health_consumption","transport_consumption","communication_consumption","recreation_consumption","education_consumption","restaurants_and_hotels_consumption","miscellaneous_consumption","petrol_spending","diesel_spending","bus_fare_spending","domestic_energy_consumption","electricity_consumption","gas_consumption","has_fuel_consumption","full_rate_vat_expenditure_rate","dfe_education_spending","rail_subsidy_spending","bus_subsidy_spending","rail_usage","source_household_id","source_year","source_household_key","household_source_id","household_support_channel","household_support_clone_index","household_is_spi_synthetic"],"entity":"household","rows":"all"},{"columns":["age","gender","marital_status","hours_worked","care_hours","is_household_head","is_benunit_head","is_parent","is_uc_claimant","employment_income","self_employment_income","private_pension_income","tax_free_savings_income","savings_interest_income","dividend_income","property_income","maintenance_income","miscellaneous_income","private_transfer_income","lump_sum_income","student_loan_repayments","statutory_sick_pay","statutory_maternity_pay","student_loans","access_fund","education_grants","healthy_start_vouchers","free_school_breakfasts","free_school_fruit_veg","free_school_meals","council_tax_benefit_reported","maintenance_expenses","childcare_expenses","personal_pension_contributions","employee_pension_contributions","pension_contributions_via_salary_sacrifice","salary_sacrifice_reported","salary_sacrifice_asked","child_benefit_reported","income_support_reported","housing_benefit_reported","attendance_allowance_reported","dla_sc_reported","dla_m_reported","iidb_reported","carers_allowance_reported","would_claim_carers_allowance","sda_reported","afcs_reported","ssmg_reported","pension_credit_reported","child_tax_credit_reported","working_tax_credit_reported","state_pension_reported","winter_fuel_allowance_reported","incapacity_benefit_reported","universal_credit_reported","pip_m_reported","pip_dl_reported","jsa_contrib_reported","jsa_income_reported","esa_contrib_reported","esa_income_reported","bsp_reported","relationship_to_head","ons_family_role","ons_family_index","employment_status","employment_sector","sic_industry_division","aa_category","dla_sc_category","dla_m_category","pip_m_category","pip_dl_category","is_disabled_for_benefits","is_enhanced_disabled_for_benefits","is_severely_disabled_for_benefits","current_education","highest_education","is_in_non_advanced_education","is_in_approved_training","age_started_or_accepted_current_education_or_training","is_before_universal_credit_qualifying_young_person_terminal_date","adult_ema","child_ema","receives_benefits_in_own_right","legacy_jobseeker_proxy","esa_health_condition_proxy","esa_support_group_proxy","disabled_students_allowance_eligible_expenses","would_claim_marriage_allowance","would_claim_scp","attends_private_school_random_draw","tax_free_childcare_spend_routed_share","student_loan_balance","a_and_e_visits","admitted_patient_visits","outpatient_visits","nhs_a_and_e_spending","nhs_admitted_patient_spending","nhs_outpatient_spending","hmrc_spi_pay","hmrc_spi_unemployment_benefit_income","hmrc_spi_incapacity_benefit_income","ossben_identifiable_subset","srp_regular_code5","employer_pension_contributions","person_source_id","person_support_channel","person_support_clone_index","charitable_investment_gifts","gift_aid","other_investment_income","hmrc_spi_employment_benefits","hmrc_spi_employment_expenses","hmrc_spi_other_social_security_income","hmrc_spi_taxable_termination_pay","hmrc_spi_miscellaneous_employment_income","hmrc_spi_other_income","hmrc_spi_state_pension_income","hmrc_spi_employed_income","hmrc_spi_total_earned_income","hmrc_spi_total_investment_income","hmrc_spi_assessable_income"],"entity":"person","rows":"all"}],"kernel":"uk.stage.expand.cgt_incidence_clone@1","mass":"conserve","outputs":[],"params":{"expand_cells":[["household","household_is_capital_gains_clone","bool"],["person","capital_gains","float64"]],"expand_weight_entity":"household","expand_weight_kind":"importance","stage":"cgt_incidence_clone","stage_contract_sha256":"ee30278543cc0297a5366d855b7753aa04af5518971594c193c52be36bf8a7b4","time_period":"2024"},"population":null,"sources":["frs"],"structural":"expand","weights":null},{"base":null,"citation":"","description":"Own the cells materialized by cgt_incidence_clone.","id":"cgt_incidence_clone.owned","inputs":[],"kernel":"uk.claim@1","mass":"conserve","outputs":[{"column":"household_is_capital_gains_clone","dtype":"bool","entity":"household","ownership":"produced","rows":"all"},{"column":"capital_gains","dtype":"float64","entity":"person","ownership":"produced","rows":"all"}],"params":{"materialized_expand_outputs":["household.household_is_capital_gains_clone","person.capital_gains"]},"population":"cgt_incidence_clone","sources":[],"structural":"none","weights":null},{"base":"cgt_incidence_clone","citation":"","description":"Run structural UK stage cgt_band_donors.","id":"cgt_band_donors","inputs":[{"columns":["frs_benunit_capital","is_married","dependent_children","would_claim_child_benefit","child_benefit_opts_out","would_claim_pc","would_claim_uc","would_claim_tfc","would_claim_extended_childcare","would_claim_universal_childcare","would_claim_targeted_childcare","would_claim_uc_childcare","maximum_extended_childcare_hours_usage","benunit_source_id","benunit_support_channel","benunit_support_clone_index","uc_reported_capital","uc_deduction_random_draw","uc_deduction_type_random_draw","uc_latent_deduction_rate","uc_deduction_combination"],"entity":"benunit","rows":"all"},{"columns":["region","tenure_type","accommodation_type","num_bedrooms","council_tax_reported","council_tax_band","council_tax_rebate","council_tax_single_adult_raw","water_and_sewerage_charges","domestic_rates","rent","subrent","mortgage_interest_repayment","mortgage_capital_repayment","structural_insurance_payments","housing_service_charges","external_child_payments","ons_household_type","council_tax","household_owns_tv","would_evade_tv_licence_fee","main_residential_property_purchased_is_first_home","property_purchased","brma","owned_land","property_wealth","corporate_wealth","private_pension_wealth","gross_financial_wealth","net_financial_wealth","main_residence_value","other_residential_property_value","non_residential_property_value","savings","num_vehicles","cash_isa","stocks_and_shares_isa","mortgage_debt","consumer_debt","food_and_non_alcoholic_beverages_consumption","alcohol_and_tobacco_consumption","clothing_and_footwear_consumption","housing_water_and_electricity_consumption","household_furnishings_consumption","health_consumption","transport_consumption","communication_consumption","recreation_consumption","education_consumption","restaurants_and_hotels_consumption","miscellaneous_consumption","petrol_spending","diesel_spending","bus_fare_spending","domestic_energy_consumption","electricity_consumption","gas_consumption","has_fuel_consumption","full_rate_vat_expenditure_rate","dfe_education_spending","rail_subsidy_spending","bus_subsidy_spending","rail_usage","source_household_id","source_year","source_household_key","household_source_id","household_support_channel","household_support_clone_index","household_is_spi_synthetic","household_is_capital_gains_clone"],"entity":"household","rows":"all"},{"columns":["age","gender","marital_status","hours_worked","care_hours","is_household_head","is_benunit_head","is_parent","is_uc_claimant","employment_income","self_employment_income","private_pension_income","tax_free_savings_income","savings_interest_income","dividend_income","property_income","maintenance_income","miscellaneous_income","private_transfer_income","lump_sum_income","student_loan_repayments","statutory_sick_pay","statutory_maternity_pay","student_loans","access_fund","education_grants","healthy_start_vouchers","free_school_breakfasts","free_school_fruit_veg","free_school_meals","council_tax_benefit_reported","maintenance_expenses","childcare_expenses","personal_pension_contributions","employee_pension_contributions","pension_contributions_via_salary_sacrifice","salary_sacrifice_reported","salary_sacrifice_asked","child_benefit_reported","income_support_reported","housing_benefit_reported","attendance_allowance_reported","dla_sc_reported","dla_m_reported","iidb_reported","carers_allowance_reported","would_claim_carers_allowance","sda_reported","afcs_reported","ssmg_reported","pension_credit_reported","child_tax_credit_reported","working_tax_credit_reported","state_pension_reported","winter_fuel_allowance_reported","incapacity_benefit_reported","universal_credit_reported","pip_m_reported","pip_dl_reported","jsa_contrib_reported","jsa_income_reported","esa_contrib_reported","esa_income_reported","bsp_reported","relationship_to_head","ons_family_role","ons_family_index","employment_status","employment_sector","sic_industry_division","aa_category","dla_sc_category","dla_m_category","pip_m_category","pip_dl_category","is_disabled_for_benefits","is_enhanced_disabled_for_benefits","is_severely_disabled_for_benefits","current_education","highest_education","is_in_non_advanced_education","is_in_approved_training","age_started_or_accepted_current_education_or_training","is_before_universal_credit_qualifying_young_person_terminal_date","adult_ema","child_ema","receives_benefits_in_own_right","legacy_jobseeker_proxy","esa_health_condition_proxy","esa_support_group_proxy","disabled_students_allowance_eligible_expenses","would_claim_marriage_allowance","would_claim_scp","attends_private_school_random_draw","tax_free_childcare_spend_routed_share","student_loan_balance","a_and_e_visits","admitted_patient_visits","outpatient_visits","nhs_a_and_e_spending","nhs_admitted_patient_spending","nhs_outpatient_spending","hmrc_spi_pay","hmrc_spi_unemployment_benefit_income","hmrc_spi_incapacity_benefit_income","ossben_identifiable_subset","srp_regular_code5","employer_pension_contributions","person_source_id","person_support_channel","person_support_clone_index","charitable_investment_gifts","gift_aid","other_investment_income","hmrc_spi_employment_benefits","hmrc_spi_employment_expenses","hmrc_spi_other_social_security_income","hmrc_spi_taxable_termination_pay","hmrc_spi_miscellaneous_employment_income","hmrc_spi_other_income","hmrc_spi_state_pension_income","hmrc_spi_employed_income","hmrc_spi_total_earned_income","hmrc_spi_total_investment_income","hmrc_spi_assessable_income","capital_gains"],"entity":"person","rows":"all"}],"kernel":"uk.stage.expand.cgt_band_donors@1","mass":"free","outputs":[],"params":{"expand_cells":[["household","household_is_cgt_band_donor","bool"],["person","capital_gains","float64"]],"expand_weight_entity":"household","expand_weight_kind":"importance","stage":"cgt_band_donors","stage_contract_sha256":"6fde981ff62f0435f9d708ef4048f64b8edfdec34bab521a19dec677d2603694","time_period":"2024"},"population":null,"sources":["frs"],"structural":"expand","weights":null},{"base":null,"citation":"","description":"Own the cells materialized by cgt_band_donors.","id":"cgt_band_donors.owned","inputs":[],"kernel":"uk.claim@1","mass":"conserve","outputs":[{"column":"household_is_cgt_band_donor","dtype":"bool","entity":"household","ownership":"produced","rows":"all"},{"column":"capital_gains","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"}],"params":{"materialized_expand_outputs":["household.household_is_cgt_band_donor"]},"population":"cgt_band_donors","sources":[],"structural":"none","weights":null},{"base":"cgt_band_donors","citation":"","description":"Ownership boundary before hmrc_cgt_gains_spine rewrites.","id":"hmrc_cgt_gains_spine.boundary","inputs":[{"columns":["frs_benunit_capital","is_married","dependent_children","would_claim_child_benefit","child_benefit_opts_out","would_claim_pc","would_claim_uc","would_claim_tfc","would_claim_extended_childcare","would_claim_universal_childcare","would_claim_targeted_childcare","would_claim_uc_childcare","maximum_extended_childcare_hours_usage","benunit_source_id","benunit_support_channel","benunit_support_clone_index","uc_reported_capital","uc_deduction_random_draw","uc_deduction_type_random_draw","uc_latent_deduction_rate","uc_deduction_combination"],"entity":"benunit","rows":"all"},{"columns":["region","tenure_type","accommodation_type","num_bedrooms","council_tax_reported","council_tax_band","council_tax_rebate","council_tax_single_adult_raw","water_and_sewerage_charges","domestic_rates","rent","subrent","mortgage_interest_repayment","mortgage_capital_repayment","structural_insurance_payments","housing_service_charges","external_child_payments","ons_household_type","council_tax","household_owns_tv","would_evade_tv_licence_fee","main_residential_property_purchased_is_first_home","property_purchased","brma","owned_land","property_wealth","corporate_wealth","private_pension_wealth","gross_financial_wealth","net_financial_wealth","main_residence_value","other_residential_property_value","non_residential_property_value","savings","num_vehicles","cash_isa","stocks_and_shares_isa","mortgage_debt","consumer_debt","food_and_non_alcoholic_beverages_consumption","alcohol_and_tobacco_consumption","clothing_and_footwear_consumption","housing_water_and_electricity_consumption","household_furnishings_consumption","health_consumption","transport_consumption","communication_consumption","recreation_consumption","education_consumption","restaurants_and_hotels_consumption","miscellaneous_consumption","petrol_spending","diesel_spending","bus_fare_spending","domestic_energy_consumption","electricity_consumption","gas_consumption","has_fuel_consumption","full_rate_vat_expenditure_rate","dfe_education_spending","rail_subsidy_spending","bus_subsidy_spending","rail_usage","source_household_id","source_year","source_household_key","household_source_id","household_support_channel","household_support_clone_index","household_is_spi_synthetic","household_is_capital_gains_clone","household_is_cgt_band_donor"],"entity":"household","rows":"all"},{"columns":["age","gender","marital_status","hours_worked","care_hours","is_household_head","is_benunit_head","is_parent","is_uc_claimant","employment_income","self_employment_income","private_pension_income","tax_free_savings_income","savings_interest_income","dividend_income","property_income","maintenance_income","miscellaneous_income","private_transfer_income","lump_sum_income","student_loan_repayments","statutory_sick_pay","statutory_maternity_pay","student_loans","access_fund","education_grants","healthy_start_vouchers","free_school_breakfasts","free_school_fruit_veg","free_school_meals","council_tax_benefit_reported","maintenance_expenses","childcare_expenses","personal_pension_contributions","employee_pension_contributions","pension_contributions_via_salary_sacrifice","salary_sacrifice_reported","salary_sacrifice_asked","child_benefit_reported","income_support_reported","housing_benefit_reported","attendance_allowance_reported","dla_sc_reported","dla_m_reported","iidb_reported","carers_allowance_reported","would_claim_carers_allowance","sda_reported","afcs_reported","ssmg_reported","pension_credit_reported","child_tax_credit_reported","working_tax_credit_reported","state_pension_reported","winter_fuel_allowance_reported","incapacity_benefit_reported","universal_credit_reported","pip_m_reported","pip_dl_reported","jsa_contrib_reported","jsa_income_reported","esa_contrib_reported","esa_income_reported","bsp_reported","relationship_to_head","ons_family_role","ons_family_index","employment_status","employment_sector","sic_industry_division","aa_category","dla_sc_category","dla_m_category","pip_m_category","pip_dl_category","is_disabled_for_benefits","is_enhanced_disabled_for_benefits","is_severely_disabled_for_benefits","current_education","highest_education","is_in_non_advanced_education","is_in_approved_training","age_started_or_accepted_current_education_or_training","is_before_universal_credit_qualifying_young_person_terminal_date","adult_ema","child_ema","receives_benefits_in_own_right","legacy_jobseeker_proxy","esa_health_condition_proxy","esa_support_group_proxy","disabled_students_allowance_eligible_expenses","would_claim_marriage_allowance","would_claim_scp","attends_private_school_random_draw","tax_free_childcare_spend_routed_share","student_loan_balance","a_and_e_visits","admitted_patient_visits","outpatient_visits","nhs_a_and_e_spending","nhs_admitted_patient_spending","nhs_outpatient_spending","hmrc_spi_pay","hmrc_spi_unemployment_benefit_income","hmrc_spi_incapacity_benefit_income","ossben_identifiable_subset","srp_regular_code5","employer_pension_contributions","person_source_id","person_support_channel","person_support_clone_index","charitable_investment_gifts","gift_aid","other_investment_income","hmrc_spi_employment_benefits","hmrc_spi_employment_expenses","hmrc_spi_other_social_security_income","hmrc_spi_taxable_termination_pay","hmrc_spi_miscellaneous_employment_income","hmrc_spi_other_income","hmrc_spi_state_pension_income","hmrc_spi_employed_income","hmrc_spi_total_earned_income","hmrc_spi_total_investment_income","hmrc_spi_assessable_income","capital_gains"],"entity":"person","rows":"all"}],"kernel":"uk.identity@1","mass":"conserve","outputs":[],"params":{},"population":null,"sources":[],"structural":"filter","weights":null},{"base":null,"citation":"","description":"Run UK spine stage hmrc_cgt_gains_spine.","id":"hmrc_cgt_gains_spine","inputs":[{"columns":["frs_benunit_capital"],"entity":"benunit","rows":"all"},{"columns":["region"],"entity":"household","rows":"all"},{"columns":["age","dividend_income","employment_income","miscellaneous_income","private_pension_income","property_income","savings_interest_income","self_employment_income","state_pension_reported","tax_free_savings_income"],"entity":"person","rows":"all"}],"kernel":"uk.stage.hmrc_cgt_gains_spine@1","mass":"conserve","outputs":[{"column":"capital_gains","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"}],"params":{"stage":"hmrc_cgt_gains_spine","stage_contract_sha256":"2914b5187377b195d201a55b10792d6f602ae85475580a262bf84d10d9766e5a","time_period":"2024"},"population":"hmrc_cgt_gains_spine.boundary","sources":["frs"],"structural":"none","weights":null},{"base":null,"citation":"","description":"Run UK spine stage hmrc_cgt_asset_type_spine.","id":"hmrc_cgt_asset_type_spine","inputs":[{"columns":["frs_benunit_capital"],"entity":"benunit","rows":"all"},{"columns":["region"],"entity":"household","rows":"all"},{"columns":["age","capital_gains"],"entity":"person","rows":"all"}],"kernel":"uk.stage.hmrc_cgt_asset_type_spine@1","mass":"conserve","outputs":[{"column":"capital_gains_asset_type","dtype":"string","entity":"person","ownership":"produced","rows":"all"},{"column":"capital_gains_residential_property","dtype":"float64","entity":"person","ownership":"produced","rows":"all"}],"params":{"stage":"hmrc_cgt_asset_type_spine","stage_contract_sha256":"529706ff53425e28907eca2451a6d7c8ccb5b0f82643ed62eb3947ed4f71dfc3","time_period":"2024"},"population":"hmrc_cgt_gains_spine.boundary","sources":["frs"],"structural":"none","weights":null},{"base":null,"citation":"","description":"Run UK spine stage salary_sacrifice.","id":"salary_sacrifice","inputs":[{"columns":["frs_benunit_capital","is_married","dependent_children","would_claim_child_benefit","child_benefit_opts_out","would_claim_pc","would_claim_uc","would_claim_tfc","would_claim_extended_childcare","would_claim_universal_childcare","would_claim_targeted_childcare","would_claim_uc_childcare","maximum_extended_childcare_hours_usage","benunit_source_id","benunit_support_channel","benunit_support_clone_index","uc_reported_capital","uc_deduction_random_draw","uc_deduction_type_random_draw","uc_latent_deduction_rate","uc_deduction_combination"],"entity":"benunit","rows":"all"},{"columns":["region","tenure_type","accommodation_type","num_bedrooms","council_tax_reported","council_tax_band","council_tax_rebate","council_tax_single_adult_raw","water_and_sewerage_charges","domestic_rates","rent","subrent","mortgage_interest_repayment","mortgage_capital_repayment","structural_insurance_payments","housing_service_charges","external_child_payments","ons_household_type","council_tax","household_owns_tv","would_evade_tv_licence_fee","main_residential_property_purchased_is_first_home","property_purchased","brma","owned_land","property_wealth","corporate_wealth","private_pension_wealth","gross_financial_wealth","net_financial_wealth","main_residence_value","other_residential_property_value","non_residential_property_value","savings","num_vehicles","cash_isa","stocks_and_shares_isa","mortgage_debt","consumer_debt","food_and_non_alcoholic_beverages_consumption","alcohol_and_tobacco_consumption","clothing_and_footwear_consumption","housing_water_and_electricity_consumption","household_furnishings_consumption","health_consumption","transport_consumption","communication_consumption","recreation_consumption","education_consumption","restaurants_and_hotels_consumption","miscellaneous_consumption","petrol_spending","diesel_spending","bus_fare_spending","domestic_energy_consumption","electricity_consumption","gas_consumption","has_fuel_consumption","full_rate_vat_expenditure_rate","dfe_education_spending","rail_subsidy_spending","bus_subsidy_spending","rail_usage","source_household_id","source_year","source_household_key","household_source_id","household_support_channel","household_support_clone_index","household_is_spi_synthetic","household_is_capital_gains_clone","household_is_cgt_band_donor"],"entity":"household","rows":"all"},{"columns":["age","gender","marital_status","hours_worked","care_hours","is_household_head","is_benunit_head","is_parent","is_uc_claimant","employment_income","self_employment_income","private_pension_income","tax_free_savings_income","savings_interest_income","dividend_income","property_income","maintenance_income","miscellaneous_income","private_transfer_income","lump_sum_income","student_loan_repayments","statutory_sick_pay","statutory_maternity_pay","student_loans","access_fund","education_grants","healthy_start_vouchers","free_school_breakfasts","free_school_fruit_veg","free_school_meals","council_tax_benefit_reported","maintenance_expenses","childcare_expenses","personal_pension_contributions","salary_sacrifice_reported","salary_sacrifice_asked","child_benefit_reported","income_support_reported","housing_benefit_reported","attendance_allowance_reported","dla_sc_reported","dla_m_reported","iidb_reported","carers_allowance_reported","would_claim_carers_allowance","sda_reported","afcs_reported","ssmg_reported","pension_credit_reported","child_tax_credit_reported","working_tax_credit_reported","state_pension_reported","winter_fuel_allowance_reported","incapacity_benefit_reported","universal_credit_reported","pip_m_reported","pip_dl_reported","jsa_contrib_reported","jsa_income_reported","esa_contrib_reported","esa_income_reported","bsp_reported","relationship_to_head","ons_family_role","ons_family_index","employment_status","employment_sector","sic_industry_division","aa_category","dla_sc_category","dla_m_category","pip_m_category","pip_dl_category","is_disabled_for_benefits","is_enhanced_disabled_for_benefits","is_severely_disabled_for_benefits","current_education","highest_education","is_in_non_advanced_education","is_in_approved_training","age_started_or_accepted_current_education_or_training","is_before_universal_credit_qualifying_young_person_terminal_date","adult_ema","child_ema","receives_benefits_in_own_right","legacy_jobseeker_proxy","esa_health_condition_proxy","esa_support_group_proxy","disabled_students_allowance_eligible_expenses","would_claim_marriage_allowance","would_claim_scp","attends_private_school_random_draw","tax_free_childcare_spend_routed_share","student_loan_balance","a_and_e_visits","admitted_patient_visits","outpatient_visits","nhs_a_and_e_spending","nhs_admitted_patient_spending","nhs_outpatient_spending","hmrc_spi_pay","hmrc_spi_unemployment_benefit_income","hmrc_spi_incapacity_benefit_income","ossben_identifiable_subset","srp_regular_code5","employer_pension_contributions","person_source_id","person_support_channel","person_support_clone_index","charitable_investment_gifts","gift_aid","other_investment_income","hmrc_spi_employment_benefits","hmrc_spi_employment_expenses","hmrc_spi_other_social_security_income","hmrc_spi_taxable_termination_pay","hmrc_spi_miscellaneous_employment_income","hmrc_spi_other_income","hmrc_spi_state_pension_income","hmrc_spi_employed_income","hmrc_spi_total_earned_income","hmrc_spi_total_investment_income","hmrc_spi_assessable_income","capital_gains","capital_gains_asset_type","capital_gains_residential_property"],"entity":"person","rows":"all"}],"kernel":"uk.stage.salary_sacrifice@1","mass":"conserve","outputs":[{"column":"pension_contributions_via_salary_sacrifice","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"employee_pension_contributions","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"}],"params":{"stage":"salary_sacrifice","stage_contract_sha256":"a4af925af21cc6766eb7c8855b6743a3aa4eeeb38df12cfc83063d7ad6890539","time_period":"2024"},"population":"hmrc_cgt_gains_spine.boundary","sources":["frs"],"structural":"none","weights":null},{"base":null,"citation":"","description":"Run UK spine stage student_loans.","id":"student_loans","inputs":[{"columns":["frs_benunit_capital"],"entity":"benunit","rows":"all"},{"columns":["region"],"entity":"household","rows":"all"},{"columns":["age","current_education","employee_pension_contributions","highest_education","student_loan_repayments","student_loans"],"entity":"person","rows":"all"}],"kernel":"uk.stage.student_loans@1","mass":"conserve","outputs":[{"column":"student_loan_plan","dtype":"string","entity":"person","ownership":"produced","rows":"all"}],"params":{"stage":"student_loans","stage_contract_sha256":"7c4c76398c2e80b41c7f941a5f06246b57d89b5eb0c36d42e3116ac35e82e203","time_period":"2024"},"population":"hmrc_cgt_gains_spine.boundary","sources":["frs"],"structural":"none","weights":null}],"sources":[{"codec":"csv-tables","description":"Content-bound UK FRS and donor fixture/source bundle.","name":"frs"}]} +{"country":"uk","nodes":[{"base":null,"citation":"","description":"Load the source-bound UK FRS root population.","id":"create_uk_frs","inputs":[],"kernel":"uk.create@1","mass":"conserve","outputs":[{"column":"age","dtype":"int64","entity":"person","ownership":"produced","rows":"all"},{"column":"gender","dtype":"string","entity":"person","ownership":"produced","rows":"all"},{"column":"marital_status","dtype":"string","entity":"person","ownership":"produced","rows":"all"},{"column":"hours_worked","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"care_hours","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"is_household_head","dtype":"bool","entity":"person","ownership":"produced","rows":"all"},{"column":"is_benunit_head","dtype":"bool","entity":"person","ownership":"produced","rows":"all"},{"column":"is_parent","dtype":"bool","entity":"person","ownership":"produced","rows":"all"},{"column":"is_uc_claimant","dtype":"bool","entity":"person","ownership":"produced","rows":"all"},{"column":"employment_income","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"self_employment_income","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"private_pension_income","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"tax_free_savings_income","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"savings_interest_income","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"dividend_income","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"property_income","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"maintenance_income","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"miscellaneous_income","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"private_transfer_income","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"lump_sum_income","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"student_loan_repayments","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"statutory_sick_pay","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"statutory_maternity_pay","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"student_loans","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"access_fund","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"education_grants","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"healthy_start_vouchers","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"free_school_breakfasts","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"free_school_fruit_veg","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"free_school_meals","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"council_tax_benefit_reported","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"maintenance_expenses","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"childcare_expenses","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"personal_pension_contributions","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"employee_pension_contributions","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"pension_contributions_via_salary_sacrifice","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"salary_sacrifice_reported","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"salary_sacrifice_asked","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"child_benefit_reported","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"income_support_reported","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"housing_benefit_reported","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"attendance_allowance_reported","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"dla_sc_reported","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"dla_m_reported","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"iidb_reported","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"carers_allowance_reported","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"would_claim_carers_allowance","dtype":"bool","entity":"person","ownership":"produced","rows":"all"},{"column":"sda_reported","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"afcs_reported","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"ssmg_reported","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"pension_credit_reported","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"child_tax_credit_reported","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"working_tax_credit_reported","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"state_pension_reported","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"winter_fuel_allowance_reported","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"incapacity_benefit_reported","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"universal_credit_reported","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"pip_m_reported","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"pip_dl_reported","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"jsa_contrib_reported","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"jsa_income_reported","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"esa_contrib_reported","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"esa_income_reported","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"bsp_reported","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"frs_benunit_capital","dtype":"float64","entity":"benunit","ownership":"produced","rows":"all"},{"column":"is_married","dtype":"bool","entity":"benunit","ownership":"produced","rows":"all"},{"column":"dependent_children","dtype":"int64","entity":"benunit","ownership":"produced","rows":"all"},{"column":"region","dtype":"string","entity":"household","ownership":"produced","rows":"all"},{"column":"tenure_type","dtype":"string","entity":"household","ownership":"produced","rows":"all"},{"column":"accommodation_type","dtype":"string","entity":"household","ownership":"produced","rows":"all"},{"column":"num_bedrooms","dtype":"int64","entity":"household","ownership":"produced","rows":"all"},{"column":"council_tax_reported","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"council_tax_band","dtype":"string","entity":"household","ownership":"produced","rows":"all"},{"column":"council_tax_rebate","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"council_tax_single_adult_raw","dtype":"int64","entity":"household","ownership":"produced","rows":"all"},{"column":"water_and_sewerage_charges","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"domestic_rates","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"rent","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"subrent","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"mortgage_interest_repayment","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"mortgage_capital_repayment","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"structural_insurance_payments","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"housing_service_charges","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"external_child_payments","dtype":"float64","entity":"household","ownership":"produced","rows":"all"}],"params":{"sample_fraction":1.0,"sample_seed":578,"stage_contract_sha256":"047d30937efb21e39259fdbed5597751e4f5a0c341ea583c7446e4c9c11eba48","time_period":"2024"},"population":null,"sources":["frs"],"structural":"create","weights":null},{"base":"create_uk_frs","citation":"","description":"Ownership boundary for the source-assembling root stage.","id":"frs_spine.boundary","inputs":[{"columns":["frs_benunit_capital","is_married","dependent_children"],"entity":"benunit","rows":"all"},{"columns":["region","tenure_type","accommodation_type","num_bedrooms","council_tax_reported","council_tax_band","council_tax_rebate","council_tax_single_adult_raw","water_and_sewerage_charges","domestic_rates","rent","subrent","mortgage_interest_repayment","mortgage_capital_repayment","structural_insurance_payments","housing_service_charges","external_child_payments"],"entity":"household","rows":"all"},{"columns":["age","gender","marital_status","hours_worked","care_hours","is_household_head","is_benunit_head","is_parent","is_uc_claimant","employment_income","self_employment_income","private_pension_income","tax_free_savings_income","savings_interest_income","dividend_income","property_income","maintenance_income","miscellaneous_income","private_transfer_income","lump_sum_income","student_loan_repayments","statutory_sick_pay","statutory_maternity_pay","student_loans","access_fund","education_grants","healthy_start_vouchers","free_school_breakfasts","free_school_fruit_veg","free_school_meals","council_tax_benefit_reported","maintenance_expenses","childcare_expenses","personal_pension_contributions","employee_pension_contributions","pension_contributions_via_salary_sacrifice","salary_sacrifice_reported","salary_sacrifice_asked","child_benefit_reported","income_support_reported","housing_benefit_reported","attendance_allowance_reported","dla_sc_reported","dla_m_reported","iidb_reported","carers_allowance_reported","would_claim_carers_allowance","sda_reported","afcs_reported","ssmg_reported","pension_credit_reported","child_tax_credit_reported","working_tax_credit_reported","state_pension_reported","winter_fuel_allowance_reported","incapacity_benefit_reported","universal_credit_reported","pip_m_reported","pip_dl_reported","jsa_contrib_reported","jsa_income_reported","esa_contrib_reported","esa_income_reported","bsp_reported"],"entity":"person","rows":"all"}],"kernel":"uk.identity@1","mass":"conserve","outputs":[],"params":{},"population":null,"sources":[],"structural":"filter","weights":null},{"base":null,"citation":"","description":"Claim the cells assembled by the UK FRS root transform.","id":"frs_spine","inputs":[],"kernel":"uk.claim@1","mass":"conserve","outputs":[{"column":"age","dtype":"int64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"gender","dtype":"string","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"marital_status","dtype":"string","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"hours_worked","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"care_hours","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"is_household_head","dtype":"bool","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"is_benunit_head","dtype":"bool","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"is_parent","dtype":"bool","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"is_uc_claimant","dtype":"bool","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"employment_income","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"self_employment_income","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"private_pension_income","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"tax_free_savings_income","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"savings_interest_income","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"dividend_income","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"property_income","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"maintenance_income","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"miscellaneous_income","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"private_transfer_income","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"lump_sum_income","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"student_loan_repayments","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"statutory_sick_pay","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"statutory_maternity_pay","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"student_loans","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"access_fund","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"education_grants","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"healthy_start_vouchers","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"free_school_breakfasts","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"free_school_fruit_veg","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"free_school_meals","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"council_tax_benefit_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"maintenance_expenses","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"childcare_expenses","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"personal_pension_contributions","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"employee_pension_contributions","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"pension_contributions_via_salary_sacrifice","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"salary_sacrifice_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"salary_sacrifice_asked","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"child_benefit_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"income_support_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"housing_benefit_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"attendance_allowance_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"dla_sc_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"dla_m_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"iidb_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"carers_allowance_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"would_claim_carers_allowance","dtype":"bool","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"sda_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"afcs_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"ssmg_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"pension_credit_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"child_tax_credit_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"working_tax_credit_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"state_pension_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"winter_fuel_allowance_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"incapacity_benefit_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"universal_credit_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"pip_m_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"pip_dl_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"jsa_contrib_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"jsa_income_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"esa_contrib_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"esa_income_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"bsp_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"frs_benunit_capital","dtype":"float64","entity":"benunit","ownership":"produced","rewrite":true,"rows":"all"},{"column":"is_married","dtype":"bool","entity":"benunit","ownership":"produced","rewrite":true,"rows":"all"},{"column":"dependent_children","dtype":"int64","entity":"benunit","ownership":"produced","rewrite":true,"rows":"all"},{"column":"region","dtype":"string","entity":"household","ownership":"produced","rewrite":true,"rows":"all"},{"column":"tenure_type","dtype":"string","entity":"household","ownership":"produced","rewrite":true,"rows":"all"},{"column":"accommodation_type","dtype":"string","entity":"household","ownership":"produced","rewrite":true,"rows":"all"},{"column":"num_bedrooms","dtype":"int64","entity":"household","ownership":"produced","rewrite":true,"rows":"all"},{"column":"council_tax_reported","dtype":"float64","entity":"household","ownership":"produced","rewrite":true,"rows":"all"},{"column":"council_tax_band","dtype":"string","entity":"household","ownership":"produced","rewrite":true,"rows":"all"},{"column":"council_tax_rebate","dtype":"float64","entity":"household","ownership":"produced","rewrite":true,"rows":"all"},{"column":"council_tax_single_adult_raw","dtype":"int64","entity":"household","ownership":"produced","rewrite":true,"rows":"all"},{"column":"water_and_sewerage_charges","dtype":"float64","entity":"household","ownership":"produced","rewrite":true,"rows":"all"},{"column":"domestic_rates","dtype":"float64","entity":"household","ownership":"produced","rewrite":true,"rows":"all"},{"column":"rent","dtype":"float64","entity":"household","ownership":"produced","rewrite":true,"rows":"all"},{"column":"subrent","dtype":"float64","entity":"household","ownership":"produced","rewrite":true,"rows":"all"},{"column":"mortgage_interest_repayment","dtype":"float64","entity":"household","ownership":"produced","rewrite":true,"rows":"all"},{"column":"mortgage_capital_repayment","dtype":"float64","entity":"household","ownership":"produced","rewrite":true,"rows":"all"},{"column":"structural_insurance_payments","dtype":"float64","entity":"household","ownership":"produced","rewrite":true,"rows":"all"},{"column":"housing_service_charges","dtype":"float64","entity":"household","ownership":"produced","rewrite":true,"rows":"all"},{"column":"external_child_payments","dtype":"float64","entity":"household","ownership":"produced","rewrite":true,"rows":"all"}],"params":{},"population":"frs_spine.boundary","sources":[],"structural":"none","weights":null},{"base":"frs_spine.boundary","citation":"","description":"Ownership boundary before age_tail rewrites.","id":"age_tail.boundary","inputs":[{"columns":["frs_benunit_capital","is_married","dependent_children"],"entity":"benunit","rows":"all"},{"columns":["region","tenure_type","accommodation_type","num_bedrooms","council_tax_reported","council_tax_band","council_tax_rebate","council_tax_single_adult_raw","water_and_sewerage_charges","domestic_rates","rent","subrent","mortgage_interest_repayment","mortgage_capital_repayment","structural_insurance_payments","housing_service_charges","external_child_payments"],"entity":"household","rows":"all"},{"columns":["age","gender","marital_status","hours_worked","care_hours","is_household_head","is_benunit_head","is_parent","is_uc_claimant","employment_income","self_employment_income","private_pension_income","tax_free_savings_income","savings_interest_income","dividend_income","property_income","maintenance_income","miscellaneous_income","private_transfer_income","lump_sum_income","student_loan_repayments","statutory_sick_pay","statutory_maternity_pay","student_loans","access_fund","education_grants","healthy_start_vouchers","free_school_breakfasts","free_school_fruit_veg","free_school_meals","council_tax_benefit_reported","maintenance_expenses","childcare_expenses","personal_pension_contributions","employee_pension_contributions","pension_contributions_via_salary_sacrifice","salary_sacrifice_reported","salary_sacrifice_asked","child_benefit_reported","income_support_reported","housing_benefit_reported","attendance_allowance_reported","dla_sc_reported","dla_m_reported","iidb_reported","carers_allowance_reported","would_claim_carers_allowance","sda_reported","afcs_reported","ssmg_reported","pension_credit_reported","child_tax_credit_reported","working_tax_credit_reported","state_pension_reported","winter_fuel_allowance_reported","incapacity_benefit_reported","universal_credit_reported","pip_m_reported","pip_dl_reported","jsa_contrib_reported","jsa_income_reported","esa_contrib_reported","esa_income_reported","bsp_reported"],"entity":"person","rows":"all"}],"kernel":"uk.identity@1","mass":"conserve","outputs":[],"params":{},"population":null,"sources":[],"structural":"filter","weights":null},{"base":null,"citation":"","description":"Run UK spine stage age_tail.","id":"age_tail","inputs":[{"columns":["frs_benunit_capital"],"entity":"benunit","rows":"all"},{"columns":["external_child_payments","region"],"entity":"household","rows":"all"},{"columns":["gender"],"entity":"person","rows":"all"}],"kernel":"uk.stage.age_tail@1","mass":"conserve","outputs":[{"column":"age","dtype":"int64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"}],"params":{"stage":"age_tail","stage_contract_sha256":"963a34f38caede2525e3b8735f904bb70a1bbef5a272f0eefd4346343444484f","time_period":"2024"},"population":"age_tail.boundary","sources":["frs"],"structural":"none","weights":null},{"base":null,"citation":"","description":"Run UK spine stage frs_relationships.","id":"frs_relationships","inputs":[{"columns":["frs_benunit_capital"],"entity":"benunit","rows":"all"},{"columns":["region"],"entity":"household","rows":"all"},{"columns":["age","is_household_head"],"entity":"person","rows":"all"}],"kernel":"uk.stage.frs_relationships@1","mass":"conserve","outputs":[{"column":"relationship_to_head","dtype":"string","entity":"person","ownership":"produced","rows":"all"},{"column":"ons_family_role","dtype":"string","entity":"person","ownership":"produced","rows":"all"},{"column":"ons_family_index","dtype":"int64","entity":"person","ownership":"produced","rows":"all"},{"column":"ons_household_type","dtype":"string","entity":"household","ownership":"produced","rows":"all"}],"params":{"stage":"frs_relationships","stage_contract_sha256":"30628808d06d9829ebcbcf460cd355a01261e78c7c73e6006c75bec2e3d60c33","time_period":"2024"},"population":"age_tail.boundary","sources":["frs"],"structural":"none","weights":null},{"base":null,"citation":"","description":"Run UK spine stage frs_employment.","id":"frs_employment","inputs":[{"columns":["frs_benunit_capital"],"entity":"benunit","rows":"all"},{"columns":["ons_household_type","region"],"entity":"household","rows":"all"},{"columns":["age"],"entity":"person","rows":"all"}],"kernel":"uk.stage.frs_employment@1","mass":"conserve","outputs":[{"column":"employment_status","dtype":"string","entity":"person","ownership":"produced","rows":"all"},{"column":"employment_sector","dtype":"string","entity":"person","ownership":"produced","rows":"all"},{"column":"sic_industry_division","dtype":"int64","entity":"person","ownership":"produced","rows":"all"}],"params":{"stage":"frs_employment","stage_contract_sha256":"ddbefaf05b44788d794a6e4b0e8926c14318d66e50d2f11f50ca549538bbf60c","time_period":"2024"},"population":"age_tail.boundary","sources":["frs"],"structural":"none","weights":null},{"base":null,"citation":"","description":"Run UK spine stage frs_council_tax.","id":"frs_council_tax","inputs":[{"columns":["frs_benunit_capital"],"entity":"benunit","rows":"all"},{"columns":["region"],"entity":"household","rows":"all"},{"columns":["age","sic_industry_division"],"entity":"person","rows":"all"}],"kernel":"uk.stage.frs_council_tax@1","mass":"conserve","outputs":[{"column":"council_tax","dtype":"float64","entity":"household","ownership":"produced","rows":"all"}],"params":{"stage":"frs_council_tax","stage_contract_sha256":"3381cd9f7a736514d5073c57480e07f5098890f3ca9ed3865392043594771eb9","time_period":"2024"},"population":"age_tail.boundary","sources":["frs"],"structural":"none","weights":null},{"base":null,"citation":"","description":"Run UK spine stage frs_disability.","id":"frs_disability","inputs":[{"columns":["frs_benunit_capital"],"entity":"benunit","rows":"all"},{"columns":["council_tax","region"],"entity":"household","rows":"all"},{"columns":["afcs_reported","age","attendance_allowance_reported","dla_m_reported","dla_sc_reported","esa_contrib_reported","esa_income_reported","iidb_reported","incapacity_benefit_reported","pip_dl_reported","pip_m_reported","sda_reported"],"entity":"person","rows":"all"}],"kernel":"uk.stage.frs_disability@1","mass":"conserve","outputs":[{"column":"aa_category","dtype":"string","entity":"person","ownership":"produced","rows":"all"},{"column":"dla_sc_category","dtype":"string","entity":"person","ownership":"produced","rows":"all"},{"column":"dla_m_category","dtype":"string","entity":"person","ownership":"produced","rows":"all"},{"column":"pip_m_category","dtype":"string","entity":"person","ownership":"produced","rows":"all"},{"column":"pip_dl_category","dtype":"string","entity":"person","ownership":"produced","rows":"all"},{"column":"is_disabled_for_benefits","dtype":"bool","entity":"person","ownership":"produced","rows":"all"},{"column":"is_enhanced_disabled_for_benefits","dtype":"bool","entity":"person","ownership":"produced","rows":"all"},{"column":"is_severely_disabled_for_benefits","dtype":"bool","entity":"person","ownership":"produced","rows":"all"}],"params":{"stage":"frs_disability","stage_contract_sha256":"ac500b4cb1bc04d4fb72d198592dd376d61da8c150921a6ac53914ac18be0508","time_period":"2024"},"population":"age_tail.boundary","sources":["frs"],"structural":"none","weights":null},{"base":null,"citation":"","description":"Run UK spine stage frs_education.","id":"frs_education","inputs":[{"columns":["frs_benunit_capital"],"entity":"benunit","rows":"all"},{"columns":["region"],"entity":"household","rows":"all"},{"columns":["age","esa_contrib_reported","esa_income_reported","is_severely_disabled_for_benefits","jsa_contrib_reported","jsa_income_reported","universal_credit_reported"],"entity":"person","rows":"all"}],"kernel":"uk.stage.frs_education@1","mass":"conserve","outputs":[{"column":"current_education","dtype":"string","entity":"person","ownership":"produced","rows":"all"},{"column":"highest_education","dtype":"string","entity":"person","ownership":"produced","rows":"all"},{"column":"is_in_non_advanced_education","dtype":"bool","entity":"person","ownership":"produced","rows":"all"},{"column":"is_in_approved_training","dtype":"bool","entity":"person","ownership":"produced","rows":"all"},{"column":"age_started_or_accepted_current_education_or_training","dtype":"int64","entity":"person","ownership":"produced","rows":"all"},{"column":"is_before_universal_credit_qualifying_young_person_terminal_date","dtype":"bool","entity":"person","ownership":"produced","rows":"all"},{"column":"adult_ema","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"child_ema","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"receives_benefits_in_own_right","dtype":"bool","entity":"person","ownership":"produced","rows":"all"}],"params":{"stage":"frs_education","stage_contract_sha256":"836cb0f5a582fee1425190e96c9cb81bdef859bd236c8b6bc27660b6e3d0c2f8","time_period":"2024"},"population":"age_tail.boundary","sources":["frs"],"structural":"none","weights":null},{"base":null,"citation":"","description":"Run UK spine stage frs_legacy_proxies.","id":"frs_legacy_proxies","inputs":[{"columns":["frs_benunit_capital","is_married","dependent_children"],"entity":"benunit","rows":"all"},{"columns":["region","tenure_type","accommodation_type","num_bedrooms","council_tax_reported","council_tax_band","council_tax_rebate","council_tax_single_adult_raw","water_and_sewerage_charges","domestic_rates","rent","subrent","mortgage_interest_repayment","mortgage_capital_repayment","structural_insurance_payments","housing_service_charges","external_child_payments","ons_household_type","council_tax"],"entity":"household","rows":"all"},{"columns":["age","gender","marital_status","hours_worked","care_hours","is_household_head","is_benunit_head","is_parent","is_uc_claimant","employment_income","self_employment_income","private_pension_income","tax_free_savings_income","savings_interest_income","dividend_income","property_income","maintenance_income","miscellaneous_income","private_transfer_income","lump_sum_income","student_loan_repayments","statutory_sick_pay","statutory_maternity_pay","student_loans","access_fund","education_grants","healthy_start_vouchers","free_school_breakfasts","free_school_fruit_veg","free_school_meals","council_tax_benefit_reported","maintenance_expenses","childcare_expenses","personal_pension_contributions","employee_pension_contributions","pension_contributions_via_salary_sacrifice","salary_sacrifice_reported","salary_sacrifice_asked","child_benefit_reported","income_support_reported","housing_benefit_reported","attendance_allowance_reported","dla_sc_reported","dla_m_reported","iidb_reported","carers_allowance_reported","would_claim_carers_allowance","sda_reported","afcs_reported","ssmg_reported","pension_credit_reported","child_tax_credit_reported","working_tax_credit_reported","state_pension_reported","winter_fuel_allowance_reported","incapacity_benefit_reported","universal_credit_reported","pip_m_reported","pip_dl_reported","jsa_contrib_reported","jsa_income_reported","esa_contrib_reported","esa_income_reported","bsp_reported","relationship_to_head","ons_family_role","ons_family_index","employment_status","employment_sector","sic_industry_division","aa_category","dla_sc_category","dla_m_category","pip_m_category","pip_dl_category","is_disabled_for_benefits","is_enhanced_disabled_for_benefits","is_severely_disabled_for_benefits","current_education","highest_education","is_in_non_advanced_education","is_in_approved_training","age_started_or_accepted_current_education_or_training","is_before_universal_credit_qualifying_young_person_terminal_date","adult_ema","child_ema","receives_benefits_in_own_right"],"entity":"person","rows":"all"}],"kernel":"uk.stage.frs_legacy_proxies@1","mass":"conserve","outputs":[{"column":"legacy_jobseeker_proxy","dtype":"bool","entity":"person","ownership":"produced","rows":"all"},{"column":"esa_health_condition_proxy","dtype":"bool","entity":"person","ownership":"produced","rows":"all"},{"column":"esa_support_group_proxy","dtype":"bool","entity":"person","ownership":"produced","rows":"all"}],"params":{"stage":"frs_legacy_proxies","stage_contract_sha256":"1ecf761cf3fa7180da15659e138e67a8654e58272b4fde28344aa27a874ad0aa","time_period":"2024"},"population":"age_tail.boundary","sources":["frs"],"structural":"none","weights":null},{"base":"age_tail.boundary","citation":"","description":"Ownership boundary before frs_education_grant_split rewrites.","id":"frs_education_grant_split.boundary","inputs":[{"columns":["frs_benunit_capital","is_married","dependent_children"],"entity":"benunit","rows":"all"},{"columns":["region","tenure_type","accommodation_type","num_bedrooms","council_tax_reported","council_tax_band","council_tax_rebate","council_tax_single_adult_raw","water_and_sewerage_charges","domestic_rates","rent","subrent","mortgage_interest_repayment","mortgage_capital_repayment","structural_insurance_payments","housing_service_charges","external_child_payments","ons_household_type","council_tax"],"entity":"household","rows":"all"},{"columns":["age","gender","marital_status","hours_worked","care_hours","is_household_head","is_benunit_head","is_parent","is_uc_claimant","employment_income","self_employment_income","private_pension_income","tax_free_savings_income","savings_interest_income","dividend_income","property_income","maintenance_income","miscellaneous_income","private_transfer_income","lump_sum_income","student_loan_repayments","statutory_sick_pay","statutory_maternity_pay","student_loans","access_fund","education_grants","healthy_start_vouchers","free_school_breakfasts","free_school_fruit_veg","free_school_meals","council_tax_benefit_reported","maintenance_expenses","childcare_expenses","personal_pension_contributions","employee_pension_contributions","pension_contributions_via_salary_sacrifice","salary_sacrifice_reported","salary_sacrifice_asked","child_benefit_reported","income_support_reported","housing_benefit_reported","attendance_allowance_reported","dla_sc_reported","dla_m_reported","iidb_reported","carers_allowance_reported","would_claim_carers_allowance","sda_reported","afcs_reported","ssmg_reported","pension_credit_reported","child_tax_credit_reported","working_tax_credit_reported","state_pension_reported","winter_fuel_allowance_reported","incapacity_benefit_reported","universal_credit_reported","pip_m_reported","pip_dl_reported","jsa_contrib_reported","jsa_income_reported","esa_contrib_reported","esa_income_reported","bsp_reported","relationship_to_head","ons_family_role","ons_family_index","employment_status","employment_sector","sic_industry_division","aa_category","dla_sc_category","dla_m_category","pip_m_category","pip_dl_category","is_disabled_for_benefits","is_enhanced_disabled_for_benefits","is_severely_disabled_for_benefits","current_education","highest_education","is_in_non_advanced_education","is_in_approved_training","age_started_or_accepted_current_education_or_training","is_before_universal_credit_qualifying_young_person_terminal_date","adult_ema","child_ema","receives_benefits_in_own_right","legacy_jobseeker_proxy","esa_health_condition_proxy","esa_support_group_proxy"],"entity":"person","rows":"all"}],"kernel":"uk.identity@1","mass":"conserve","outputs":[],"params":{},"population":null,"sources":[],"structural":"filter","weights":null},{"base":null,"citation":"","description":"Run UK spine stage frs_education_grant_split.","id":"frs_education_grant_split","inputs":[{"columns":["frs_benunit_capital","is_married","dependent_children"],"entity":"benunit","rows":"all"},{"columns":["region","tenure_type","accommodation_type","num_bedrooms","council_tax_reported","council_tax_band","council_tax_rebate","council_tax_single_adult_raw","water_and_sewerage_charges","domestic_rates","rent","subrent","mortgage_interest_repayment","mortgage_capital_repayment","structural_insurance_payments","housing_service_charges","external_child_payments","ons_household_type","council_tax"],"entity":"household","rows":"all"},{"columns":["age","gender","marital_status","hours_worked","care_hours","is_household_head","is_benunit_head","is_parent","is_uc_claimant","employment_income","self_employment_income","private_pension_income","tax_free_savings_income","savings_interest_income","dividend_income","property_income","maintenance_income","miscellaneous_income","private_transfer_income","lump_sum_income","student_loan_repayments","statutory_sick_pay","statutory_maternity_pay","student_loans","access_fund","healthy_start_vouchers","free_school_breakfasts","free_school_fruit_veg","free_school_meals","council_tax_benefit_reported","maintenance_expenses","childcare_expenses","personal_pension_contributions","employee_pension_contributions","pension_contributions_via_salary_sacrifice","salary_sacrifice_reported","salary_sacrifice_asked","child_benefit_reported","income_support_reported","housing_benefit_reported","attendance_allowance_reported","dla_sc_reported","dla_m_reported","iidb_reported","carers_allowance_reported","would_claim_carers_allowance","sda_reported","afcs_reported","ssmg_reported","pension_credit_reported","child_tax_credit_reported","working_tax_credit_reported","state_pension_reported","winter_fuel_allowance_reported","incapacity_benefit_reported","universal_credit_reported","pip_m_reported","pip_dl_reported","jsa_contrib_reported","jsa_income_reported","esa_contrib_reported","esa_income_reported","bsp_reported","relationship_to_head","ons_family_role","ons_family_index","employment_status","employment_sector","sic_industry_division","aa_category","dla_sc_category","dla_m_category","pip_m_category","pip_dl_category","is_disabled_for_benefits","is_enhanced_disabled_for_benefits","is_severely_disabled_for_benefits","current_education","highest_education","is_in_non_advanced_education","is_in_approved_training","age_started_or_accepted_current_education_or_training","is_before_universal_credit_qualifying_young_person_terminal_date","adult_ema","child_ema","receives_benefits_in_own_right","legacy_jobseeker_proxy","esa_health_condition_proxy","esa_support_group_proxy"],"entity":"person","rows":"all"}],"kernel":"uk.stage.frs_education_grant_split@1","mass":"conserve","outputs":[{"column":"disabled_students_allowance_eligible_expenses","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"education_grants","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"}],"params":{"stage":"frs_education_grant_split","stage_contract_sha256":"8718f014b90498c2cc7c754289775ca4a41452d9b978d8cbc5f34b64cbdc7df2","time_period":"2024"},"population":"frs_education_grant_split.boundary","sources":["frs"],"structural":"none","weights":null},{"base":null,"citation":"","description":"Run UK spine stage frs_take_up.","id":"frs_take_up","inputs":[{"columns":["frs_benunit_capital","is_married"],"entity":"benunit","rows":"all"},{"columns":["region"],"entity":"household","rows":"all"},{"columns":["age","child_benefit_reported","education_grants","pension_credit_reported","universal_credit_reported"],"entity":"person","rows":"all"}],"kernel":"uk.stage.frs_take_up@1","mass":"conserve","outputs":[{"column":"would_claim_child_benefit","dtype":"bool","entity":"benunit","ownership":"produced","rows":"all"},{"column":"child_benefit_opts_out","dtype":"bool","entity":"benunit","ownership":"produced","rows":"all"},{"column":"would_claim_pc","dtype":"bool","entity":"benunit","ownership":"produced","rows":"all"},{"column":"would_claim_uc","dtype":"bool","entity":"benunit","ownership":"produced","rows":"all"},{"column":"would_claim_tfc","dtype":"bool","entity":"benunit","ownership":"produced","rows":"all"},{"column":"would_claim_extended_childcare","dtype":"bool","entity":"benunit","ownership":"produced","rows":"all"},{"column":"would_claim_universal_childcare","dtype":"bool","entity":"benunit","ownership":"produced","rows":"all"},{"column":"would_claim_targeted_childcare","dtype":"bool","entity":"benunit","ownership":"produced","rows":"all"},{"column":"would_claim_uc_childcare","dtype":"bool","entity":"benunit","ownership":"produced","rows":"all"},{"column":"maximum_extended_childcare_hours_usage","dtype":"float64","entity":"benunit","ownership":"produced","rows":"all"}],"params":{"stage":"frs_take_up","stage_contract_sha256":"067d31b7fbe733d1d182588705e9ce79df0c8e5f6e8f519314765af49b1b00d7","time_period":"2024"},"population":"frs_education_grant_split.boundary","sources":["frs"],"structural":"none","weights":null},{"base":null,"citation":"","description":"Run UK spine stage frs_person_draws.","id":"frs_person_draws","inputs":[{"columns":["frs_benunit_capital","maximum_extended_childcare_hours_usage"],"entity":"benunit","rows":"all"},{"columns":["region"],"entity":"household","rows":"all"},{"columns":["age"],"entity":"person","rows":"all"}],"kernel":"uk.stage.frs_person_draws@1","mass":"conserve","outputs":[{"column":"would_claim_marriage_allowance","dtype":"bool","entity":"person","ownership":"produced","rows":"all"},{"column":"would_claim_scp","dtype":"bool","entity":"person","ownership":"produced","rows":"all"},{"column":"attends_private_school_random_draw","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"tax_free_childcare_spend_routed_share","dtype":"float64","entity":"person","ownership":"produced","rows":"all"}],"params":{"stage":"frs_person_draws","stage_contract_sha256":"7e0af955541ec5669dcdf1de4e1e6ca642853e48b775ec4e25238b41491df324","time_period":"2024"},"population":"frs_education_grant_split.boundary","sources":["frs"],"structural":"none","weights":null},{"base":null,"citation":"","description":"Run UK spine stage frs_household_draws.","id":"frs_household_draws","inputs":[{"columns":["frs_benunit_capital"],"entity":"benunit","rows":"all"},{"columns":["region"],"entity":"household","rows":"all"},{"columns":["age","tax_free_childcare_spend_routed_share"],"entity":"person","rows":"all"}],"kernel":"uk.stage.frs_household_draws@1","mass":"conserve","outputs":[{"column":"household_owns_tv","dtype":"bool","entity":"household","ownership":"produced","rows":"all"},{"column":"would_evade_tv_licence_fee","dtype":"bool","entity":"household","ownership":"produced","rows":"all"},{"column":"main_residential_property_purchased_is_first_home","dtype":"bool","entity":"household","ownership":"produced","rows":"all"},{"column":"property_purchased","dtype":"bool","entity":"household","ownership":"produced","rows":"all"}],"params":{"stage":"frs_household_draws","stage_contract_sha256":"c74c63b09264319c4bf0049dabba00ecd0ce660beb7b53d6dae71518434bc949","time_period":"2024"},"population":"frs_education_grant_split.boundary","sources":["frs"],"structural":"none","weights":null},{"base":null,"citation":"","description":"Run UK spine stage frs_brma.","id":"frs_brma","inputs":[{"columns":["frs_benunit_capital","is_married","dependent_children","would_claim_child_benefit","child_benefit_opts_out","would_claim_pc","would_claim_uc","would_claim_tfc","would_claim_extended_childcare","would_claim_universal_childcare","would_claim_targeted_childcare","would_claim_uc_childcare","maximum_extended_childcare_hours_usage"],"entity":"benunit","rows":"all"},{"columns":["region","tenure_type","accommodation_type","num_bedrooms","council_tax_reported","council_tax_band","council_tax_rebate","council_tax_single_adult_raw","water_and_sewerage_charges","domestic_rates","rent","subrent","mortgage_interest_repayment","mortgage_capital_repayment","structural_insurance_payments","housing_service_charges","external_child_payments","ons_household_type","council_tax","household_owns_tv","would_evade_tv_licence_fee","main_residential_property_purchased_is_first_home","property_purchased"],"entity":"household","rows":"all"},{"columns":["age","gender","marital_status","hours_worked","care_hours","is_household_head","is_benunit_head","is_parent","is_uc_claimant","employment_income","self_employment_income","private_pension_income","tax_free_savings_income","savings_interest_income","dividend_income","property_income","maintenance_income","miscellaneous_income","private_transfer_income","lump_sum_income","student_loan_repayments","statutory_sick_pay","statutory_maternity_pay","student_loans","access_fund","education_grants","healthy_start_vouchers","free_school_breakfasts","free_school_fruit_veg","free_school_meals","council_tax_benefit_reported","maintenance_expenses","childcare_expenses","personal_pension_contributions","employee_pension_contributions","pension_contributions_via_salary_sacrifice","salary_sacrifice_reported","salary_sacrifice_asked","child_benefit_reported","income_support_reported","housing_benefit_reported","attendance_allowance_reported","dla_sc_reported","dla_m_reported","iidb_reported","carers_allowance_reported","would_claim_carers_allowance","sda_reported","afcs_reported","ssmg_reported","pension_credit_reported","child_tax_credit_reported","working_tax_credit_reported","state_pension_reported","winter_fuel_allowance_reported","incapacity_benefit_reported","universal_credit_reported","pip_m_reported","pip_dl_reported","jsa_contrib_reported","jsa_income_reported","esa_contrib_reported","esa_income_reported","bsp_reported","relationship_to_head","ons_family_role","ons_family_index","employment_status","employment_sector","sic_industry_division","aa_category","dla_sc_category","dla_m_category","pip_m_category","pip_dl_category","is_disabled_for_benefits","is_enhanced_disabled_for_benefits","is_severely_disabled_for_benefits","current_education","highest_education","is_in_non_advanced_education","is_in_approved_training","age_started_or_accepted_current_education_or_training","is_before_universal_credit_qualifying_young_person_terminal_date","adult_ema","child_ema","receives_benefits_in_own_right","legacy_jobseeker_proxy","esa_health_condition_proxy","esa_support_group_proxy","disabled_students_allowance_eligible_expenses","would_claim_marriage_allowance","would_claim_scp","attends_private_school_random_draw","tax_free_childcare_spend_routed_share"],"entity":"person","rows":"all"}],"kernel":"uk.stage.frs_brma@1","mass":"conserve","outputs":[{"column":"brma","dtype":"string","entity":"household","ownership":"produced","rows":"all"}],"params":{"stage":"frs_brma","stage_contract_sha256":"5b8f0b361310c7cd3efbaae3762b347649b7d4afb9e9943131d9bef9266fc3c8","time_period":"2024"},"population":"frs_education_grant_split.boundary","sources":["frs"],"structural":"none","weights":null},{"base":"frs_education_grant_split.boundary","citation":"","description":"Freeze the assembled-spine gate population.","id":"frs_brma.checkpoint","inputs":[{"columns":["frs_benunit_capital","is_married","dependent_children","would_claim_child_benefit","child_benefit_opts_out","would_claim_pc","would_claim_uc","would_claim_tfc","would_claim_extended_childcare","would_claim_universal_childcare","would_claim_targeted_childcare","would_claim_uc_childcare","maximum_extended_childcare_hours_usage"],"entity":"benunit","rows":"all"},{"columns":["region","tenure_type","accommodation_type","num_bedrooms","council_tax_reported","council_tax_band","council_tax_rebate","council_tax_single_adult_raw","water_and_sewerage_charges","domestic_rates","rent","subrent","mortgage_interest_repayment","mortgage_capital_repayment","structural_insurance_payments","housing_service_charges","external_child_payments","ons_household_type","council_tax","household_owns_tv","would_evade_tv_licence_fee","main_residential_property_purchased_is_first_home","property_purchased","brma"],"entity":"household","rows":"all"},{"columns":["age","gender","marital_status","hours_worked","care_hours","is_household_head","is_benunit_head","is_parent","is_uc_claimant","employment_income","self_employment_income","private_pension_income","tax_free_savings_income","savings_interest_income","dividend_income","property_income","maintenance_income","miscellaneous_income","private_transfer_income","lump_sum_income","student_loan_repayments","statutory_sick_pay","statutory_maternity_pay","student_loans","access_fund","education_grants","healthy_start_vouchers","free_school_breakfasts","free_school_fruit_veg","free_school_meals","council_tax_benefit_reported","maintenance_expenses","childcare_expenses","personal_pension_contributions","employee_pension_contributions","pension_contributions_via_salary_sacrifice","salary_sacrifice_reported","salary_sacrifice_asked","child_benefit_reported","income_support_reported","housing_benefit_reported","attendance_allowance_reported","dla_sc_reported","dla_m_reported","iidb_reported","carers_allowance_reported","would_claim_carers_allowance","sda_reported","afcs_reported","ssmg_reported","pension_credit_reported","child_tax_credit_reported","working_tax_credit_reported","state_pension_reported","winter_fuel_allowance_reported","incapacity_benefit_reported","universal_credit_reported","pip_m_reported","pip_dl_reported","jsa_contrib_reported","jsa_income_reported","esa_contrib_reported","esa_income_reported","bsp_reported","relationship_to_head","ons_family_role","ons_family_index","employment_status","employment_sector","sic_industry_division","aa_category","dla_sc_category","dla_m_category","pip_m_category","pip_dl_category","is_disabled_for_benefits","is_enhanced_disabled_for_benefits","is_severely_disabled_for_benefits","current_education","highest_education","is_in_non_advanced_education","is_in_approved_training","age_started_or_accepted_current_education_or_training","is_before_universal_credit_qualifying_young_person_terminal_date","adult_ema","child_ema","receives_benefits_in_own_right","legacy_jobseeker_proxy","esa_health_condition_proxy","esa_support_group_proxy","disabled_students_allowance_eligible_expenses","would_claim_marriage_allowance","would_claim_scp","attends_private_school_random_draw","tax_free_childcare_spend_routed_share"],"entity":"person","rows":"all"}],"kernel":"uk.identity@1","mass":"conserve","outputs":[],"params":{},"population":null,"sources":[],"structural":"filter","weights":null},{"base":null,"citation":"","description":"Run UK spine stage was_wealth.","id":"was_wealth","inputs":[{"columns":["frs_benunit_capital","is_married","dependent_children","would_claim_child_benefit","child_benefit_opts_out","would_claim_pc","would_claim_uc","would_claim_tfc","would_claim_extended_childcare","would_claim_universal_childcare","would_claim_targeted_childcare","would_claim_uc_childcare","maximum_extended_childcare_hours_usage"],"entity":"benunit","rows":"all"},{"columns":["region","tenure_type","accommodation_type","num_bedrooms","council_tax_reported","council_tax_band","council_tax_rebate","council_tax_single_adult_raw","water_and_sewerage_charges","domestic_rates","rent","subrent","mortgage_interest_repayment","mortgage_capital_repayment","structural_insurance_payments","housing_service_charges","external_child_payments","ons_household_type","council_tax","household_owns_tv","would_evade_tv_licence_fee","main_residential_property_purchased_is_first_home","property_purchased","brma"],"entity":"household","rows":"all"},{"columns":["age","gender","marital_status","hours_worked","care_hours","is_household_head","is_benunit_head","is_parent","is_uc_claimant","employment_income","self_employment_income","private_pension_income","tax_free_savings_income","savings_interest_income","dividend_income","property_income","maintenance_income","miscellaneous_income","private_transfer_income","lump_sum_income","student_loan_repayments","statutory_sick_pay","statutory_maternity_pay","student_loans","access_fund","education_grants","healthy_start_vouchers","free_school_breakfasts","free_school_fruit_veg","free_school_meals","council_tax_benefit_reported","maintenance_expenses","childcare_expenses","personal_pension_contributions","employee_pension_contributions","pension_contributions_via_salary_sacrifice","salary_sacrifice_reported","salary_sacrifice_asked","child_benefit_reported","income_support_reported","housing_benefit_reported","attendance_allowance_reported","dla_sc_reported","dla_m_reported","iidb_reported","carers_allowance_reported","would_claim_carers_allowance","sda_reported","afcs_reported","ssmg_reported","pension_credit_reported","child_tax_credit_reported","working_tax_credit_reported","state_pension_reported","winter_fuel_allowance_reported","incapacity_benefit_reported","universal_credit_reported","pip_m_reported","pip_dl_reported","jsa_contrib_reported","jsa_income_reported","esa_contrib_reported","esa_income_reported","bsp_reported","relationship_to_head","ons_family_role","ons_family_index","employment_status","employment_sector","sic_industry_division","aa_category","dla_sc_category","dla_m_category","pip_m_category","pip_dl_category","is_disabled_for_benefits","is_enhanced_disabled_for_benefits","is_severely_disabled_for_benefits","current_education","highest_education","is_in_non_advanced_education","is_in_approved_training","age_started_or_accepted_current_education_or_training","is_before_universal_credit_qualifying_young_person_terminal_date","adult_ema","child_ema","receives_benefits_in_own_right","legacy_jobseeker_proxy","esa_health_condition_proxy","esa_support_group_proxy","disabled_students_allowance_eligible_expenses","would_claim_marriage_allowance","would_claim_scp","attends_private_school_random_draw","tax_free_childcare_spend_routed_share"],"entity":"person","rows":"all"}],"kernel":"uk.stage.was_wealth@1","mass":"conserve","outputs":[{"column":"owned_land","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"property_wealth","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"corporate_wealth","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"private_pension_wealth","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"gross_financial_wealth","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"net_financial_wealth","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"main_residence_value","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"other_residential_property_value","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"non_residential_property_value","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"savings","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"num_vehicles","dtype":"int64","entity":"household","ownership":"produced","rows":"all"},{"column":"cash_isa","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"stocks_and_shares_isa","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"mortgage_debt","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"consumer_debt","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"student_loan_balance","dtype":"float64","entity":"person","ownership":"produced","rows":"all"}],"params":{"stage":"was_wealth","stage_contract_sha256":"366e019c41ee2a28bcd61bbaa62a024ed0bb4b30b3ffc4d87ab23b340b4076bf","time_period":"2024"},"population":"frs_brma.checkpoint","sources":["frs"],"structural":"none","weights":null},{"base":"frs_brma.checkpoint","citation":"","description":"Ownership boundary before regional_property_uprating rewrites.","id":"regional_property_uprating.boundary","inputs":[{"columns":["frs_benunit_capital","is_married","dependent_children","would_claim_child_benefit","child_benefit_opts_out","would_claim_pc","would_claim_uc","would_claim_tfc","would_claim_extended_childcare","would_claim_universal_childcare","would_claim_targeted_childcare","would_claim_uc_childcare","maximum_extended_childcare_hours_usage"],"entity":"benunit","rows":"all"},{"columns":["region","tenure_type","accommodation_type","num_bedrooms","council_tax_reported","council_tax_band","council_tax_rebate","council_tax_single_adult_raw","water_and_sewerage_charges","domestic_rates","rent","subrent","mortgage_interest_repayment","mortgage_capital_repayment","structural_insurance_payments","housing_service_charges","external_child_payments","ons_household_type","council_tax","household_owns_tv","would_evade_tv_licence_fee","main_residential_property_purchased_is_first_home","property_purchased","brma","owned_land","property_wealth","corporate_wealth","private_pension_wealth","gross_financial_wealth","net_financial_wealth","main_residence_value","other_residential_property_value","non_residential_property_value","savings","num_vehicles","cash_isa","stocks_and_shares_isa","mortgage_debt","consumer_debt"],"entity":"household","rows":"all"},{"columns":["age","gender","marital_status","hours_worked","care_hours","is_household_head","is_benunit_head","is_parent","is_uc_claimant","employment_income","self_employment_income","private_pension_income","tax_free_savings_income","savings_interest_income","dividend_income","property_income","maintenance_income","miscellaneous_income","private_transfer_income","lump_sum_income","student_loan_repayments","statutory_sick_pay","statutory_maternity_pay","student_loans","access_fund","education_grants","healthy_start_vouchers","free_school_breakfasts","free_school_fruit_veg","free_school_meals","council_tax_benefit_reported","maintenance_expenses","childcare_expenses","personal_pension_contributions","employee_pension_contributions","pension_contributions_via_salary_sacrifice","salary_sacrifice_reported","salary_sacrifice_asked","child_benefit_reported","income_support_reported","housing_benefit_reported","attendance_allowance_reported","dla_sc_reported","dla_m_reported","iidb_reported","carers_allowance_reported","would_claim_carers_allowance","sda_reported","afcs_reported","ssmg_reported","pension_credit_reported","child_tax_credit_reported","working_tax_credit_reported","state_pension_reported","winter_fuel_allowance_reported","incapacity_benefit_reported","universal_credit_reported","pip_m_reported","pip_dl_reported","jsa_contrib_reported","jsa_income_reported","esa_contrib_reported","esa_income_reported","bsp_reported","relationship_to_head","ons_family_role","ons_family_index","employment_status","employment_sector","sic_industry_division","aa_category","dla_sc_category","dla_m_category","pip_m_category","pip_dl_category","is_disabled_for_benefits","is_enhanced_disabled_for_benefits","is_severely_disabled_for_benefits","current_education","highest_education","is_in_non_advanced_education","is_in_approved_training","age_started_or_accepted_current_education_or_training","is_before_universal_credit_qualifying_young_person_terminal_date","adult_ema","child_ema","receives_benefits_in_own_right","legacy_jobseeker_proxy","esa_health_condition_proxy","esa_support_group_proxy","disabled_students_allowance_eligible_expenses","would_claim_marriage_allowance","would_claim_scp","attends_private_school_random_draw","tax_free_childcare_spend_routed_share","student_loan_balance"],"entity":"person","rows":"all"}],"kernel":"uk.identity@1","mass":"conserve","outputs":[],"params":{},"population":null,"sources":[],"structural":"filter","weights":null},{"base":null,"citation":"","description":"Run UK spine stage regional_property_uprating.","id":"regional_property_uprating","inputs":[{"columns":["frs_benunit_capital"],"entity":"benunit","rows":"all"},{"columns":["region"],"entity":"household","rows":"all"},{"columns":["age","student_loan_balance"],"entity":"person","rows":"all"}],"kernel":"uk.stage.regional_property_uprating@1","mass":"conserve","outputs":[{"column":"main_residence_value","dtype":"float64","entity":"household","ownership":"produced","rewrite":true,"rows":"all"},{"column":"property_wealth","dtype":"float64","entity":"household","ownership":"produced","rewrite":true,"rows":"all"}],"params":{"stage":"regional_property_uprating","stage_contract_sha256":"acb342e8ca1770556c5682a9411c5ead5d61dd1420cfe6b0305ed14a3407fb25","time_period":"2024"},"population":"regional_property_uprating.boundary","sources":["frs"],"structural":"none","weights":null},{"base":null,"citation":"","description":"Run UK spine stage lcfs_consumption.","id":"lcfs_consumption","inputs":[{"columns":["frs_benunit_capital","is_married","dependent_children","would_claim_child_benefit","child_benefit_opts_out","would_claim_pc","would_claim_uc","would_claim_tfc","would_claim_extended_childcare","would_claim_universal_childcare","would_claim_targeted_childcare","would_claim_uc_childcare","maximum_extended_childcare_hours_usage"],"entity":"benunit","rows":"all"},{"columns":["region","tenure_type","accommodation_type","num_bedrooms","council_tax_reported","council_tax_band","council_tax_rebate","council_tax_single_adult_raw","water_and_sewerage_charges","domestic_rates","rent","subrent","mortgage_interest_repayment","mortgage_capital_repayment","structural_insurance_payments","housing_service_charges","external_child_payments","ons_household_type","council_tax","household_owns_tv","would_evade_tv_licence_fee","main_residential_property_purchased_is_first_home","property_purchased","brma","owned_land","property_wealth","corporate_wealth","private_pension_wealth","gross_financial_wealth","net_financial_wealth","main_residence_value","other_residential_property_value","non_residential_property_value","savings","num_vehicles","cash_isa","stocks_and_shares_isa","mortgage_debt","consumer_debt"],"entity":"household","rows":"all"},{"columns":["age","gender","marital_status","hours_worked","care_hours","is_household_head","is_benunit_head","is_parent","is_uc_claimant","employment_income","self_employment_income","private_pension_income","tax_free_savings_income","savings_interest_income","dividend_income","property_income","maintenance_income","miscellaneous_income","private_transfer_income","lump_sum_income","student_loan_repayments","statutory_sick_pay","statutory_maternity_pay","student_loans","access_fund","education_grants","healthy_start_vouchers","free_school_breakfasts","free_school_fruit_veg","free_school_meals","council_tax_benefit_reported","maintenance_expenses","childcare_expenses","personal_pension_contributions","employee_pension_contributions","pension_contributions_via_salary_sacrifice","salary_sacrifice_reported","salary_sacrifice_asked","child_benefit_reported","income_support_reported","housing_benefit_reported","attendance_allowance_reported","dla_sc_reported","dla_m_reported","iidb_reported","carers_allowance_reported","would_claim_carers_allowance","sda_reported","afcs_reported","ssmg_reported","pension_credit_reported","child_tax_credit_reported","working_tax_credit_reported","state_pension_reported","winter_fuel_allowance_reported","incapacity_benefit_reported","universal_credit_reported","pip_m_reported","pip_dl_reported","jsa_contrib_reported","jsa_income_reported","esa_contrib_reported","esa_income_reported","bsp_reported","relationship_to_head","ons_family_role","ons_family_index","employment_status","employment_sector","sic_industry_division","aa_category","dla_sc_category","dla_m_category","pip_m_category","pip_dl_category","is_disabled_for_benefits","is_enhanced_disabled_for_benefits","is_severely_disabled_for_benefits","current_education","highest_education","is_in_non_advanced_education","is_in_approved_training","age_started_or_accepted_current_education_or_training","is_before_universal_credit_qualifying_young_person_terminal_date","adult_ema","child_ema","receives_benefits_in_own_right","legacy_jobseeker_proxy","esa_health_condition_proxy","esa_support_group_proxy","disabled_students_allowance_eligible_expenses","would_claim_marriage_allowance","would_claim_scp","attends_private_school_random_draw","tax_free_childcare_spend_routed_share","student_loan_balance"],"entity":"person","rows":"all"}],"kernel":"uk.stage.lcfs_consumption@1","mass":"conserve","outputs":[{"column":"food_and_non_alcoholic_beverages_consumption","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"alcohol_and_tobacco_consumption","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"clothing_and_footwear_consumption","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"housing_water_and_electricity_consumption","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"household_furnishings_consumption","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"health_consumption","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"transport_consumption","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"communication_consumption","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"recreation_consumption","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"education_consumption","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"restaurants_and_hotels_consumption","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"miscellaneous_consumption","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"petrol_spending","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"diesel_spending","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"bus_fare_spending","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"domestic_energy_consumption","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"electricity_consumption","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"gas_consumption","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"has_fuel_consumption","dtype":"bool","entity":"household","ownership":"produced","rows":"all"}],"params":{"stage":"lcfs_consumption","stage_contract_sha256":"e321f404b2e3a6c0c8bb29861290046db1d4987be071b5c9e3f6fca69a780daf","time_period":"2024"},"population":"regional_property_uprating.boundary","sources":["frs"],"structural":"none","weights":null},{"base":null,"citation":"","description":"Run UK spine stage etb_vat.","id":"etb_vat","inputs":[{"columns":["frs_benunit_capital","is_married","dependent_children","would_claim_child_benefit","child_benefit_opts_out","would_claim_pc","would_claim_uc","would_claim_tfc","would_claim_extended_childcare","would_claim_universal_childcare","would_claim_targeted_childcare","would_claim_uc_childcare","maximum_extended_childcare_hours_usage"],"entity":"benunit","rows":"all"},{"columns":["region","tenure_type","accommodation_type","num_bedrooms","council_tax_reported","council_tax_band","council_tax_rebate","council_tax_single_adult_raw","water_and_sewerage_charges","domestic_rates","rent","subrent","mortgage_interest_repayment","mortgage_capital_repayment","structural_insurance_payments","housing_service_charges","external_child_payments","ons_household_type","council_tax","household_owns_tv","would_evade_tv_licence_fee","main_residential_property_purchased_is_first_home","property_purchased","brma","owned_land","property_wealth","corporate_wealth","private_pension_wealth","gross_financial_wealth","net_financial_wealth","main_residence_value","other_residential_property_value","non_residential_property_value","savings","num_vehicles","cash_isa","stocks_and_shares_isa","mortgage_debt","consumer_debt","food_and_non_alcoholic_beverages_consumption","alcohol_and_tobacco_consumption","clothing_and_footwear_consumption","housing_water_and_electricity_consumption","household_furnishings_consumption","health_consumption","transport_consumption","communication_consumption","recreation_consumption","education_consumption","restaurants_and_hotels_consumption","miscellaneous_consumption","petrol_spending","diesel_spending","bus_fare_spending","domestic_energy_consumption","electricity_consumption","gas_consumption","has_fuel_consumption"],"entity":"household","rows":"all"},{"columns":["age","gender","marital_status","hours_worked","care_hours","is_household_head","is_benunit_head","is_parent","is_uc_claimant","employment_income","self_employment_income","private_pension_income","tax_free_savings_income","savings_interest_income","dividend_income","property_income","maintenance_income","miscellaneous_income","private_transfer_income","lump_sum_income","student_loan_repayments","statutory_sick_pay","statutory_maternity_pay","student_loans","access_fund","education_grants","healthy_start_vouchers","free_school_breakfasts","free_school_fruit_veg","free_school_meals","council_tax_benefit_reported","maintenance_expenses","childcare_expenses","personal_pension_contributions","employee_pension_contributions","pension_contributions_via_salary_sacrifice","salary_sacrifice_reported","salary_sacrifice_asked","child_benefit_reported","income_support_reported","housing_benefit_reported","attendance_allowance_reported","dla_sc_reported","dla_m_reported","iidb_reported","carers_allowance_reported","would_claim_carers_allowance","sda_reported","afcs_reported","ssmg_reported","pension_credit_reported","child_tax_credit_reported","working_tax_credit_reported","state_pension_reported","winter_fuel_allowance_reported","incapacity_benefit_reported","universal_credit_reported","pip_m_reported","pip_dl_reported","jsa_contrib_reported","jsa_income_reported","esa_contrib_reported","esa_income_reported","bsp_reported","relationship_to_head","ons_family_role","ons_family_index","employment_status","employment_sector","sic_industry_division","aa_category","dla_sc_category","dla_m_category","pip_m_category","pip_dl_category","is_disabled_for_benefits","is_enhanced_disabled_for_benefits","is_severely_disabled_for_benefits","current_education","highest_education","is_in_non_advanced_education","is_in_approved_training","age_started_or_accepted_current_education_or_training","is_before_universal_credit_qualifying_young_person_terminal_date","adult_ema","child_ema","receives_benefits_in_own_right","legacy_jobseeker_proxy","esa_health_condition_proxy","esa_support_group_proxy","disabled_students_allowance_eligible_expenses","would_claim_marriage_allowance","would_claim_scp","attends_private_school_random_draw","tax_free_childcare_spend_routed_share","student_loan_balance"],"entity":"person","rows":"all"}],"kernel":"uk.stage.etb_vat@1","mass":"conserve","outputs":[{"column":"full_rate_vat_expenditure_rate","dtype":"float64","entity":"household","ownership":"produced","rows":"all"}],"params":{"stage":"etb_vat","stage_contract_sha256":"5a2b777d88315bcccb72477c088a136c634131e0bc1a339b7287217ee5cfd8d3","time_period":"2024"},"population":"regional_property_uprating.boundary","sources":["frs"],"structural":"none","weights":null},{"base":null,"citation":"","description":"Run UK spine stage etb_services.","id":"etb_services","inputs":[{"columns":["frs_benunit_capital","is_married","dependent_children","would_claim_child_benefit","child_benefit_opts_out","would_claim_pc","would_claim_uc","would_claim_tfc","would_claim_extended_childcare","would_claim_universal_childcare","would_claim_targeted_childcare","would_claim_uc_childcare","maximum_extended_childcare_hours_usage"],"entity":"benunit","rows":"all"},{"columns":["region","tenure_type","accommodation_type","num_bedrooms","council_tax_reported","council_tax_band","council_tax_rebate","council_tax_single_adult_raw","water_and_sewerage_charges","domestic_rates","rent","subrent","mortgage_interest_repayment","mortgage_capital_repayment","structural_insurance_payments","housing_service_charges","external_child_payments","ons_household_type","council_tax","household_owns_tv","would_evade_tv_licence_fee","main_residential_property_purchased_is_first_home","property_purchased","brma","owned_land","property_wealth","corporate_wealth","private_pension_wealth","gross_financial_wealth","net_financial_wealth","main_residence_value","other_residential_property_value","non_residential_property_value","savings","num_vehicles","cash_isa","stocks_and_shares_isa","mortgage_debt","consumer_debt","food_and_non_alcoholic_beverages_consumption","alcohol_and_tobacco_consumption","clothing_and_footwear_consumption","housing_water_and_electricity_consumption","household_furnishings_consumption","health_consumption","transport_consumption","communication_consumption","recreation_consumption","education_consumption","restaurants_and_hotels_consumption","miscellaneous_consumption","petrol_spending","diesel_spending","bus_fare_spending","domestic_energy_consumption","electricity_consumption","gas_consumption","has_fuel_consumption","full_rate_vat_expenditure_rate"],"entity":"household","rows":"all"},{"columns":["age","gender","marital_status","hours_worked","care_hours","is_household_head","is_benunit_head","is_parent","is_uc_claimant","employment_income","self_employment_income","private_pension_income","tax_free_savings_income","savings_interest_income","dividend_income","property_income","maintenance_income","miscellaneous_income","private_transfer_income","lump_sum_income","student_loan_repayments","statutory_sick_pay","statutory_maternity_pay","student_loans","access_fund","education_grants","healthy_start_vouchers","free_school_breakfasts","free_school_fruit_veg","free_school_meals","council_tax_benefit_reported","maintenance_expenses","childcare_expenses","personal_pension_contributions","employee_pension_contributions","pension_contributions_via_salary_sacrifice","salary_sacrifice_reported","salary_sacrifice_asked","child_benefit_reported","income_support_reported","housing_benefit_reported","attendance_allowance_reported","dla_sc_reported","dla_m_reported","iidb_reported","carers_allowance_reported","would_claim_carers_allowance","sda_reported","afcs_reported","ssmg_reported","pension_credit_reported","child_tax_credit_reported","working_tax_credit_reported","state_pension_reported","winter_fuel_allowance_reported","incapacity_benefit_reported","universal_credit_reported","pip_m_reported","pip_dl_reported","jsa_contrib_reported","jsa_income_reported","esa_contrib_reported","esa_income_reported","bsp_reported","relationship_to_head","ons_family_role","ons_family_index","employment_status","employment_sector","sic_industry_division","aa_category","dla_sc_category","dla_m_category","pip_m_category","pip_dl_category","is_disabled_for_benefits","is_enhanced_disabled_for_benefits","is_severely_disabled_for_benefits","current_education","highest_education","is_in_non_advanced_education","is_in_approved_training","age_started_or_accepted_current_education_or_training","is_before_universal_credit_qualifying_young_person_terminal_date","adult_ema","child_ema","receives_benefits_in_own_right","legacy_jobseeker_proxy","esa_health_condition_proxy","esa_support_group_proxy","disabled_students_allowance_eligible_expenses","would_claim_marriage_allowance","would_claim_scp","attends_private_school_random_draw","tax_free_childcare_spend_routed_share","student_loan_balance"],"entity":"person","rows":"all"}],"kernel":"uk.stage.etb_services@1","mass":"conserve","outputs":[{"column":"dfe_education_spending","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"rail_subsidy_spending","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"bus_subsidy_spending","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"rail_usage","dtype":"float64","entity":"household","ownership":"produced","rows":"all"},{"column":"a_and_e_visits","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"admitted_patient_visits","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"outpatient_visits","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"nhs_a_and_e_spending","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"nhs_admitted_patient_spending","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"nhs_outpatient_spending","dtype":"float64","entity":"person","ownership":"produced","rows":"all"}],"params":{"stage":"etb_services","stage_contract_sha256":"a354a6bbf0be88a4001970a0879600511ff83f1194836dafd8c56fc11b31278e","time_period":"2024"},"population":"regional_property_uprating.boundary","sources":["frs"],"structural":"none","weights":null},{"base":null,"citation":"","description":"Run UK spine stage frs_hmrc_spine_leaves.","id":"frs_hmrc_spine_leaves","inputs":[{"columns":["frs_benunit_capital"],"entity":"benunit","rows":"all"},{"columns":["region"],"entity":"household","rows":"all"},{"columns":["age","employee_pension_contributions","nhs_outpatient_spending"],"entity":"person","rows":"all"}],"kernel":"uk.stage.frs_hmrc_spine_leaves@1","mass":"conserve","outputs":[{"column":"hmrc_spi_pay","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"hmrc_spi_unemployment_benefit_income","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"hmrc_spi_incapacity_benefit_income","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"ossben_identifiable_subset","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"srp_regular_code5","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"employer_pension_contributions","dtype":"float64","entity":"person","ownership":"produced","rows":"all"}],"params":{"stage":"frs_hmrc_spine_leaves","stage_contract_sha256":"2bb3d068003489b47cc8676ac26c203ce3c39a9b6e92f3ecd0a3c06acd6addc4","time_period":"2024"},"population":"regional_property_uprating.boundary","sources":["frs"],"structural":"none","weights":null},{"base":"regional_property_uprating.boundary","citation":"","description":"Run structural UK stage spi_support_channel.","id":"spi_support_channel","inputs":[{"columns":["frs_benunit_capital","is_married","dependent_children","would_claim_child_benefit","child_benefit_opts_out","would_claim_pc","would_claim_uc","would_claim_tfc","would_claim_extended_childcare","would_claim_universal_childcare","would_claim_targeted_childcare","would_claim_uc_childcare","maximum_extended_childcare_hours_usage"],"entity":"benunit","rows":"all"},{"columns":["region","tenure_type","accommodation_type","num_bedrooms","council_tax_reported","council_tax_band","council_tax_rebate","council_tax_single_adult_raw","water_and_sewerage_charges","domestic_rates","rent","subrent","mortgage_interest_repayment","mortgage_capital_repayment","structural_insurance_payments","housing_service_charges","external_child_payments","ons_household_type","council_tax","household_owns_tv","would_evade_tv_licence_fee","main_residential_property_purchased_is_first_home","property_purchased","brma","owned_land","property_wealth","corporate_wealth","private_pension_wealth","gross_financial_wealth","net_financial_wealth","main_residence_value","other_residential_property_value","non_residential_property_value","savings","num_vehicles","cash_isa","stocks_and_shares_isa","mortgage_debt","consumer_debt","food_and_non_alcoholic_beverages_consumption","alcohol_and_tobacco_consumption","clothing_and_footwear_consumption","housing_water_and_electricity_consumption","household_furnishings_consumption","health_consumption","transport_consumption","communication_consumption","recreation_consumption","education_consumption","restaurants_and_hotels_consumption","miscellaneous_consumption","petrol_spending","diesel_spending","bus_fare_spending","domestic_energy_consumption","electricity_consumption","gas_consumption","has_fuel_consumption","full_rate_vat_expenditure_rate","dfe_education_spending","rail_subsidy_spending","bus_subsidy_spending","rail_usage"],"entity":"household","rows":"all"},{"columns":["age","gender","marital_status","hours_worked","care_hours","is_household_head","is_benunit_head","is_parent","is_uc_claimant","employment_income","self_employment_income","private_pension_income","tax_free_savings_income","savings_interest_income","dividend_income","property_income","maintenance_income","miscellaneous_income","private_transfer_income","lump_sum_income","student_loan_repayments","statutory_sick_pay","statutory_maternity_pay","student_loans","access_fund","education_grants","healthy_start_vouchers","free_school_breakfasts","free_school_fruit_veg","free_school_meals","council_tax_benefit_reported","maintenance_expenses","childcare_expenses","personal_pension_contributions","employee_pension_contributions","pension_contributions_via_salary_sacrifice","salary_sacrifice_reported","salary_sacrifice_asked","child_benefit_reported","income_support_reported","housing_benefit_reported","attendance_allowance_reported","dla_sc_reported","dla_m_reported","iidb_reported","carers_allowance_reported","would_claim_carers_allowance","sda_reported","afcs_reported","ssmg_reported","pension_credit_reported","child_tax_credit_reported","working_tax_credit_reported","state_pension_reported","winter_fuel_allowance_reported","incapacity_benefit_reported","universal_credit_reported","pip_m_reported","pip_dl_reported","jsa_contrib_reported","jsa_income_reported","esa_contrib_reported","esa_income_reported","bsp_reported","relationship_to_head","ons_family_role","ons_family_index","employment_status","employment_sector","sic_industry_division","aa_category","dla_sc_category","dla_m_category","pip_m_category","pip_dl_category","is_disabled_for_benefits","is_enhanced_disabled_for_benefits","is_severely_disabled_for_benefits","current_education","highest_education","is_in_non_advanced_education","is_in_approved_training","age_started_or_accepted_current_education_or_training","is_before_universal_credit_qualifying_young_person_terminal_date","adult_ema","child_ema","receives_benefits_in_own_right","legacy_jobseeker_proxy","esa_health_condition_proxy","esa_support_group_proxy","disabled_students_allowance_eligible_expenses","would_claim_marriage_allowance","would_claim_scp","attends_private_school_random_draw","tax_free_childcare_spend_routed_share","student_loan_balance","a_and_e_visits","admitted_patient_visits","outpatient_visits","nhs_a_and_e_spending","nhs_admitted_patient_spending","nhs_outpatient_spending","hmrc_spi_pay","hmrc_spi_unemployment_benefit_income","hmrc_spi_incapacity_benefit_income","ossben_identifiable_subset","srp_regular_code5","employer_pension_contributions"],"entity":"person","rows":"all"}],"kernel":"uk.stage.expand.spi_support_channel@1","mass":"declared","outputs":[],"params":{"expand_cells":[["person","person_source_id","int64"],["person","person_support_channel","string"],["person","person_support_clone_index","int64"],["benunit","benunit_source_id","int64"],["benunit","benunit_support_channel","string"],["benunit","benunit_support_clone_index","int64"],["household","source_household_id","int64"],["household","source_year","int64"],["household","source_household_key","string"],["household","household_source_id","int64"],["household","household_support_channel","string"],["household","household_support_clone_index","int64"],["household","household_is_spi_synthetic","bool"]],"expand_weight_entity":"household","expand_weight_kind":"importance","stage":"spi_support_channel","stage_contract_sha256":"4b10f1a4a215cdf2c406c9974de2d1e580eb3cd3a25ed34973c1b63abf4a54bb","time_period":"2024"},"population":null,"sources":["frs"],"structural":"expand","weights":null},{"base":null,"citation":"","description":"Own the cells materialized by spi_support_channel.","id":"spi_support_channel.owned","inputs":[],"kernel":"uk.claim@1","mass":"conserve","outputs":[{"column":"person_source_id","dtype":"int64","entity":"person","ownership":"produced","rows":"all"},{"column":"person_support_channel","dtype":"string","entity":"person","ownership":"produced","rows":"all"},{"column":"person_support_clone_index","dtype":"int64","entity":"person","ownership":"produced","rows":"all"},{"column":"benunit_source_id","dtype":"int64","entity":"benunit","ownership":"produced","rows":"all"},{"column":"benunit_support_channel","dtype":"string","entity":"benunit","ownership":"produced","rows":"all"},{"column":"benunit_support_clone_index","dtype":"int64","entity":"benunit","ownership":"produced","rows":"all"},{"column":"source_household_id","dtype":"int64","entity":"household","ownership":"produced","rows":"all"},{"column":"source_year","dtype":"int64","entity":"household","ownership":"produced","rows":"all"},{"column":"source_household_key","dtype":"string","entity":"household","ownership":"produced","rows":"all"},{"column":"household_source_id","dtype":"int64","entity":"household","ownership":"produced","rows":"all"},{"column":"household_support_channel","dtype":"string","entity":"household","ownership":"produced","rows":"all"},{"column":"household_support_clone_index","dtype":"int64","entity":"household","ownership":"produced","rows":"all"},{"column":"household_is_spi_synthetic","dtype":"bool","entity":"household","ownership":"produced","rows":"all"}],"params":{"materialized_expand_outputs":["person.person_source_id","person.person_support_channel","person.person_support_clone_index","benunit.benunit_source_id","benunit.benunit_support_channel","benunit.benunit_support_clone_index","household.source_household_id","household.source_year","household.source_household_key","household.household_source_id","household.household_support_channel","household.household_support_clone_index","household.household_is_spi_synthetic"]},"population":"spi_support_channel","sources":[],"structural":"none","weights":null},{"base":null,"citation":"","description":"Run UK spine stage hmrc_spi_income_spine.","id":"hmrc_spi_income_spine","inputs":[{"columns":["frs_benunit_capital","is_married","dependent_children","would_claim_child_benefit","child_benefit_opts_out","would_claim_pc","would_claim_uc","would_claim_tfc","would_claim_extended_childcare","would_claim_universal_childcare","would_claim_targeted_childcare","would_claim_uc_childcare","maximum_extended_childcare_hours_usage","benunit_source_id","benunit_support_channel","benunit_support_clone_index"],"entity":"benunit","rows":"all"},{"columns":["region","tenure_type","accommodation_type","num_bedrooms","council_tax_reported","council_tax_band","council_tax_rebate","council_tax_single_adult_raw","water_and_sewerage_charges","domestic_rates","rent","subrent","mortgage_interest_repayment","mortgage_capital_repayment","structural_insurance_payments","housing_service_charges","external_child_payments","ons_household_type","council_tax","household_owns_tv","would_evade_tv_licence_fee","main_residential_property_purchased_is_first_home","property_purchased","brma","owned_land","property_wealth","corporate_wealth","private_pension_wealth","gross_financial_wealth","net_financial_wealth","main_residence_value","other_residential_property_value","non_residential_property_value","savings","num_vehicles","cash_isa","stocks_and_shares_isa","mortgage_debt","consumer_debt","food_and_non_alcoholic_beverages_consumption","alcohol_and_tobacco_consumption","clothing_and_footwear_consumption","housing_water_and_electricity_consumption","household_furnishings_consumption","health_consumption","transport_consumption","communication_consumption","recreation_consumption","education_consumption","restaurants_and_hotels_consumption","miscellaneous_consumption","petrol_spending","diesel_spending","bus_fare_spending","domestic_energy_consumption","electricity_consumption","gas_consumption","has_fuel_consumption","full_rate_vat_expenditure_rate","dfe_education_spending","rail_subsidy_spending","bus_subsidy_spending","rail_usage","source_household_id","source_year","source_household_key","household_source_id","household_support_channel","household_support_clone_index","household_is_spi_synthetic"],"entity":"household","rows":"all"},{"columns":["age","gender","marital_status","hours_worked","care_hours","is_household_head","is_benunit_head","is_parent","is_uc_claimant","maintenance_income","miscellaneous_income","private_transfer_income","lump_sum_income","student_loan_repayments","statutory_sick_pay","statutory_maternity_pay","student_loans","access_fund","education_grants","healthy_start_vouchers","free_school_breakfasts","free_school_fruit_veg","free_school_meals","maintenance_expenses","childcare_expenses","salary_sacrifice_reported","salary_sacrifice_asked","ssmg_reported","incapacity_benefit_reported","relationship_to_head","ons_family_role","ons_family_index","employment_status","employment_sector","sic_industry_division","current_education","highest_education","is_in_non_advanced_education","is_in_approved_training","age_started_or_accepted_current_education_or_training","is_before_universal_credit_qualifying_young_person_terminal_date","adult_ema","child_ema","receives_benefits_in_own_right","legacy_jobseeker_proxy","esa_health_condition_proxy","esa_support_group_proxy","disabled_students_allowance_eligible_expenses","would_claim_marriage_allowance","would_claim_scp","attends_private_school_random_draw","tax_free_childcare_spend_routed_share","student_loan_balance","a_and_e_visits","admitted_patient_visits","outpatient_visits","nhs_a_and_e_spending","nhs_admitted_patient_spending","nhs_outpatient_spending","ossben_identifiable_subset","srp_regular_code5","person_source_id","person_support_channel","person_support_clone_index"],"entity":"person","rows":"all"}],"kernel":"uk.stage.hmrc_spi_income_spine@1","mass":"conserve","outputs":[{"column":"charitable_investment_gifts","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"gift_aid","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"other_investment_income","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"hmrc_spi_employment_benefits","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"hmrc_spi_employment_expenses","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"hmrc_spi_other_social_security_income","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"hmrc_spi_taxable_termination_pay","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"hmrc_spi_miscellaneous_employment_income","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"hmrc_spi_other_income","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"hmrc_spi_state_pension_income","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"hmrc_spi_employed_income","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"hmrc_spi_total_earned_income","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"hmrc_spi_total_investment_income","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"hmrc_spi_assessable_income","dtype":"float64","entity":"person","ownership":"produced","rows":"all"},{"column":"employment_income","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"self_employment_income","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"savings_interest_income","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"dividend_income","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"private_pension_income","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"property_income","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"employee_pension_contributions","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"employer_pension_contributions","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"personal_pension_contributions","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"pension_contributions_via_salary_sacrifice","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"tax_free_savings_income","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"universal_credit_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"pension_credit_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"child_benefit_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"housing_benefit_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"income_support_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"working_tax_credit_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"child_tax_credit_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"attendance_allowance_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"state_pension_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"dla_sc_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"dla_m_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"pip_m_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"pip_dl_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"sda_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"carers_allowance_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"iidb_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"afcs_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"bsp_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"winter_fuel_allowance_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"council_tax_benefit_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"jsa_contrib_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"jsa_income_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"esa_contrib_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"esa_income_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"hmrc_spi_pay","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"hmrc_spi_unemployment_benefit_income","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"hmrc_spi_incapacity_benefit_income","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"aa_category","dtype":"string","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"dla_sc_category","dtype":"string","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"dla_m_category","dtype":"string","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"pip_m_category","dtype":"string","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"pip_dl_category","dtype":"string","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"is_disabled_for_benefits","dtype":"bool","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"is_enhanced_disabled_for_benefits","dtype":"bool","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"is_severely_disabled_for_benefits","dtype":"bool","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"would_claim_carers_allowance","dtype":"bool","entity":"person","ownership":"produced","rewrite":true,"rows":"all"}],"params":{"stage":"hmrc_spi_income_spine","stage_contract_sha256":"4c2edca1f77416a00cff10c7e9d0a9da50e332a2bb6a995ef9ac1bdd00ff97a9","time_period":"2024"},"population":"spi_support_channel","sources":["frs"],"structural":"none","weights":null},{"base":"spi_support_channel","citation":"","description":"Ownership boundary before uc_reporter_redraw rewrites.","id":"uc_reporter_redraw.boundary","inputs":[{"columns":["frs_benunit_capital","is_married","dependent_children","would_claim_child_benefit","child_benefit_opts_out","would_claim_pc","would_claim_uc","would_claim_tfc","would_claim_extended_childcare","would_claim_universal_childcare","would_claim_targeted_childcare","would_claim_uc_childcare","maximum_extended_childcare_hours_usage","benunit_source_id","benunit_support_channel","benunit_support_clone_index"],"entity":"benunit","rows":"all"},{"columns":["region","tenure_type","accommodation_type","num_bedrooms","council_tax_reported","council_tax_band","council_tax_rebate","council_tax_single_adult_raw","water_and_sewerage_charges","domestic_rates","rent","subrent","mortgage_interest_repayment","mortgage_capital_repayment","structural_insurance_payments","housing_service_charges","external_child_payments","ons_household_type","council_tax","household_owns_tv","would_evade_tv_licence_fee","main_residential_property_purchased_is_first_home","property_purchased","brma","owned_land","property_wealth","corporate_wealth","private_pension_wealth","gross_financial_wealth","net_financial_wealth","main_residence_value","other_residential_property_value","non_residential_property_value","savings","num_vehicles","cash_isa","stocks_and_shares_isa","mortgage_debt","consumer_debt","food_and_non_alcoholic_beverages_consumption","alcohol_and_tobacco_consumption","clothing_and_footwear_consumption","housing_water_and_electricity_consumption","household_furnishings_consumption","health_consumption","transport_consumption","communication_consumption","recreation_consumption","education_consumption","restaurants_and_hotels_consumption","miscellaneous_consumption","petrol_spending","diesel_spending","bus_fare_spending","domestic_energy_consumption","electricity_consumption","gas_consumption","has_fuel_consumption","full_rate_vat_expenditure_rate","dfe_education_spending","rail_subsidy_spending","bus_subsidy_spending","rail_usage","source_household_id","source_year","source_household_key","household_source_id","household_support_channel","household_support_clone_index","household_is_spi_synthetic"],"entity":"household","rows":"all"},{"columns":["age","gender","marital_status","hours_worked","care_hours","is_household_head","is_benunit_head","is_parent","is_uc_claimant","employment_income","self_employment_income","private_pension_income","tax_free_savings_income","savings_interest_income","dividend_income","property_income","maintenance_income","miscellaneous_income","private_transfer_income","lump_sum_income","student_loan_repayments","statutory_sick_pay","statutory_maternity_pay","student_loans","access_fund","education_grants","healthy_start_vouchers","free_school_breakfasts","free_school_fruit_veg","free_school_meals","council_tax_benefit_reported","maintenance_expenses","childcare_expenses","personal_pension_contributions","employee_pension_contributions","pension_contributions_via_salary_sacrifice","salary_sacrifice_reported","salary_sacrifice_asked","child_benefit_reported","income_support_reported","housing_benefit_reported","attendance_allowance_reported","dla_sc_reported","dla_m_reported","iidb_reported","carers_allowance_reported","would_claim_carers_allowance","sda_reported","afcs_reported","ssmg_reported","pension_credit_reported","child_tax_credit_reported","working_tax_credit_reported","state_pension_reported","winter_fuel_allowance_reported","incapacity_benefit_reported","universal_credit_reported","pip_m_reported","pip_dl_reported","jsa_contrib_reported","jsa_income_reported","esa_contrib_reported","esa_income_reported","bsp_reported","relationship_to_head","ons_family_role","ons_family_index","employment_status","employment_sector","sic_industry_division","aa_category","dla_sc_category","dla_m_category","pip_m_category","pip_dl_category","is_disabled_for_benefits","is_enhanced_disabled_for_benefits","is_severely_disabled_for_benefits","current_education","highest_education","is_in_non_advanced_education","is_in_approved_training","age_started_or_accepted_current_education_or_training","is_before_universal_credit_qualifying_young_person_terminal_date","adult_ema","child_ema","receives_benefits_in_own_right","legacy_jobseeker_proxy","esa_health_condition_proxy","esa_support_group_proxy","disabled_students_allowance_eligible_expenses","would_claim_marriage_allowance","would_claim_scp","attends_private_school_random_draw","tax_free_childcare_spend_routed_share","student_loan_balance","a_and_e_visits","admitted_patient_visits","outpatient_visits","nhs_a_and_e_spending","nhs_admitted_patient_spending","nhs_outpatient_spending","hmrc_spi_pay","hmrc_spi_unemployment_benefit_income","hmrc_spi_incapacity_benefit_income","ossben_identifiable_subset","srp_regular_code5","employer_pension_contributions","person_source_id","person_support_channel","person_support_clone_index","charitable_investment_gifts","gift_aid","other_investment_income","hmrc_spi_employment_benefits","hmrc_spi_employment_expenses","hmrc_spi_other_social_security_income","hmrc_spi_taxable_termination_pay","hmrc_spi_miscellaneous_employment_income","hmrc_spi_other_income","hmrc_spi_state_pension_income","hmrc_spi_employed_income","hmrc_spi_total_earned_income","hmrc_spi_total_investment_income","hmrc_spi_assessable_income"],"entity":"person","rows":"all"}],"kernel":"uk.identity@1","mass":"conserve","outputs":[],"params":{},"population":null,"sources":[],"structural":"filter","weights":null},{"base":null,"citation":"","description":"Run UK spine stage uc_reporter_redraw.","id":"uc_reporter_redraw","inputs":[{"columns":["frs_benunit_capital","is_married","dependent_children","would_claim_child_benefit","child_benefit_opts_out","would_claim_pc","would_claim_uc","would_claim_tfc","would_claim_extended_childcare","would_claim_universal_childcare","would_claim_targeted_childcare","would_claim_uc_childcare","maximum_extended_childcare_hours_usage","benunit_source_id","benunit_support_channel","benunit_support_clone_index"],"entity":"benunit","rows":"all"},{"columns":["region","tenure_type","accommodation_type","num_bedrooms","council_tax_reported","council_tax_band","council_tax_rebate","council_tax_single_adult_raw","water_and_sewerage_charges","domestic_rates","rent","subrent","mortgage_interest_repayment","mortgage_capital_repayment","structural_insurance_payments","housing_service_charges","external_child_payments","ons_household_type","council_tax","household_owns_tv","would_evade_tv_licence_fee","main_residential_property_purchased_is_first_home","property_purchased","brma","owned_land","property_wealth","corporate_wealth","private_pension_wealth","gross_financial_wealth","net_financial_wealth","main_residence_value","other_residential_property_value","non_residential_property_value","savings","num_vehicles","cash_isa","stocks_and_shares_isa","mortgage_debt","consumer_debt","food_and_non_alcoholic_beverages_consumption","alcohol_and_tobacco_consumption","clothing_and_footwear_consumption","housing_water_and_electricity_consumption","household_furnishings_consumption","health_consumption","transport_consumption","communication_consumption","recreation_consumption","education_consumption","restaurants_and_hotels_consumption","miscellaneous_consumption","petrol_spending","diesel_spending","bus_fare_spending","domestic_energy_consumption","electricity_consumption","gas_consumption","has_fuel_consumption","full_rate_vat_expenditure_rate","dfe_education_spending","rail_subsidy_spending","bus_subsidy_spending","rail_usage","source_household_id","source_year","source_household_key","household_source_id","household_support_channel","household_support_clone_index","household_is_spi_synthetic"],"entity":"household","rows":"all"},{"columns":["age","gender","marital_status","hours_worked","care_hours","is_household_head","is_benunit_head","is_parent","is_uc_claimant","employment_income","self_employment_income","private_pension_income","tax_free_savings_income","savings_interest_income","dividend_income","property_income","maintenance_income","miscellaneous_income","private_transfer_income","lump_sum_income","student_loan_repayments","statutory_sick_pay","statutory_maternity_pay","student_loans","access_fund","education_grants","healthy_start_vouchers","free_school_breakfasts","free_school_fruit_veg","free_school_meals","council_tax_benefit_reported","maintenance_expenses","childcare_expenses","personal_pension_contributions","employee_pension_contributions","pension_contributions_via_salary_sacrifice","salary_sacrifice_reported","salary_sacrifice_asked","child_benefit_reported","income_support_reported","housing_benefit_reported","attendance_allowance_reported","dla_sc_reported","dla_m_reported","iidb_reported","carers_allowance_reported","would_claim_carers_allowance","sda_reported","afcs_reported","ssmg_reported","pension_credit_reported","child_tax_credit_reported","working_tax_credit_reported","state_pension_reported","winter_fuel_allowance_reported","incapacity_benefit_reported","pip_m_reported","pip_dl_reported","jsa_contrib_reported","jsa_income_reported","esa_contrib_reported","esa_income_reported","bsp_reported","relationship_to_head","ons_family_role","ons_family_index","employment_status","employment_sector","sic_industry_division","aa_category","dla_sc_category","dla_m_category","pip_m_category","pip_dl_category","is_disabled_for_benefits","is_enhanced_disabled_for_benefits","is_severely_disabled_for_benefits","current_education","highest_education","is_in_non_advanced_education","is_in_approved_training","age_started_or_accepted_current_education_or_training","is_before_universal_credit_qualifying_young_person_terminal_date","adult_ema","child_ema","receives_benefits_in_own_right","legacy_jobseeker_proxy","esa_health_condition_proxy","esa_support_group_proxy","disabled_students_allowance_eligible_expenses","would_claim_marriage_allowance","would_claim_scp","attends_private_school_random_draw","tax_free_childcare_spend_routed_share","student_loan_balance","a_and_e_visits","admitted_patient_visits","outpatient_visits","nhs_a_and_e_spending","nhs_admitted_patient_spending","nhs_outpatient_spending","hmrc_spi_pay","hmrc_spi_unemployment_benefit_income","hmrc_spi_incapacity_benefit_income","ossben_identifiable_subset","srp_regular_code5","employer_pension_contributions","person_source_id","person_support_channel","person_support_clone_index","charitable_investment_gifts","gift_aid","other_investment_income","hmrc_spi_employment_benefits","hmrc_spi_employment_expenses","hmrc_spi_other_social_security_income","hmrc_spi_taxable_termination_pay","hmrc_spi_miscellaneous_employment_income","hmrc_spi_other_income","hmrc_spi_state_pension_income","hmrc_spi_employed_income","hmrc_spi_total_earned_income","hmrc_spi_total_investment_income","hmrc_spi_assessable_income"],"entity":"person","rows":"all"}],"kernel":"uk.stage.uc_reporter_redraw@1","mass":"conserve","outputs":[{"column":"universal_credit_reported","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"}],"params":{"stage":"uc_reporter_redraw","stage_contract_sha256":"7e94e034cad29c5bae566ffb875c42cddf3a1025c15496907e783ceff1144f8b","time_period":"2024"},"population":"uc_reporter_redraw.boundary","sources":["frs"],"structural":"none","weights":null},{"base":"uc_reporter_redraw.boundary","citation":"","description":"Ownership boundary before uc_capital_coherence rewrites.","id":"uc_capital_coherence.boundary","inputs":[{"columns":["frs_benunit_capital","is_married","dependent_children","would_claim_child_benefit","child_benefit_opts_out","would_claim_pc","would_claim_uc","would_claim_tfc","would_claim_extended_childcare","would_claim_universal_childcare","would_claim_targeted_childcare","would_claim_uc_childcare","maximum_extended_childcare_hours_usage","benunit_source_id","benunit_support_channel","benunit_support_clone_index"],"entity":"benunit","rows":"all"},{"columns":["region","tenure_type","accommodation_type","num_bedrooms","council_tax_reported","council_tax_band","council_tax_rebate","council_tax_single_adult_raw","water_and_sewerage_charges","domestic_rates","rent","subrent","mortgage_interest_repayment","mortgage_capital_repayment","structural_insurance_payments","housing_service_charges","external_child_payments","ons_household_type","council_tax","household_owns_tv","would_evade_tv_licence_fee","main_residential_property_purchased_is_first_home","property_purchased","brma","owned_land","property_wealth","corporate_wealth","private_pension_wealth","gross_financial_wealth","net_financial_wealth","main_residence_value","other_residential_property_value","non_residential_property_value","savings","num_vehicles","cash_isa","stocks_and_shares_isa","mortgage_debt","consumer_debt","food_and_non_alcoholic_beverages_consumption","alcohol_and_tobacco_consumption","clothing_and_footwear_consumption","housing_water_and_electricity_consumption","household_furnishings_consumption","health_consumption","transport_consumption","communication_consumption","recreation_consumption","education_consumption","restaurants_and_hotels_consumption","miscellaneous_consumption","petrol_spending","diesel_spending","bus_fare_spending","domestic_energy_consumption","electricity_consumption","gas_consumption","has_fuel_consumption","full_rate_vat_expenditure_rate","dfe_education_spending","rail_subsidy_spending","bus_subsidy_spending","rail_usage","source_household_id","source_year","source_household_key","household_source_id","household_support_channel","household_support_clone_index","household_is_spi_synthetic"],"entity":"household","rows":"all"},{"columns":["age","gender","marital_status","hours_worked","care_hours","is_household_head","is_benunit_head","is_parent","is_uc_claimant","employment_income","self_employment_income","private_pension_income","tax_free_savings_income","savings_interest_income","dividend_income","property_income","maintenance_income","miscellaneous_income","private_transfer_income","lump_sum_income","student_loan_repayments","statutory_sick_pay","statutory_maternity_pay","student_loans","access_fund","education_grants","healthy_start_vouchers","free_school_breakfasts","free_school_fruit_veg","free_school_meals","council_tax_benefit_reported","maintenance_expenses","childcare_expenses","personal_pension_contributions","employee_pension_contributions","pension_contributions_via_salary_sacrifice","salary_sacrifice_reported","salary_sacrifice_asked","child_benefit_reported","income_support_reported","housing_benefit_reported","attendance_allowance_reported","dla_sc_reported","dla_m_reported","iidb_reported","carers_allowance_reported","would_claim_carers_allowance","sda_reported","afcs_reported","ssmg_reported","pension_credit_reported","child_tax_credit_reported","working_tax_credit_reported","state_pension_reported","winter_fuel_allowance_reported","incapacity_benefit_reported","universal_credit_reported","pip_m_reported","pip_dl_reported","jsa_contrib_reported","jsa_income_reported","esa_contrib_reported","esa_income_reported","bsp_reported","relationship_to_head","ons_family_role","ons_family_index","employment_status","employment_sector","sic_industry_division","aa_category","dla_sc_category","dla_m_category","pip_m_category","pip_dl_category","is_disabled_for_benefits","is_enhanced_disabled_for_benefits","is_severely_disabled_for_benefits","current_education","highest_education","is_in_non_advanced_education","is_in_approved_training","age_started_or_accepted_current_education_or_training","is_before_universal_credit_qualifying_young_person_terminal_date","adult_ema","child_ema","receives_benefits_in_own_right","legacy_jobseeker_proxy","esa_health_condition_proxy","esa_support_group_proxy","disabled_students_allowance_eligible_expenses","would_claim_marriage_allowance","would_claim_scp","attends_private_school_random_draw","tax_free_childcare_spend_routed_share","student_loan_balance","a_and_e_visits","admitted_patient_visits","outpatient_visits","nhs_a_and_e_spending","nhs_admitted_patient_spending","nhs_outpatient_spending","hmrc_spi_pay","hmrc_spi_unemployment_benefit_income","hmrc_spi_incapacity_benefit_income","ossben_identifiable_subset","srp_regular_code5","employer_pension_contributions","person_source_id","person_support_channel","person_support_clone_index","charitable_investment_gifts","gift_aid","other_investment_income","hmrc_spi_employment_benefits","hmrc_spi_employment_expenses","hmrc_spi_other_social_security_income","hmrc_spi_taxable_termination_pay","hmrc_spi_miscellaneous_employment_income","hmrc_spi_other_income","hmrc_spi_state_pension_income","hmrc_spi_employed_income","hmrc_spi_total_earned_income","hmrc_spi_total_investment_income","hmrc_spi_assessable_income"],"entity":"person","rows":"all"}],"kernel":"uk.identity@1","mass":"conserve","outputs":[],"params":{},"population":null,"sources":[],"structural":"filter","weights":null},{"base":null,"citation":"","description":"Run UK spine stage uc_capital_coherence.","id":"uc_capital_coherence","inputs":[{"columns":["benunit_support_channel","dependent_children","is_married"],"entity":"benunit","rows":"all"},{"columns":["region"],"entity":"household","rows":"all"},{"columns":["age","is_benunit_head","is_parent","person_support_channel","universal_credit_reported"],"entity":"person","rows":"all"}],"kernel":"uk.stage.uc_capital_coherence@1","mass":"conserve","outputs":[{"column":"uc_reported_capital","dtype":"float64","entity":"benunit","ownership":"produced","rows":"all"},{"column":"frs_benunit_capital","dtype":"float64","entity":"benunit","ownership":"produced","rewrite":true,"rows":"all"},{"column":"would_claim_uc","dtype":"bool","entity":"benunit","ownership":"produced","rewrite":true,"rows":"all"}],"params":{"stage":"uc_capital_coherence","stage_contract_sha256":"059f27ee687ee15385b246d43068b17aeecbe3645ccc41fd52fcad06a006a081","time_period":"2024"},"population":"uc_capital_coherence.boundary","sources":["frs"],"structural":"none","weights":null},{"base":null,"citation":"","description":"Run UK spine stage uc_deduction_attributes.","id":"uc_deduction_attributes","inputs":[{"columns":["frs_benunit_capital","would_claim_uc"],"entity":"benunit","rows":"all"},{"columns":["region"],"entity":"household","rows":"all"},{"columns":["age"],"entity":"person","rows":"all"}],"kernel":"uk.stage.uc_deduction_attributes@1","mass":"conserve","outputs":[{"column":"uc_deduction_random_draw","dtype":"float64","entity":"benunit","ownership":"produced","rows":"all"},{"column":"uc_deduction_type_random_draw","dtype":"float64","entity":"benunit","ownership":"produced","rows":"all"},{"column":"uc_latent_deduction_rate","dtype":"float64","entity":"benunit","ownership":"produced","rows":"all"},{"column":"uc_deduction_combination","dtype":"string","entity":"benunit","ownership":"produced","rows":"all"}],"params":{"stage":"uc_deduction_attributes","stage_contract_sha256":"6bc7e3ec1a5712e23ca1b7d2956e8b17f2526b94431ba3e5a9294b04658e4fe0","time_period":"2024"},"population":"uc_capital_coherence.boundary","sources":["frs"],"structural":"none","weights":null},{"base":"uc_capital_coherence.boundary","citation":"","description":"Run structural UK stage cgt_incidence_clone.","id":"cgt_incidence_clone","inputs":[{"columns":["frs_benunit_capital","is_married","dependent_children","would_claim_child_benefit","child_benefit_opts_out","would_claim_pc","would_claim_uc","would_claim_tfc","would_claim_extended_childcare","would_claim_universal_childcare","would_claim_targeted_childcare","would_claim_uc_childcare","maximum_extended_childcare_hours_usage","benunit_source_id","benunit_support_channel","benunit_support_clone_index","uc_reported_capital","uc_deduction_random_draw","uc_deduction_type_random_draw","uc_latent_deduction_rate","uc_deduction_combination"],"entity":"benunit","rows":"all"},{"columns":["region","tenure_type","accommodation_type","num_bedrooms","council_tax_reported","council_tax_band","council_tax_rebate","council_tax_single_adult_raw","water_and_sewerage_charges","domestic_rates","rent","subrent","mortgage_interest_repayment","mortgage_capital_repayment","structural_insurance_payments","housing_service_charges","external_child_payments","ons_household_type","council_tax","household_owns_tv","would_evade_tv_licence_fee","main_residential_property_purchased_is_first_home","property_purchased","brma","owned_land","property_wealth","corporate_wealth","private_pension_wealth","gross_financial_wealth","net_financial_wealth","main_residence_value","other_residential_property_value","non_residential_property_value","savings","num_vehicles","cash_isa","stocks_and_shares_isa","mortgage_debt","consumer_debt","food_and_non_alcoholic_beverages_consumption","alcohol_and_tobacco_consumption","clothing_and_footwear_consumption","housing_water_and_electricity_consumption","household_furnishings_consumption","health_consumption","transport_consumption","communication_consumption","recreation_consumption","education_consumption","restaurants_and_hotels_consumption","miscellaneous_consumption","petrol_spending","diesel_spending","bus_fare_spending","domestic_energy_consumption","electricity_consumption","gas_consumption","has_fuel_consumption","full_rate_vat_expenditure_rate","dfe_education_spending","rail_subsidy_spending","bus_subsidy_spending","rail_usage","source_household_id","source_year","source_household_key","household_source_id","household_support_channel","household_support_clone_index","household_is_spi_synthetic"],"entity":"household","rows":"all"},{"columns":["age","gender","marital_status","hours_worked","care_hours","is_household_head","is_benunit_head","is_parent","is_uc_claimant","employment_income","self_employment_income","private_pension_income","tax_free_savings_income","savings_interest_income","dividend_income","property_income","maintenance_income","miscellaneous_income","private_transfer_income","lump_sum_income","student_loan_repayments","statutory_sick_pay","statutory_maternity_pay","student_loans","access_fund","education_grants","healthy_start_vouchers","free_school_breakfasts","free_school_fruit_veg","free_school_meals","council_tax_benefit_reported","maintenance_expenses","childcare_expenses","personal_pension_contributions","employee_pension_contributions","pension_contributions_via_salary_sacrifice","salary_sacrifice_reported","salary_sacrifice_asked","child_benefit_reported","income_support_reported","housing_benefit_reported","attendance_allowance_reported","dla_sc_reported","dla_m_reported","iidb_reported","carers_allowance_reported","would_claim_carers_allowance","sda_reported","afcs_reported","ssmg_reported","pension_credit_reported","child_tax_credit_reported","working_tax_credit_reported","state_pension_reported","winter_fuel_allowance_reported","incapacity_benefit_reported","universal_credit_reported","pip_m_reported","pip_dl_reported","jsa_contrib_reported","jsa_income_reported","esa_contrib_reported","esa_income_reported","bsp_reported","relationship_to_head","ons_family_role","ons_family_index","employment_status","employment_sector","sic_industry_division","aa_category","dla_sc_category","dla_m_category","pip_m_category","pip_dl_category","is_disabled_for_benefits","is_enhanced_disabled_for_benefits","is_severely_disabled_for_benefits","current_education","highest_education","is_in_non_advanced_education","is_in_approved_training","age_started_or_accepted_current_education_or_training","is_before_universal_credit_qualifying_young_person_terminal_date","adult_ema","child_ema","receives_benefits_in_own_right","legacy_jobseeker_proxy","esa_health_condition_proxy","esa_support_group_proxy","disabled_students_allowance_eligible_expenses","would_claim_marriage_allowance","would_claim_scp","attends_private_school_random_draw","tax_free_childcare_spend_routed_share","student_loan_balance","a_and_e_visits","admitted_patient_visits","outpatient_visits","nhs_a_and_e_spending","nhs_admitted_patient_spending","nhs_outpatient_spending","hmrc_spi_pay","hmrc_spi_unemployment_benefit_income","hmrc_spi_incapacity_benefit_income","ossben_identifiable_subset","srp_regular_code5","employer_pension_contributions","person_source_id","person_support_channel","person_support_clone_index","charitable_investment_gifts","gift_aid","other_investment_income","hmrc_spi_employment_benefits","hmrc_spi_employment_expenses","hmrc_spi_other_social_security_income","hmrc_spi_taxable_termination_pay","hmrc_spi_miscellaneous_employment_income","hmrc_spi_other_income","hmrc_spi_state_pension_income","hmrc_spi_employed_income","hmrc_spi_total_earned_income","hmrc_spi_total_investment_income","hmrc_spi_assessable_income"],"entity":"person","rows":"all"}],"kernel":"uk.stage.expand.cgt_incidence_clone@1","mass":"conserve","outputs":[],"params":{"expand_cells":[["household","household_is_capital_gains_clone","bool"],["person","capital_gains","float64"]],"expand_weight_entity":"household","expand_weight_kind":"importance","stage":"cgt_incidence_clone","stage_contract_sha256":"ee30278543cc0297a5366d855b7753aa04af5518971594c193c52be36bf8a7b4","time_period":"2024"},"population":null,"sources":["frs"],"structural":"expand","weights":null},{"base":null,"citation":"","description":"Own the cells materialized by cgt_incidence_clone.","id":"cgt_incidence_clone.owned","inputs":[],"kernel":"uk.claim@1","mass":"conserve","outputs":[{"column":"household_is_capital_gains_clone","dtype":"bool","entity":"household","ownership":"produced","rows":"all"},{"column":"capital_gains","dtype":"float64","entity":"person","ownership":"produced","rows":"all"}],"params":{"materialized_expand_outputs":["household.household_is_capital_gains_clone","person.capital_gains"]},"population":"cgt_incidence_clone","sources":[],"structural":"none","weights":null},{"base":"cgt_incidence_clone","citation":"","description":"Run structural UK stage cgt_band_donors.","id":"cgt_band_donors","inputs":[{"columns":["frs_benunit_capital","is_married","dependent_children","would_claim_child_benefit","child_benefit_opts_out","would_claim_pc","would_claim_uc","would_claim_tfc","would_claim_extended_childcare","would_claim_universal_childcare","would_claim_targeted_childcare","would_claim_uc_childcare","maximum_extended_childcare_hours_usage","benunit_source_id","benunit_support_channel","benunit_support_clone_index","uc_reported_capital","uc_deduction_random_draw","uc_deduction_type_random_draw","uc_latent_deduction_rate","uc_deduction_combination"],"entity":"benunit","rows":"all"},{"columns":["region","tenure_type","accommodation_type","num_bedrooms","council_tax_reported","council_tax_band","council_tax_rebate","council_tax_single_adult_raw","water_and_sewerage_charges","domestic_rates","rent","subrent","mortgage_interest_repayment","mortgage_capital_repayment","structural_insurance_payments","housing_service_charges","external_child_payments","ons_household_type","council_tax","household_owns_tv","would_evade_tv_licence_fee","main_residential_property_purchased_is_first_home","property_purchased","brma","owned_land","property_wealth","corporate_wealth","private_pension_wealth","gross_financial_wealth","net_financial_wealth","main_residence_value","other_residential_property_value","non_residential_property_value","savings","num_vehicles","cash_isa","stocks_and_shares_isa","mortgage_debt","consumer_debt","food_and_non_alcoholic_beverages_consumption","alcohol_and_tobacco_consumption","clothing_and_footwear_consumption","housing_water_and_electricity_consumption","household_furnishings_consumption","health_consumption","transport_consumption","communication_consumption","recreation_consumption","education_consumption","restaurants_and_hotels_consumption","miscellaneous_consumption","petrol_spending","diesel_spending","bus_fare_spending","domestic_energy_consumption","electricity_consumption","gas_consumption","has_fuel_consumption","full_rate_vat_expenditure_rate","dfe_education_spending","rail_subsidy_spending","bus_subsidy_spending","rail_usage","source_household_id","source_year","source_household_key","household_source_id","household_support_channel","household_support_clone_index","household_is_spi_synthetic","household_is_capital_gains_clone"],"entity":"household","rows":"all"},{"columns":["age","gender","marital_status","hours_worked","care_hours","is_household_head","is_benunit_head","is_parent","is_uc_claimant","employment_income","self_employment_income","private_pension_income","tax_free_savings_income","savings_interest_income","dividend_income","property_income","maintenance_income","miscellaneous_income","private_transfer_income","lump_sum_income","student_loan_repayments","statutory_sick_pay","statutory_maternity_pay","student_loans","access_fund","education_grants","healthy_start_vouchers","free_school_breakfasts","free_school_fruit_veg","free_school_meals","council_tax_benefit_reported","maintenance_expenses","childcare_expenses","personal_pension_contributions","employee_pension_contributions","pension_contributions_via_salary_sacrifice","salary_sacrifice_reported","salary_sacrifice_asked","child_benefit_reported","income_support_reported","housing_benefit_reported","attendance_allowance_reported","dla_sc_reported","dla_m_reported","iidb_reported","carers_allowance_reported","would_claim_carers_allowance","sda_reported","afcs_reported","ssmg_reported","pension_credit_reported","child_tax_credit_reported","working_tax_credit_reported","state_pension_reported","winter_fuel_allowance_reported","incapacity_benefit_reported","universal_credit_reported","pip_m_reported","pip_dl_reported","jsa_contrib_reported","jsa_income_reported","esa_contrib_reported","esa_income_reported","bsp_reported","relationship_to_head","ons_family_role","ons_family_index","employment_status","employment_sector","sic_industry_division","aa_category","dla_sc_category","dla_m_category","pip_m_category","pip_dl_category","is_disabled_for_benefits","is_enhanced_disabled_for_benefits","is_severely_disabled_for_benefits","current_education","highest_education","is_in_non_advanced_education","is_in_approved_training","age_started_or_accepted_current_education_or_training","is_before_universal_credit_qualifying_young_person_terminal_date","adult_ema","child_ema","receives_benefits_in_own_right","legacy_jobseeker_proxy","esa_health_condition_proxy","esa_support_group_proxy","disabled_students_allowance_eligible_expenses","would_claim_marriage_allowance","would_claim_scp","attends_private_school_random_draw","tax_free_childcare_spend_routed_share","student_loan_balance","a_and_e_visits","admitted_patient_visits","outpatient_visits","nhs_a_and_e_spending","nhs_admitted_patient_spending","nhs_outpatient_spending","hmrc_spi_pay","hmrc_spi_unemployment_benefit_income","hmrc_spi_incapacity_benefit_income","ossben_identifiable_subset","srp_regular_code5","employer_pension_contributions","person_source_id","person_support_channel","person_support_clone_index","charitable_investment_gifts","gift_aid","other_investment_income","hmrc_spi_employment_benefits","hmrc_spi_employment_expenses","hmrc_spi_other_social_security_income","hmrc_spi_taxable_termination_pay","hmrc_spi_miscellaneous_employment_income","hmrc_spi_other_income","hmrc_spi_state_pension_income","hmrc_spi_employed_income","hmrc_spi_total_earned_income","hmrc_spi_total_investment_income","hmrc_spi_assessable_income","capital_gains"],"entity":"person","rows":"all"}],"kernel":"uk.stage.expand.cgt_band_donors@1","mass":"free","outputs":[],"params":{"expand_cells":[["household","household_is_cgt_band_donor","bool"],["person","capital_gains","float64"]],"expand_weight_entity":"household","expand_weight_kind":"importance","stage":"cgt_band_donors","stage_contract_sha256":"6fde981ff62f0435f9d708ef4048f64b8edfdec34bab521a19dec677d2603694","time_period":"2024"},"population":null,"sources":["frs"],"structural":"expand","weights":null},{"base":null,"citation":"","description":"Own the cells materialized by cgt_band_donors.","id":"cgt_band_donors.owned","inputs":[],"kernel":"uk.claim@1","mass":"conserve","outputs":[{"column":"household_is_cgt_band_donor","dtype":"bool","entity":"household","ownership":"produced","rows":"all"},{"column":"capital_gains","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"}],"params":{"materialized_expand_outputs":["household.household_is_cgt_band_donor"]},"population":"cgt_band_donors","sources":[],"structural":"none","weights":null},{"base":"cgt_band_donors","citation":"","description":"Ownership boundary before hmrc_cgt_gains_spine rewrites.","id":"hmrc_cgt_gains_spine.boundary","inputs":[{"columns":["frs_benunit_capital","is_married","dependent_children","would_claim_child_benefit","child_benefit_opts_out","would_claim_pc","would_claim_uc","would_claim_tfc","would_claim_extended_childcare","would_claim_universal_childcare","would_claim_targeted_childcare","would_claim_uc_childcare","maximum_extended_childcare_hours_usage","benunit_source_id","benunit_support_channel","benunit_support_clone_index","uc_reported_capital","uc_deduction_random_draw","uc_deduction_type_random_draw","uc_latent_deduction_rate","uc_deduction_combination"],"entity":"benunit","rows":"all"},{"columns":["region","tenure_type","accommodation_type","num_bedrooms","council_tax_reported","council_tax_band","council_tax_rebate","council_tax_single_adult_raw","water_and_sewerage_charges","domestic_rates","rent","subrent","mortgage_interest_repayment","mortgage_capital_repayment","structural_insurance_payments","housing_service_charges","external_child_payments","ons_household_type","council_tax","household_owns_tv","would_evade_tv_licence_fee","main_residential_property_purchased_is_first_home","property_purchased","brma","owned_land","property_wealth","corporate_wealth","private_pension_wealth","gross_financial_wealth","net_financial_wealth","main_residence_value","other_residential_property_value","non_residential_property_value","savings","num_vehicles","cash_isa","stocks_and_shares_isa","mortgage_debt","consumer_debt","food_and_non_alcoholic_beverages_consumption","alcohol_and_tobacco_consumption","clothing_and_footwear_consumption","housing_water_and_electricity_consumption","household_furnishings_consumption","health_consumption","transport_consumption","communication_consumption","recreation_consumption","education_consumption","restaurants_and_hotels_consumption","miscellaneous_consumption","petrol_spending","diesel_spending","bus_fare_spending","domestic_energy_consumption","electricity_consumption","gas_consumption","has_fuel_consumption","full_rate_vat_expenditure_rate","dfe_education_spending","rail_subsidy_spending","bus_subsidy_spending","rail_usage","source_household_id","source_year","source_household_key","household_source_id","household_support_channel","household_support_clone_index","household_is_spi_synthetic","household_is_capital_gains_clone","household_is_cgt_band_donor"],"entity":"household","rows":"all"},{"columns":["age","gender","marital_status","hours_worked","care_hours","is_household_head","is_benunit_head","is_parent","is_uc_claimant","employment_income","self_employment_income","private_pension_income","tax_free_savings_income","savings_interest_income","dividend_income","property_income","maintenance_income","miscellaneous_income","private_transfer_income","lump_sum_income","student_loan_repayments","statutory_sick_pay","statutory_maternity_pay","student_loans","access_fund","education_grants","healthy_start_vouchers","free_school_breakfasts","free_school_fruit_veg","free_school_meals","council_tax_benefit_reported","maintenance_expenses","childcare_expenses","personal_pension_contributions","employee_pension_contributions","pension_contributions_via_salary_sacrifice","salary_sacrifice_reported","salary_sacrifice_asked","child_benefit_reported","income_support_reported","housing_benefit_reported","attendance_allowance_reported","dla_sc_reported","dla_m_reported","iidb_reported","carers_allowance_reported","would_claim_carers_allowance","sda_reported","afcs_reported","ssmg_reported","pension_credit_reported","child_tax_credit_reported","working_tax_credit_reported","state_pension_reported","winter_fuel_allowance_reported","incapacity_benefit_reported","universal_credit_reported","pip_m_reported","pip_dl_reported","jsa_contrib_reported","jsa_income_reported","esa_contrib_reported","esa_income_reported","bsp_reported","relationship_to_head","ons_family_role","ons_family_index","employment_status","employment_sector","sic_industry_division","aa_category","dla_sc_category","dla_m_category","pip_m_category","pip_dl_category","is_disabled_for_benefits","is_enhanced_disabled_for_benefits","is_severely_disabled_for_benefits","current_education","highest_education","is_in_non_advanced_education","is_in_approved_training","age_started_or_accepted_current_education_or_training","is_before_universal_credit_qualifying_young_person_terminal_date","adult_ema","child_ema","receives_benefits_in_own_right","legacy_jobseeker_proxy","esa_health_condition_proxy","esa_support_group_proxy","disabled_students_allowance_eligible_expenses","would_claim_marriage_allowance","would_claim_scp","attends_private_school_random_draw","tax_free_childcare_spend_routed_share","student_loan_balance","a_and_e_visits","admitted_patient_visits","outpatient_visits","nhs_a_and_e_spending","nhs_admitted_patient_spending","nhs_outpatient_spending","hmrc_spi_pay","hmrc_spi_unemployment_benefit_income","hmrc_spi_incapacity_benefit_income","ossben_identifiable_subset","srp_regular_code5","employer_pension_contributions","person_source_id","person_support_channel","person_support_clone_index","charitable_investment_gifts","gift_aid","other_investment_income","hmrc_spi_employment_benefits","hmrc_spi_employment_expenses","hmrc_spi_other_social_security_income","hmrc_spi_taxable_termination_pay","hmrc_spi_miscellaneous_employment_income","hmrc_spi_other_income","hmrc_spi_state_pension_income","hmrc_spi_employed_income","hmrc_spi_total_earned_income","hmrc_spi_total_investment_income","hmrc_spi_assessable_income","capital_gains"],"entity":"person","rows":"all"}],"kernel":"uk.identity@1","mass":"conserve","outputs":[],"params":{},"population":null,"sources":[],"structural":"filter","weights":null},{"base":null,"citation":"","description":"Run UK spine stage hmrc_cgt_gains_spine.","id":"hmrc_cgt_gains_spine","inputs":[{"columns":["frs_benunit_capital"],"entity":"benunit","rows":"all"},{"columns":["region"],"entity":"household","rows":"all"},{"columns":["age","dividend_income","employment_income","miscellaneous_income","private_pension_income","property_income","savings_interest_income","self_employment_income","state_pension_reported","tax_free_savings_income"],"entity":"person","rows":"all"}],"kernel":"uk.stage.hmrc_cgt_gains_spine@1","mass":"conserve","outputs":[{"column":"capital_gains","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"}],"params":{"stage":"hmrc_cgt_gains_spine","stage_contract_sha256":"f94db1195058327ddd5ddbff93e7510561aaf7773e478d1e76f3e87bbf6f1dfc","time_period":"2024"},"population":"hmrc_cgt_gains_spine.boundary","sources":["frs"],"structural":"none","weights":null},{"base":null,"citation":"","description":"Run UK spine stage hmrc_cgt_asset_type_spine.","id":"hmrc_cgt_asset_type_spine","inputs":[{"columns":["frs_benunit_capital"],"entity":"benunit","rows":"all"},{"columns":["region"],"entity":"household","rows":"all"},{"columns":["age","capital_gains"],"entity":"person","rows":"all"}],"kernel":"uk.stage.hmrc_cgt_asset_type_spine@1","mass":"conserve","outputs":[{"column":"capital_gains_asset_type","dtype":"string","entity":"person","ownership":"produced","rows":"all"},{"column":"capital_gains_residential_property","dtype":"float64","entity":"person","ownership":"produced","rows":"all"}],"params":{"stage":"hmrc_cgt_asset_type_spine","stage_contract_sha256":"529706ff53425e28907eca2451a6d7c8ccb5b0f82643ed62eb3947ed4f71dfc3","time_period":"2024"},"population":"hmrc_cgt_gains_spine.boundary","sources":["frs"],"structural":"none","weights":null},{"base":"hmrc_cgt_gains_spine.boundary","citation":"","description":"Run structural UK stage cgt_incidence_anchor.","id":"cgt_incidence_anchor","inputs":[{"columns":["frs_benunit_capital"],"entity":"benunit","rows":"all"},{"columns":["household_is_capital_gains_clone","household_is_cgt_band_donor","region"],"entity":"household","rows":"all"},{"columns":["age","capital_gains","capital_gains_residential_property","dividend_income","employment_income","miscellaneous_income","private_pension_income","property_income","savings_interest_income","self_employment_income","state_pension_reported"],"entity":"person","rows":"all"}],"kernel":"uk.stage.expand.cgt_incidence_anchor@1","mass":"conserve","outputs":[],"params":{"expand_cells":[],"expand_weight_entity":"household","expand_weight_kind":"importance","stage":"cgt_incidence_anchor","stage_contract_sha256":"00fbe9edc8bd1f31fd79b05cf20b3d5519f9a5b91ded0da35fae1316c1cbb9e0","time_period":"2024"},"population":null,"sources":["frs"],"structural":"expand","weights":null},{"base":null,"citation":"","description":"Run UK spine stage salary_sacrifice.","id":"salary_sacrifice","inputs":[{"columns":["frs_benunit_capital","is_married","dependent_children","would_claim_child_benefit","child_benefit_opts_out","would_claim_pc","would_claim_uc","would_claim_tfc","would_claim_extended_childcare","would_claim_universal_childcare","would_claim_targeted_childcare","would_claim_uc_childcare","maximum_extended_childcare_hours_usage","benunit_source_id","benunit_support_channel","benunit_support_clone_index","uc_reported_capital","uc_deduction_random_draw","uc_deduction_type_random_draw","uc_latent_deduction_rate","uc_deduction_combination"],"entity":"benunit","rows":"all"},{"columns":["region","tenure_type","accommodation_type","num_bedrooms","council_tax_reported","council_tax_band","council_tax_rebate","council_tax_single_adult_raw","water_and_sewerage_charges","domestic_rates","rent","subrent","mortgage_interest_repayment","mortgage_capital_repayment","structural_insurance_payments","housing_service_charges","external_child_payments","ons_household_type","council_tax","household_owns_tv","would_evade_tv_licence_fee","main_residential_property_purchased_is_first_home","property_purchased","brma","owned_land","property_wealth","corporate_wealth","private_pension_wealth","gross_financial_wealth","net_financial_wealth","main_residence_value","other_residential_property_value","non_residential_property_value","savings","num_vehicles","cash_isa","stocks_and_shares_isa","mortgage_debt","consumer_debt","food_and_non_alcoholic_beverages_consumption","alcohol_and_tobacco_consumption","clothing_and_footwear_consumption","housing_water_and_electricity_consumption","household_furnishings_consumption","health_consumption","transport_consumption","communication_consumption","recreation_consumption","education_consumption","restaurants_and_hotels_consumption","miscellaneous_consumption","petrol_spending","diesel_spending","bus_fare_spending","domestic_energy_consumption","electricity_consumption","gas_consumption","has_fuel_consumption","full_rate_vat_expenditure_rate","dfe_education_spending","rail_subsidy_spending","bus_subsidy_spending","rail_usage","source_household_id","source_year","source_household_key","household_source_id","household_support_channel","household_support_clone_index","household_is_spi_synthetic","household_is_capital_gains_clone","household_is_cgt_band_donor"],"entity":"household","rows":"all"},{"columns":["age","gender","marital_status","hours_worked","care_hours","is_household_head","is_benunit_head","is_parent","is_uc_claimant","employment_income","self_employment_income","private_pension_income","tax_free_savings_income","savings_interest_income","dividend_income","property_income","maintenance_income","miscellaneous_income","private_transfer_income","lump_sum_income","student_loan_repayments","statutory_sick_pay","statutory_maternity_pay","student_loans","access_fund","education_grants","healthy_start_vouchers","free_school_breakfasts","free_school_fruit_veg","free_school_meals","council_tax_benefit_reported","maintenance_expenses","childcare_expenses","personal_pension_contributions","salary_sacrifice_reported","salary_sacrifice_asked","child_benefit_reported","income_support_reported","housing_benefit_reported","attendance_allowance_reported","dla_sc_reported","dla_m_reported","iidb_reported","carers_allowance_reported","would_claim_carers_allowance","sda_reported","afcs_reported","ssmg_reported","pension_credit_reported","child_tax_credit_reported","working_tax_credit_reported","state_pension_reported","winter_fuel_allowance_reported","incapacity_benefit_reported","universal_credit_reported","pip_m_reported","pip_dl_reported","jsa_contrib_reported","jsa_income_reported","esa_contrib_reported","esa_income_reported","bsp_reported","relationship_to_head","ons_family_role","ons_family_index","employment_status","employment_sector","sic_industry_division","aa_category","dla_sc_category","dla_m_category","pip_m_category","pip_dl_category","is_disabled_for_benefits","is_enhanced_disabled_for_benefits","is_severely_disabled_for_benefits","current_education","highest_education","is_in_non_advanced_education","is_in_approved_training","age_started_or_accepted_current_education_or_training","is_before_universal_credit_qualifying_young_person_terminal_date","adult_ema","child_ema","receives_benefits_in_own_right","legacy_jobseeker_proxy","esa_health_condition_proxy","esa_support_group_proxy","disabled_students_allowance_eligible_expenses","would_claim_marriage_allowance","would_claim_scp","attends_private_school_random_draw","tax_free_childcare_spend_routed_share","student_loan_balance","a_and_e_visits","admitted_patient_visits","outpatient_visits","nhs_a_and_e_spending","nhs_admitted_patient_spending","nhs_outpatient_spending","hmrc_spi_pay","hmrc_spi_unemployment_benefit_income","hmrc_spi_incapacity_benefit_income","ossben_identifiable_subset","srp_regular_code5","employer_pension_contributions","person_source_id","person_support_channel","person_support_clone_index","charitable_investment_gifts","gift_aid","other_investment_income","hmrc_spi_employment_benefits","hmrc_spi_employment_expenses","hmrc_spi_other_social_security_income","hmrc_spi_taxable_termination_pay","hmrc_spi_miscellaneous_employment_income","hmrc_spi_other_income","hmrc_spi_state_pension_income","hmrc_spi_employed_income","hmrc_spi_total_earned_income","hmrc_spi_total_investment_income","hmrc_spi_assessable_income","capital_gains","capital_gains_asset_type","capital_gains_residential_property"],"entity":"person","rows":"all"}],"kernel":"uk.stage.salary_sacrifice@1","mass":"conserve","outputs":[{"column":"pension_contributions_via_salary_sacrifice","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"},{"column":"employee_pension_contributions","dtype":"float64","entity":"person","ownership":"produced","rewrite":true,"rows":"all"}],"params":{"stage":"salary_sacrifice","stage_contract_sha256":"a4af925af21cc6766eb7c8855b6743a3aa4eeeb38df12cfc83063d7ad6890539","time_period":"2024"},"population":"cgt_incidence_anchor","sources":["frs"],"structural":"none","weights":null},{"base":null,"citation":"","description":"Run UK spine stage student_loans.","id":"student_loans","inputs":[{"columns":["frs_benunit_capital"],"entity":"benunit","rows":"all"},{"columns":["region"],"entity":"household","rows":"all"},{"columns":["age","current_education","employee_pension_contributions","highest_education","student_loan_repayments","student_loans"],"entity":"person","rows":"all"}],"kernel":"uk.stage.student_loans@1","mass":"conserve","outputs":[{"column":"student_loan_plan","dtype":"string","entity":"person","ownership":"produced","rows":"all"}],"params":{"stage":"student_loans","stage_contract_sha256":"7c4c76398c2e80b41c7f941a5f06246b57d89b5eb0c36d42e3116ac35e82e203","time_period":"2024"},"population":"cgt_incidence_anchor","sources":["frs"],"structural":"none","weights":null}],"sources":[{"codec":"csv-tables","description":"Content-bound UK FRS and donor fixture/source bundle.","name":"frs"}]} diff --git a/tools/build_uk_frs_spine.py b/tools/build_uk_frs_spine.py index 68255437fc..48bf36e9fe 100644 --- a/tools/build_uk_frs_spine.py +++ b/tools/build_uk_frs_spine.py @@ -64,6 +64,7 @@ from microcosm.build.uk_runtime.cgt_imputation import uk_cgt_spine_stage_transform from microcosm.build.uk_runtime.cgt_structure import ( UKCGTBandDonorStageTransform, + UKCGTIncidenceAnchorStageTransform, UKCGTIncidenceCloneStageTransform, ) from microcosm.build.uk_runtime.content_identity import uk_frame_content_identity @@ -1557,6 +1558,12 @@ def main(argv: list[str] | None = None) -> int: stages_by_name["hmrc_cgt_asset_type_spine"] ) ) + if "cgt_incidence_anchor" in stage_names: + implementations["cgt_incidence_anchor"] = ( + UKCGTIncidenceAnchorStageTransform( + stage=stages_by_name["cgt_incidence_anchor"] + ) + ) if "salary_sacrifice" in stage_names: implementations["salary_sacrifice"] = UKSalarySacrificeStageTransform( stage=stages_by_name["salary_sacrifice"] diff --git a/tools/build_uk_release_input_coverage_manifest.py b/tools/build_uk_release_input_coverage_manifest.py index 7a888328d2..822960d348 100644 --- a/tools/build_uk_release_input_coverage_manifest.py +++ b/tools/build_uk_release_input_coverage_manifest.py @@ -804,6 +804,10 @@ def build_manifest( stage_name="hmrc_cgt_asset_type_spine", candidate_source=candidate_source, ), + "cgt_incidence_anchor": _source_stage_family_coverage_contract( + stage_name="cgt_incidence_anchor", + candidate_source=candidate_source, + ), "salary_sacrifice": _source_stage_family_coverage_contract( stage_name="salary_sacrifice", candidate_source=candidate_source, @@ -962,6 +966,7 @@ def _cgt_spine_family_coverage_contract( required_artifacts = { "policy_parameters", "cgt_conditioning_facts", + "capital_gains_within_band_distribution", } required_operations = { "verify_vendored_fact_resource", diff --git a/tools/graph_uk_spine_fixture.py b/tools/graph_uk_spine_fixture.py index a29ce0310d..09742b065c 100644 --- a/tools/graph_uk_spine_fixture.py +++ b/tools/graph_uk_spine_fixture.py @@ -41,6 +41,7 @@ ) from microcosm.build.uk_runtime.cgt_structure import ( UKCGTBandDonorStageTransform, + UKCGTIncidenceAnchorStageTransform, UKCGTIncidenceCloneStageTransform, ) from microcosm.build.uk_runtime.content_identity import uk_frame_content_identity @@ -136,7 +137,7 @@ _SPI_DONOR_SAMPLE_SIZE = 64 #: The packaged FRS spine roster the fixture exercises (manifest minus the #: certified-pair exclusions); moves whenever a spine stage is added. -UK_FIXTURE_STAGE_COUNT = 30 +UK_FIXTURE_STAGE_COUNT = 31 _QRF_ESTIMATORS = 4 # These are the complete object-string surface observed in the unchanged @@ -1228,6 +1229,9 @@ def capture_root(frame: Frame) -> Frame: facts=cgt_asset_type_facts, parameters=cgt_parameters, ), + "cgt_incidence_anchor": UKCGTIncidenceAnchorStageTransform( + stage=stages["cgt_incidence_anchor"], parameters=cgt_parameters + ), "salary_sacrifice": UKSalarySacrificeStageTransform( stage=stages["salary_sacrifice"] ), @@ -1411,7 +1415,7 @@ def generate(output: Path) -> None: _normalization_markdown(), encoding="utf-8" ) (output / "PRODUCED_BY.txt").write_text( - "tools/graph_uk_spine_fixture.py; current 30-transform legacy " + "tools/graph_uk_spine_fixture.py; current 31-transform legacy " "StagePlan oracle with parsed private-source seams. The acceptance " "test runs both sides from frs_raw in-process (root weights differ " "by one ulp between machines); the captured root tables serve the " diff --git a/tools/verify_uk_identity_stability.py b/tools/verify_uk_identity_stability.py index 82c0662c4b..ea2c0a7237 100644 --- a/tools/verify_uk_identity_stability.py +++ b/tools/verify_uk_identity_stability.py @@ -425,9 +425,12 @@ def recompute(person_t, benunit_t, household_t) -> dict[str, pd.DataFrame]: # the later stage's receipt surface, not E6's; and every later stage that # redistributes mass leaves these rows carrying a fraction of the weight # E6 normalized against. Three layers stack today (SPI support channel, - # capital-gains clone, CGT band donors) and two of them move mass. + # capital-gains clone, CGT band donors) and two of them move mass: the + # SPI share is divided back out, the clone's mass (equal halves before + # the #970 anchor, pair-conserving afterwards) is folded back by pair. spine_mask = _unstacked_mask(household) if spine_mask is not None: + household["household_weight"] = _pre_clone_household_weights(frame) household = household.loc[spine_mask].reset_index(drop=True) spine_household_ids = set(household["household_id"].tolist()) person = person.loc[ @@ -670,6 +673,105 @@ def recompute(person_t, benunit_t, household_t) -> dict[str, pd.DataFrame]: } +def _e8_clone_pairs(frame, problems: dict[str, object]) -> dict[str, object]: + """Check the clone/original pairs, before or after the #970 anchor. + + Before the anchor both halves carry the same weight. After it (the + frame's mass log carries the anchor's record) each pair still sums to + its pre-clone weight, the clone side never exceeds the original, and the + anchor is recomputed from the reconstructed pre-anchor state (both halves + at half the pair sum, the clone stage's split up to rounding) in original + and reversed person order and compared with the stored weights. + """ + + from microcosm.build.uk_runtime.cgt_imputation import uk_cgt_policy_parameters + from microcosm.build.uk_runtime.cgt_structure import ( + CGT_ANCHOR_MASS_CHANGE_REASON, + anchor_cgt_incidence, + load_advani_summers_distribution, + pair_clone_households, + ) + from microcosm.build.uk_runtime.national_frame import ( + uk_household_weight_kind, + uk_national_frame, + ) + + person = frame.table("person") + benunit = frame.table("benunit") + household = frame.table("household") + weights = np.asarray(frame.weights_for("household").values, dtype=float) + anchor_records = [ + index + for index, record in enumerate(frame.mass_log) + if record.reason == CGT_ANCHOR_MASS_CHANGE_REASON + ] + receipt: dict[str, object] = {"anchored": bool(anchor_records)} + try: + clone_positions, original_positions, _ = pair_clone_households( + person, benunit, household + ) + except ValueError as error: + problems["clone_pairing"] = str(error) + return receipt + left = weights[original_positions] + right = weights[clone_positions] + receipt["pairs"] = int(len(clone_positions)) + if not anchor_records: + if not np.allclose(left, right, rtol=1e-12, atol=1e-6): + problems["clone_pair_weights"] = int( + (~np.isclose(left, right, rtol=1e-12, atol=1e-6)).sum() + ) + if not np.isclose(left.sum(), right.sum(), rtol=1e-12, atol=1e-6): + problems["clone_half_masses"] = [float(left.sum()), float(right.sum())] + return receipt + exceeds = right > left * (1.0 + 1e-12) + 1e-6 + if exceeds.any(): + problems["clone_exceeds_original"] = int(exceeds.sum()) + pre = weights.copy() + halves = 0.5 * (left + right) + pre[original_positions] = halves + pre[clone_positions] = halves + pre_frame = uk_national_frame( + person=person.copy(), + benunit=benunit.copy(), + household=household.copy(), + time_period=uk_time_period(frame), + weight_kind=uk_household_weight_kind(frame), + household_weights=pre, + mass_log=tuple(frame.mass_log[: anchor_records[0]]), + ) + distribution = load_advani_summers_distribution() + parameters = uk_cgt_policy_parameters(uk_time_period(frame)) + recomputed = anchor_cgt_incidence( + pre_frame, distribution=distribution, parameters=parameters + ) + stored = pd.Series(weights, index=household["household_id"].to_numpy()) + + def by_household_id(result) -> np.ndarray: + return ( + pd.Series( + result.frame.weights_for("household").values, + index=result.frame.table("household")["household_id"].to_numpy(), + ) + .reindex(stored.index) + .to_numpy(dtype=float) + ) + + again = by_household_id(recomputed) + close = np.isclose(again, stored.to_numpy(), rtol=1e-9, atol=1e-6) + receipt["anchor_max_abs_weight_diff"] = float(np.abs(again - stored).max()) + receipt["anchor_targets"] = dict(recomputed.targets) + receipt["anchor_after"] = dict(recomputed.after) + if not close.all(): + problems["anchor_recompute"] = int((~close).sum()) + permuted = anchor_cgt_incidence( + _reverse_rows(pre_frame), distribution=distribution, parameters=parameters + ) + if not np.array_equal(by_household_id(permuted), again): + problems["anchor_permutation"] = True + return receipt + + def e8_identity_receipt( frame, *, @@ -678,16 +780,19 @@ def e8_identity_receipt( """Receipt E8 deterministic layers under row permutation by entity id. Covered: (1) the clone-pair structure — the non-donor population splits - into equal-count original/clone halves whose paired household weights - agree to the exact-total correction tolerance and whose half-masses - match; (2) the CGT band-donor selection recomputed from the committed + into equal-count original/clone halves paired by the clone stage's id + offset; before the #970 anchor their paired weights agree to the + exact-total correction tolerance and their half-masses match, after it + the clone side never exceeds the original and the anchor recomputed + from the reconstructed pre-anchor halves reproduces the stored weights + in original and reversed person order; (2) the CGT band-donor selection recomputed from the committed resources over id-sorted candidates in original and permuted row order (set equality with the flagged donors, 30 donors per band, band-exact stored weights and carrier gains); (3) the student-loan plan column recomputed in full (identity-keyed top-ups at the release calibration year) in original and permuted row order against the stored column. - The A&S prior amounts (overwritten by the Table 3 redraw except the - sub-AEA remainder), the redraw's seeded within-band draws (covered by + The A&S prior amounts (overwritten by the Table 3 redraw and its + sub-AEA remainder mapping), the redraw's seeded within-band draws (covered by the merged #560 embedded published-surface tests), and the salary-sacrifice QRF and conversion (the pre-conversion state is consumed by the stage) are covered by twin-build determinism. @@ -729,15 +834,7 @@ def e8_identity_receipt( ) if len(originals) != len(clones): problems["clone_half_counts"] = [len(originals), len(clones)] - else: - left = originals["household_weight"].to_numpy(dtype=float) - right = clones["household_weight"].to_numpy(dtype=float) - if not np.allclose(left, right, rtol=1e-12, atol=1e-6): - problems["clone_pair_weights"] = int( - (~np.isclose(left, right, rtol=1e-12, atol=1e-6)).sum() - ) - if not np.isclose(left.sum(), right.sum(), rtol=1e-12, atol=1e-6): - problems["clone_half_masses"] = [float(left.sum()), float(right.sum())] + clone_pairs = _e8_clone_pairs(frame, problems) # (2) Band-donor selection recomputed from the committed resources. # Same contract as the E6 NHS check: age_tail now runs immediately after @@ -871,7 +968,9 @@ def select_donors(person_t: pd.DataFrame) -> np.ndarray: "identical_under_permutation": bool( "donor_selection_permutation" not in problems and "student_loan_plan_permutation" not in problems + and "anchor_permutation" not in problems ), + "clone_pairs": clone_pairs, "permutation_mismatches": { key: value for key, value in problems.items() @@ -887,7 +986,9 @@ def select_donors(person_t: pd.DataFrame) -> np.ndarray: if not key.endswith("_permutation") }, "tolerance_policy": ( - "clone-pair weights and half-masses: rtol 1e-12 / atol 1e-6 " + "anchored clone pairs: recomputed #970 anchor weights rtol 1e-9 / " + "atol 1e-6, reversed-order recompute exact; unanchored clone-pair " + "weights and half-masses: rtol 1e-12 / atol 1e-6 " "(the exact-total correction may move single weights by bit " "corrections); donor stored weights: rtol 1e-12 bitwise-class " "against published band taxpayers / 30; donor selection and " @@ -903,7 +1004,7 @@ def select_donors(person_t: pd.DataFrame) -> np.ndarray: }, "qrf_draw_columns_scope": ( "excluded: the A&S prior amounts (overwritten by the Table 3 " - "redraw except the sub-AEA remainder), the redraw's seeded " + "redraw and its sub-AEA remainder mapping), the redraw's seeded " "within-band draws (the merged #560 embedded published-surface " "tests cover the amounts logic), and the salary-sacrifice QRF " "and conversion (the pre-conversion column state is consumed " @@ -1163,10 +1264,40 @@ def main() -> int: #: skews the comparison in the opposite direction. _MASS_STAGE_BY_FLAG = { "household_is_spi_synthetic": "spi_support_channel", - "household_is_capital_gains_clone": "cgt_incidence_clone", } +def _pre_clone_household_weights(frame) -> np.ndarray: + """Household weights with each clone's mass folded back onto its original. + + Before the #970 anchor both halves carry half the source weight; after it + the split varies per pair while the pair sum is still the pre-clone + weight (the anchor conserves every pair to rounding). Folding the clone + onto its original is therefore exact either way, where a uniform + ``mass_split`` divisor would be wrong after the anchor. An artifact + without the clone layer is returned unchanged; one with the clone flag + but no donor flag is treated as carrying no donors. + """ + + from microcosm.build.uk_runtime.cgt_structure import ( + HOUSEHOLD_IS_CGT_BAND_DONOR, + HOUSEHOLD_IS_CGT_CLONE, + pair_clone_households, + ) + + household = frame.table("household") + weights = np.asarray(frame.weights_for("household").values, dtype=float).copy() + if HOUSEHOLD_IS_CGT_CLONE not in household.columns: + return weights + if HOUSEHOLD_IS_CGT_BAND_DONOR not in household.columns: + household = household.assign(**{HOUSEHOLD_IS_CGT_BAND_DONOR: False}) + clone_positions, original_positions, _ = pair_clone_households( + frame.table("person"), frame.table("benunit"), household + ) + weights[original_positions] += weights[clone_positions] + return weights + + def _unstacked_mask(household: pd.DataFrame): """Rows that were present when the pre-stacking stages ran, or None.""" @@ -1186,12 +1317,13 @@ def _stage_time_weight_divisor(*, after_stages: Sequence[str]) -> float: allocation's budget normalization is absolute, not relative — must divide that back out or it compares against a different grossing scale. - On the E8 roster two stages do this: `spi_support_channel` reserves - ``share`` of prior mass for the synthetic channel, and - `cgt_incidence_clone` splits each household's weight across its copies by - ``mass_split``. Both factors are read from the declared operations rather - than hardcoded, so a change to either is picked up automatically; a *new* - mass-redistributing op kind still has to be added here. + On the E8 roster one stage does this uniformly: `spi_support_channel` + reserves ``share`` of prior mass for the synthetic channel. The factor + is read from the declared operation rather than hardcoded, so a change + is picked up automatically; a *new* uniform mass-redistributing op kind + still has to be added here. The capital-gains clone is not uniform once + the #970 anchor has run, so its mass is restored by pair sum instead + (``_pre_clone_household_weights``). ``after_stages`` names only the stages that actually ran in the artifact under receipt — see ``_MASS_STAGE_BY_FLAG``. Deriving it from the @@ -1216,8 +1348,6 @@ def _stage_time_weight_divisor(*, after_stages: Sequence[str]) -> float: kind = operation.get("kind") if kind == "allocate_zero_weight_prior_mass": divisor *= 1.0 - float(operation["share"]) - elif kind == "clone_records": - divisor *= float(operation["mass_split"]) if divisor <= 0.0: raise ValueError( "stage-time weight divisor collapsed to zero; the declared mass " @@ -1230,7 +1360,9 @@ def _frs_only_frame(frame): """Scope the artifact to the unstacked survey rows (raw FRS only). On the E8 roster this leaves the 16,288 raw FRS households, which is the - population the pre-stacking stages actually drew for. + population the pre-stacking stages actually drew for. Their weights fold + each clone back onto its original, exact before and after the #970 + anchor. """ from microcosm.build.uk_runtime.national_frame import ( @@ -1244,7 +1376,7 @@ def _frs_only_frame(frame): return frame stacked = household[flags].astype(bool).any(axis=1) keep = ~stacked - weights = frame.weights_for("household").values[keep.to_numpy()] + weights = _pre_clone_household_weights(frame)[keep.to_numpy()] household = household.loc[keep].reset_index(drop=True) ids = set(household["household_id"].tolist()) person = (