Repository navigation
Conversation
SEINCAM2 records a self-employment loss as a negative profit ("Any losses
are recorded as such", FRS methodology glossary). The build floored it at
zero, so about 74,000 people's £1.28bn a year of losses (2024-25, before
uprating) disappeared. policyengine-uk wants the profit and the loss as two
non-negative inputs, because its programmes treat a loss differently:
Income Tax and tax credits set it against other income, means-tested
benefits do not, and HBAI counts it as negative income.
Split SEINCAM2 into self_employment_income (the profit, unchanged) and
trading_loss (the loss as a positive amount). SPI-donor rows of the
enhanced FRS carry no trading loss: their self-employment profits come
from the SPI, so their FRS donor's loss would sit beside another
taxpayer's imputed incomes.
Tests: Hypothesis properties of the split (non-negative, exclusive,
conserves the reported profit, monotone) and data checks on the built FRS
and enhanced FRS. Adds hypothesis to the dev extras.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review r1 on c3bc616 (REQUEST CHANGES): - Zeroing the SPI-donor rows' losses left the calibrated enhanced FRS with £0.29bn of losses on about 23,000 weighted people, against £1.28bn on 74,000 in the FRS. SPI copies hold 28% of household weight. The SPI has no current-year loss field, so impute trading_loss on those rows by the second-stage QRF, from FRS respondents with similar demographics and imputed incomes, like the other FRS-only variables. It is appended last: microimpute imputes the outputs in order with a per-output seeded generator, so every earlier imputation is unchanged and no global random state is used. A profit and a loss are never both kept on one row, as SEINCAM2 nets a person's trades. - trading_loss gets the self-employment rows in uprating_factors.csv and uprating_growth_factors.csv (what the table generator produces once policyengine-uk#2085 gives the variable that uprating). - The data checks assert on violation counts, so a failure never prints record-level amounts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review r2 nit: the fragment still said SPI-donor rows carry no loss. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 3, 2026
21 of 52 tasks
This branch has not been deployed
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
FRS 2024-25 records self-employment losses as negative SEINCAM2 ("Any losses are recorded as such", FRS methodology glossary). The build floored them at zero, so about 74,000 people (weighted) and £1.28bn a year of losses (2024-25 prices, before uprating) disappeared from the data.
This PR splits SEINCAM2 into two non-negative inputs:
self_employment_income: the profit, unchanged (stays non-negative, astest_non_negative_incomesrequires);trading_loss: the loss as a positive amount.Their difference is the reported profit. policyengine-uk needs them apart because its programmes treat a loss differently: Income Tax and tax credits set it against other income, Class 4 against trading profits only, means-tested benefits never offset it against other earnings, and HBAI counts it as negative income (PolicyEngine/policyengine-uk#2085).
Checked before relying on the sign: none of the negative values is a survey missing-value code. The five small whole-number values between −£9 and −£1 a week are all different, and each of those adults reports a loss on a self-employed job (PROFIT2).
SPI-donor rows get their losses from the second-stage QRF. Their incomes are imputed from the SPI, which has no current-year loss field (only
LOSSBF, losses brought forward). Sotrading_lossjoins the FRS-only variables thatimpute_frs_only_variablesrewrites on those rows, drawn from FRS respondents with similar demographics and imputed incomes. It is appended last: microimpute imputes the outputs in order, each with its own seeded generator, so every earlier imputation and NumPy's global random state are unchanged. (Python'srandommodule state does change; no later build step uses it.) As in the FRS, where SEINCAM2 nets a person's trades, a row never keeps both a profit and a loss.Why not zero them: the first build zeroed the SPI-donor losses, and the calibrated enhanced FRS then carried £0.29bn of losses on about 23,000 weighted people, against £1.28bn on 74,000 in the FRS. SPI-donor rows hold 28% of household weight.
trading_lossalso gets the self-employment rows inuprating_factors.csvanduprating_growth_factors.csv: the table generator produces those once policyengine-uk#2085 gives the variable that uprating.Nothing else in the build changes. The split adds no random draws, and the QRF output is appended after the others. policyengine-uk releases before #2085 read
trading_lossonly as a deduction from self-employment income, which is zero for every FRS loss-maker, so calibration targets are unchanged.Tests
test_trading_loss.py: Hypothesis properties ofsplit_self_employment_profitfor any weekly profits, including missing values: both outputs non-negative, never both positive, profit less loss equals the annualised reported profit, and monotone in the reported profit. Data checks on the built FRS and on the enhanced FRS's SPI-donor rows (no negative loss, never a profit and a loss together), asserting on counts only so a failure never prints a record's amounts.test_non_negative_incomes.pycoverstrading_loss.hypothesisto the dev extras (the same line as Say which joint occupiers each FRS non-dependant family resides with #522, Supply is_claimant_or_partner from the FRS adult table #524 and Set UC gainful self-employment from the FRS main-job status #525; whichever lands second relocks).Impact
Real runs on a seeded build of 94bd3d7 (cf7042b changes only the changelog), against policyengine-uk main 84f5ad43a and #2085 at 905b41aef. Aggregates only; any cell resting on fewer than 10 FRS households is suppressed.
Losses in the data. The calibrated enhanced FRS (2024 data year) carries £314m of losses on 27,000 weighted people: £290m on FRS rows and £25m imputed onto SPI-donor rows. The raw FRS has £1.28bn on 74,000 people. Calibration gives FRS loss-makers about a third of their survey weight (no calibration target covers trading losses), and SPI-donor rows hold 28% of household weight. With losses zeroed on SPI rows (the first version) the total was £290m.
With policyengine-uk main (no rule reads the losses yet): every output is identical to the same build with
trading_losszeroed, in each year 2025-26 to 2030-31 (37 arrays of taxes, benefits, household incomes and poverty flags; only the input itself differs). The data change is neutral until #2085 is released.With #2085, against the same build with losses zeroed (#2085 is itself bitwise neutral without losses):
In 2026-27, 27,000 people (61 FRS households) have a loss, 23,500 (50 households) get relief and 9,100 (35 households) pay less Income Tax. Fewer than 10 FRS households move across a poverty line, so those counts are not published.
Merge
Part of the batched uk-data release (d833): approved on gates, then merged with the batch on Max's go. Lands after PolicyEngine/policyengine-uk#2085 is released, so the data's losses are read by the new rules.
Review
🤖 Generated with Claude Code