Skip to content

Instantly share code, notes, and snippets.

@MaxGhenis
Created August 23, 2026 10:26
Show Gist options
  • Select an option

  • Save MaxGhenis/87410065329a516c357da4446398e971 to your computer and use it in GitHub Desktop.

Select an option

Save MaxGhenis/87410065329a516c357da4446398e971 to your computer and use it in GitHub Desktop.
policyengine-uk-data#452/#448 diagnosis — aggregate measurement scripts and outputs (no microdata); 2026-08-22

Official council-tax comparators (public statistics, fetched 2026-08-22)

nation year measure value source
Scotland 2024-25 total council tax billed, net of Council Tax Reduction (net of discounts/exemptions; excludes water & sewerage per the series definition — confirm on page 7 "Data and Methodology") £3.077bn (£3,076,770,000) https://www.gov.scot/publications/council-tax-collection-statistics-2024-25/pages/3/
Scotland 2024-25 reduction in liability due to CTR ≈ £389m https://www.gov.scot/news/council-tax-collection-statistics-2024-25/
Scotland 2025-26 total council tax billed, net of CTR £3.389bn (£3,388,884,000); collected £3.227bn (95.2%) https://www.gov.scot/publications/council-tax-collection-statistics-2025-26/pages/3/
Wales 2024-25 collectable, excluding council tax reduction scheme £2,332m (collected £2,228m) https://www.gov.wales/council-tax-collection-rates-april-2024-march-2025-html
Wales 2025-26 collectable, excluding council tax reduction scheme £2,524m (collected £2,404m) https://www.gov.wales/council-tax-collection-rates-april-2025-march-2026-html
England 2025-26 council tax requirement (net of local council tax support) £44.1bn (+£2.9bn vs 2024-25 → 2024-25 ≈ £41.2bn); tax base 19.3m band-D equivalents https://www.gov.uk/government/statistics/council-tax-levels-set-by-local-authorities-in-england-2025-to-2026

Not yet pinned: Scotland CTR 2025-26 amount; Wales CTRS cost 2024-25/2025-26; England LCTS cost 2025-26. The model's reduction-scheme totals (Scotland £249m, Wales £10m, England £2.0bn at 2025, v1.56.14) are compared against the nearest pinned figure in the report with the year basis stated.

Admin UC comparators

See chronicle#188 facts (map/chronicle-feed.md) — not copied here.

Model (GB, benefit units with universal_credit > 0 at 2025) vs DWP admin (GB, Apr–Dec 2025 monthly averages; childcare = Jan–Aug 2025 published)

