Repository navigation
Default the BRMA from the household's region - #2021
Merged
Merged
Conversation
brma had no formula and defaulted to Maidstone, so a household calculation that set a region but no BRMA got Maidstone's LHA rates everywhere. It now gets its region's BRMA with the most entries in the list of rents (the pool policyengine-uk-data samples BRMAs from), and Maidstone only if the region is unknown. A formula stops policyengine-core carrying an earlier year's input forward, so the formula does that itself: the latest earlier BRMA applies while the household's region is the same as in that year. Fixes #2020 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The list of rents' Scottish, Welsh and Northern Ireland lists are copies of English lists (policyengine-uk-data#515), so their entry counts say nothing about where people rent. Each region's default is now its BRMA with the most private-rented households in the 2021 (England, Wales, Northern Ireland) and 2022 (Scotland) censuses, mapped to BRMAs. Scotland moves to Lothian, Yorkshire to Leeds and the East of England to Central Norfolk & Norwich. The new YAML tests no longer pin LHA rates, which #2022 changes; a Python test checks instead that each default gives the same rates as inputting it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The repo ignores *.csv, so the previous commit left the table out. Store it as .csv.gz beside lha_list_of_rents.csv.gz (gzip -n, so the archive is reproducible); the uncompressed content is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reading region for an earlier year from inside brma could cache the wrong region for that year: core's carry-over returns region's default (London) for a year once a later year is known. That dropped carried BRMA inputs and changed other results for the earlier year, such as income tax. The formula now never calculates region for an earlier year. It takes the region in effect in the BRMA's year as the last region known at or before it (or region's default if none is), and carries the BRMA forward unless a region known after it, up to this year, differs. A household that leaves a region and returns now gets the region default in any calculation order. Tests: the property test now asserts the exact rule in any order; a second property test inputs region in only some years and checks the rule and that calculating brma leaves every year's region unchanged; the review's repros are regression tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Northern Ireland private-rented split came from ONS Postcode Directory records under the LPS end user licence, which does not clearly allow publishing derived figures. The rows now use only OGL sources: NISRA households by postcode district, mapped to NIHE's postcode-district BRMAs, times Northern Ireland's private-rented share (policyengine-uk-data#516, d3b5ba0). England, Wales and Scotland are byte-identical, and every default is unchanged; Belfast now leads Lough Neagh Upper by 79 households. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Calculating this year's region from brma could still change earlier years' region, and so their income tax, through core's carry-over, which gives region's default for a year once a later year is known. And a BRMA stored only on another branch was recalculated year by year until core's spiral limit gave Maidstone. brma now reads only stored values. Each year's region is the last region known at or before it that this branch can read, or region's default; the latest earlier BRMA is the latest one this branch can read. brma never calculates region or an earlier year's BRMA. Tests: the review's three repros as regression tests, and the sparse-region property test now calculates brma in a random order and checks that every year's region is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolve the LHA.yaml and EntitledTo #1 pins against #2022's published LHA rates: with no BRMA input the household is in Inner South East London, so the 2020 two-bedroom rate is GBP 310.68 a week (16,155.36 a year) and EntitledTo #1's Universal Credit and net income rise by GBP 2,462.04 to 6,001.68 and 16,627.14. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Collaborator
Author
|
Merge audit: squash-merged at head bf27e88.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #2020
What changes
brmagets a formula. A household with no BRMA input is placed in its region's default BRMA,REGION_DEFAULT_BRMAinvariables/household/BRMA.py. Before this, it got Maidstone, whatever the region. Explicit inputs still win: policyengine-core only runs a formula for a period that has no input.For a single 40-year-old private renter in 2025 (LHA category B), the annual LHA rate moves as below. The rates are the published ones that #2022 brought in, computed on this branch at bf27e88 with each BRMA set explicitly. Lothian's and Maidstone's one-bedroom rates are both £172.60 a week.
Which BRMA each region gets
Each region gets its BRMA with the most private-rented households, counting only the part of the BRMA inside that region. The counts are in
parameters/gov/dwp/LHA/brma_private_rented_households.csv.gz(936 rows; columnsregion, brma, bedrooms, households; the uncompressed CSV's sha256 isfd40dae019e5…c2ae1f, identical to policyengine-uk-data#516 at d3b5ba0).Region.UNKNOWNkeeps Maidstone. The table is written out explicitly in code, andtest_brma_region_default.pyrecomputes it from the CSV.Sources. Private-rented households (private landlord or letting agency, plus other private rented) from four censuses, each mapped to BRMAs:
bedrooms = all. The defaults sum over bedrooms, so this doesn't affect them.Why not the list of rents. The first version of this PR used the BRMA with the most entries in
lha_list_of_rents.csv.gz. policyengine-uk-data#515 found that every Scottish, Welsh and Northern Irish list in that file is a copy of an English BRMA's list, so those entry counts are English sample sizes. I checked this here: all 480 of those (BRMA, category, year) lists match an English list from the same year, sorted rent for rent. That version had made Scotland's default Aberdeen and Shire. The census changes three defaults:Inputs carry forward
Before this PR, a
brmainput for one year carried into later years throughauto_carry_over_input_variables. policyengine-core only does that when a variable's formula returns nothing: see theif array is None:branch inSimulation._calculate. A variable with a formula would therefore drop an earlier input in later years. That would affect household calculations that set BRMA for one year, and microsimulation years after the dataset's last projected year (2030).The formula restores this, reading only values already stored:
brmanever calculatesregion, or an earlier year'sbrma. Two earlier versions did, and the independent reviews caught both:regionfor an earlier year could cache London there, because core's carry-over gives region's default for a year once a later year is known.regionhad the same effect on earlier years, through core's carry-over. For example, with region set for 2024 and nothing else, calculatingbrmafor 2026 changed 2025's income tax.Gaps of any length work, because the latest earlier year is read directly.
Test pins that changed
Both moved only because the household sets no BRMA and is in London (set explicitly, or by default):
tests/policy/baseline/finance/benefit/family/LHA.yaml, "BRMA default value": the household sets no region, so it is in London.brma:MAIDSTONEtoINNER_SOUTH_EAST_LONDON.BRMA_LHA_rate(2020, category C): 9,753.12 to 16,155.36. That is VOA's April 2020 two-bedroom rate for Inner South East London, £310.68 a week; Maidstone's is £187.56.tests/policy/integration/entitledto_scenarios.yaml, EntitledTo Add contributory JSA #1 (single 30-year-old, London, private renter): the household gets the Universal Credit shared accommodation rate published from April 2024 and held for 2025-26. That is £650.00 a month for Inner South East London, against £444.83 for Maidstone.universal_credit: 3,539.64 to 6,001.68.household_net_income: 14,165.10 to 16,627.14.brma: INNER_SOUTH_EAST_LONDONset explicitly.New tests
tests/policy/baseline/household/brma.yaml:brmaonly, not LHA rates, so they don't move when Use published LHA determinations and apply Sch 3B paras 3 and 3A #2022 changes the rates. Run against main, the region-default cases fail, because main givesMAIDSTONE.tests/test_brma_region_default.py:UNKNOWN. The CSV covers every BRMA inBRMAName.BRMA_LHA_rateanduc_LHA_capas setting that BRMA explicitly. This is a differential check between the formula path and the input path.brma, with or without a BRMA input, leavesregionand 2025 income tax unchanged; region carries forward until its next input; a BRMA stored only on another branch isn't carried (11 years, beyond core's spiral limit).regionis unchanged.Invariants
The rule: in a year with a BRMA input,
brmais that input. Otherwise, if there is an earlier input and the household's region has been the same in every year from the latest such input to this year,brmais that input. If neither holds,brmais the default for this year's region. Each year's region is the latest region input at or before it, or London if there is none.brmanever changesregionfor any year, nor anything that depends on it.Two Hypothesis property tests check invariants 1 and 2, with regions input every year or in only some years. Each runs 40 derandomised examples with a random order of calculation. Mutation checks:
Downstream
add_yearly_variables(services/household_calculation_service.py) fills every variable a household leaves unset. Input-only variables get their default value; variables with a formula are sent as null and calculated. On main, every API household calculation therefore sentbrma: MAIDSTONEexplicitly. With this PR,isInputVariableforbrmabecomesfalse, so the API sends null and the formula sets the BRMA from the household's region. The app-v2 household builder rendersbasicInputswithout checking that flag (libs/metadataUtils.ts,getBasicInputFields), so users can still choose a BRMA. The app's starter household sets neitherregionnorbrma. It is in London by default, so its LHA rates move from Maidstone's to Inner South East London's.to_input_dict()andto_input_dataframe()export only input-only variables by default, so they dropbrmaonce it has a formula, as they already do forcountryandcurrent_education. policyengine-uk's ownUKSingleYearDataset.from_simulationusessimulation.input_variablesand still keeps an inputbrma; checked on this branch.extend_single_year_dataset) and setbrmafor every household in each year, so the formula only runs after 2030, where it carries the 2030 value forward. Checked: I ran main (7b9fc37) and this branch on the enhanced FRS 2024-25 (policyengine-uk-data 1.57.4), whose years run 2024 to 2030. I ran e77f3c7, 15eeb50, d1b0ade and 14eb69d of this branch. After merging main, I re-ran d0bebbc against main 2cbf20c (core 3.32.13): all 24 arrays are still bitwise identical. In 2025, 2030, 2031 and 2034 these arrays are bitwise identical to main:brma,BRMA_LHA_rate,LHA_cap,universal_credit,housing_benefitandhousehold_net_income. In 2031 and 2034,brmacomes from the formula's carry-forward and matches core's carry-over on main.release_input_coverage.py). The pinnedefrs_parity_reference.jsonlists 13 formula-owned overrides; regenerating it with this release addsbrma, and two tests that expect 13 will need updating.brmaexplicitly. Its BRMA imputation still samples the list of rents; policyengine-uk-data#515 tracks moving it to the same census table.Interaction with #2022 (published LHA rates)
#2022 merged on 4 October, and it changed the same two pins. The merge with main, bf27e88, resolves both conflicts to Inner South East London's published rates, shown above. The new
brma.yamldoesn't pin rates, and the rate differential test compares the default with an explicit input under whichever rates are in force, so neither needed re-pinning.Known limits
brmaorregionin a year, policyengine-core fills the others with the variable's default (Maidstone, London) for that year, and the formula doesn't run for them. A filled-in Maidstone then carries forward like an input. Main behaves the same way.regioninput for 2025, calculating 2040 before 2026 gave London for 2026, andbrma, which reads the region inputs, could then disagree withregion. Carry over only inputs, the latest at or before the requested period policyengine-core#562 fixed it in the core main now locks, 3.32.27. On this branch, 2026 now comes out Scotland in either order.brmaset withsimulation.set_inputafter the simulation is built is dropped on the marginal-tax-rate branches:marginal_tax_rateand its employer-cost and capital-gains variants delete every array that is neither in the construction-timesimulation.input_variablesnor input-only. Abrmagiven in the situation or dataset is unaffected. Main already does the same for every formula-backed input: there,countryset this way is Wales on the simulation but Scotland on theadult_1_pay_risebranch. A general fix is a separate change.Commands run
ruff format --check .andruff check .: clean.make teston e77f3c7 (first version): YAML 1,378 passed; pytest, with the microsimulation tests on the real dataset, 397 passed, 1 skipped, 1 xfailed.pytest policyengine_uk/tests/test_brma_region_default.pygave 26 passed.test_brma_region_default.pygave 26 passed;axiom: n/a: input default for household calculations (BRMA location), not a statutory rule
🤖 Generated with Claude Code