From f84bc24b46ab08b7b318fd3861276948c96e2717 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mar=C3=ADa=20Juaristi?= <127882282+juaristi22@users.noreply.github.com> Date: Sat, 17 Oct 2026 04:09:20 +0100 Subject: [PATCH 1/3] Document employer National Insurance incidence assumptions --- changelog.d/2168.changed.md | 1 + .../book/assumptions/employer-ni-incidence.md | 183 ++++++++++++++++++ .../policyengine/employer_ni/README.md | 24 ++- .../employer_ni/capital_incidence.yaml | 6 +- .../employer_ni/consumer_incidence.yaml | 6 +- .../employer_ni/employee_incidence.yaml | 6 +- 6 files changed, 222 insertions(+), 4 deletions(-) create mode 100644 changelog.d/2168.changed.md create mode 100644 docs/book/assumptions/employer-ni-incidence.md diff --git a/changelog.d/2168.changed.md b/changelog.d/2168.changed.md new file mode 100644 index 0000000000..1d6027d123 --- /dev/null +++ b/changelog.d/2168.changed.md @@ -0,0 +1 @@ +- Documented employer NI incidence assumptions, input handling, revenue measures and current household examples. diff --git a/docs/book/assumptions/employer-ni-incidence.md b/docs/book/assumptions/employer-ni-incidence.md new file mode 100644 index 0000000000..b5021aa515 --- /dev/null +++ b/docs/book/assumptions/employer-ni-incidence.md @@ -0,0 +1,183 @@ +# Employer National Insurance incidence + +Employer National Insurance reforms can change wages, tax receipts and household +income through the model's incidence assumptions. The parameters under +`gov.contrib.policyengine.employer_ni` default to: + +| Parameter | Default | Implemented role | +|---|---:|---| +| `employee_incidence` | 1 | Scales the computed fixed-employer-cost pay adjustment | +| `consumer_incidence` | 0.5 | Allocates remaining employer-cost changes by consumption shares | +| `capital_incidence` | 0.5 | Allocates remaining employer-cost changes by corporate wealth shares | + +These values are adjustable model assumptions. No empirical estimate is claimed +for the defaults, and this page does not select a preferred incidence assumption. + +## Wage adjustment and supplied income + +The wage channel compares the baseline and reform employer-NI rate, secondary +threshold and employer-pension exemption. For its eligible earnings branch, it +solves for pay holding modeled employer cost fixed, then multiplies the pay change +by `employee_incidence`. At `1`, the full computed adjustment applies in the same +model year. The formula returns zero without a baseline, without a relevant +parameter change, at employee incidence zero, or for prior pay below the baseline +secondary threshold. This describes the implemented calculation, including its +threshold guard; it does not assert a general equilibrium response for every worker. + +`employment_income` adds this adjustment to `employment_income_before_lsr` and +other earnings components. An input actually stored in the final +`employment_income` holder overrides its formula. The UK `Simulation` constructor +moves situation inputs named `employment_income` to `employment_income_before_lsr`. +Consequently, putting that field in a constructor situation can still allow the +wage response. The example below demonstrates an override with `set_input` after +construction and before calculation. Dataset and other input routes should be +checked for which earnings holder they populate. + +The [wage formula](https://github.com/PolicyEngine/policyengine-uk/blob/b4dbaf57a4ebb1d1f0ba5f7a384795422828db75/policyengine_uk/variables/contrib/policyengine/employer_ni/employer_ni_fixed_employer_cost_change.py), +[earnings components](https://github.com/PolicyEngine/policyengine-uk/blob/b4dbaf57a4ebb1d1f0ba5f7a384795422828db75/policyengine_uk/variables/input/employment_income.py) +and [constructor remapping](https://github.com/PolicyEngine/policyengine-uk/blob/b4dbaf57a4ebb1d1f0ba5f7a384795422828db75/policyengine_uk/simulation.py#L350) +provide the implementation details. + +## Consumer and capital channels + +Consumer and capital incidence apply to the remaining change in modeled employer +cost after the wage response. Each is zero when its own parameter is zero or +employee incidence is one. Otherwise, the formulas use person weights and allocate +that change using household consumption or corporate wealth per person. Their +weighted aggregate exposure must be nonzero; they also return zero when the +simulation has no dataset attribute. + +An attribute can be present with `dataset=None`, so that guard alone is not a +promise that every household calculation has zero residual incidence. The example +below has no representative dataset or consumption/wealth exposures and returns +zero for both residual channels. It cannot show their population effects. + +Setting `employee_incidence` to zero alone leaves the consumer and capital +parameters at `0.5`. Set **all three parameters to zero** to disable these three +employer-NI incidence channels. This does not disable unrelated labor-supply or +other behavioral responses. + +The response amounts enter `household_tax`; those same variables are not added to +`gov_tax`. See the [consumer](https://github.com/PolicyEngine/policyengine-uk/blob/b4dbaf57a4ebb1d1f0ba5f7a384795422828db75/policyengine_uk/variables/contrib/policyengine/employer_ni/employer_ni_response_consumer_incidence.py) +and [capital](https://github.com/PolicyEngine/policyengine-uk/blob/b4dbaf57a4ebb1d1f0ba5f7a384795422828db75/policyengine_uk/variables/contrib/policyengine/employer_ni/employer_ni_response_capital_incidence.py) +allocation formulas. + +## Measures and aggregation + +Report these quantities separately: + +- Gross employer-NI liability or its reform change: `ni_employer`, a person amount. +- Net government balance: `gov_balance = gov_tax - gov_spending`, a household amount. + A positive reform change improves the modeled balance. Wage changes can affect + income tax, employee NI and benefits, so this is not a gross employer-NI yield. +- Household incidence here: the change in nominal `household_net_income`, defined + as `household_market_income + household_benefits - household_tax - pension_contributions`. + A negative change is a household loss under this measure. + +The example sums each output over one person or one household, as appropriate. +Population results require the matching person or household weights. Do not sum +household values repeated on person records as if they were distinct households. +These measures do not by themselves establish a representative budget costing. + +## Worked household example + +For model year 2026, consider one 40-year-old London employee with £40,000 of +pre-response earnings, no pension contributions and no other supplied income. +Raise the employer rate from 15% to 16%, holding the other parameters fixed. Current +main includes the annual secondary-threshold correction: £5,000 rather than the +old rounded-weekly equivalent of £4,992. Each comparison below applies the same +incidence assumptions to its baseline and reform. + +Annual changes in pounds, rounded to two decimal places: + +| Input and incidence assumptions | Employer NI | Income tax | Employee NI | Gross pay | Government balance | Household net income | +|---|---:|---:|---:|---:|---:|---:| +| Components; employee 1, consumer/capital 0.5 | +301.72 | −60.34 | −24.14 | −301.72 | +217.24 | −217.24 | +| Components; employee 0, consumer/capital 0.5 | +350.00 | 0.00 | 0.00 | 0.00 | +350.00 | 0.00 | +| Components; all three zero | +350.00 | 0.00 | 0.00 | 0.00 | +350.00 | 0.00 | +| Pay set after construction; employee 1 | +350.00 | 0.00 | 0.00 | 0.00 | +350.00 | 0.00 | + +At fixed pay, the extra liability is `0.01 × (40,000 − 5,000) = £350`. +With full wage incidence, the baseline employer cost is £45,250. Solving +`pay + 0.16 × (pay − 5,000) = 45,250` gives pay of approximately £39,698.28. +The income-tax and employee-NI changes partly offset the wage fall. The last three +rows coincide in this example; they do not imply identical population incidence. + +Run this snippet from a checkout's supported Python environment: + +```python +from policyengine_core.reforms import Reform +from policyengine_uk import Simulation + +YEAR = 2026 +SPAN = "2026-01-01.2026-12-31" +INCIDENCE = "gov.contrib.policyengine.employer_ni." +RATE = "gov.hmrc.national_insurance.class_1.rates.employer" +VARIABLES = [ + "ni_employer", "income_tax", "ni_employee", "employment_income", + "gov_balance", "household_net_income", +] + + +def result(employee, consumer, capital, direct_pay=False): + pay_input = "employment_income" if direct_pay else "employment_income_before_lsr" + situation = { + "people": {"worker": {"age": {YEAR: 40}, pay_input: {YEAR: 40_000}}}, + "benunits": {"family": {"members": ["worker"]}}, + "households": {"home": {"members": ["worker"], "region": {YEAR: "LONDON"}}}, + } + assumptions = { + INCIDENCE + "employee_incidence": {SPAN: employee}, + INCIDENCE + "consumer_incidence": {SPAN: consumer}, + INCIDENCE + "capital_incidence": {SPAN: capital}, + } + baseline = Simulation( + situation=situation, + reform=Reform.from_dict(assumptions, country_id="uk"), + ) + reform = Simulation( + situation=situation, + reform=Reform.from_dict({**assumptions, RATE: {SPAN: 0.16}}, country_id="uk"), + ) + if direct_pay: + # UK construction moves situation earnings to the pre-response input. + # Explicitly supply the final earnings holder after construction. + baseline.set_input("employment_income", YEAR, [40_000]) + reform.set_input("employment_income", YEAR, [40_000]) + before = {v: float(baseline.calculate(v, YEAR).sum()) for v in VARIABLES} + after = {v: float(reform.calculate(v, YEAR).sum()) for v in VARIABLES} + return { + "baseline": before, "reform": after, + "delta": {v: after[v] - before[v] for v in VARIABLES}, + "residual_channels": { + v: float(reform.calculate(v, YEAR).sum()) for v in ( + "employer_ni_response_consumer_incidence", + "employer_ni_response_capital_incidence", + ) + }, + } + + +rows = { + "Employee 1; consumer/capital 0.5": result(1, 0.5, 0.5), + "Employee 0; consumer/capital 0.5": result(0, 0.5, 0.5), + "All three zero": result(0, 0, 0), + "Pay set after construction; employee 1": result(1, 0.5, 0.5, direct_pay=True), +} +for label, row in rows.items(): + print(label, {v: round(value, 2) for v, value in row["delta"].items()}) +``` + +The residual-channel outputs are retained in `rows` for inspection. No private +microdata, representative population estimate or empirical calibration is used. + +## Comparing an official costing + +Match the definition and horizon of the specific official figure before comparing +it with a model result. For example, the +[OBR's October 2024 Economic and fiscal outlook](https://obr.uk/efo/economic-and-fiscal-outlook-october-2024/) +reported £25.7 billion of employer-NICs revenue in 2029–30 before indirect economic +effects (overview paragraph 1.7). Its paragraph 2.14 assumed about 60% of the cost +would pass to employees in the short term, with more adjustment later through +prices and nominal wages. These are dated forecast assumptions, not evidence for +the model's defaults. Other official headlines can use different definitions. diff --git a/policyengine_uk/parameters/gov/contrib/policyengine/employer_ni/README.md b/policyengine_uk/parameters/gov/contrib/policyengine/employer_ni/README.md index 079c942a05..fecfda74b4 100644 --- a/policyengine_uk/parameters/gov/contrib/policyengine/employer_ni/README.md +++ b/policyengine_uk/parameters/gov/contrib/policyengine/employer_ni/README.md @@ -1 +1,23 @@ -# Employer NI +# Employer NI incidence + +These parameters describe how the model allocates a change in employer National +Insurance costs. Their defaults, unchanged from 2010, are employee incidence `1`, +consumer incidence `0.5` and capital incidence `0.5`. They are adjustable modeling +assumptions; no empirical estimate is claimed for the defaults. + +For eligible earnings computed from components, employee incidence `1` applies the +computed fixed-employer-cost pay adjustment in the same model year. This changes +income tax and employee NI as well as gross pay. A supplied final +`employment_income` holder overrides that formula. The UK `Simulation` constructor +moves situation earnings into `employment_income_before_lsr`, so a constructor +field alone does not demonstrate that override. + +Setting employee incidence to `0` leaves the consumer and capital channels active +where population weights and exposure totals support them. Set all three incidence +parameters to `0` to disable these three channels. Other labor-supply responses +have separate controls. + +Report employer-NI liability, net `gov_balance` and household income effects +separately. The [assumptions page](../../../../../../docs/book/assumptions/employer-ni-incidence.md) +explains the implemented guards, aggregation, current worked example and a dated +OBR comparison. A single-household example does not establish population incidence. diff --git a/policyengine_uk/parameters/gov/contrib/policyengine/employer_ni/capital_incidence.yaml b/policyengine_uk/parameters/gov/contrib/policyengine/employer_ni/capital_incidence.yaml index edb1a27015..bf1ed0685d 100644 --- a/policyengine_uk/parameters/gov/contrib/policyengine/employer_ni/capital_incidence.yaml +++ b/policyengine_uk/parameters/gov/contrib/policyengine/employer_ni/capital_incidence.yaml @@ -1,6 +1,10 @@ -description: Fraction of (remaining after employee incidence) employer NI that is borne by capital. +description: >- + Share of the remaining modeled employer-cost change allocated to capital after employee incidence. The default is 0.5. This channel is inactive at employee incidence 1 or capital incidence 0; otherwise it uses population person weights and household corporate wealth shares, with zero returned when the dataset attribute is absent or total wealth is zero. The amount enters household_tax. Set all three incidence parameters to 0 to disable these three channels. No empirical estimate is claimed for this default. values: 2010-01-01: 0.5 metadata: label: Employer NI capital incidence unit: /1 + reference: + - title: PolicyEngine-UK implemented employer NI incidence mechanism + href: https://github.com/PolicyEngine/policyengine-uk/blob/b4dbaf57a4ebb1d1f0ba5f7a384795422828db75/policyengine_uk/variables/contrib/policyengine/employer_ni/employer_ni_response_capital_incidence.py diff --git a/policyengine_uk/parameters/gov/contrib/policyengine/employer_ni/consumer_incidence.yaml b/policyengine_uk/parameters/gov/contrib/policyengine/employer_ni/consumer_incidence.yaml index 765298c718..dccb0d44b9 100644 --- a/policyengine_uk/parameters/gov/contrib/policyengine/employer_ni/consumer_incidence.yaml +++ b/policyengine_uk/parameters/gov/contrib/policyengine/employer_ni/consumer_incidence.yaml @@ -1,6 +1,10 @@ -description: Fraction of (remaining after employee incidence) employer NI that is borne by consumers. +description: >- + Share of the remaining modeled employer-cost change allocated to consumers after employee incidence. The default is 0.5. This channel is inactive at employee incidence 1 or consumer incidence 0; otherwise it uses population person weights and household consumption shares, with zero returned when the dataset attribute is absent or total consumption is zero. The amount enters household_tax. Set all three incidence parameters to 0 to disable these three channels. No empirical estimate is claimed for this default. values: 2010-01-01: 0.5 metadata: label: Employer NI consumer incidence unit: /1 + reference: + - title: PolicyEngine-UK implemented employer NI incidence mechanism + href: https://github.com/PolicyEngine/policyengine-uk/blob/b4dbaf57a4ebb1d1f0ba5f7a384795422828db75/policyengine_uk/variables/contrib/policyengine/employer_ni/employer_ni_response_consumer_incidence.py diff --git a/policyengine_uk/parameters/gov/contrib/policyengine/employer_ni/employee_incidence.yaml b/policyengine_uk/parameters/gov/contrib/policyengine/employer_ni/employee_incidence.yaml index b0aaf7efeb..7ca09ae21b 100644 --- a/policyengine_uk/parameters/gov/contrib/policyengine/employer_ni/employee_incidence.yaml +++ b/policyengine_uk/parameters/gov/contrib/policyengine/employer_ni/employee_incidence.yaml @@ -1,6 +1,10 @@ -description: Fraction of employer NI that is borne by employees. +description: >- + Share of the computed fixed-employer-cost pay change passed to employees in the same model year. The default of 1 applies the full adjustment to eligible earnings computed from components; 0 disables this wage channel. A supplied final employment_income holder overrides that formula, while UK Simulation construction remaps situation earnings to employment_income_before_lsr. The consumer and capital assumptions allocate remaining modeled employer-cost changes. This is a modeling assumption, not an empirically estimated default. values: 2010-01-01: 1 metadata: label: Employer NI employee incidence unit: /1 + reference: + - title: PolicyEngine-UK implemented employer NI incidence mechanism + href: https://github.com/PolicyEngine/policyengine-uk/blob/b4dbaf57a4ebb1d1f0ba5f7a384795422828db75/policyengine_uk/variables/contrib/policyengine/employer_ni/employer_ni_fixed_employer_cost_change.py From 2e16ce0b60386329905f5321972641b0cc028c3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mar=C3=ADa=20Juaristi?= <127882282+juaristi22@users.noreply.github.com> Date: Sat, 17 Oct 2026 04:29:23 +0100 Subject: [PATCH 2/3] Format employer NI documentation example --- docs/book/assumptions/employer-ni-incidence.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/docs/book/assumptions/employer-ni-incidence.md b/docs/book/assumptions/employer-ni-incidence.md index b5021aa515..78fadb32a6 100644 --- a/docs/book/assumptions/employer-ni-incidence.md +++ b/docs/book/assumptions/employer-ni-incidence.md @@ -114,8 +114,12 @@ SPAN = "2026-01-01.2026-12-31" INCIDENCE = "gov.contrib.policyengine.employer_ni." RATE = "gov.hmrc.national_insurance.class_1.rates.employer" VARIABLES = [ - "ni_employer", "income_tax", "ni_employee", "employment_income", - "gov_balance", "household_net_income", + "ni_employer", + "income_tax", + "ni_employee", + "employment_income", + "gov_balance", + "household_net_income", ] @@ -147,10 +151,12 @@ def result(employee, consumer, capital, direct_pay=False): before = {v: float(baseline.calculate(v, YEAR).sum()) for v in VARIABLES} after = {v: float(reform.calculate(v, YEAR).sum()) for v in VARIABLES} return { - "baseline": before, "reform": after, + "baseline": before, + "reform": after, "delta": {v: after[v] - before[v] for v in VARIABLES}, "residual_channels": { - v: float(reform.calculate(v, YEAR).sum()) for v in ( + v: float(reform.calculate(v, YEAR).sum()) + for v in ( "employer_ni_response_consumer_incidence", "employer_ni_response_capital_incidence", ) From f72a29be5f4fcafd642c352defe11b840382f5ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mar=C3=ADa=20Juaristi?= <127882282+juaristi22@users.noreply.github.com> Date: Sun, 18 Oct 2026 23:33:26 +0100 Subject: [PATCH 3/3] Fix issues from review: shorten employer NI incidence descriptions --- .../gov/contrib/policyengine/employer_ni/capital_incidence.yaml | 2 +- .../contrib/policyengine/employer_ni/consumer_incidence.yaml | 2 +- .../contrib/policyengine/employer_ni/employee_incidence.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/policyengine_uk/parameters/gov/contrib/policyengine/employer_ni/capital_incidence.yaml b/policyengine_uk/parameters/gov/contrib/policyengine/employer_ni/capital_incidence.yaml index bf1ed0685d..89b8d73a41 100644 --- a/policyengine_uk/parameters/gov/contrib/policyengine/employer_ni/capital_incidence.yaml +++ b/policyengine_uk/parameters/gov/contrib/policyengine/employer_ni/capital_incidence.yaml @@ -1,5 +1,5 @@ description: >- - Share of the remaining modeled employer-cost change allocated to capital after employee incidence. The default is 0.5. This channel is inactive at employee incidence 1 or capital incidence 0; otherwise it uses population person weights and household corporate wealth shares, with zero returned when the dataset attribute is absent or total wealth is zero. The amount enters household_tax. Set all three incidence parameters to 0 to disable these three channels. No empirical estimate is claimed for this default. + Share of the remaining employer-cost change allocated to capital after employee incidence; set all three incidence parameters to 0 to disable these channels. values: 2010-01-01: 0.5 metadata: diff --git a/policyengine_uk/parameters/gov/contrib/policyengine/employer_ni/consumer_incidence.yaml b/policyengine_uk/parameters/gov/contrib/policyengine/employer_ni/consumer_incidence.yaml index dccb0d44b9..f23aab2df3 100644 --- a/policyengine_uk/parameters/gov/contrib/policyengine/employer_ni/consumer_incidence.yaml +++ b/policyengine_uk/parameters/gov/contrib/policyengine/employer_ni/consumer_incidence.yaml @@ -1,5 +1,5 @@ description: >- - Share of the remaining modeled employer-cost change allocated to consumers after employee incidence. The default is 0.5. This channel is inactive at employee incidence 1 or consumer incidence 0; otherwise it uses population person weights and household consumption shares, with zero returned when the dataset attribute is absent or total consumption is zero. The amount enters household_tax. Set all three incidence parameters to 0 to disable these three channels. No empirical estimate is claimed for this default. + Share of the remaining employer-cost change allocated to consumers after employee incidence; set all three incidence parameters to 0 to disable these channels. values: 2010-01-01: 0.5 metadata: diff --git a/policyengine_uk/parameters/gov/contrib/policyengine/employer_ni/employee_incidence.yaml b/policyengine_uk/parameters/gov/contrib/policyengine/employer_ni/employee_incidence.yaml index 7ca09ae21b..4925deb028 100644 --- a/policyengine_uk/parameters/gov/contrib/policyengine/employer_ni/employee_incidence.yaml +++ b/policyengine_uk/parameters/gov/contrib/policyengine/employer_ni/employee_incidence.yaml @@ -1,5 +1,5 @@ description: >- - Share of the computed fixed-employer-cost pay change passed to employees in the same model year. The default of 1 applies the full adjustment to eligible earnings computed from components; 0 disables this wage channel. A supplied final employment_income holder overrides that formula, while UK Simulation construction remaps situation earnings to employment_income_before_lsr. The consumer and capital assumptions allocate remaining modeled employer-cost changes. This is a modeling assumption, not an empirically estimated default. + Share of the fixed-employer-cost pay change passed to employees in the same model year; set all three incidence parameters to 0 to disable these channels. values: 2010-01-01: 1 metadata: