Repository navigation
Stop counting FRS property losses as income; count sub-let rent for every tenure - #509
Merged
Merged
Conversation
FRS CVPAY is the rent a boarder or lodger pays the householder, after deducting any state benefits to help with rent (FRS question CvPay, asked about each person not related to the HRP in the second and later benefit units of a conventional household). frs.py added it to the payer's own property_income. Move the property income calculation into a tested helper, frs_property_income, that keeps SUBRENT and ROYYR1 and drops CVPAY. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of #503: the seeded cases only draw non-negative amounts, so the floor at zero was never active, and every fixture had a unique index while create_frs stacks the adult and child tables. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… tenure ROYYR1 holds a property loss as a positive amount flagged by RENTPROF = 2, and the build counted it as income. A loss now counts as zero. SUBRENT was counted only for owner-occupiers, although the FRS asks every household about sub-letting. The tenure restriction is removed. SUBRENT stays as reported whether SUBALLOW says it is before or after allowable expenses; the FRS collects no expense amount. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Negative FRS values are missing-value codes (-1 to -9), not amounts. Flooring the sum let a missing ROYYR1 code cancel the household reference person's SUBRENT, which now matters for renting households too. Floor each amount instead. FRS 2024-25 has no negative codes in either field, so the build is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
Author
|
Handing this PR to the UK hub; it lands only in Max's uk-data release batch (d833).
|
Contributor
Author
|
Queued in release PR #544 for the 10/8 uk-data batch. It lands only on Max's go (d833). |
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.
Summary
Stacked on #503: its two commits appear here until it merges, after which this branch is rebased onto main. Review only the last two commits.
frs_property_income(added in #503) builds each person'sproperty_incomefrom two FRS amounts: ROYYR1 (rent from other property) and SUBRENT (rent from letting part of the home). Three problems found while reviewing #503:The helper also now floors each amount at zero separately. Negative FRS values are missing-value codes (−1 to −9), not amounts. Flooring the sum let a missing ROYYR1 code cancel the household reference person's SUBRENT, and that case now arises for renting households as well. FRS 2024-25 has no negative codes in either field, so this part changes nothing in the current build.
Evidence
Sources: the documentation shipped with FRS 2024-25 (UK Data Service SN 9563), the older FRS derived-variable specification (SN 4149), and aggregates from four raw FRS years (2020-21, 2022-23, 2023-24 and 2024-25). Cells under 10 survey records are suppressed throughout.
Losses (ROYYR1, RENTPROF)
Why zero and not a negative amount.
trading_loss,loss_relief).property_incomemust be non-negative (tests/test_non_negative_incomes.py).Sub-letting (SUBLET, SUBRENT)
No documentation supports the owner-only restriction, so it is removed. SUBRENT still goes to the household reference person, as before.
SUBRENT before or after expenses (SUBALLOW)
SubAllow asks whether the SubRent amount is before or after allowable expenses (1 = before, 2 = after). ROYYR1 is always after expenses.
SUBRENT stays as reported, whichever answer was given:
sublet_incomeonce policyengine-uk reads that variable, for rent-a-room relief and the means-test disregards. There, the SUBALLOW = 1 answers are gross receipts and the SUBALLOW = 2 answers are net, and Move SUBRENT from property_income to sublet_income, after policyengine-uk reads sublet_income #508 records that distinction. Until then, before-expenses SUBRENT sits inproperty_incomenext to after-expenses ROYYR1. policyengine-uk then deducts the £1,000 property allowance from the total; policyengine-uk#1900 tracks that allowance being applied to an already-net amount.Invariants and tests
frs_property_income(person, household)should satisfy these for every valid pair of tables, meaning every household inhouseholdhas exactly one person withhrpid == 1, as in the FRS. "Floored" below means floored at zero, which is how missing-value codes are handled.WEEKS_IN_YEAR× (floored SUBRENT summed over all households + floored ROYYR1 summed over everyone not coded as a loss). Each amount is counted exactly once.WEEKS_IN_YEAR× £x, or not at all if the amount is coded as a loss. Raising one household's floored SUBRENT by £x changes only that household's reference person's result, byWEEKS_IN_YEAR× £x.WEEKS_IN_YEAR× floored ROYYR1 if it was counted, otherwise nothing.tests/test_frs_property_income.pychecks each invariant on 200 seeded random sets of households. The random tables include missing-value codes, losses and all eight tenures. Hypothesis is not a dependency of this repo, so the tests follow the file's existing seeded style. A differential test checks the vectorised helper against a one-person-at-a-time loop version of the same rules on the same 200 sets. Example tests cover:create_frsproduces.Built dataset against raw tables.
test_built_frs_matches_the_raw_tablesreads ROYYR1, RENTPROF, HRPID and SUBRENT straight from the raw tables and joins them on SERNUM. It then compares the result withproperty_incomein the built base FRS. This covers the call site increate_frsand the raw column names and codes. It runs in CI, which builds the dataset, and its assertion carries no values. Locally, it fails on the base FRS built from #503's head, fails on a build with only the loss fix, and passes on this branch.Mutation check. Each of seven deliberate bugs makes between 424 and 665 of the file's 1,218 synthetic tests fail:
Measured impact
All figures are aggregates. Simulations use policyengine-uk main (
da947eee). The baseline is #503's head (0812cd1). The impact of #503 itself is in that PR.Base FRS 2024-25 (survey weights, no imputation or calibration)
I built the base FRS three times with
create_frs: on #503's head, on this branch, and on this branch with the owner-only SUBRENT rule put back (the loss fix alone). I compared every column of every table.property_incomechanges. It is the only column that differs between any two of the three builds.property_incomefalls from £16.18bn to £15.86bn (−£0.32bn). People with any property income fall from 1.87m to 1.80m.property_incometo fewer than 10 records in 2024-25, so its 2024-25 effect is not shown. That includes any total from which it could be worked out by subtraction. The pooled four-year figure above gives its scale.Loss fix alone, 2026:
Universal Credit, council tax reduction and poverty change for fewer than 10 survey units each, so those changes are not shown.
Enhanced FRS (full rebuild)
I built the enhanced FRS on #503's head (
0812cd1) and on this branch, twice each, with two different seeds. The branch builds are atbe4d935(seed 0) and the current headcc937e0(seed 1). Those two commits give an identical 2024-25 base FRS in every column. All builds used production settings (512 epochs,PE_UK_DATA_OA_CLONES=1), the repo'suv.lock(policyengine-uk 2.93.0), the same raw inputs and the same cached trained models, in worktrees set up the same way.Two outside inputs are pinned, by a wrapper that changes no repo code:
Check that this makes builds comparable. I built #503's head twice with seed 0 in two worktrees, one with live downloads and one from the cache. The two builds are identical in every column of every table, household weights included.
So within each seed, the difference between the baseline and the branch is caused by the code change. It does not come only through the records whose income changed. Two steps before calibration propagate it, and neither depends on the seed, so the same differences appear in both pairs:
stack_cgt_band_donorsdraws 270 donor households with a fixed seed and probabilities that depend on total income. Baseline and branch share 82 of the 270.frs_only.pyrefits a model on the FRS persons in every build, withproperty_incomeamong its predictors. Among the 52,576 non-donor households, which are the same in both builds, 33 person columns differ. For example, reported Universal Credit differs on 894 person rows and capital gains on 23,801.Calibration then reweights every household, and that step does depend on the seed.
Calibration targets
Each build's fit to the national targets at the calibration year (2025), from
get_loss_results. Every build is scored against the same cached targets.No property income target's relative error moves by more than 0.025 in either seed. The largest moves in both seeds are the same targets: three DWP Universal Credit payment bands and three HMRC capital gains bands.
Not in this PR
sublet_income. That is Move SUBRENT from property_income to sublet_income, after policyengine-uk reads sublet_income #508, which has to follow a policyengine-uk release that readssublet_income.Merge note
This PR lands only in the single uk-data release batch that Max approves (d833), after #503 in that batch. Every merge to uk-data main builds and publishes a release, so nothing here merges on its own.
If #503 lands as a merge commit, this branch needs no rebase. If it is squash-merged, drop its two commits rather than replaying them (
git rebase --onto origin/main 0812cd1), then re-run CI on the rebased head.axiom: n/a: data pipeline change (FRS variable mapping), no rule change.
🤖 Generated with Claude Code