Model: enhanced_frs_2024_25.h5 — v1.56.14 (microcosm parity pin) and v1.56.16; policyengine-uk 2.91.0 @ c93e1a05; year 2025; Northern Ireland excluded to match the GB admin geography (K03000001). Admin: chronicle@7754c8c packages (PR #188 + #184), monthly values listed in map/chronicle-feed.md.

series admin avg (m) model v1.56.14 (m) gap model v1.56.16 (m) gap records on UC (v1.56.14)
households (deductions-derived, #735 target) 6.759 6.179 -8.6% 6.199 -8.3% 3111
housing entitlement 4.304 4.856 +12.8% 4.868 +13.1% 2070
LCWRA entitlement 2.417 2.089 -13.6% 2.102 -13.0% 922
carer entitlement 1.134 0.562 -50.5% 0.641 -43.5% 224
childcare element (Jan–Aug 2025, published) 0.184 0.454 +146.8% 0.471 +156.1% 129
single, no children 3.447 2.994 -13.1% 2.969 -13.9%
single, with children 2.226 2.147 -3.5% 2.135 -4.1%
couple, no children 0.284 0.221 -22.5% 0.225 -20.9%
couple, with children 0.900 0.818 -9.1% 0.871 -3.2%
0 children 3.731 3.214 -13.9% 3.194 -14.4%
1 child 1.279 1.220 -4.6% 1.234 -3.5%
2 children 1.101 1.024 -7.0% 1.037 -5.8%
3 children 0.495 0.510 +3.1% 0.499 +1.0%
4 children 0.175 0.133 -23.9% 0.156 -10.7%
5+ children 0.077 0.079 +1.5% 0.079 +2.1%

Stat-Xplore family-type total (Apr–Dec avg): 6.857m vs deductions-derived households avg 6.759m (+1.5%).

Shares of caseload (v1.56.14 vs admin): single, no children: 48.4% vs 50.3%; single, with children: 34.7% vs 32.5%; couple, no children: 3.6% vs 4.1%; couple, with children: 13.2% vs 13.1%

UK-basis aggregate (incl. NI) for reference: v1.56.14 6.328m vs #735 target 6.759m = −6.4%; GB-basis 6.180m vs 6.759m = -8.6%.

{
"artifact": "enhanced_frs_2024_25.h5",
"sha256": "97a07f9ccb54019e4550e70980c561c985523e6bbc43d21938d01536e37d6c3e",
"tag": "efrs2425_v1_56_14",
"year": 2025,
"policyengine_uk_version": "2.91.0",
"policyengine_core_version": "3.31.0",
"model_commit": "c93e1a05",
"uc_benunits_total_m": 6.3275,
"uc_spend_bn": 74.728,
"benunits_unweighted_on_uc": 3615,
"benunits_unweighted_total": 61223,
"would_claim_uc_m": 21.8698,
"would_claim_uc_unweighted": 33709,
"is_uc_entitled_m": 6.3275,
"is_uc_entitled_unweighted": 3615,
"is_uc_eligible_m": 10.7141,
"is_uc_eligible_unweighted": 14897,
"uc_reported_error": "boolean index did not match indexed array along axis 0; size of axis is 61223 but size of corresponding boolean axis is ",
"entitled_not_claiming_m": 0.0,
"entitled_and_claiming_m": 6.3275,
"entitled_claiming_but_uc_zero_m": 0.0,
"max_support_entitled_m": 6.3275,
"elements": {
"childcare": {
"on_uc_with_element_m": 0.4585,
"any_with_element_m": 1.7447,
"element_spend_bn_on_uc": 2.254,
"unweighted_on_uc_with_element": 151
},
"housing": {
"on_uc_with_element_m": 4.9758,
"any_with_element_m": 11.9289,
"element_spend_bn_on_uc": 36.784,
"unweighted_on_uc_with_element": 2370
},
"lcwra": {
"on_uc_with_element_m": 2.1615,
"any_with_element_m": 6.5037,
"element_spend_bn_on_uc": 11.84,
"unweighted_on_uc_with_element": 1140
},
"carer": {
"on_uc_with_element_m": 0.5942,
"any_with_element_m": 1.193,
"element_spend_bn_on_uc": 1.438,
"unweighted_on_uc_with_element": 288
},
"child": {
"on_uc_with_element_m": 3.0492,
"any_with_element_m": 8.8333,
"element_spend_bn_on_uc": 18.858,
"unweighted_on_uc_with_element": 1225
},
"disability_any": {
"on_uc_with_element_m": 2.1615,
"any_with_element_m": 6.5037,
"element_spend_bn_on_uc": 14.254,
"unweighted_on_uc_with_element": 1140
}
},
"family_type_on_uc_m": {
"COUPLE_NO_CHILDREN": 0.2309,
"COUPLE_WITH_CHILDREN": 0.8383,
"LONE_PARENT": 2.2,
"SINGLE": 3.0583
},
"children_on_uc_m": {
"0": 3.2892,
"1": 1.2492,
"2": 1.0497,
"3": 0.5203,
"4": 0.1401,
"5_or_more": 0.079
},
"with_children_on_uc_m": 3.0383,
"no_children_on_uc_m": 3.2892,
"gb": {
"uc_benunits_total_m": 6.1795,
"uc_spend_bn": 73.034,
"records_on_uc": 3111,
"ni_on_uc_m": 0.148,
"elements": {
"childcare": {
"on_uc_with_element_m": 0.4542,
"records": 129
},
"housing": {
"on_uc_with_element_m": 4.8555,
"records": 2070
},
"lcwra": {
"on_uc_with_element_m": 2.0886,
"records": 922
},
"carer": {
"on_uc_with_element_m": 0.5616,
"records": 224
},
"child": {
"on_uc_with_element_m": 2.9759,
"records": 1031
},
"disability_any": {
"on_uc_with_element_m": 2.0886,
"records": 922
}
},
"family_type_on_uc_m": {
"COUPLE_NO_CHILDREN": 0.2205,
"COUPLE_WITH_CHILDREN": 0.8178,
"LONE_PARENT": 2.1473,
"SINGLE": 2.9939
},
"children_on_uc_m": {
"0": 3.2144,
"1": 1.2196,
"2": 1.0241,
"3": 0.5099,
"4": 0.133,
"5_or_more": 0.0785
}
},
"family_type_all_benunits_m": {
"COUPLE_NO_CHILDREN": 12.763,
"COUPLE_WITH_CHILDREN": 6.2712,
"LONE_PARENT": 2.8953,
"SINGLE": 13.5566
},
"households_m": 31.254,
"council_tax": {
"ENGLAND": {
"households_m": 26.1777,
"gross_bn": 42.937,
"ctr_bn": 2.01,
"net_bn": 40.927,
"avg_gross_gbp": 1640.0,
"avg_net_gbp": 1563.0,
"share_ct_zero": 0.0745
},
"NORTHERN_IRELAND": {
"households_m": 0.7883,
"gross_bn": 0.0,
"ctr_bn": 0.008,
"net_bn": -0.008,
"avg_gross_gbp": 0.0,
"avg_net_gbp": -10.0,
"share_ct_zero": 1.0
},
"SCOTLAND": {
"households_m": 2.7078,
"gross_bn": 3.804,
"ctr_bn": 0.249,
"net_bn": 3.555,
"avg_gross_gbp": 1405.0,
"avg_net_gbp": 1313.0,
"share_ct_zero": 0.0609
},
"WALES": {
"households_m": 1.5802,
"gross_bn": 2.619,
"ctr_bn": 0.01,
"net_bn": 2.609,
"avg_gross_gbp": 1657.0,
"avg_net_gbp": 1651.0,
"share_ct_zero": 0.0218
}
},
"council_tax_band_shares": {
"ENGLAND": {
"A": 0.2367,
"B": 0.1948,
"C": 0.2197,
"D": 0.1566,
"E": 0.0984,
"F": 0.0524,
"G": 0.0357,
"H": 0.0056
},
"NORTHERN_IRELAND": {
"A": 1.0
},
"SCOTLAND": {
"A": 0.1911,
"B": 0.223,
"C": 0.1624,
"D": 0.1418,
"E": 0.1397,
"F": 0.0814,
"G": 0.0548,
"H": 0.0058
},
"WALES": {
"A": 0.1424,
"B": 0.2031,
"C": 0.2148,
"D": 0.1586,
"E": 0.1328,
"F": 0.082,
"G": 0.0368,
"H": 0.0089,
"I": 0.0205
}
},
"owned_land": {
"ENGLAND": {
"total_bn": 326.291,
"avg_per_hh_gbp": 12464.0,
"share_hh_positive": 0.0143,
"unweighted_positive": 348,
"top10_records_share_of_nation_total": 0.797,
"share_of_nation_household_land_value": 0.0702
},
"NORTHERN_IRELAND": {
"total_bn": 14.985,
"avg_per_hh_gbp": 19009.0,
"share_hh_positive": 0.0833,
"unweighted_positive": 336,
"top10_records_share_of_nation_total": 0.675,
"share_of_nation_household_land_value": 0.2716
},
"SCOTLAND": {
"total_bn": 62.011,
"avg_per_hh_gbp": 22901.0,
"share_hh_positive": 0.0163,
"unweighted_positive": 16,
"top10_records_share_of_nation_total": 1.0,
"share_of_nation_household_land_value": 0.2159
},
"WALES": {
"total_bn": 17.833,
"avg_per_hh_gbp": 11286.0,
"share_hh_positive": 0.0199,
"unweighted_positive": 56,
"top10_records_share_of_nation_total": 0.974,
"share_of_nation_household_land_value": 0.0948
},
"UK_total_bn": 421.121,
"UK_share_of_household_land_value": 0.0813
}
}
{
"tag": "efrs2425_v1_56_14",
"top_keys": [
"benunit",
"household",
"person",
"time_period"
],
"year_keys": [],
"person_ncols": 2,
"person_cols_of_interest": [],
"benunit_ncols": 2,
"benunit_cols_of_interest": [],
"household_ncols": 2,
"household_cols_of_interest": [],
"ctr": {
"ENGLAND": {
"benunits_m": 29.468,
"reported_ctr_pos_m": 3.6713,
"reported_ctr_pos_n": 5155,
"would_claim_ctr_m": 3.6713,
"scheme_supported_share": 0.238,
"ctb_total_bn": 2.01,
"ctb_pos_m": 3.1852,
"on_uc_m": 5.214,
"ctb_pos_among_reporters_m": 3.1852,
"reporters_with_zero_simulated_ctb_m": 0.4862,
"supported_and_reporter_m": 1.1054,
"unsupported_and_reporter_m": 2.5659
},
"NORTHERN_IRELAND": {
"benunits_m": 1.025,
"reported_ctr_pos_m": 0.0124,
"reported_ctr_pos_n": 68,
"would_claim_ctr_m": 0.0124,
"scheme_supported_share": 0.0,
"ctb_total_bn": 0.008,
"ctb_pos_m": 0.0124,
"on_uc_m": 0.148,
"ctb_pos_among_reporters_m": 0.0124,
"reporters_with_zero_simulated_ctb_m": 0.0,
"supported_and_reporter_m": 0.0,
"unsupported_and_reporter_m": 0.0124
},
"SCOTLAND": {
"benunits_m": 3.216,
"reported_ctr_pos_m": 0.5881,
"reported_ctr_pos_n": 712,
"would_claim_ctr_m": 0.5881,
"scheme_supported_share": 0.819,
"ctb_total_bn": 0.249,
"ctb_pos_m": 0.3055,
"on_uc_m": 0.585,
"ctb_pos_among_reporters_m": 0.3055,
"reporters_with_zero_simulated_ctb_m": 0.2826,
"supported_and_reporter_m": 0.5714,
"unsupported_and_reporter_m": 0.0167
},
"WALES": {
"benunits_m": 1.777,
"reported_ctr_pos_m": 0.2409,
"reported_ctr_pos_n": 600,
"would_claim_ctr_m": 0.2409,
"scheme_supported_share": 0.867,
"ctb_total_bn": 0.01,
"ctb_pos_m": 0.0134,
"on_uc_m": 0.38,
"ctb_pos_among_reporters_m": 0.0134,
"reporters_with_zero_simulated_ctb_m": 0.2275,
"supported_and_reporter_m": 0.2378,
"unsupported_and_reporter_m": 0.0031
}
},
"uc_reported_capital": {
"benunits_with_override_ge0_n": 0,
"reporters_with_override_n": 0,
"reporters_n": 6447,
"reporters_cap_over_16k_n": 2983,
"reporters_cap_over_16k_with_override_n": 0
},
"capital_components_among_overlimit_reporters": {
"savings": {
"reporters_over_16k_share_with_component_gt_16k": 0.515,
"reporters_over_16k_mean_component_gbp": 65841.0
},
"other_residential_property_value": {
"reporters_over_16k_share_with_component_gt_16k": 0.144,
"reporters_over_16k_mean_component_gbp": 50598.0
},
"non_residential_property_value": {
"reporters_over_16k_share_with_component_gt_16k": 0.02,
"reporters_over_16k_mean_component_gbp": 4497.0
},
"corporate_wealth": {
"reporters_over_16k_share_with_component_gt_16k": 0.964,
"reporters_over_16k_mean_component_gbp": 250013.0
}
}
}
{
"tag": "efrs2425_v1_56_14",
"WALES": {
"reporters_supported_m": 0.2378,
"reporters_supported_n": 578,
"zero_award_m": 0.2244,
"zero_award_n": 484,
"zero_not_household_head_n": 0,
"zero_head_savings_over_16k_n": 208,
"zero_head_cap_ok_prelim_zero_n": 276,
"zero_head_cap_ok_prelim_pos_n": 0,
"zero_liability_zero_n": 52,
"prelim_zero_with_uc_pos_n": 26,
"mean_applicable_income_zero_group": 37020.0,
"mean_applicable_amount_zero_group": 12762.0,
"mean_liability_zero_group": 1036.0,
"positive_award_m": 0.0134,
"ctb_total_bn_supported_reporters": 0.01
},
"SCOTLAND": {
"reporters_supported_m": 0.5714,
"reporters_supported_n": 669,
"zero_award_m": 0.2662,
"zero_award_n": 475,
"zero_not_household_head_n": 0,
"zero_head_savings_over_16k_n": 180,
"zero_head_cap_ok_prelim_zero_n": 295,
"zero_head_cap_ok_prelim_pos_n": 0,
"zero_liability_zero_n": 4,
"prelim_zero_with_uc_pos_n": 61,
"mean_applicable_income_zero_group": 39845.0,
"mean_applicable_amount_zero_group": 10223.0,
"mean_liability_zero_group": 1151.0,
"positive_award_m": 0.3052,
"ctb_total_bn_supported_reporters": 0.249
},
"ENGLAND": {
"reporters_supported_m": 1.1054,
"reporters_supported_n": 2115,
"zero_award_m": 0.4627,
"zero_award_n": 1596,
"zero_not_household_head_n": 16,
"zero_head_savings_over_16k_n": 659,
"zero_head_cap_ok_prelim_zero_n": 921,
"zero_head_cap_ok_prelim_pos_n": 0,
"zero_liability_zero_n": 367,
"prelim_zero_with_uc_pos_n": 8,
"mean_applicable_income_zero_group": 43312.0,
"mean_applicable_amount_zero_group": 18694.0,
"mean_liability_zero_group": 1027.0,
"positive_award_m": 0.6427,
"ctb_total_bn_supported_reporters": 0.68
}
}
{
"tag": "efrs2425_v1_56_14",
"capital_limit": 16000.0,
"dropped_reporters_m": 1.7965,
"dropped_reporters_n": 4870,
"dropped_would_claim_false_m": 0.8817,
"dropped_would_claim_false_n": 2905,
"dropped_wc_true_not_eligible_m": 0.7959,
"dropped_wc_true_not_eligible_n": 1635,
" of which capital_over_limit_n": 1629,
" of which no_WA_adult_n": 8,
"dropped_wc_true_eligible_award_zero_m": 0.1189,
"dropped_wc_true_eligible_award_zero_n": 330,
" of which max_amount_zero_n": 0,
" of which precap_zero_maxamt_pos_n": 330,
"all_reporters_m": 5.7041,
"all_reporters_n": 6447,
"reporters_would_claim_false_m": 0.8817,
"reporters_would_claim_false_n": 2905,
"reporters_not_eligible_m": 1.31,
"reporters_not_eligible_n": 2995,
"reporters_capital_over_limit_n": 2983,
"reporters_no_WA_adult_n": 14,
"nonreporters_on_uc_m": 2.4199,
"nonreporters_on_uc_n": 2038,
"nonreporters_eligible_m": 6.32,
"nonreporters_eligible_n": 11445,
"nonreporters_eligible_wc_true_m": 4.4488,
"nonreporters_eligible_wc_true_n": 6342,
"nonreporters_eligible_wc_true_award_pos_m": 2.4199,
"would_claim_rate_among_reporters": 0.5494,
"would_claim_rate_among_nonreporters": 0.5507,
"would_claim_rate_among_eligible_nonreporters": 0.5541,
"dropped_wc_true_eligible_income_reduction_ge_max_n": 330
}
{
"tag": "efrs2425_v1_56_14",
"year": 2025,
"reported_uc_m": 5.7041,
"reported_unweighted": 6447,
"reported_and_on_uc_m": 3.9076,
"reported_not_on_uc_m": 1.7965,
"reported_not_on_uc_unweighted": 4870,
"on_uc_not_reported_m": 2.4199,
"on_uc_not_reported_unweighted": 2038,
"on_uc_m": 6.3275,
"on_uc_unweighted": 3615,
"eligible_m": 10.7141,
"eligible_unweighted": 14897,
"eligible_and_would_claim_m": 8.4753,
"eligible_and_would_claim_unweighted": 8249,
"eligible_not_would_claim_m": 2.2388,
"eligible_not_would_claim_unweighted": 6648,
"eligible_would_claim_but_uc_zero_unweighted": 4634,
"would_claim_m": 21.8698,
"would_claim_unweighted": 33709,
"benunits_unweighted": 61223,
"claims_all_entitled_unweighted": 0,
"claims_all_entitled_m": 0.0,
"reported_and_claims_all_unweighted": 0,
"would_claim_given_reported_unweighted": 3542,
"mean_weight_all": 579.6,
"mean_weight_on_uc": 1750.3,
"mean_weight_reported_on_uc": 2477.9,
"mean_weight_imputed_on_uc": 1187.4,
"weight_p90_on_uc": 4920.6,
"weight_p90_all": 665.0,
"share_of_uc_caseload_in_top10pct_weights": 0.657
}
{
"tag": "efrs2425_v1_56_14",
"households_synthetic_n": 20089,
"households_n": 52846,
"synthetic_weight_share": 0.2692,
"benunits_synthetic_n": 23315,
"uc_reporters_n": 6447,
"uc_reporters_synthetic_n": 2010,
"uc_reporters_synthetic_m": 1.0883,
"uc_reporters_real_m": 4.6158,
"dropped_reporters_n": 4870,
"dropped_reporters_synthetic_n": 1892,
"dropped_reporters_synthetic_m": 0.9323,
"dropped_reporters_real_m": 0.8642,
"dropped_real_would_claim_false_n": 2002,
"dropped_real_cap_over_n": 878,
"dropped_synth_would_claim_false_n": 903,
"dropped_synth_cap_over_n": 751,
"on_uc_n": 3615,
"on_uc_synthetic_n": 880,
"on_uc_synthetic_m": 1.1647,
"on_uc_real_m": 5.1629,
"ctr_reporters_n": 6535,
"ctr_reporters_synthetic_n": 2812,
"ctr_reporters_synthetic_m": 1.2427,
"ctr_reporters_real_m": 3.2701,
"ctr_reporters_zero_award_n": 2737,
"ctr_reporters_zero_award_synthetic_n": 1389,
"ctr_reporters_zero_award_real_n": 1348,
"ctr_reporters_zero_award_real_m": 0.5732,
"ctr_by_nation": {
"WALES": {
"reporters_supported_n": 578,
"real_n": 341,
"real_m": 0.1722,
"synthetic_n": 237,
"synthetic_m": 0.0656,
"real_positive_n": 80,
"real_positive_m": 0.0119,
"synthetic_positive_n": 14,
"synthetic_positive_m": 0.0015,
"real_zero_savings_over_16k_n": 102,
"real_zero_income_taper_n": 159,
"real_zero_m": 0.1603,
"ctb_real_bn": 0.009,
"ctb_synthetic_bn": 0.001,
"mean_weight_real_positive": 148.5,
"mean_weight_real_zero": 614.1
},
"SCOTLAND": {
"reporters_supported_n": 669,
"real_n": 451,
"real_m": 0.4663,
"synthetic_n": 218,
"synthetic_m": 0.1051,
"real_positive_n": 192,
"real_positive_m": 0.3041,
"synthetic_positive_n": 2,
"synthetic_positive_m": 0.0011,
"real_zero_savings_over_16k_n": 98,
"real_zero_income_taper_n": 161,
"real_zero_m": 0.1621,
"ctb_real_bn": 0.247,
"ctb_synthetic_bn": 0.002,
"mean_weight_real_positive": 1584.1,
"mean_weight_real_zero": 625.9
},
"ENGLAND": {
"reporters_supported_n": 2115,
"real_n": 1204,
"real_m": 0.858,
"synthetic_n": 911,
"synthetic_m": 0.2474,
"real_positive_n": 420,
"real_positive_m": 0.6129,
"synthetic_positive_n": 99,
"synthetic_positive_m": 0.0298,
"real_zero_savings_over_16k_n": 353,
"real_zero_income_taper_n": 431,
"real_zero_m": 0.245,
"ctb_real_bn": 0.652,
"ctb_synthetic_bn": 0.028,
"mean_weight_real_positive": 1459.3,
"mean_weight_real_zero": 312.6
}
}
}
{
"artifact": "enhanced_frs_2024_25.h5",
"sha256": "e433e532b17bd8ce76030156285816e33d44e93edabd2204adbef71d19a68712",
"tag": "efrs2425_v1_56_16",
"year": 2025,
"policyengine_uk_version": "2.91.0",
"policyengine_core_version": "3.31.0",
"model_commit": "c93e1a05",
"uc_benunits_total_m": 6.3462,
"uc_spend_bn": 75.301,
"benunits_unweighted_on_uc": 4830,
"benunits_unweighted_total": 61223,
"would_claim_uc_m": 20.8856,
"would_claim_uc_unweighted": 33715,
"is_uc_entitled_m": 6.3462,
"is_uc_entitled_unweighted": 4830,
"is_uc_eligible_m": 10.5156,
"is_uc_eligible_unweighted": 14886,
"uc_reported_error": "boolean index did not match indexed array along axis 0; size of axis is 61223 but size of corresponding boolean axis is ",
"entitled_not_claiming_m": 0.0,
"entitled_and_claiming_m": 6.3462,
"entitled_claiming_but_uc_zero_m": 0.0,
"max_support_entitled_m": 6.3462,
"elements": {
"childcare": {
"on_uc_with_element_m": 0.4745,
"any_with_element_m": 1.7793,
"element_spend_bn_on_uc": 2.04,
"unweighted_on_uc_with_element": 223
},
"housing": {
"on_uc_with_element_m": 4.9915,
"any_with_element_m": 11.9039,
"element_spend_bn_on_uc": 37.685,
"unweighted_on_uc_with_element": 3375
},
"lcwra": {
"on_uc_with_element_m": 2.1709,
"any_with_element_m": 6.4845,
"element_spend_bn_on_uc": 11.699,
"unweighted_on_uc_with_element": 1528
},
"carer": {
"on_uc_with_element_m": 0.6731,
"any_with_element_m": 1.1895,
"element_spend_bn_on_uc": 1.629,
"unweighted_on_uc_with_element": 405
},
"child": {
"on_uc_with_element_m": 3.092,
"any_with_element_m": 8.7534,
"element_spend_bn_on_uc": 19.413,
"unweighted_on_uc_with_element": 1862
},
"disability_any": {
"on_uc_with_element_m": 2.1709,
"any_with_element_m": 6.4845,
"element_spend_bn_on_uc": 13.954,
"unweighted_on_uc_with_element": 1528
}
},
"family_type_on_uc_m": {
"COUPLE_NO_CHILDREN": 0.2348,
"COUPLE_WITH_CHILDREN": 0.8893,
"LONE_PARENT": 2.1896,
"SINGLE": 3.0325
},
"children_on_uc_m": {
"0": 3.2673,
"1": 1.2635,
"2": 1.0627,
"3": 0.5121,
"4": 0.1614,
"5_or_more": 0.0792
},
"with_children_on_uc_m": 3.0789,
"no_children_on_uc_m": 3.2673,
"gb": {
"uc_benunits_total_m": 6.199,
"uc_spend_bn": 73.659,
"records_on_uc": 4181,
"ni_on_uc_m": 0.1472,
"elements": {
"childcare": {
"on_uc_with_element_m": 0.4712,
"records": 191
},
"housing": {
"on_uc_with_element_m": 4.8684,
"records": 2958
},
"lcwra": {
"on_uc_with_element_m": 2.1017,
"records": 1260
},
"carer": {
"on_uc_with_element_m": 0.6414,
"records": 327
},
"child": {
"on_uc_with_element_m": 3.0183,
"records": 1555
},
"disability_any": {
"on_uc_with_element_m": 2.1017,
"records": 1260
}
},
"family_type_on_uc_m": {
"COUPLE_NO_CHILDREN": 0.225,
"COUPLE_WITH_CHILDREN": 0.8705,
"LONE_PARENT": 2.1347,
"SINGLE": 2.9688
},
"children_on_uc_m": {
"0": 3.1938,
"1": 1.2337,
"2": 1.0371,
"3": 0.4995,
"4": 0.156,
"5_or_more": 0.079
}
},
"family_type_all_benunits_m": {
"COUPLE_NO_CHILDREN": 12.8761,
"COUPLE_WITH_CHILDREN": 6.2602,
"LONE_PARENT": 2.8083,
"SINGLE": 13.5167
},
"households_m": 31.3395,
"council_tax": {
"ENGLAND": {
"households_m": 26.2549,
"gross_bn": 43.124,
"ctr_bn": 2.017,
"net_bn": 41.107,
"avg_gross_gbp": 1643.0,
"avg_net_gbp": 1566.0,
"share_ct_zero": 0.0725
},
"NORTHERN_IRELAND": {
"households_m": 0.7809,
"gross_bn": 0.0,
"ctr_bn": 0.008,
"net_bn": -0.008,
"avg_gross_gbp": 0.0,
"avg_net_gbp": -10.0,
"share_ct_zero": 1.0
},
"SCOTLAND": {
"households_m": 2.7189,
"gross_bn": 3.81,
"ctr_bn": 0.249,
"net_bn": 3.561,
"avg_gross_gbp": 1401.0,
"avg_net_gbp": 1310.0,
"share_ct_zero": 0.0605
},
"WALES": {
"households_m": 1.5847,
"gross_bn": 2.635,
"ctr_bn": 0.015,
"net_bn": 2.62,
"avg_gross_gbp": 1663.0,
"avg_net_gbp": 1653.0,
"share_ct_zero": 0.0323
}
},
"council_tax_band_shares": {
"ENGLAND": {
"A": 0.2366,
"B": 0.1952,
"C": 0.2195,
"D": 0.1565,
"E": 0.0983,
"F": 0.0524,
"G": 0.0358,
"H": 0.0056
},
"NORTHERN_IRELAND": {
"A": 1.0
},
"SCOTLAND": {
"A": 0.1907,
"B": 0.2215,
"C": 0.1625,
"D": 0.1418,
"E": 0.1417,
"F": 0.0811,
"G": 0.0549,
"H": 0.0057
},
"WALES": {
"A": 0.1432,
"B": 0.2026,
"C": 0.2134,
"D": 0.1593,
"E": 0.1323,
"F": 0.0813,
"G": 0.0371,
"H": 0.0087,
"I": 0.0221
}
},
"owned_land": {
"ENGLAND": {
"total_bn": 323.604,
"avg_per_hh_gbp": 12325.0,
"share_hh_positive": 0.0145,
"unweighted_positive": 352,
"top10_records_share_of_nation_total": 0.802,
"share_of_nation_household_land_value": 0.0694
},
"NORTHERN_IRELAND": {
"total_bn": 14.028,
"avg_per_hh_gbp": 17964.0,
"share_hh_positive": 0.0796,
"unweighted_positive": 340,
"top10_records_share_of_nation_total": 0.627,
"share_of_nation_household_land_value": 0.2597
},
"SCOTLAND": {
"total_bn": 62.055,
"avg_per_hh_gbp": 22823.0,
"share_hh_positive": 0.0166,
"unweighted_positive": 16,
"top10_records_share_of_nation_total": 1.0,
"share_of_nation_household_land_value": 0.216
},
"WALES": {
"total_bn": 18.343,
"avg_per_hh_gbp": 11575.0,
"share_hh_positive": 0.0208,
"unweighted_positive": 56,
"top10_records_share_of_nation_total": 0.975,
"share_of_nation_household_land_value": 0.0974
},
"UK_total_bn": 418.029,
"UK_share_of_household_land_value": 0.0805
}
}
{
"tag": "efrs2425_v1_56_16",
"note": "weights unchanged (incumbent calibrated weights); record-level counterfactuals only",
"A_baseline": {
"uk_m": 6.3462,
"gb_m": 6.199,
"records": 4830,
"spend_bn": 75.301,
"gb_housing_m": 4.8684,
"gb_lcwra_m": 2.1017,
"gb_carer_m": 0.6414,
"gb_childcare_m": 0.4712,
"gb_child_m": 3.0183,
"gb_single_m": 2.9688,
"gb_lone_parent_m": 2.1347,
"gb_couple_no_children_m": 0.225,
"gb_couple_with_children_m": 0.8705
},
"B_anchor_reporters": {
"uk_m": 6.3632,
"gb_m": 6.214,
"records": 4892,
"spend_bn": 75.401,
"gb_housing_m": 4.8734,
"gb_lcwra_m": 2.1115,
"gb_carer_m": 0.6422,
"gb_childcare_m": 0.4712,
"gb_child_m": 3.0197,
"gb_single_m": 2.9807,
"gb_lone_parent_m": 2.1347,
"gb_couple_no_children_m": 0.2268,
"gb_couple_with_children_m": 0.8718
},
"C_no_corporate_wealth": {
"uk_m": 6.8811,
"gb_m": 6.6933,
"records": 6594,
"spend_bn": 78.231,
"gb_housing_m": 5.0564,
"gb_lcwra_m": 2.1931,
"gb_carer_m": 0.682,
"gb_childcare_m": 0.4923,
"gb_child_m": 3.1004,
"gb_single_m": 3.2767,
"gb_lone_parent_m": 2.1602,
"gb_couple_no_children_m": 0.3271,
"gb_couple_with_children_m": 0.9293
},
"D_both": {
"uk_m": 6.9518,
"gb_m": 6.7615,
"records": 6724,
"spend_bn": 78.665,
"gb_housing_m": 5.0934,
"gb_lcwra_m": 2.2439,
"gb_carer_m": 0.683,
"gb_childcare_m": 0.4994,
"gb_child_m": 3.1104,
"gb_single_m": 3.3282,
"gb_lone_parent_m": 2.1604,
"gb_couple_no_children_m": 0.3338,
"gb_couple_with_children_m": 0.9391
}
}
{
"tag": "efrs2425_v1_56_16",
"year": 2025,
"reported_uc_m": 5.9252,
"reported_unweighted": 6447,
"reported_and_on_uc_m": 4.3178,
"reported_not_on_uc_m": 1.6073,
"reported_not_on_uc_unweighted": 3645,
"on_uc_not_reported_m": 2.0284,
"on_uc_not_reported_unweighted": 2028,
"on_uc_m": 6.3462,
"on_uc_unweighted": 4830,
"eligible_m": 10.5156,
"eligible_unweighted": 14886,
"eligible_and_would_claim_m": 8.5644,
"eligible_and_would_claim_unweighted": 9724,
"eligible_not_would_claim_m": 1.9512,
"eligible_not_would_claim_unweighted": 5162,
"eligible_would_claim_but_uc_zero_unweighted": 4894,
"would_claim_m": 20.8856,
"would_claim_unweighted": 33715,
"benunits_unweighted": 61223,
"claims_all_entitled_unweighted": 0,
"claims_all_entitled_m": 0.0,
"reported_and_claims_all_unweighted": 0,
"would_claim_given_reported_unweighted": 5650,
"mean_weight_all": 579.2,
"mean_weight_on_uc": 1313.9,
"mean_weight_reported_on_uc": 1541.0,
"mean_weight_imputed_on_uc": 1000.2,
"weight_p90_on_uc": 3674.1,
"weight_p90_all": 701.9,
"share_of_uc_caseload_in_top10pct_weights": 0.612
}
import sys, json, h5py, numpy as np
from policyengine_uk import Microsimulation
path, tag, year = sys.argv[1], sys.argv[2], 2025
out = {"tag": tag}
with h5py.File(path) as f:
out["top_keys"] = list(f.keys())[:12]
def cols(g): return sorted(list(g.keys()))
grp = f[list(f.keys())[0]] if "person" not in f else f
try:
years = [k for k in f.keys() if k.isdigit()]
g = f[years[0]] if years else f
out["year_keys"] = years[:5]
for ent in ["person","benunit","household"]:
if ent in g:
c = cols(g[ent]); out[f"{ent}_ncols"] = len(c)
out[f"{ent}_cols_of_interest"] = [x for x in c if any(s in x for s in ["claim","reported","capital","council","would","takeup","take_up","owned_land","weight","savings","corporate","property"])]
except Exception as e:
out["h5_error"] = str(e)
sim = Microsimulation(dataset=path)
def a(v, map_to=None):
x = sim.calculate(v, year, map_to=map_to) if map_to else sim.calculate(v, year)
vals = x.values if hasattr(x, "values") else x
return np.asarray(vals.decode_to_str() if hasattr(vals, "decode_to_str") else vals)
w = a("benunit_weight").astype(float); hw = a("household_weight").astype(float)
nation_h = a("country")
reg = a("benunit_region"); nation_b = np.where(reg == "SCOTLAND", "SCOTLAND", np.where(reg == "WALES", "WALES", np.where(reg == "NORTHERN_IRELAND", "NORTHERN_IRELAND", "ENGLAND")))
ctbr = a("council_tax_benefit_reported", map_to="benunit").astype(float) > 0
ctb = a("council_tax_benefit").astype(float)
wcctr = a("would_claim_council_tax_reduction").astype(bool)
supported = a("council_tax_reduction_scheme_supported", map_to="benunit").astype(float) > 0.5
rc = a("uc_reported_capital").astype(float); uc = a("universal_credit").astype(float) > 0
rep = a("universal_credit_reported", map_to="benunit").astype(float) > 0
cap = a("uc_assessable_capital").astype(float)
out["ctr"] = {}
for n in sorted(set(nation_b.tolist())):
m = nation_b == n
out["ctr"][n] = {"benunits_m": round(float(w[m].sum())/1e6,3), "reported_ctr_pos_m": round(float(w[m & ctbr].sum())/1e6,4), "reported_ctr_pos_n": int((m & ctbr).sum()),
"would_claim_ctr_m": round(float(w[m & wcctr].sum())/1e6,4), "scheme_supported_share": round(float(w[m & supported].sum()/w[m].sum()),3),
"ctb_total_bn": round(float((w*ctb)[m].sum())/1e9,3), "ctb_pos_m": round(float(w[m & (ctb>0)].sum())/1e6,4), "on_uc_m": round(float(w[m & uc].sum())/1e6,3), "ctb_pos_among_reporters_m": round(float(w[m & ctbr & (ctb>0)].sum())/1e6,4), "reporters_with_zero_simulated_ctb_m": round(float(w[m & ctbr & (ctb<=0)].sum())/1e6,4), "supported_and_reporter_m": round(float(w[m & ctbr & supported].sum())/1e6,4), "unsupported_and_reporter_m": round(float(w[m & ctbr & ~supported].sum())/1e6,4)}
out["uc_reported_capital"] = {"benunits_with_override_ge0_n": int((rc >= 0).sum()), "reporters_with_override_n": int((rep & (rc >= 0)).sum()), "reporters_n": int(rep.sum()),
"reporters_cap_over_16k_n": int((rep & (cap > 16000)).sum()), "reporters_cap_over_16k_with_override_n": int((rep & (cap > 16000) & (rc >= 0)).sum())}
try:
comps = {}
for v in ["savings","other_residential_property_value","non_residential_property_value","corporate_wealth"]:
x = a(v, map_to="benunit").astype(float)
comps[v] = {"reporters_over_16k_share_with_component_gt_16k": round(float((rep & (cap>16000) & (x>16000)).sum()/max((rep & (cap>16000)).sum(),1)),3),
"reporters_over_16k_mean_component_gbp": round(float(x[rep & (cap>16000)].mean()),0)}
out["capital_components_among_overlimit_reporters"] = comps
except Exception as e:
out["comp_error"] = str(e)[:200]
json.dump(out, open(f"/Users/maxghenis/PolicyEngine/_uk-defects-452-448/measure/{tag}_columns_{year}.json","w"), indent=1)
print(json.dumps(out, indent=1))
"""Aggregate-only: which simulated-CTR gate zeroes FRS CTR reporters, by nation (2025)."""
import sys, json, numpy as np
from policyengine_uk import Microsimulation
path, tag, year = sys.argv[1], sys.argv[2], 2025
sim = Microsimulation(dataset=path)
def a(v, map_to=None):
x = sim.calculate(v, year, map_to=map_to) if map_to else sim.calculate(v, year)
vals = x.values if hasattr(x, "values") else x
return np.asarray(vals.decode_to_str() if hasattr(vals, "decode_to_str") else vals)
w = a("benunit_weight").astype(float)
reg = a("benunit_region"); nation = np.where(reg=="SCOTLAND","SCOTLAND",np.where(reg=="WALES","WALES",np.where(reg=="NORTHERN_IRELAND","NI","ENGLAND")))
rep = a("council_tax_benefit_reported", map_to="benunit").astype(float) > 0
ctb = a("council_tax_benefit").astype(float)
supported = a("council_tax_reduction_scheme_supported", map_to="benunit").astype(float) > 0.5
head = a("benunit_contains_household_head").astype(bool)
sav = a("savings", map_to="benunit").astype(float) # household savings projected; benunit share irrelevant: formula uses household savings
hh_sav = a("savings");
liab = a("council_tax_reduction_maximum_eligible_liability", map_to="benunit").astype(float)
appinc = a("council_tax_reduction_applicable_income").astype(float); appamt = a("council_tax_reduction_applicable_amount").astype(float)
ndd = a("council_tax_reduction_non_dep_deductions").astype(float)
uc = a("universal_credit").astype(float)
excess = np.maximum(0, appinc - appamt)
prelim = np.maximum(0, liab*1.0 - excess*0.2 - ndd)
out = {"tag": tag}
for n in ["WALES","SCOTLAND","ENGLAND"]:
m = (nation == n) & rep & supported
z = m & (ctb <= 0)
def pm(mask): return round(float(w[mask].sum())/1e6, 4)
out[n] = {"reporters_supported_m": pm(m), "reporters_supported_n": int(m.sum()),
"zero_award_m": pm(z), "zero_award_n": int(z.sum()),
"zero_not_household_head_n": int((z & ~head).sum()),
"zero_head_savings_over_16k_n": int((z & head & (sav > 16000)).sum()),
"zero_head_cap_ok_prelim_zero_n": int((z & head & (sav <= 16000) & (prelim <= 0)).sum()),
"zero_head_cap_ok_prelim_pos_n": int((z & head & (sav <= 16000) & (prelim > 0)).sum()),
"zero_liability_zero_n": int((z & (liab <= 0)).sum()),
"prelim_zero_with_uc_pos_n": int((z & head & (sav <= 16000) & (prelim <= 0) & (uc > 0)).sum()),
"mean_applicable_income_zero_group": round(float(appinc[z & head & (sav<=16000) & (prelim<=0)].mean()),0) if (z & head & (sav<=16000) & (prelim<=0)).any() else None,
"mean_applicable_amount_zero_group": round(float(appamt[z & head & (sav<=16000) & (prelim<=0)].mean()),0) if (z & head & (sav<=16000) & (prelim<=0)).any() else None,
"mean_liability_zero_group": round(float(liab[z & head & (sav<=16000) & (prelim<=0)].mean()),0) if (z & head & (sav<=16000) & (prelim<=0)).any() else None,
"positive_award_m": pm(m & (ctb > 0)), "ctb_total_bn_supported_reporters": round(float((w*ctb)[m].sum())/1e9,3)}
json.dump(out, open(f"/Users/maxghenis/PolicyEngine/_uk-defects-452-448/measure/{tag}_ctr_gates_{year}.json","w"), indent=1)
print(json.dumps(out, indent=1))
"""Aggregate-only: why FRS-reported UC recipients carry zero modelled UC at 2025."""
import sys, json, numpy as np
from policyengine_uk import Microsimulation
path, tag, year = sys.argv[1], sys.argv[2], 2025
sim = Microsimulation(dataset=path)
def a(v, map_to=None):
x = sim.calculate(v, year, map_to=map_to) if map_to else sim.calculate(v, year)
vals = x.values if hasattr(x, "values") else x
return np.asarray(vals.decode_to_str() if hasattr(vals, "decode_to_str") else vals)
w = a("benunit_weight").astype(float); uc = a("universal_credit").astype(float) > 0
rep = a("universal_credit_reported", map_to="benunit").astype(float) > 0
wc = a("would_claim_uc").astype(bool); el = a("is_uc_eligible").astype(bool)
cap = a("uc_assessable_capital").astype(float); wa = a("is_WA_adult", map_to="benunit").astype(float) > 0
limit = float(sim.tax_benefit_system.parameters(f"{year}-01-01").gov.dwp.universal_credit.means_test.capital.limit)
maxamt = a("uc_maximum_amount").astype(float); precap = a("universal_credit_pre_benefit_cap").astype(float)
# use would_claim-independent view of the means test: recompute pre-cap gate as max amount > income reduction proxy
try:
inc_red = a("uc_income_reduction").astype(float)
except Exception:
inc_red = None
drop = rep & ~uc
def m(mask): return round(float(w[mask].sum())/1e6, 4)
def n(mask): return int(mask.sum())
out = {"tag": tag, "capital_limit": limit,
"dropped_reporters_m": m(drop), "dropped_reporters_n": n(drop),
"dropped_would_claim_false_m": m(drop & ~wc), "dropped_would_claim_false_n": n(drop & ~wc),
"dropped_wc_true_not_eligible_m": m(drop & wc & ~el), "dropped_wc_true_not_eligible_n": n(drop & wc & ~el),
" of which capital_over_limit_n": n(drop & wc & ~el & (cap > limit)), " of which no_WA_adult_n": n(drop & wc & ~el & ~wa),
"dropped_wc_true_eligible_award_zero_m": m(drop & wc & el & ~uc), "dropped_wc_true_eligible_award_zero_n": n(drop & wc & el & ~uc),
" of which max_amount_zero_n": n(drop & wc & el & (maxamt <= 0)),
" of which precap_zero_maxamt_pos_n": n(drop & wc & el & (maxamt > 0) & (precap <= 0)),
"all_reporters_m": m(rep), "all_reporters_n": n(rep),
"reporters_would_claim_false_m": m(rep & ~wc), "reporters_would_claim_false_n": n(rep & ~wc),
"reporters_not_eligible_m": m(rep & ~el), "reporters_not_eligible_n": n(rep & ~el),
"reporters_capital_over_limit_n": n(rep & (cap > limit)), "reporters_no_WA_adult_n": n(rep & ~wa),
"nonreporters_on_uc_m": m(uc & ~rep), "nonreporters_on_uc_n": n(uc & ~rep),
"nonreporters_eligible_m": m(el & ~rep), "nonreporters_eligible_n": n(el & ~rep),
"nonreporters_eligible_wc_true_m": m(el & ~rep & wc), "nonreporters_eligible_wc_true_n": n(el & ~rep & wc),
"nonreporters_eligible_wc_true_award_pos_m": m(el & ~rep & wc & uc),
"would_claim_rate_among_reporters": round(n(rep & wc)/n(rep), 4), "would_claim_rate_among_nonreporters": round(n(~rep & wc)/n(~rep), 4),
"would_claim_rate_among_eligible_nonreporters": round(n(el & ~rep & wc)/max(n(el & ~rep),1), 4)}
if inc_red is not None:
out["dropped_wc_true_eligible_income_reduction_ge_max_n"] = n(drop & wc & el & (inc_red >= maxamt) & (maxamt > 0))
json.dump(out, open(f"/Users/maxghenis/PolicyEngine/_uk-defects-452-448/measure/{tag}_dropped_{year}.json", "w"), indent=1)
print(json.dumps(out, indent=1))
"""Aggregate-only: FRS-reported vs imputed UC receipt cross-tabs + weight concentration."""
import sys, json, numpy as np
from policyengine_uk import Microsimulation
path, tag, year = sys.argv[1], sys.argv[2], 2025
sim = Microsimulation(dataset=path)
def a(v, map_to=None):
x = sim.calculate(v, year, map_to=map_to) if map_to else sim.calculate(v, year)
vals = x.values if hasattr(x, "values") else x
return np.asarray(vals.decode_to_str() if hasattr(vals, "decode_to_str") else vals)
w = a("benunit_weight").astype(float); uc = a("universal_credit").astype(float) > 0
rep = a("universal_credit_reported", map_to="benunit").astype(float) > 0
wc = a("would_claim_uc").astype(bool); el = a("is_uc_eligible").astype(bool)
cae = a("claims_all_entitled_benefits").astype(bool)
def m(mask): return round(float(w[mask].sum())/1e6, 4)
out = {"tag": tag, "year": year,
"reported_uc_m": m(rep), "reported_unweighted": int(rep.sum()),
"reported_and_on_uc_m": m(rep & uc), "reported_not_on_uc_m": m(rep & ~uc), "reported_not_on_uc_unweighted": int((rep & ~uc).sum()),
"on_uc_not_reported_m": m(uc & ~rep), "on_uc_not_reported_unweighted": int((uc & ~rep).sum()),
"on_uc_m": m(uc), "on_uc_unweighted": int(uc.sum()),
"eligible_m": m(el), "eligible_unweighted": int(el.sum()),
"eligible_and_would_claim_m": m(el & wc), "eligible_and_would_claim_unweighted": int((el & wc).sum()),
"eligible_not_would_claim_m": m(el & ~wc), "eligible_not_would_claim_unweighted": int((el & ~wc).sum()),
"eligible_would_claim_but_uc_zero_unweighted": int((el & wc & ~uc).sum()),
"would_claim_m": m(wc), "would_claim_unweighted": int(wc.sum()), "benunits_unweighted": int(len(w)),
"claims_all_entitled_unweighted": int(cae.sum()), "claims_all_entitled_m": m(cae),
"reported_and_claims_all_unweighted": int((rep & cae).sum()),
"would_claim_given_reported_unweighted": int((rep & wc).sum()),
"mean_weight_all": round(float(w.mean()), 1), "mean_weight_on_uc": round(float(w[uc].mean()), 1),
"mean_weight_reported_on_uc": round(float(w[rep & uc].mean()), 1) if (rep & uc).any() else None,
"mean_weight_imputed_on_uc": round(float(w[uc & ~rep].mean()), 1) if (uc & ~rep).any() else None,
"weight_p90_on_uc": round(float(np.percentile(w[uc], 90)), 1), "weight_p90_all": round(float(np.percentile(w, 90)), 1),
"share_of_uc_caseload_in_top10pct_weights": round(float(np.sort(w[uc])[-int(0.1*uc.sum()):].sum()/w[uc].sum()), 3)}
json.dump(out, open(f"/Users/maxghenis/PolicyEngine/_uk-defects-452-448/measure/{tag}_reported_{year}.json", "w"), indent=1)
print(json.dumps(out, indent=1))
import sys, json, numpy as np, pandas as pd, h5py
from policyengine_uk import Microsimulation
path, tag, year = sys.argv[1], sys.argv[2], 2025
# read the synthetic flag straight from the h5 (household table)
with h5py.File(path) as f:
keys = list(f.keys())
flag = None
try:
hh = pd.read_hdf(path, "household") if "household" in keys else None
if hh is not None and "household_is_spi_synthetic" in hh.columns: flag = hh["household_is_spi_synthetic"].to_numpy().astype(bool); hid = hh["household_id"].to_numpy()
except Exception as e:
err = str(e)
if flag is None:
with h5py.File(path) as f:
g = f[[k for k in keys if k.isdigit()][0]] if any(k.isdigit() for k in keys) else f
flag = np.asarray(g["household"]["household_is_spi_synthetic"][:]).astype(bool); hid = np.asarray(g["household"]["household_id"][:])
sim = Microsimulation(dataset=path)
def a(v, map_to=None):
x = sim.calculate(v, year, map_to=map_to) if map_to else sim.calculate(v, year)
vals = x.values if hasattr(x, "values") else x
return np.asarray(vals.decode_to_str() if hasattr(vals, "decode_to_str") else vals)
hid_sim = a("household_id").astype(np.int64); order = pd.Series(flag, index=hid).reindex(hid_sim).to_numpy().astype(bool)
# benunit-level synthetic flag via person -> benunit
pflag = pd.Series(order, index=hid_sim).reindex(np.rint(a("household_id", map_to="person")).astype(np.int64)).to_numpy()
pb = a("person_benunit_id").astype(np.int64); bid = a("benunit_id").astype(np.int64)
bflag = pd.Series(pflag).groupby(pb).first().reindex(bid).to_numpy().astype(bool)
w = a("benunit_weight").astype(float); hw = a("household_weight").astype(float)
uc = a("universal_credit").astype(float) > 0; rep = a("universal_credit_reported", map_to="benunit").astype(float) > 0
wc = a("would_claim_uc").astype(bool); cap = a("uc_assessable_capital").astype(float)
ctr_rep = a("council_tax_benefit_reported", map_to="benunit").astype(float) > 0; ctb = a("council_tax_benefit").astype(float)
def m(mask): return round(float(w[mask].sum())/1e6,4)
out = {"tag": tag, "households_synthetic_n": int(order.sum()), "households_n": int(len(order)), "synthetic_weight_share": round(float(hw[order].sum()/hw.sum()),4),
"benunits_synthetic_n": int(bflag.sum()),
"uc_reporters_n": int(rep.sum()), "uc_reporters_synthetic_n": int((rep & bflag).sum()), "uc_reporters_synthetic_m": m(rep & bflag), "uc_reporters_real_m": m(rep & ~bflag),
"dropped_reporters_n": int((rep & ~uc).sum()), "dropped_reporters_synthetic_n": int((rep & ~uc & bflag).sum()), "dropped_reporters_synthetic_m": m(rep & ~uc & bflag), "dropped_reporters_real_m": m(rep & ~uc & ~bflag),
"dropped_real_would_claim_false_n": int((rep & ~uc & ~bflag & ~wc).sum()), "dropped_real_cap_over_n": int((rep & ~uc & ~bflag & wc & (cap > 16000)).sum()),
"dropped_synth_would_claim_false_n": int((rep & ~uc & bflag & ~wc).sum()), "dropped_synth_cap_over_n": int((rep & ~uc & bflag & wc & (cap > 16000)).sum()),
"on_uc_n": int(uc.sum()), "on_uc_synthetic_n": int((uc & bflag).sum()), "on_uc_synthetic_m": m(uc & bflag), "on_uc_real_m": m(uc & ~bflag),
"ctr_reporters_n": int(ctr_rep.sum()), "ctr_reporters_synthetic_n": int((ctr_rep & bflag).sum()), "ctr_reporters_synthetic_m": m(ctr_rep & bflag), "ctr_reporters_real_m": m(ctr_rep & ~bflag),
"ctr_reporters_zero_award_n": int((ctr_rep & (ctb<=0)).sum()), "ctr_reporters_zero_award_synthetic_n": int((ctr_rep & (ctb<=0) & bflag).sum()),
"ctr_reporters_zero_award_real_n": int((ctr_rep & (ctb<=0) & ~bflag).sum()), "ctr_reporters_zero_award_real_m": m(ctr_rep & (ctb<=0) & ~bflag)}
reg = a("benunit_region"); nation = np.where(reg=="SCOTLAND","SCOTLAND",np.where(reg=="WALES","WALES",np.where(reg=="NORTHERN_IRELAND","NI","ENGLAND")))
sav = a("savings", map_to="benunit").astype(float); head = a("benunit_contains_household_head").astype(bool)
supported = a("council_tax_reduction_scheme_supported", map_to="benunit").astype(float) > 0.5
out["ctr_by_nation"] = {}
for n in ["WALES","SCOTLAND","ENGLAND"]:
nm = (nation == n) & ctr_rep & supported
out["ctr_by_nation"][n] = {"reporters_supported_n": int(nm.sum()), "real_n": int((nm & ~bflag).sum()), "real_m": m(nm & ~bflag), "synthetic_n": int((nm & bflag).sum()), "synthetic_m": m(nm & bflag),
"real_positive_n": int((nm & ~bflag & (ctb>0)).sum()), "real_positive_m": m(nm & ~bflag & (ctb>0)), "synthetic_positive_n": int((nm & bflag & (ctb>0)).sum()), "synthetic_positive_m": m(nm & bflag & (ctb>0)),
"real_zero_savings_over_16k_n": int((nm & ~bflag & (ctb<=0) & (sav>16000)).sum()), "real_zero_income_taper_n": int((nm & ~bflag & (ctb<=0) & (sav<=16000)).sum()), "real_zero_m": m(nm & ~bflag & (ctb<=0)),
"ctb_real_bn": round(float((w*ctb)[nm & ~bflag].sum())/1e9,3), "ctb_synthetic_bn": round(float((w*ctb)[nm & bflag].sum())/1e9,3),
"mean_weight_real_positive": round(float(w[nm & ~bflag & (ctb>0)].mean()),1) if (nm & ~bflag & (ctb>0)).any() else None, "mean_weight_real_zero": round(float(w[nm & ~bflag & (ctb<=0)].mean()),1) if (nm & ~bflag & (ctb<=0)).any() else None}
json.dump(out, open(f"/Users/maxghenis/PolicyEngine/_uk-defects-452-448/measure/{tag}_spi_{year}.json","w"), indent=1)
print(json.dumps(out, indent=1))
"""Aggregate-only measurement of UC caseload by element/family type, council tax by
nation, and owned_land by nation on an enhanced FRS artifact. Prints NO individual
records (UKDS data-protection rule). Usage: python measure_uc_ct.py <h5> <tag> [year]"""
import sys, json, hashlib, subprocess, numpy as np, pandas as pd
from policyengine_uk import Microsimulation
import policyengine_uk, policyengine_core
path, tag = sys.argv[1], sys.argv[2]
year = int(sys.argv[3]) if len(sys.argv) > 3 else 2025
sha = hashlib.sha256(open(path, "rb").read()).hexdigest()
sim = Microsimulation(dataset=path)
def arr(v, y=year, map_to=None):
x = sim.calculate(v, y, map_to=map_to) if map_to else sim.calculate(v, y)
vals = x.values if hasattr(x, "values") else x
if hasattr(vals, "decode_to_str"):
vals = vals.decode_to_str()
return np.asarray(vals)
out = {"artifact": path.split("/")[-1], "sha256": sha, "tag": tag, "year": year,
"policyengine_uk_version": __import__("importlib.metadata").metadata.version("policyengine-uk"),
"policyengine_core_version": __import__("importlib.metadata").metadata.version("policyengine-core"),
"model_commit": subprocess.run(["git", "-C", "/Users/maxghenis/PolicyEngine/_worktrees/ukdef-uk-model", "rev-parse", "--short", "HEAD"], capture_output=True, text=True).stdout.strip()}
# ---- benunit level ----
w = arr("benunit_weight").astype(float)
uc = arr("universal_credit").astype(float)
on_uc = uc > 0
out["uc_benunits_total_m"] = round(float(w[on_uc].sum()) / 1e6, 4)
out["uc_spend_bn"] = round(float((w * uc).sum()) / 1e9, 3)
out["benunits_unweighted_on_uc"] = int(on_uc.sum())
out["benunits_unweighted_total"] = int(len(w))
for v in ["would_claim_uc", "is_uc_entitled", "is_uc_eligible"]:
try:
b = arr(v).astype(bool)
out[f"{v}_m"] = round(float(w[b].sum()) / 1e6, 4)
out[f"{v}_unweighted"] = int(b.sum())
except Exception as e:
out[f"{v}_error"] = str(e)[:120]
try:
rep = arr("universal_credit_reported").astype(float) > 0
out["uc_reported_benunits_m"] = round(float(w[rep].sum()) / 1e6, 4)
out["uc_reported_and_on_uc_m"] = round(float(w[rep & on_uc].sum()) / 1e6, 4)
out["uc_on_uc_not_reported_m"] = round(float(w[on_uc & ~rep].sum()) / 1e6, 4)
out["uc_reported_unweighted"] = int(rep.sum())
except Exception as e:
out["uc_reported_error"] = str(e)[:120]
try:
ent = arr("is_uc_entitled").astype(bool); wc = arr("would_claim_uc").astype(bool)
out["entitled_not_claiming_m"] = round(float(w[ent & ~wc].sum()) / 1e6, 4)
out["entitled_and_claiming_m"] = round(float(w[ent & wc].sum()) / 1e6, 4)
out["entitled_claiming_but_uc_zero_m"] = round(float(w[ent & wc & ~on_uc].sum()) / 1e6, 4)
out["max_support_entitled_m"] = round(float(w[ent].sum()) / 1e6, 4)
except Exception as e:
out["entitlement_error"] = str(e)[:120]
elements = {"childcare": "uc_childcare_element", "housing": "uc_housing_costs_element",
"lcwra": "uc_LCWRA_element", "carer": "uc_carer_element", "child": "uc_child_element",
"disability_any": "uc_disability_elements"}
out["elements"] = {}
for k, v in elements.items():
try:
e = arr(v).astype(float)
out["elements"][k] = {"on_uc_with_element_m": round(float(w[on_uc & (e > 0)].sum()) / 1e6, 4),
"any_with_element_m": round(float(w[e > 0].sum()) / 1e6, 4),
"element_spend_bn_on_uc": round(float((w * e)[on_uc].sum()) / 1e9, 3),
"unweighted_on_uc_with_element": int((on_uc & (e > 0)).sum())}
except Exception as e:
out["elements"][k] = {"error": str(e)[:160]}
ft = arr("family_type"); nk = arr("benunit_count_children").astype(int)
out["family_type_on_uc_m"] = {str(f): round(float(w[on_uc & (ft == f)].sum()) / 1e6, 4) for f in sorted(set(ft.tolist()))}
nkb = np.minimum(nk, 5)
out["children_on_uc_m"] = {("5_or_more" if n == 5 else str(n)): round(float(w[on_uc & (nkb == n)].sum()) / 1e6, 4) for n in range(6)}
has_kids = nk > 0
out["with_children_on_uc_m"] = round(float(w[on_uc & has_kids].sum()) / 1e6, 4)
out["no_children_on_uc_m"] = round(float(w[on_uc & ~has_kids].sum()) / 1e6, 4)
# ---- GB-only (admin UC facts are Great Britain) ----
reg = arr("benunit_region"); gb = reg != "NORTHERN_IRELAND"
out["gb"] = {"uc_benunits_total_m": round(float(w[on_uc & gb].sum())/1e6,4), "uc_spend_bn": round(float((w*uc)[on_uc & gb].sum())/1e9,3), "records_on_uc": int((on_uc & gb).sum()),
"ni_on_uc_m": round(float(w[on_uc & ~gb].sum())/1e6,4)}
out["gb"]["elements"] = {}
for k, v in elements.items():
e = arr(v).astype(float)
out["gb"]["elements"][k] = {"on_uc_with_element_m": round(float(w[on_uc & gb & (e > 0)].sum())/1e6,4), "records": int((on_uc & gb & (e>0)).sum())}
out["gb"]["family_type_on_uc_m"] = {str(f): round(float(w[on_uc & gb & (ft == f)].sum())/1e6,4) for f in sorted(set(ft.tolist()))}
out["gb"]["children_on_uc_m"] = {("5_or_more" if n == 5 else str(n)): round(float(w[on_uc & gb & (nkb == n)].sum())/1e6,4) for n in range(6)}
out["family_type_all_benunits_m"] = {str(f): round(float(w[ft == f].sum()) / 1e6, 4) for f in sorted(set(ft.tolist()))}
# ---- household level ----
hw = arr("household_weight").astype(float)
try:
nation = arr("country")
except Exception:
reg = arr("region"); nation = np.where(reg == "SCOTLAND", "SCOTLAND", np.where(reg == "WALES", "WALES", np.where(reg == "NORTHERN_IRELAND", "NORTHERN_IRELAND", "ENGLAND")))
out["households_m"] = round(float(hw.sum()) / 1e6, 4)
ct = arr("council_tax").astype(float)
try:
ctb = arr("council_tax_benefit", map_to="household").astype(float)
except Exception as e:
ctb = np.zeros_like(ct); out["ctb_error"] = str(e)[:120]
out["council_tax"] = {}
for n in sorted(set(nation.tolist())):
m = nation == n
hh = float(hw[m].sum())
out["council_tax"][n] = {"households_m": round(hh / 1e6, 4),
"gross_bn": round(float((hw * ct)[m].sum()) / 1e9, 3),
"ctr_bn": round(float((hw * ctb)[m].sum()) / 1e9, 3),
"net_bn": round(float((hw * (ct - ctb))[m].sum()) / 1e9, 3),
"avg_gross_gbp": round(float((hw * ct)[m].sum() / hh), 0),
"avg_net_gbp": round(float((hw * (ct - ctb))[m].sum() / hh), 0),
"share_ct_zero": round(float(hw[m & (ct <= 0)].sum() / hh), 4)}
try:
band = arr("council_tax_band")
out["council_tax_band_shares"] = {}
for n in sorted(set(nation.tolist())):
m = nation == n; hh = float(hw[m].sum())
out["council_tax_band_shares"][n] = {str(b): round(float(hw[m & (band == b)].sum() / hh), 4) for b in sorted(set(band[m].tolist()))}
except Exception as e:
out["band_error"] = str(e)[:120]
try:
ol = arr("owned_land").astype(float)
out["owned_land"] = {}
for n in sorted(set(nation.tolist())):
m = nation == n; hh = float(hw[m].sum())
tot = float((hw * ol)[m].sum())
out["owned_land"][n] = {"total_bn": round(tot / 1e9, 3), "avg_per_hh_gbp": round(tot / hh, 0),
"share_hh_positive": round(float(hw[m & (ol > 0)].sum() / hh), 4),
"unweighted_positive": int((m & (ol > 0)).sum()),
"top10_records_share_of_nation_total": round(float(np.sort((hw * ol)[m])[-10:].sum() / tot), 3) if tot > 0 else None}
out["owned_land"]["UK_total_bn"] = round(float((hw * ol).sum()) / 1e9, 3)
try:
hlv = arr("household_land_value").astype(float)
out["owned_land"]["UK_share_of_household_land_value"] = round(float((hw * ol).sum() / (hw * hlv).sum()), 4)
for n in sorted(set(nation.tolist())):
m = nation == n
out["owned_land"][n]["share_of_nation_household_land_value"] = round(float((hw * ol)[m].sum() / (hw * hlv)[m].sum()), 4)
except Exception as e:
out["owned_land"]["hlv_error"] = str(e)[:120]
except Exception as e:
out["owned_land_error"] = str(e)[:120]
json.dump(out, open(f"/Users/maxghenis/PolicyEngine/_uk-defects-452-448/measure/{tag}_{year}.json", "w"), indent=1)
print(json.dumps({k: v for k, v in out.items() if not isinstance(v, dict)}, indent=1))

Licensed measurements — aggregates only (UKDS rules)

Engine: policyengine-uk 2.91.0 (editable worktree @ c93e1a05), policyengine-core 3.31.0. Year 2025 (the July-2025 UC overlay in scenarios/uc_reform.py only rewrites years ≥2026). Script: measure/measure_uc_ct.py, measure/measure_reported.py.

artifact sha256 release
enhanced_frs_2024_25.h5 97a07f9ccb54… uk-data v1.56.14 (microcosm parity pin, HF revision a2039519)
enhanced_frs_2024_25.h5 e433e532b17b… uk-data v1.56.16 (latest cached release)

UC caseload (benefit units with universal_credit > 0, annual basis)

measure v1.56.14 v1.56.16
UC benefit units (m) 6.3275 6.3462
UC spend (£bn) 74.728 75.301
unweighted records on UC 3615 4830
benunit records total 61223 61223
would_claim_uc (m weighted) 21.8698 20.8856
would_claim_uc (unweighted) 33709 33715
is_uc_eligible (m) 10.7141 10.5156
is_uc_eligible (unweighted) 14897 14886
is_uc_entitled (m) 6.3275 6.3462
households (m) 31.254 31.3395

would_claim_uc share of all benunit records: v1.56.14 0.5506; v1.56.16 0.5507; int(0.55 × 61,223) = 33672.

UC by element (on UC and element > 0)

element v1.56.14 (m) records v1.56.16 (m) records element spend on UC v1.56.14 (£bn)
childcare 0.4585 151 0.4745 223 2.254
housing 4.9758 2370 4.9915 3375 36.784
lcwra 2.1615 1140 2.1709 1528 11.84
carer 0.5942 288 0.6731 405 1.438
child 3.0492 1225 3.092 1862 18.858

UC by family type and number of children (on UC)

split v1.56.14 (m) v1.56.16 (m)
COUPLE_NO_CHILDREN 0.2309 0.2348
COUPLE_WITH_CHILDREN 0.8383 0.8893
LONE_PARENT 2.2 2.1896
SINGLE 3.0583 3.0325
children = 0 3.2892 3.2673
children = 1 1.2492 1.2635
children = 2 1.0497 1.0627
children = 3 0.5203 0.5121
children = 4 0.1401 0.1614
children = 5_or_more 0.079 0.0792
with children 3.0383 3.0789
no children 3.2892 3.2673

FRS-reported vs imputed receipt (v1.56.14, 2025)

  • reported_uc_m: 5.7041
  • reported_unweighted: 6447
  • reported_and_on_uc_m: 3.9076
  • reported_not_on_uc_m: 1.7965
  • reported_not_on_uc_unweighted: 4870
  • on_uc_not_reported_m: 2.4199
  • on_uc_not_reported_unweighted: 2038
  • on_uc_m: 6.3275
  • on_uc_unweighted: 3615
  • eligible_m: 10.7141
  • eligible_unweighted: 14897
  • eligible_and_would_claim_m: 8.4753
  • eligible_and_would_claim_unweighted: 8249
  • eligible_not_would_claim_m: 2.2388
  • eligible_not_would_claim_unweighted: 6648
  • eligible_would_claim_but_uc_zero_unweighted: 4634
  • would_claim_m: 21.8698
  • would_claim_unweighted: 33709
  • benunits_unweighted: 61223
  • claims_all_entitled_unweighted: 0
  • claims_all_entitled_m: 0.0
  • reported_and_claims_all_unweighted: 0
  • would_claim_given_reported_unweighted: 3542
  • mean_weight_all: 579.6
  • mean_weight_on_uc: 1750.3
  • mean_weight_reported_on_uc: 2477.9
  • mean_weight_imputed_on_uc: 1187.4
  • weight_p90_on_uc: 4920.6
  • weight_p90_all: 665.0
  • share_of_uc_caseload_in_top10pct_weights: 0.657

Council tax by nation (household level, 2025)

nation release households (m) gross £bn CTR £bn net £bn avg gross £ avg net £ share CT=0
ENGLAND v1.56.14 26.1777 42.937 2.01 40.927 1640.0 1563.0 0.0745
NORTHERN_IRELAND v1.56.14 0.7883 0.0 0.008 -0.008 0.0 -10.0 1.0
SCOTLAND v1.56.14 2.7078 3.804 0.249 3.555 1405.0 1313.0 0.0609
WALES v1.56.14 1.5802 2.619 0.01 2.609 1657.0 1651.0 0.0218
ENGLAND v1.56.16 26.2549 43.124 2.017 41.107 1643.0 1566.0 0.0725
NORTHERN_IRELAND v1.56.16 0.7809 0.0 0.008 -0.008 0.0 -10.0 1.0
SCOTLAND v1.56.16 2.7189 3.81 0.249 3.561 1401.0 1310.0 0.0605
WALES v1.56.16 1.5847 2.635 0.015 2.62 1663.0 1653.0 0.0323

Council tax band shares (v1.56.14)

  • ENGLAND: A 0.237, B 0.195, C 0.220, D 0.157, E 0.098, F 0.052, G 0.036, H 0.006
  • NORTHERN_IRELAND: A 1.000
  • SCOTLAND: A 0.191, B 0.223, C 0.162, D 0.142, E 0.140, F 0.081, G 0.055, H 0.006
  • WALES: A 0.142, B 0.203, C 0.215, D 0.159, E 0.133, F 0.082, G 0.037, H 0.009, I 0.021

owned_land by nation (household level, 2025)

nation release total £bn avg/hh £ share hh > 0 records > 0 top-10 records' share of nation total share of nation household land value
ENGLAND v1.56.14 326.291 12464.0 0.0143 348 0.797 0.0702
NORTHERN_IRELAND v1.56.14 14.985 19009.0 0.0833 336 0.675 0.2716
SCOTLAND v1.56.14 62.011 22901.0 0.0163 16 1.0 0.2159
WALES v1.56.14 17.833 11286.0 0.0199 56 0.974 0.0948
UK total v1.56.14 421.121 0.0813
ENGLAND v1.56.16 323.604 12325.0 0.0145 352 0.802 0.0694
NORTHERN_IRELAND v1.56.16 14.028 17964.0 0.0796 340 0.627 0.2597
SCOTLAND v1.56.16 62.055 22823.0 0.0166 16 1.0 0.216
WALES v1.56.16 18.343 11575.0 0.0208 56 0.975 0.0974
UK total v1.56.16 418.029 0.0805
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment