shareholding, corporate_land_value and consumption_shareholding depend on who is simulated

未关闭
#2,896 0 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

维护者通常 2 天内回复

还没有人认领这个 Issue。

评估

难度
5/5
预计耗时
一周以上
新手友好度
28/100
Issue 类型
缺陷
描述清晰度
基本清楚
活跃度
活跃
技术栈
python
领域
backend, data

调研方向

Start with variables/household/wealth/shareholding.py, then compare corporate_land_value.py and consumption_share.py; read the tests named in the issue, especially tests/test_corporate_land_value.py and tests/test_sdlt_counted_once_properties.py. First determine which of the five separately scoped proposals is intended for a change, since the issue leaves modelling choices open. Done should include tests showing that household shares behave as expected for a single household, weighted data and partial UK simulations.

由索引模型根据 Issue 内容生成。

描述

Summary

shareholding, corporate_land_value and consumption_shareholding each divide a household's key by the key's weighted total over the households in the simulation. So a household's value depends on which other households are simulated. Only a run over the whole UK at UK weights gets it right.

The most visible case is a bug. Since #1296 (August 2025), a household situation with any corporate wealth gets shareholding 1, and since #1824 (2.92.0) so does one with any private pension wealth. The household then pays all UK business rates (£36.20bn in 2026-27) and all corporate SDLT in England and Northern Ireland (£4.57bn). The household below ends up with household_net_income of −£40.77bn.

policyengine-api and policyengine.py's household calculator build households this way. The app's household builder offers private_pension_wealth under Assets.

What each formula does:

  • shareholding is a household's share of corporate_sector_wealth.
    • business_rates and corporate_sdlt multiply it by UK receipts. Both levels are in household_tax, and so in household_net_income; gov_tax lists them too.
    • Under a reform, corporate_incident_tax_revenue_change multiplies it by the reform's revenue change.
  • corporate_land_value is the same share times the corporate land parameter (£2,058.6bn, its 2024 value held flat).
    • The land value tax LVT reads it directly at its corporate rate, and through land_value at its full rate.
    • All LVT rates are 0 at current law. hbai_household_net_income subtracts LVT, so under an LVT reform the share reaches HBAI incomes too.
  • consumption_shareholding is a household's share of consumption. No formula reads it. consumer_incident_tax_revenue_change and employer_ni_response_consumer_incidence compute their own copies of the share.

The same thing happens in any simulation of part of the UK: a filtered region, a set of axes, an extract, a subsample, or Microcosm UK's per-clone engine blocks.

Separately, three modelling choices are open: the key, the incidence, and where the levels sit. These are not bugs.

Each proposal below can land as its own PR:

  1. Household situations stop using the weighted total. This is the bug fix.
  2. A national total of each key that the data can carry, so any part of the UK gets its own share.
  3. A decision on where business rates and corporate SDLT sit.
  4. A review of the key.
  5. Remove consumption_shareholding, or have the formulas that copy it read it.

