Skip to content

Put FRS self-employment losses in trading_loss - #528

Draft
MaxGhenis wants to merge 3 commits into
mainfrom
frs-trading-loss
Draft

MaxGhenis wants to merge 3 commits into
mainfrom
frs-trading-loss

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

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, as test_non_negative_incomes requires);
  • 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). So trading_loss joins the FRS-only variables that impute_frs_only_variables rewrites 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's random module 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_loss also gets the self-employment rows in uprating_factors.csv and uprating_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_loss only as a deduction from self-employment income, which is zero for every FRS loss-maker, so calibration targets are unchanged.

Tests

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_loss zeroed, 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):

2025-26 2026-27 2030-31
Trading losses £319m £330m £387m
Trade loss relief against general income £77m £80m £102m
Income Tax −£13.1m −£14.0m −£18.4m
NICs, means-tested benefits, tax credits £0 £0 £0
HBAI household net income −£306m −£316m −£368m
Household market income −£319m −£330m −£387m

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

  • Round 1 (independent Opus review of c3bc616): REQUEST CHANGES. It asked for count-only assertions and uprating rows, and measured that zeroing left £0.29bn of calibrated losses, which led to the QRF change. All addressed in 94bd3d7.
  • Round 2 (independent Opus review of 94bd3d7): APPROVE, with one nit (the changelog still described zeroing), fixed in cf7042b. A delta review of cf7042b is pending.

🤖 Generated with Claude Code

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 branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant