shareholding, corporate_land_value and consumption_shareholding depend on who is simulated
维护者通常 2 天内回复
还没有人认领这个 Issue。
评估
调研方向
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:
shareholdingis a household's share ofcorporate_sector_wealth.business_ratesandcorporate_sdltmultiply it by UK receipts. Both levels are inhousehold_tax, and so inhousehold_net_income;gov_taxlists them too.- Under a reform,
corporate_incident_tax_revenue_changemultiplies it by the reform's revenue change.
corporate_land_valueis the same share times the corporate land parameter (£2,058.6bn, its 2024 value held flat).- The land value tax
LVTreads it directly at its corporate rate, and throughland_valueat its full rate. - All LVT rates are 0 at current law.
hbai_household_net_incomesubtractsLVT, so under an LVT reform the share reaches HBAI incomes too.
- The land value tax
consumption_shareholdingis a household's share of consumption. No formula reads it.consumer_incident_tax_revenue_changeandemployer_ni_response_consumer_incidencecompute 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:
- Household situations stop using the weighted total. This is the bug fix.
- A national total of each key that the data can carry, so any part of the UK gets its own share.
- A decision on where business rates and corporate SDLT sit.
- A review of the key.
- 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
shareholding0.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_changeat 1 (£1bn), itscorporate_incident_tax_revenue_changeis £1bn. - Under a 1% land value tax (
gov.contrib.ubi_center.land_value_tax.rate), itsLVTis £20.59bn and itshbai_household_net_incomefalls from £32,420 to 0.
- With
- Zero weight. Setting
household_weightto 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 throughpolicyengine_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) hidescorporate_wealthas a dataset-only input, but offersprivate_pension_wealthunder Assets. - Not run end to end in the app.
- policyengine-api (
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 itsshareholdingis 1/w. The extract already carriesattends_private_schoolandmonths_since_last_birthdayfrom 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-23takes it whenhousehold.simulation.dataset is not None.- #1296 made
build_from_situationstore a dataset for every situation (simulation.py:1182-1202). household_weightdefaults to 1 (variables/household/demographic/household_weight.py:11).
- #1296 made
- Other formulas.
variables/household/wealth/corporate_land_value.py:13-27has no such check.variables/household/consumption/consumption_share.py:12-19andvariables/contrib/policyengine/consumer_incident_tax_revenue_change.py:11-22use the same gate asshareholding.- So do
pre_budget_change_ons_household_income_decileand 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.pysetshareholdingdirectly. 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.yamldeclares the uprating on the parent node, and it does not reachfinancial_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_wealthaddsprivate_pension_wealthtocorporate_wealth, because pension funds hold corporate equity (#1824).corporate_sector_wealth.py:6-16calls the addition a compatibility proxy.private_pension_wealthincludes 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 inhousehold_net_income. - VAT, and since #2319 private school VAT, enter
household_taxonly as changes. The employer NICs level is only ingov_tax(data/fiscal_partition.yaml:797-804, 811-817). - Fuel, alcohol, tobacco and gambling duties enter
household_taxas levels. hbai_household_net_incomealready leaves business rates and corporate SDLT out (variables/household/income/hbai_excluded_income.py).
- The levels of business rates and corporate SDLT are in
- 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
shareholdingthe 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
shareholdinga 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_valueback 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.
- #1583 (2.79.2) moved
- 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
-
Situations (the bug; can land on its own).
- Take the weighted branch only when
built_from_data(simulation)(utils/data_source.py), asattends_private_schooldoes. Givecorporate_land_valuethe 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_schoolreturns. 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-43and check 5 oftests/test_sdlt_counted_once_properties.pywould build small datasets instead, astests/test_weighted_incidence.pydoes. - Test through
policyengine_uk.Simulation(situation=...), with and without axes: a household'sshareholdingdoes not depend on its own weight or on the number of axis points. - Nothing changes on data.
- Take the weighted branch only when
-
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, andconsumptionifconsumption_shareholdingstays. The formulas divide by it when it is supplied, and keep today's weighted total otherwise. - Uprating:
- both parts of
corporate_sector_wealthuprate with OBR GDP per capita, andhousehold_weightwith 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.
- both parts of
- 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_schoolwaits for (attends_private_school.py:72-78).
- Add a household input that holds the national weighted total of each key, the same on every row:
-
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.mdfor reforms; they differ only in the baseline level ofhousehold_net_income. The third takes reforms to these taxes out ofhousehold_net_income. -
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_wealthand the ratepayers outside the corporate sector. That project leaves business rates out of scope, so business rates need their own decision. -
consumption_shareholding. No formula reads it in 2.174.1, nor in the commit that added it (#961). Remove it, or haveconsumer_incident_tax_revenue_changeandemployer_ni_response_consumer_incidenceread 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_subsetandWHOLE_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.
- The property allowance election never works these formulas out (
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
环境准备
- 没有 Dockerfile 或 Docker Compose 文件
- 有 Pull Request 模板
- 阅读贡献指南
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
PolicyEngine/policyengine-uk 的其他 Issue
-
Lift the Universal Credit benefit cap for a claimant who receives War Widow's Pension可能已有人在做 @juaristi22 于 6 天前认领。 未关闭
难度 2/5 1-3 小时 新手友好度 72/100
PolicyEngine/policyengine-uk#2792 · 1 条评论 · 已指派 1 人 ·
维护者通常 2 天内回复
-
难度 5/5 一周以上 新手友好度 25/100
PolicyEngine/policyengine-uk#2894 ·
维护者通常 2 天内回复
-
Scottish Budget 2027-28: Scottish property income rates, council tax growth and a freeze switch, Bands I and J, and missing Scottish payments可能已有人在做 @vahid-ahmadi 于 3 天前认领。 未关闭
PolicyEngine/policyengine-uk#2891 · 1 条评论 · 已指派 1 人 ·
维护者通常 2 天内回复
-
January 2027 price cap: kWh, a mains gas flag and tariff parameters, the cap's consumption basis, and Northern Ireland可能已有人在做 @vahid-ahmadi 于 3 天前认领。 未关闭
PolicyEngine/policyengine-uk#2890 · 1 条评论 · 已指派 1 人 ·
维护者通常 2 天内回复
-
PIP review: Adult Disability Payment in Scotland, a single-activity test, and PIP as the gateway to UC health support可能已有人在做 @vahid-ahmadi 于 3 天前认领。 未关闭
PolicyEngine/policyengine-uk#2889 · 1 条评论 · 已指派 1 人 ·
维护者通常 2 天内回复
查看 PolicyEngine/policyengine-uk 的全部 Issue
相似的 Issue
-
enhancement
难度 2/5 1-3 小时 新手友好度 72/100
维护者通常 2 天内回复
-
bug
难度 2/5 1-3 小时 新手友好度 78/100
cunla/fakeredis-py#636 ·
维护者通常 2 天内回复
-
namespace
难度 2/5 1-3 小时 新手友好度 62/100
EclipseFdn/open-vsx.org#15470 ·
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 78/100
opensafely-core/job-server#6180 · 1 条评论 ·
维护者通常 1 天内回复
-
area:models enhancement
难度 2/5 1-3 小时 新手友好度 76/100
维护者通常 1 天内回复