File and line references are to policyengine-uk 2.174.1. On main (2.176.1) the formulas are unchanged, and household_tax takes its terms from programs.yaml (#2809).

What happens

policyengine_uk.Simulation(situation=...) gives the household the default household_weight of 1. Here is a household with private pension wealth (the comments are the outputs on 2.174.1):

from policyengine_uk import Simulation

situation = {
    'people': {'adult': {'age': {2026: 45}, 'employment_income': {2026: 40_000}}},
    'benunits': {'bu': {'members': ['adult']}},
    'households': {'hh': {'members': ['adult'], 'region': {2026: 'LONDON'},
                          'private_pension_wealth': {2026: 50_000}}},
}
sim = Simulation(situation=situation)
print(sim.calculate('shareholding', 2026))          # [1.]
print(sim.calculate('business_rates', 2026))        # [3.620134e+10]
print(sim.calculate('household_net_income', 2026))  # [-4.0774574e+10]

situation['axes'] = [[{'name': 'employment_income', 'count': 5,
                       'min': 0, 'max': 80_000, 'period': 2026}]]
print(Simulation(situation=situation).calculate('shareholding', 2026))  # [0.2 0.2 0.2 0.2 0.2]
variable 2026
shareholding 1
business_rates £36.20bn
corporate_sdlt £4.57bn
corporate_land_value £2,058.6bn
household_net_income −£40.77bn
hbai_household_net_income £32,420
  • Axes. The axis copies share the totals. Five copies each get shareholding 0.2 and £7.24bn of business rates, so the result depends on the number of axis points.
  • Reforms. The household takes the whole change.
    • With gov.contrib.policyengine.budget.corporate_incident_tax_change at 1 (£1bn), its corporate_incident_tax_revenue_change is £1bn.
    • Under a 1% land value tax (gov.contrib.ubi_center.land_value_tax.rate), its LVT is £20.59bn and its hbai_household_net_income falls from £32,420 to 0.
  • Zero weight. Setting household_weight to 0 shows the fallback that proposal 1 builds on: the key over household financial net worth. It charges this household about £383 of business rates and corporate SDLT.
  • Who builds households this way.
    • policyengine-api (policyengine_api/country.py) and policyengine.py's household calculator (src/policyengine/tax_benefit_models/uk/household.py) both build through policyengine_uk.Simulation(situation=...).
    • policyengine-api runs policyengine-uk 2.151.0, which has the same code.
    • PolicyEngine/policyengine-app-v2's household builder (app/src/libs/flagship/householdVariableGroups.ts) hides corporate_wealth as a dataset-only input, but offers private_pension_wealth under Assets.
    • Not run end to end in the app.

Expected: a household's shareholding does not depend on which other households are simulated, or on its own weight.

Any simulation of part of the UK hands the UK totals to whichever of its households hold the key:

  • Regions filtered from a UK dataset. Nations come from the national data; constituencies and local authorities come from the local-area release. This is PolicyEngine/policyengine.py#567.
  • Runs that replace the weights with an area's weights, such as policyengine.py's legacy WeightReplacementStrategy. The area's households carry the whole total.
  • filter_dataset (data/filter_dataset.py). An extracted household keeps its weight w, so on its own its shareholding is 1/w. The extract already carries attends_private_school and months_since_last_birthday from the whole population (lines 47-57), but not these shares.
  • Microcosm UK's per-clone engine blocks.
    • They keep the UK totals by rescaling weights. But once clones differ (PolicyEngine/microcosm#931), each block's share of a total follows its weight, not its key.
    • PolicyEngine/microcosm#1115 already accepts a block run only when every block carries the same weighted key.
    • Microcosm will correct for this in its own build. A total carried by the data (proposal 2) would make that correction unnecessary.

Cause, read in the code

  • The weighted branch. variables/household/wealth/shareholding.py:12-23 takes it when household.simulation.dataset is not None.
    • #1296 made build_from_situation store a dataset for every situation (simulation.py:1182-1202).
    • household_weight defaults to 1 (variables/household/demographic/household_weight.py:11).
  • Other formulas.
    • variables/household/wealth/corporate_land_value.py:13-27 has no such check.
    • variables/household/consumption/consumption_share.py:12-19 and variables/contrib/policyengine/consumer_incident_tax_revenue_change.py:11-22 use the same gate as shareholding.
    • So do pre_budget_change_ons_household_income_decile and the NHS, education and other public spending budget changes, so situations take their data branch too.
  • Tests.
    • #774's closing comment (April 2026) treats bare situations as taking the fallback, and suggests a narrower issue if that path matters. #1296 had already sent situations to the weighted branch eight months earlier.
    • #2448 builds its fallback test through policyengine-core for that reason (tests/test_weighted_incidence.py:63-64).
    • The YAML tests and tests/test_fiscal_partition.py set shareholding directly. The latter says why (tests/test_fiscal_partition.py:230-232): derived from wealth, it would split all UK corporate taxes across the test's few households.
    • Two tests rely on the weighted branch in situations, treating a two-household situation as the whole UK:
      • tests/test_corporate_land_value.py:5-43 (#1583);
      • check 5 of tests/test_sdlt_counted_once_properties.py (lines 176-180).
    • No test checks a one-household situation.
  • The fallback. It divides by household financial net worth at its 2021 value. parameters/household/wealth/national_balance_sheet.yaml declares the uprating on the parent node, and it does not reach financial_net_worth. #2557 noted this and kept it as an exception in its uprating test.

Open modelling choices

These are not bugs. Proposals 3 and 4 ask for a decision on each.

The weighted total keeps the allocations summing to the receipts on any full dataset. #774's closing comment credits #840 with that. What the formulas spread, and onto whom, are choices the code calls modelling conventions (parameters/gov/hmrc/business_rates/statistics.yaml:17-20; parameters/gov/hmrc/stamp_duty/statistics.yaml:21-24).

  • The key.
    • corporate_sector_wealth adds private_pension_wealth to corporate_wealth, because pension funds hold corporate equity (#1824). corporate_sector_wealth.py:6-16 calls the addition a compatibility proxy.
    • private_pension_wealth includes retained defined benefit rights and pensions in payment, valued at the SCAPE rate (variables/input/private_pension_wealth.py:6-31). Defined benefit rights are promised amounts that do not move with company returns, and most public service schemes are unfunded.
    • There is no overseas share. #224 (2021) kept it out so that the accounts add up. Overseas investors held 58.8% of UK quoted shares at the end of 2024 (ONS, Ownership of UK quoted shares: 2024, 29 January 2026).
    • The rating lists include public bodies, charities and unincorporated businesses. Corporate SDLT payers include partnerships and collective investment schemes.
  • The incidence.
    • The ONS's Effects of taxes and benefits counts rates on commercial and industrial property, and stamp duties, among intermediate taxes. It assumes they fall on consumers, allocates them through household spending, and deducts them in post-tax income, not disposable income (technical report, financial year ending 2019, section 7).
    • HM Treasury's distributional analysis excludes business taxes (Impact on households: distributional analysis to accompany Budget 2026, paragraph 1.2).
    • Bond, Denny, Hall and McCluskey (Fiscal Studies, 1996) find that much of the burden of business rates shifts to property owners through lower rents in the long run, while occupiers bear more of it in the short run.
  • The placement.
    • The levels of business rates and corporate SDLT are in household_tax, and so in household_net_income.
    • VAT, and since #2319 private school VAT, enter household_tax only as changes. The employer NICs level is only in gov_tax (data/fiscal_partition.yaml:797-804, 811-817).
    • Fuel, alcohol, tobacco and gambling duties enter household_tax as levels.
    • hbai_household_net_income already leaves business rates and corporate SDLT out (variables/household/income/hbai_excluded_income.py).
  • Corporate land. Ownership of corporate equity is a reasonable first attribution for a tax on corporate land. Its caveats (overseas owners, housing associations, public corporations) are scoped as D7 in PolicyEngine/uk-land-value-tax#20.
History

The denominator.

  • December 2021.
    • e89fd37f6 ("Ensure land value calculation works when individual") moved land value to parameter totals.
    • 7f98371dc, 23 minutes later in the same PR (#398), went back to weighted totals, with a parameter fallback when the total is zero.
    • 51757e8f8 (#402), the next day, added shareholding the same way, falling back to household financial net worth.
  • April 2022: regions.
    • #578 moved land value and carbon to parameter totals "so that when we filter to regions, we don't retain the same land value".
    • 82232754f (in #597, "Fix business rates when countries are filtered") did the same for shareholding a week later.
  • April 2024. f813a3655 (#840) went back to the weighted total for shareholding.
    • #840 is an OBR forecast update, and its description does not mention the change.
    • #774's closing comment credits it with making the weighted shares sum to one, so that a carbon tax's revenue stopped moving with its incidence split.
    • #845 then guarded a zero total (#843).
  • 2026.
    • #1583 (2.79.2) moved corporate_land_value back to the weighted total, because its parameter total of corporate wealth was stale. It does not discuss #578's reason.
    • #1824 (2.92.0) moved both keys to corporate_sector_wealth.
    • #2448 guards a zero weighted total.
    • #1905's caveats report the constituency case.
  • So each denominator has a known failure. A parameter total does not match the data (#774, #1583). A weighted total gives any part of the UK the UK totals (#578, 82232754f, PolicyEngine/policyengine.py#567, this issue).

The placement.

  • #413 (December 2021) put the business rates level in household_tax.
  • f5a4b6bd6 (#559, April 2022, "Improve calibration and imputations") kept only the change from baseline for business rates, corporate SDLT, and SDLT, LTT and LBTT.
  • 9ecc9dce5 (#946, 2.0.0, September 2024, the move to policyengine-uk-data) restored the levels.
  • #2235 (October 2026) counts corporate SDLT once.
  • #2322 (October 2026) wrote the "modelling convention" text and states the identity.

Proposals

  1. Situations (the bug; can land on its own).

    • Take the weighted branch only when built_from_data(simulation) (utils/data_source.py), as attends_private_school does. Give corporate_land_value the same fallback.
    • Base the fallback on a parameter holding the key's national total, uprated with the key, in place of household financial net worth at its 2021 value. That also removes #2557's exception.
    • The other choice is 0 in situations, which is what attends_private_school returns. The household calculator would then show no corporate incidence.
    • A multi-household situation with weights set would no longer act as a population. tests/test_corporate_land_value.py:5-43 and check 5 of tests/test_sdlt_counted_once_properties.py would build small datasets instead, as tests/test_weighted_incidence.py does.
    • Test through policyengine_uk.Simulation(situation=...), with and without axes: a household's shareholding does not depend on its own weight or on the number of axis points.
    • Nothing changes on data.
  2. A national total the data can carry.

    • Add a household input that holds the national weighted total of each key, the same on every row: corporate_sector_wealth, and consumption if consumption_shareholding stays. The formulas divide by it when it is supplied, and keep today's weighted total otherwise.
    • Uprating:
      • both parts of corporate_sector_wealth uprate with OBR GDP per capita, and household_weight with ONS population;
      • uprating the input by the product of the two keeps it equal to the projected weighted total;
      • a variable takes one uprating index today, so this needs a combined index;
      • a builder that sets weights year by year writes the input year by year.
    • As a column it survives row filters, filter_dataset, household subsets, subsamples and weight replacement. Each part of the UK then gets its own share of the UK total, and the full dataset keeps the identity #840 was after.
    • Microcosm UK would write it at its release weights, and at its measurement weights inside the build. policyengine-uk-data would need to write it too, for PolicyEngine/policyengine.py#567's regional runs.
    • This is option 2 of PolicyEngine/policyengine.py#567, without carrying formula values. It is the same route as the national income rank that attends_private_school waits for (attends_private_school.py:72-78).
  3. Where business rates and corporate SDLT sit. The options:

    • keep the levels, and document the incidence;
    • count only the change from baseline, as for VAT and, since #2319, private school VAT. This was the rule for business rates and corporate SDLT from April 2022 to September 2024;
    • carry them in a separate income measure.

    The first two both keep the revenue identity in docs/book/usage/simulations.md for reforms; they differ only in the baseline level of household_net_income. The third takes reforms to these taxes out of household_net_income.

  4. The key. Decide it alongside D7 in PolicyEngine/uk-land-value-tax#20, which covers overseas owners, housing associations, public corporations and corporate SDLT. Add the defined benefit part of private_pension_wealth and the ratepayers outside the corporate sector. That project leaves business rates out of scope, so business rates need their own decision.

  5. consumption_shareholding. No formula reads it in 2.174.1, nor in the commit that added it (#961). Remove it, or have consumer_incident_tax_revenue_change and employer_ni_response_consumer_incidence read it in place of their own copies of the share.

Related

  • PolicyEngine/policyengine.py#567: regional runs, the same cause.
  • #774: the revenue identity behind the weighted total.
  • #843, #2296 and #2448: zero weighted totals only.
  • #2319: private school VAT rejected a share measured on each simulation's population, partly because a household calculator would measure its own charge against itself.
  • #2874 and #2877: the data build stores the engine's random draws. Proposal 2 takes the same route for national totals.
  • #1775 (coverage roadmap): its business rates item asks for reliefs and a rateable-value basis.
  • #2732: household_subset and WHOLE_POPULATION_VARIABLES.
    • The property allowance election never works these formulas out (utils/household_subset.py:28-29).
    • The Universal Credit deductions subset from #2754 is not covered by that comment or its test.

Duplicate check

On 6 November 2026 I searched open and closed issues and PRs for shareholding, business rates, corporate SDLT, corporate land value and household situations, in policyengine-uk, policyengine.py, policyengine-uk-data, policyengine-api and policyengine-app-v2, and pe-uk's changelog.

  • Nothing tracks the situation case.
  • PolicyEngine/policyengine.py#567 tracks regional runs.
  • #578, #774, #2159, #2296 and #2320 are related and closed.

🤖 Generated with Claude Code

主要语言
Python
星标
53
派生
34
平均合并
3 天 10 小时
30 天内合并 PR
321

环境准备

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

PolicyEngine/policyengine-uk 的其他 Issue

查看 PolicyEngine/policyengine-uk 的全部 Issue

相似的 Issue

更多 Python Issue

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。