diff --git a/.vscode/python.code-snippets b/.vscode/python.code-snippets index 90b102510..4c7885278 100644 --- a/.vscode/python.code-snippets +++ b/.vscode/python.code-snippets @@ -21,7 +21,7 @@ " entity = ${4:Person}", " definition_period = ${5:YEAR}", " value_type = ${6:float}", - " unit = \"${7:currency-GBP}\"", + " unit = ${7:GBP}", "", "" ], diff --git a/changelog_entry.yaml b/changelog_entry.yaml index e69de29bb..c78b8f1dd 100644 --- a/changelog_entry.yaml +++ b/changelog_entry.yaml @@ -0,0 +1,4 @@ +- bump: minor + changes: + changed: + - Validated and standardised National Insurance variables. diff --git a/policyengine_uk/data/datasets/__init__.py b/policyengine_uk/data/datasets/__init__.py index 81b51924e..0f0cdf573 100644 --- a/policyengine_uk/data/datasets/__init__.py +++ b/policyengine_uk/data/datasets/__init__.py @@ -15,6 +15,7 @@ CalibratedSPIEnhancedPooledFRS_2018_20, CalibratedSPIEnhancedPooledFRS_2019_21, EnhancedFRS, + UKMOD_FRS_2018, ) DATASETS = [ @@ -34,4 +35,5 @@ CalibratedSPIEnhancedPooledFRS_2018_20, CalibratedSPIEnhancedPooledFRS_2019_21, EnhancedFRS, + UKMOD_FRS_2018, ] diff --git a/policyengine_uk/data/datasets/frs/__init__.py b/policyengine_uk/data/datasets/frs/__init__.py index 816709241..01bbc8317 100644 --- a/policyengine_uk/data/datasets/frs/__init__.py +++ b/policyengine_uk/data/datasets/frs/__init__.py @@ -22,3 +22,4 @@ ) from .stacked_frs import StackedFRS, PooledFRS_2018_20, PooledFRS_2019_21 from .enhanced_frs import EnhancedFRS +from .ukmod import UKMOD_FRS_2018 diff --git a/policyengine_uk/data/datasets/frs/ukmod.py b/policyengine_uk/data/datasets/frs/ukmod.py new file mode 100644 index 000000000..1dfdac0c8 --- /dev/null +++ b/policyengine_uk/data/datasets/frs/ukmod.py @@ -0,0 +1,63 @@ +import pandas as pd +from policyengine_uk.data.storage import STORAGE_FOLDER +import numpy as np +from policyengine_core.data import Dataset + + +class UKMOD_FRS_2018(Dataset): + name = "ukmod_frs_2018" + label = "UKMOD (2018-19 FRS)" + data_format = Dataset.TIME_PERIOD_ARRAYS + file_path = STORAGE_FOLDER / "ukmod_frs_2018.h5" + time_period = "2018" + + def generate(self): + data = {} + ukmod_output = pd.read_csv( + STORAGE_FOLDER / "uk_2018_std.txt", delimiter="\t" + ) + ukmod_input = pd.read_csv( + STORAGE_FOLDER / "uk_2018_a4.txt", delimiter="\t" + ) + output_columns = [ + column + for column in ukmod_output.columns + if column not in ukmod_input.columns + ] + ukmod = pd.merge( + ukmod_output[output_columns + ["idperson"]], + ukmod_input, + on="idperson", + how="right", + ) + # Add ID variables first + data["person_id"] = ukmod.idperson + data["person_benunit_id"] = person_benunit_id = ( + ukmod.idorigbenunit * 10 + ukmod.idorighh + ) + data["person_household_id"] = person_household_id = ( + ukmod.idorighh * 100 + ) + data["person_state_id"] = np.ones_like(ukmod.idperson) + + data["benunit_id"] = person_benunit_id.unique() + data["household_id"] = person_household_id.unique() + data["state_id"] = np.array([1]) + + data["age"] = ukmod.dag.values + data["gender"] = np.where( + ukmod.dgn == 0, + "FEMALE", + "MALE", + ).astype("S") + data["employment_income"] = ukmod.yem.values * 12 + data["self_employment_income"] = ukmod.yse.values * 12 + data["pension_income"] = ukmod.ypp.values * 12 + data["statutory_sick_pay"] = ukmod.bhlwk.values * 12 + data["statutory_maternity_pay"] = ukmod.bmact_s.values * 12 + data["statutory_paternity_pay"] = ukmod.bpact_s.values * 12 + + for variable in data: + data[variable] = {"2018": data[variable]} + + self.save_dataset(data) diff --git a/policyengine_uk/data/storage/__init__.py b/policyengine_uk/data/storage/__init__.py new file mode 100644 index 000000000..111a9735f --- /dev/null +++ b/policyengine_uk/data/storage/__init__.py @@ -0,0 +1,3 @@ +from pathlib import Path + +STORAGE_FOLDER = Path(__file__).parent diff --git a/policyengine_uk/parameters/gov/dwp/state_pension/female_age.yaml b/policyengine_uk/parameters/gov/dwp/state_pension/female_age.yaml index 02be4b75c..cdef4ca9c 100644 --- a/policyengine_uk/parameters/gov/dwp/state_pension/female_age.yaml +++ b/policyengine_uk/parameters/gov/dwp/state_pension/female_age.yaml @@ -5,7 +5,7 @@ values: 2012-01-01: 62 2014-01-01: 63 2016-01-01: 64 - 2018-01-01: 65 + 2019-01-01: 65 2020-01-01: 66 metadata: unit: year diff --git a/policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/thresholds/secondary_threshold.yaml b/policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/thresholds/secondary_threshold.yaml index d4bac076a..0371cf12f 100644 --- a/policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/thresholds/secondary_threshold.yaml +++ b/policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/thresholds/secondary_threshold.yaml @@ -8,7 +8,6 @@ metadata: - https://assets.publishing.service.gov.uk/government/uploads/system/uploads/attachment_data/file/882271/Table-a4.pdf - https://www.gov.uk/government/publications/rates-and-allowances-national-insurance-contributions/rates-and-allowances-national-insurance-contributions unit: currency-GBP - uprating: gov.benefit_uprating_cpi values: 2015-04-06: 155 2016-04-06: 155 @@ -24,3 +23,4 @@ values: title: The Social Security (Contributions) (Rates, Limits and Thresholds Amendments and National Insurance Funds Payments) Regulations 2022(6) value: 175 + 2024-01-01: 175 \ No newline at end of file diff --git a/policyengine_uk/tests/microsimulation/test_against_ukmod.py b/policyengine_uk/tests/microsimulation/test_against_ukmod.py new file mode 100644 index 000000000..12bfc0ce4 --- /dev/null +++ b/policyengine_uk/tests/microsimulation/test_against_ukmod.py @@ -0,0 +1,66 @@ +from policyengine_uk import Microsimulation +from policyengine_uk.data.datasets import UKMOD_FRS_2018 +from policyengine_uk.data.storage import STORAGE_FOLDER +import pandas as pd +import numpy as np +import pytest + +SKIP_UKMOD_TESTS = True + +if not SKIP_UKMOD_TESTS: + ukmod_output = pd.read_csv( + STORAGE_FOLDER / "uk_2018_std.txt", delimiter="\t" + ) + ukmod_input = pd.read_csv( + STORAGE_FOLDER / "uk_2018_a4.txt", delimiter="\t" + ) + output_columns = [ + column + for column in ukmod_output.columns + if column not in ukmod_input.columns + ] + ukmod = pd.merge( + ukmod_output[output_columns + ["idperson"]], + ukmod_input, + on="idperson", + how="right", + ) + + UKMOD_FRS_2018().generate() + sim = Microsimulation(dataset="ukmod_frs_2018") + + +@pytest.mark.skip(reason="UKMOD data not publicly shareable") +def test_ni_class_1(): + # NI Class 1 income matches. + assert np.allclose( + sim.calculate("ni_class_1_income").values, + ukmod.il_empniearns.values * 12, + atol=1, + ) + + +@pytest.mark.skip(reason="UKMOD data not publicly shareable") +def test_ni_class_1_employee(): + # NI contributions are off by more because the thresholds change mid-year, + # and PolicyEngine simulates over the full year while UKMOD simulates one + # month. + assert np.allclose( + sim.calculate("ni_class_1_employee").values, + ukmod.tscee_s.values * 12, + atol=50, + ) + + +@pytest.mark.skip(reason="UKMOD data not publicly shareable") +def test_ni_self_employed(): + # NI self-employed contributions don't match entirely for people with both + # self-employed and employment income. This might be due to a different + # interpretation of the rules around capped NI contributions (our Class 4 + # maximum uses the legislation as a reference). + + error = np.abs( + sim.calculate("ni_self_employed").values - ukmod.tscse_s.values * 12 + ) + + assert (error < 50).mean() > 0.99 diff --git a/policyengine_uk/tests/policy/baseline/finance/tax/national_insurance/class_1.yaml b/policyengine_uk/tests/policy/baseline/finance/tax/national_insurance/class_1.yaml deleted file mode 100644 index c807ee196..000000000 --- a/policyengine_uk/tests/policy/baseline/finance/tax/national_insurance/class_1.yaml +++ /dev/null @@ -1,22 +0,0 @@ -- name: NI Class 1 for earner - period: 2021 - absolute_error_margin: 10 - input: - employment_income: 30000 - output: - employee_NI_class_1: 2460 -- name: NI Class 1 for earner in 2019 - period: 2019 - absolute_error_margin: 30 - input: - employment_income: 30000 - output: - employee_NI_class_1: 2589 -- name: NI Class 1 for no earnings - period: 2019 - absolute_error_margin: 0 - input: - employment_income: 0 - output: - employee_NI_class_1: 0 - employer_NI_class_1: 0 diff --git a/policyengine_uk/tests/policy/baseline/finance/tax/national_insurance/class_2.yaml b/policyengine_uk/tests/policy/baseline/finance/tax/national_insurance/class_2.yaml deleted file mode 100644 index 047f15817..000000000 --- a/policyengine_uk/tests/policy/baseline/finance/tax/national_insurance/class_2.yaml +++ /dev/null @@ -1,21 +0,0 @@ -- name: NI Class 2 for trader - period: 2020 - absolute_error_margin: 10 - input: - self_employment_income: 30000 - output: - NI_class_2: 159 -- name: NI Class 2 for no income - period: 2020 - absolute_error_margin: 0 - input: - self_employment_income: 0 - output: - NI_class_2: 0 -- name: NI Class 2 for income under limit - period: 2020 - absolute_error_margin: 0 - input: - self_employment_income: 5000 - output: - NI_class_2: 0 diff --git a/policyengine_uk/tests/policy/baseline/finance/tax/national_insurance/class_4.yaml b/policyengine_uk/tests/policy/baseline/finance/tax/national_insurance/class_4.yaml deleted file mode 100644 index b314a093f..000000000 --- a/policyengine_uk/tests/policy/baseline/finance/tax/national_insurance/class_4.yaml +++ /dev/null @@ -1,21 +0,0 @@ -- name: NI Class 4 for no income - period: 2020 - absolute_error_margin: 0 - input: - self_employment_income: 0 - output: - NI_class_4: 0 -- name: NI Class 4 for income under upper limit - period: 2020 - absolute_error_margin: 10 - input: - self_employment_income: 50000 - output: - NI_class_4: 3723 -- name: NI Class 4 for income over upper limit - period: 2020 - absolute_error_margin: 10 - input: - self_employment_income: 60000 - output: - NI_class_4: 3923 diff --git a/policyengine_uk/tests/policy/baseline/finance/tax/national_insurance/national_insurance.yaml b/policyengine_uk/tests/policy/baseline/finance/tax/national_insurance/national_insurance.yaml deleted file mode 100644 index 64d92f871..000000000 --- a/policyengine_uk/tests/policy/baseline/finance/tax/national_insurance/national_insurance.yaml +++ /dev/null @@ -1,7 +0,0 @@ -- name: Trader with some income - period: 2020 - absolute_error_margin: 10 - input: - self_employment_income: 34003 - output: - national_insurance: 2439 diff --git a/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/class_1/ni_class_1_employee.yaml b/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/class_1/ni_class_1_employee.yaml new file mode 100644 index 000000000..d8cfd51ff --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/class_1/ni_class_1_employee.yaml @@ -0,0 +1,22 @@ +- name: NI Class 1 employee contributions - below PT + period: 2023 + input: + ni_class_1_income: 11_000 + output: + ni_class_1_employee: 0 + +- name: NI Class 1 employee contributions - between PT and UEL + period: 2023 + absolute_error_margin: 1 + input: + ni_class_1_income: 30_000 + output: + ni_class_1_employee: (30_000 - 12_570) * 0.12 + +- name: NI Class 1 employee contributions - above UEL + period: 2023 + absolute_error_margin: 1 + input: + ni_class_1_income: 70000 + output: + ni_class_1_employee: (50_270 - 12_570) * 0.12 + (70_000 - 50_270) * 0.0325 diff --git a/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/class_1/ni_class_1_employee_additional.yaml b/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/class_1/ni_class_1_employee_additional.yaml new file mode 100644 index 000000000..c3132fff8 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/class_1/ni_class_1_employee_additional.yaml @@ -0,0 +1,15 @@ +- name: NI Class 1 employee additional contributions - income below UEL + period: 2023 + absolute_error_margin: 1 + input: + ni_class_1_income: 30_000 + output: + ni_class_1_employee_additional: 0 + +- name: NI Class 1 employee additional contributions - income above UEL + period: 2023 + absolute_error_margin: 1 + input: + ni_class_1_income: 100_000 + output: + ni_class_1_employee_additional: (100_000 - 50_270) * 0.0325 diff --git a/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/class_1/ni_class_1_employee_primary.yaml b/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/class_1/ni_class_1_employee_primary.yaml new file mode 100644 index 000000000..47bdd17f4 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/class_1/ni_class_1_employee_primary.yaml @@ -0,0 +1,7 @@ +- name: £20k income has NI Class 1 liability + period: 2023 + absolute_error_margin: 1 + input: + employment_income: 20_000 + output: + ni_class_1_employee_primary: (20_000 - 12_570) * 0.12 \ No newline at end of file diff --git a/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/class_1/ni_class_1_employer.yaml b/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/class_1/ni_class_1_employer.yaml new file mode 100644 index 000000000..7b73d917e --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/class_1/ni_class_1_employer.yaml @@ -0,0 +1,15 @@ +- name: NI Class 1 employer contributions for low income in 2023 + period: 2023 + absolute_error_margin: 0.01 + input: + ni_class_1_income: 8_000 # Annual income below secondary threshold + output: + ni_class_1_employer: 0.00 # Expected employer contributions + +- name: NI Class 1 employer contributions for moderate income in 2023 + period: 2023 + absolute_error_margin: 0.01 + input: + ni_class_1_income: 30_000 # Annual income above secondary threshold but below upper limit + output: + ni_class_1_employer: (30_000 - 175 * 52) * 0.138 # Expected employer contributions diff --git a/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/class_1/ni_class_1_income.yaml b/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/class_1/ni_class_1_income.yaml new file mode 100644 index 000000000..abeae1edc --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/class_1/ni_class_1_income.yaml @@ -0,0 +1,9 @@ +- name: NI Class 1 income sums income components. + period: 2023 + input: + employment_income: 1 + statutory_sick_pay: 2 + statutory_maternity_pay: 4 + statutory_paternity_pay: 8 + output: + ni_class_1_income: 15 diff --git a/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/class_1/ni_liable.yaml b/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/class_1/ni_liable.yaml new file mode 100644 index 000000000..a0834d81c --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/class_1/ni_liable.yaml @@ -0,0 +1,20 @@ +- name: Child isn't liable for NI. + period: 2023 + input: + age: 15 + output: + ni_liable: false + +- name: Working-age adult is liable for NI. + period: 2023 + input: + age: 35 + output: + ni_liable: true + +- name: Retired adult isn't liable for NI. + period: 2023 + input: + age: 70 + output: + ni_liable: false diff --git a/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/class_2/ni_class_2.yaml b/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/class_2/ni_class_2.yaml new file mode 100644 index 000000000..3a458e7fa --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/class_2/ni_class_2.yaml @@ -0,0 +1,13 @@ +- name: NI Class 2 - under LPL. + period: 2023 + input: + self_employment_income: 5_000 + output: + ni_class_2: 0 + +- name: NI Class 2 - over LPL. + period: 2023 + input: + self_employment_income: 15_000 + output: + ni_class_2: 3.15 * 52 diff --git a/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/class_4/ni_class_4.yaml b/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/class_4/ni_class_4.yaml new file mode 100644 index 000000000..c1892ae91 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/class_4/ni_class_4.yaml @@ -0,0 +1,7 @@ +- name: NI Class 4 - under UPL. + period: 2023 + absolute_error_margin: 1 + input: + self_employment_income: 30_000 + output: + ni_class_4: 1628 \ No newline at end of file diff --git a/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/class_4/ni_class_4_main.yaml b/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/class_4/ni_class_4_main.yaml new file mode 100644 index 000000000..c4189d2a6 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/class_4/ni_class_4_main.yaml @@ -0,0 +1,7 @@ +- name: NI Class 4 - over UPL has maximum capped. + period: 2023 + absolute_error_margin: 1 + input: + self_employment_income: 100_000 + output: + ni_class_4_main: 3452 diff --git a/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/class_4/ni_class_4_maximum.yaml b/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/class_4/ni_class_4_maximum.yaml new file mode 100644 index 000000000..ca5693c7b --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/class_4/ni_class_4_maximum.yaml @@ -0,0 +1,8 @@ +- name: NI Class 4 with employment and self-employment income - over UPL has maximum capped. + period: 2023 + absolute_error_margin: 1 + input: + self_employment_income: 100_000 + employment_income: 100_000 + output: + ni_class_4_maximum: 48_661 \ No newline at end of file diff --git a/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/national_insurance.yaml b/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/national_insurance.yaml new file mode 100644 index 000000000..1bea92e94 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/national_insurance.yaml @@ -0,0 +1,9 @@ +- name: National Insurance sums non-employer-side contributions. + period: 2023 + input: + ni_class_1_employee: 1 + ni_class_2: 2 + ni_class_3: 4 + ni_class_4: 8 + output: + national_insurance: 15 diff --git a/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/ni_employee.yaml b/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/ni_employee.yaml new file mode 100644 index 000000000..bf9e244ed --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/ni_employee.yaml @@ -0,0 +1,6 @@ +- name: NI employee sums Class 1 contributions. + period: 2023 + input: + ni_class_1_employee: 1 + output: + ni_employee: 1 \ No newline at end of file diff --git a/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/ni_self_employed.yaml b/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/ni_self_employed.yaml new file mode 100644 index 000000000..c1728e1de --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/ni_self_employed.yaml @@ -0,0 +1,7 @@ +- name: NI self-employed sums Class 2 and 4 contributions. + period: 2023 + input: + ni_class_2: 1 + ni_class_4: 2 + output: + ni_self_employed: 3 \ No newline at end of file diff --git a/policyengine_uk/variables/gov/hmrc/national_insurance/class_1.py b/policyengine_uk/variables/gov/hmrc/national_insurance/class_1.py deleted file mode 100644 index e7b38649b..000000000 --- a/policyengine_uk/variables/gov/hmrc/national_insurance/class_1.py +++ /dev/null @@ -1,99 +0,0 @@ -from policyengine_uk.model_api import * - - -class NI_exempt(Variable): - value_type = bool - entity = Person - label = "Exempt from National Insurance" - documentation = "Whether a person is exempt from National Insurance" - definition_period = YEAR - reference = "Social Security Contributions and Benefits Act 1992 s. 6" - - def formula(person, period, parameters): - return ~person("over_16", period) | person("is_SP_age", period) - - -class monthly_employee_NI_class_1(Variable): - value_type = float - entity = Person - label = "Monthly employee Class 1 Contributions for National Insurance" - definition_period = MONTH - reference = "Social Security Contributions and Benefits Act 1992 s. 8" - unit = GBP - - def formula(person, period, parameters): - class_1 = parameters(period).gov.hmrc.national_insurance.class_1 - earnings = ( - person("employment_income", period.this_year) / MONTHS_IN_YEAR - ) - main_earnings = amount_between( - earnings, - class_1.thresholds.primary_threshold - * WEEKS_IN_YEAR - / MONTHS_IN_YEAR, - class_1.thresholds.upper_earnings_limit - * WEEKS_IN_YEAR - / MONTHS_IN_YEAR, - ) - add_earnings = max_( - earnings - - class_1.thresholds.upper_earnings_limit - * WEEKS_IN_YEAR - / MONTHS_IN_YEAR, - 0, - ) - main_charge = class_1.rates.employee.main * main_earnings - add_charge = class_1.rates.employee.additional * add_earnings - return main_charge + add_charge - - -class employee_NI_class_1(Variable): - label = "Employee NI class 1" - entity = Person - definition_period = YEAR - value_type = float - unit = "currency-GBP" - - def formula(person, period, parameters): - total_NI = 0 - for month in period.get_subperiods("month"): - total_NI += person("monthly_employee_NI_class_1", month) - return total_NI - - -class employer_NI_class_1(Variable): - value_type = float - entity = Person - label = "Employer Class 1 Contributions for National Insurance" - definition_period = YEAR - reference = "Social Security Contributions and Benefits Act 1992 s. 8" - unit = GBP - - def formula(person, period, parameters): - class_1 = parameters(period).gov.hmrc.national_insurance.class_1 - earnings = person("employment_income", period) - main_earnings = max_( - earnings - class_1.thresholds.secondary_threshold * WEEKS_IN_YEAR, - 0, - ) - return class_1.rates.employer * main_earnings - - -class employer_NI(Variable): - value_type = float - entity = Person - label = "Employer contributions to National Insurance" - definition_period = YEAR - unit = GBP - - adds = ["employer_NI_class_1"] - - -class total_NI(Variable): - value_type = float - entity = Person - label = "National Insurance (total)" - definition_period = YEAR - unit = GBP - - adds = ["employer_NI", "national_insurance"] diff --git a/policyengine_uk/variables/gov/hmrc/national_insurance/class_1/ni_class_1_employee.py b/policyengine_uk/variables/gov/hmrc/national_insurance/class_1/ni_class_1_employee.py new file mode 100644 index 000000000..6440582c7 --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/national_insurance/class_1/ni_class_1_employee.py @@ -0,0 +1,15 @@ +from policyengine_uk.model_api import * + + +class ni_class_1_employee(Variable): + label = "NI Class 1 employee-side contributions" + entity = Person + definition_period = MONTH + value_type = float + unit = GBP + defined_for = "ni_liable" + adds = [ + "ni_class_1_employee_primary", + "ni_class_1_employee_additional", + ] + reference = "https://www.legislation.gov.uk/ukpga/1992/4/section/8" diff --git a/policyengine_uk/variables/gov/hmrc/national_insurance/class_1/ni_class_1_employee_additional.py b/policyengine_uk/variables/gov/hmrc/national_insurance/class_1/ni_class_1_employee_additional.py new file mode 100644 index 000000000..eb23397d3 --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/national_insurance/class_1/ni_class_1_employee_additional.py @@ -0,0 +1,23 @@ +from policyengine_uk.model_api import * + + +class ni_class_1_employee_additional(Variable): + label = "NI Class 1 employee-side additional contributions" + entity = Person + definition_period = MONTH + value_type = float + unit = GBP + reference = "https://www.legislation.gov.uk/ukpga/1992/4/section/8" + + def formula(person, period, parameters): + income = person("ni_class_1_income", period) + parameters = parameters(period).gov.hmrc.national_insurance.class_1 + upper_earnings_limit = ( + parameters.thresholds.upper_earnings_limit + * WEEKS_IN_YEAR + / MONTHS_IN_YEAR + ) + upper_earnings_limit_income = max_(income - upper_earnings_limit, 0) + return ( + parameters.rates.employee.additional * upper_earnings_limit_income + ) diff --git a/policyengine_uk/variables/gov/hmrc/national_insurance/class_1/ni_class_1_employee_primary.py b/policyengine_uk/variables/gov/hmrc/national_insurance/class_1/ni_class_1_employee_primary.py new file mode 100644 index 000000000..e54c987e1 --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/national_insurance/class_1/ni_class_1_employee_primary.py @@ -0,0 +1,33 @@ +from policyengine_uk.model_api import * + + +class ni_class_1_employee_primary(Variable): + label = "NI Class 1 employee-side primary contributions" + entity = Person + definition_period = MONTH + value_type = float + unit = GBP + reference = "https://www.legislation.gov.uk/ukpga/1992/4/section/8" + + def formula(person, period, parameters): + income = person("ni_class_1_income", period) + parameters = parameters(period).gov.hmrc.national_insurance.class_1 + + # Thresholds are weekly, so multiply by weeks in year and divide by months in year + primary_threshold = ( + parameters.thresholds.primary_threshold + * WEEKS_IN_YEAR + / MONTHS_IN_YEAR + ) + upper_earnings_limit = ( + parameters.thresholds.upper_earnings_limit + * WEEKS_IN_YEAR + / MONTHS_IN_YEAR + ) + + upper_earnings_limit_income = max_(income - upper_earnings_limit, 0) + primary_threshold_income = ( + max_(income - primary_threshold, 0) - upper_earnings_limit_income + ) + + return parameters.rates.employee.main * primary_threshold_income diff --git a/policyengine_uk/variables/gov/hmrc/national_insurance/class_1/ni_class_1_employer.py b/policyengine_uk/variables/gov/hmrc/national_insurance/class_1/ni_class_1_employer.py new file mode 100644 index 000000000..4e763c8bc --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/national_insurance/class_1/ni_class_1_employer.py @@ -0,0 +1,25 @@ +from policyengine_uk.model_api import * + + +class ni_class_1_employer(Variable): + value_type = float + entity = Person + label = "NI Class 1 employer-side contributions" + definition_period = MONTH + unit = GBP + defined_for = "ni_liable" + reference = "https://www.legislation.gov.uk/ukpga/1992/4/section/9" + + def formula(person, period, parameters): + class_1 = parameters(period).gov.hmrc.national_insurance.class_1 + earnings = person("ni_class_1_income", period) + secondary_threshold = ( + class_1.thresholds.secondary_threshold + * WEEKS_IN_YEAR + / MONTHS_IN_YEAR + ) + main_earnings = max_( + earnings - secondary_threshold, + 0, + ) + return class_1.rates.employer * main_earnings diff --git a/policyengine_uk/variables/gov/hmrc/national_insurance/class_1/ni_class_1_income.py b/policyengine_uk/variables/gov/hmrc/national_insurance/class_1/ni_class_1_income.py new file mode 100644 index 000000000..f79e81fba --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/national_insurance/class_1/ni_class_1_income.py @@ -0,0 +1,17 @@ +from policyengine_uk.model_api import * + + +class ni_class_1_income(Variable): + label = "ni_class_1_income" + documentation = "Income subject to NI Class 1 contributions." + entity = Person + definition_period = YEAR + value_type = float + unit = GBP + adds = [ + "employment_income", + "statutory_sick_pay", + "statutory_maternity_pay", + "statutory_paternity_pay", + ] + reference = "https://www.legislation.gov.uk/ukpga/1992/4/section/3" diff --git a/policyengine_uk/variables/gov/hmrc/national_insurance/class_1/ni_liable.py b/policyengine_uk/variables/gov/hmrc/national_insurance/class_1/ni_liable.py new file mode 100644 index 000000000..656ba8ca1 --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/national_insurance/class_1/ni_liable.py @@ -0,0 +1,14 @@ +from policyengine_uk.model_api import * + + +class ni_liable(Variable): + label = "NI liable" + documentation = ( + "Whether this person is liable for NI contributions by their age." + ) + entity = Person + definition_period = YEAR + value_type = bool + + def formula(person, period, parameters): + return person("over_16", period) & ~person("is_SP_age", period) diff --git a/policyengine_uk/variables/gov/hmrc/national_insurance/class_2.py b/policyengine_uk/variables/gov/hmrc/national_insurance/class_2/ni_class_2.py similarity index 61% rename from policyengine_uk/variables/gov/hmrc/national_insurance/class_2.py rename to policyengine_uk/variables/gov/hmrc/national_insurance/class_2/ni_class_2.py index 9dbf1374c..a36358bb4 100644 --- a/policyengine_uk/variables/gov/hmrc/national_insurance/class_2.py +++ b/policyengine_uk/variables/gov/hmrc/national_insurance/class_2/ni_class_2.py @@ -1,26 +1,18 @@ from policyengine_uk.model_api import * -class weekly_NI_class_2(Variable): +class ni_class_2(Variable): value_type = float entity = Person - label = "Class 2 Contributions for National Insurance" + label = "NI Class 2 contributions" definition_period = YEAR reference = "Social Security and Benefits Act 1992 s. 11" unit = GBP + defined_for = "ni_liable" + reference = "https://www.legislation.gov.uk/ukpga/1992/4/section/11" def formula(person, period, parameters): class_2 = parameters(period).gov.hmrc.national_insurance.class_2 profits = person("self_employment_income", period) over_threshold = profits >= class_2.small_profits_threshold return over_threshold * class_2.flat_rate * WEEKS_IN_YEAR - - -class NI_class_2(Variable): - value_type = float - entity = Person - label = "Class 2 Contributions for National Insurance for the year" - definition_period = YEAR - unit = GBP - - adds = ["weekly_NI_class_2"] diff --git a/policyengine_uk/variables/gov/hmrc/national_insurance/class_3/ni_class_3.py b/policyengine_uk/variables/gov/hmrc/national_insurance/class_3/ni_class_3.py new file mode 100644 index 000000000..860049e4b --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/national_insurance/class_3/ni_class_3.py @@ -0,0 +1,12 @@ +from policyengine_uk.model_api import * + + +class ni_class_3(Variable): + label = "NI Class 3 contributions" + documentation = "Voluntary contributions to NI." + entity = Person + definition_period = YEAR + value_type = float + unit = GBP + defined_for = "ni_liable" + reference = "https://www.legislation.gov.uk/ukpga/1992/4/section/13" diff --git a/policyengine_uk/variables/gov/hmrc/national_insurance/class_4.py b/policyengine_uk/variables/gov/hmrc/national_insurance/class_4.py deleted file mode 100644 index 48f0ecef1..000000000 --- a/policyengine_uk/variables/gov/hmrc/national_insurance/class_4.py +++ /dev/null @@ -1,63 +0,0 @@ -from policyengine_uk.model_api import * - - -class NI_class_4(Variable): - value_type = float - entity = Person - label = "Class 4 Contributions for National Insurance for the year" - definition_period = YEAR - reference = "Social Security and Benefits Act 1992 s. 15" - unit = GBP - - def formula(person, period, parameters): - class_4 = parameters(period).gov.hmrc.national_insurance.class_4 - self_employment_income = person("self_employment_income", period) - employee_NI = person("employee_NI", period) - profits = self_employment_income - employee_NI - main_amount = amount_between( - profits, - class_4.thresholds.lower_profits_limit, - class_4.thresholds.upper_profits_limit, - ) - add_amount = max_(profits - class_4.thresholds.upper_profits_limit, 0) - main_charge = main_amount * class_4.rates.main - add_charge = add_amount * class_4.rates.additional - return main_charge + add_charge - - -class employee_NI(Variable): - value_type = float - entity = Person - label = "employee-side National Insurance" - definition_period = YEAR - unit = GBP - - def formula(person, period, parameters): - exempt = person("NI_exempt", period) - return person("employee_NI_class_1", period) * ~exempt - - -class self_employed_NI(Variable): - value_type = float - entity = Person - label = "self-employed National Insurance" - definition_period = YEAR - unit = GBP - - def formula(person, period, parameters): - exempt = person("NI_exempt", period) - return add(person, period, ("NI_class_2", "NI_class_4")) * ~exempt - - -class national_insurance(Variable): - value_type = float - entity = Person - label = "National Insurance" - documentation = "Total National Insurance contributions" - definition_period = YEAR - unit = GBP - reference = "Social Security and Benefits Act 1992 s. 1(2)" - adds = [ - "employee_NI", - "self_employed_NI", - ] diff --git a/policyengine_uk/variables/gov/hmrc/national_insurance/class_4/ni_class_4.py b/policyengine_uk/variables/gov/hmrc/national_insurance/class_4/ni_class_4.py new file mode 100644 index 000000000..90b0282ee --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/national_insurance/class_4/ni_class_4.py @@ -0,0 +1,36 @@ +from policyengine_uk.model_api import * + + +class ni_class_4(Variable): + value_type = float + entity = Person + label = "NI Class 4 main contributions" + definition_period = YEAR + unit = GBP + defined_for = "ni_liable" + + def formula(person, period, parameters): + class_4 = parameters(period).gov.hmrc.national_insurance.class_4 + self_employment_income = person("self_employment_income", period) + employee_NI = person("ni_class_1_employee", period) + profits = self_employment_income - employee_NI + add_rate_income = max_( + profits - class_4.thresholds.upper_profits_limit, + 0, + ) + main_rate_income = ( + max_( + profits - class_4.thresholds.lower_profits_limit, + 0, + ) + - add_rate_income + ) + pre_maximum_amount = ( + main_rate_income * class_4.rates.main + + add_rate_income * class_4.rates.additional + ) + maximum_amount = person("ni_class_4_maximum", period) + return max_( + min_(pre_maximum_amount, maximum_amount), + 0, + ) diff --git a/policyengine_uk/variables/gov/hmrc/national_insurance/class_4/ni_class_4_main.py b/policyengine_uk/variables/gov/hmrc/national_insurance/class_4/ni_class_4_main.py new file mode 100644 index 000000000..3b1fbac5f --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/national_insurance/class_4/ni_class_4_main.py @@ -0,0 +1,29 @@ +from policyengine_uk.model_api import * + + +class ni_class_4_main(Variable): + value_type = float + entity = Person + label = "NI Class 4 main contributions" + definition_period = YEAR + unit = GBP + defined_for = "ni_liable" + reference = "https://www.legislation.gov.uk/ukpga/1992/4/section/15" + + def formula(person, period, parameters): + class_4 = parameters(period).gov.hmrc.national_insurance.class_4 + self_employment_income = person("self_employment_income", period) + employee_NI = person("ni_class_1_employee", period) + profits = self_employment_income - employee_NI + add_rate_income = max_( + profits - class_4.thresholds.upper_profits_limit, + 0, + ) + main_rate_income = ( + max_( + profits - class_4.thresholds.lower_profits_limit, + 0, + ) + - add_rate_income + ) + return main_rate_income * class_4.rates.main diff --git a/policyengine_uk/variables/gov/hmrc/national_insurance/class_4/ni_class_4_maximum.py b/policyengine_uk/variables/gov/hmrc/national_insurance/class_4/ni_class_4_maximum.py new file mode 100644 index 000000000..426126a99 --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/national_insurance/class_4/ni_class_4_maximum.py @@ -0,0 +1,48 @@ +from policyengine_uk.model_api import * + + +class ni_class_4_maximum(Variable): + label = "NI Class 4 maximum liability" + entity = Person + definition_period = YEAR + value_type = float + unit = GBP + reference = "https://www.legislation.gov.uk/uksi/2001/1004/regulation/100" + + def formula(person, period, parameters): + ni = parameters(period).gov.hmrc.national_insurance + upl = ni.class_4.thresholds.upper_profits_limit + lpl = ni.class_4.thresholds.lower_profits_limit + step_1 = upl - lpl + main_rate = ni.class_4.rates.main + add_rate = ni.class_4.rates.additional + step_2 = step_1 * main_rate + step_3 = step_2 + 53 * ni.class_2.flat_rate + class_2_contributions = person("ni_class_2", period) + primary_class_1_contributions = person( + "ni_class_1_employee_primary", period + ) + step_4 = step_3 - class_2_contributions - primary_class_1_contributions + class_4_main_contributions = person("ni_class_4_main", period) + other_aggregate_contributions = ( + primary_class_1_contributions + + class_2_contributions + + class_4_main_contributions + ) + case_1 = (step_4 >= 0) & (step_4 > other_aggregate_contributions) + case_2 = (step_4 >= 0) & (step_4 <= other_aggregate_contributions) + case_3 = step_4 < 0 + step_5 = step_4 * 100 / 9 + profits = person("self_employment_income", period) + step_6 = lpl - min_(upl, profits) + step_7 = max_(0, step_6 - step_5) + step_8 = step_7 * add_rate + step_9 = max_(0, profits - upl) + + return select( + [ + case_1, + case_2 | case_3, + ], + [step_4, step_4 + step_8 + step_9], + ) diff --git a/policyengine_uk/variables/gov/hmrc/national_insurance/national_insurance.py b/policyengine_uk/variables/gov/hmrc/national_insurance/national_insurance.py new file mode 100644 index 000000000..429a27730 --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/national_insurance/national_insurance.py @@ -0,0 +1,17 @@ +from policyengine_uk.model_api import * + + +class national_insurance(Variable): + value_type = float + entity = Person + label = "National Insurance" + documentation = "Total National Insurance contributions" + definition_period = YEAR + unit = GBP + reference = "Social Security and Benefits Act 1992 s. 1(2)" + adds = [ + "ni_class_1_employee", + "ni_class_2", + "ni_class_3", + "ni_class_4", + ] diff --git a/policyengine_uk/variables/gov/hmrc/national_insurance/ni_employee.py b/policyengine_uk/variables/gov/hmrc/national_insurance/ni_employee.py new file mode 100644 index 000000000..a4ccedace --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/national_insurance/ni_employee.py @@ -0,0 +1,12 @@ +from policyengine_uk.model_api import * + + +class ni_employee(Variable): + value_type = float + entity = Person + label = "employee-side National Insurance" + definition_period = YEAR + unit = GBP + adds = [ + "ni_class_1_employee", + ] diff --git a/policyengine_uk/variables/gov/hmrc/national_insurance/ni_self_employed.py b/policyengine_uk/variables/gov/hmrc/national_insurance/ni_self_employed.py new file mode 100644 index 000000000..9924062d5 --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/national_insurance/ni_self_employed.py @@ -0,0 +1,13 @@ +from policyengine_uk.model_api import * + + +class ni_self_employed(Variable): + value_type = float + entity = Person + label = "self-employed National Insurance" + definition_period = YEAR + unit = GBP + adds = [ + "ni_class_2", + "ni_class_4", + ] diff --git a/policyengine_uk/variables/household/income/income.py b/policyengine_uk/variables/household/income/income.py index 36b4aa097..4a87d2318 100644 --- a/policyengine_uk/variables/household/income/income.py +++ b/policyengine_uk/variables/household/income/income.py @@ -355,3 +355,27 @@ class income_decile(Variable): def formula(person, period, parameters): return person.household("household_income_decile", period) + + +class statutory_maternity_pay(Variable): + label = "Statutory maternity pay" + entity = Person + definition_period = YEAR + value_type = float + unit = GBP + + +class statutory_paternity_pay(Variable): + label = "Statutory paternity pay" + entity = Person + definition_period = YEAR + value_type = float + unit = GBP + + +class statutory_sick_pay(Variable): + label = "Statutory sick pay" + entity = Person + definition_period = YEAR + value_type = float + unit = GBP diff --git a/policyengine_uk/variables/input/income.py b/policyengine_uk/variables/input/income.py index 8dacf08e6..54a6b6e4f 100644 --- a/policyengine_uk/variables/input/income.py +++ b/policyengine_uk/variables/input/income.py @@ -51,7 +51,7 @@ class self_employment_income(Variable): value_type = float entity = Person label = "self-employment income" - documentation = "Income from self-employment profits" + documentation = "Income from self-employment profits. This should be net of self-employment expenses." definition_period = YEAR unit = GBP reference = "Income Tax (Trading and Other Income) Act 2005 s. 1(1)(a)"