From 6e59b57f2d60343c28e27b8478d7f0d1191a73b5 Mon Sep 17 00:00:00 2001 From: Nikhil Woodruff <35577657+nikhilwoodruff@users.noreply.github.com> Date: Wed, 23 Oct 2024 15:04:42 +0100 Subject: [PATCH] Add capital gains responses (#979) * Add capital gains responses * Add changes * Versioning --- .gitignore | 4 +- CHANGELOG.md | 7 + changelog.yaml | 5 + .../capital_gains_responses/elasticity.yaml | 6 + policyengine_uk/system.py | 21 +++ .../capital_gains_tax/capital_gains_tax.py | 1 + .../gov/hmrc/capital_gains_tax/responses.py | 163 ++++++++++++++++++ .../variables/household/income/income.py | 4 + setup.py | 2 +- 9 files changed, 211 insertions(+), 2 deletions(-) create mode 100644 policyengine_uk/parameters/gov/simulation/capital_gains_responses/elasticity.yaml create mode 100644 policyengine_uk/variables/gov/hmrc/capital_gains_tax/responses.py diff --git a/.gitignore b/.gitignore index 6aa874564..0b04acc73 100644 --- a/.gitignore +++ b/.gitignore @@ -50,4 +50,6 @@ policyengine_uk/calibration/*.h5 **/*.csv **/*.pkl **/*.log -**/ukmod.json \ No newline at end of file +**/ukmod.json + +*.ipynb diff --git a/CHANGELOG.md b/CHANGELOG.md index 73af3763c..baccb70f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.12.0] - 2024-10-23 14:47:21 + +### Added + +- Capital Gains Tax elasticities. + ## [2.11.0] - 2024-10-23 10:15:26 ### Added @@ -1548,6 +1554,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 +[2.12.0]: https://github.com/PolicyEngine/openfisca-uk/compare/2.11.0...2.12.0 [2.11.0]: https://github.com/PolicyEngine/openfisca-uk/compare/2.10.0...2.11.0 [2.10.0]: https://github.com/PolicyEngine/openfisca-uk/compare/2.9.0...2.10.0 [2.9.0]: https://github.com/PolicyEngine/openfisca-uk/compare/2.8.0...2.9.0 diff --git a/changelog.yaml b/changelog.yaml index b7e7ec4ce..0467b8e34 100644 --- a/changelog.yaml +++ b/changelog.yaml @@ -1299,3 +1299,8 @@ added: - Benefit uprating for 2025/26. date: 2024-10-23 10:15:26 +- bump: minor + changes: + added: + - Capital Gains Tax elasticities. + date: 2024-10-23 14:47:21 diff --git a/policyengine_uk/parameters/gov/simulation/capital_gains_responses/elasticity.yaml b/policyengine_uk/parameters/gov/simulation/capital_gains_responses/elasticity.yaml new file mode 100644 index 000000000..0afdba34c --- /dev/null +++ b/policyengine_uk/parameters/gov/simulation/capital_gains_responses/elasticity.yaml @@ -0,0 +1,6 @@ +description: Elasticity of capital gains with respect to the capital gains marginal tax rate. +values: + 2000-01-01: 0 +metadata: + unit: /1 + label: capital gains elasticity diff --git a/policyengine_uk/system.py b/policyengine_uk/system.py index da9134cce..f1620f434 100644 --- a/policyengine_uk/system.py +++ b/policyengine_uk/system.py @@ -74,6 +74,16 @@ def __init__(self, *args, **kwargs): self.set_input("employment_income_before_lsr", known_period, array) employment_income.delete_arrays(known_period) + # Capital gains responses + + cg_holder = self.get_holder("capital_gains") + for known_period in cg_holder.get_known_periods(): + array = cg_holder.get_array(known_period) + self.set_input( + "capital_gains_before_response", known_period, array + ) + employment_income.delete_arrays(known_period) + class Microsimulation(CoreMicrosimulation): default_tax_benefit_system = CountryTaxBenefitSystem @@ -105,3 +115,14 @@ def __init__(self, *args, **kwargs): "employment_income_before_lsr", known_period, array ) employment_income.delete_arrays(known_period) + + # Capital gains responses + + for simulation in list(self.branches.values()) + [self]: + cg_holder = self.get_holder("capital_gains") + for known_period in cg_holder.get_known_periods(): + array = cg_holder.get_array(known_period) + self.set_input( + "capital_gains_before_response", known_period, array + ) + employment_income.delete_arrays(known_period) diff --git a/policyengine_uk/variables/gov/hmrc/capital_gains_tax/capital_gains_tax.py b/policyengine_uk/variables/gov/hmrc/capital_gains_tax/capital_gains_tax.py index c09876a33..dedcb6d9b 100644 --- a/policyengine_uk/variables/gov/hmrc/capital_gains_tax/capital_gains_tax.py +++ b/policyengine_uk/variables/gov/hmrc/capital_gains_tax/capital_gains_tax.py @@ -1,4 +1,5 @@ from policyengine_uk.model_api import * +from policyengine_core.holders import Holder class capital_gains_tax(Variable): diff --git a/policyengine_uk/variables/gov/hmrc/capital_gains_tax/responses.py b/policyengine_uk/variables/gov/hmrc/capital_gains_tax/responses.py new file mode 100644 index 000000000..942b9d0e1 --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/capital_gains_tax/responses.py @@ -0,0 +1,163 @@ +from policyengine_uk.model_api import * +from policyengine_core.simulations import * + + +class relative_capital_gains_mtr_change(Variable): + value_type = float + entity = Person + label = "relative change in capital gains tax rate" + unit = "/1" + definition_period = YEAR + + def formula(person, period, parameters): + simulation: Simulation = person.simulation + baseline_branch = simulation.get_branch("baseline").get_branch( + "baseline_cgr_measurement" + ) + baseline_person = baseline_branch.populations["person"] + baseline_branch.tax_benefit_system.neutralize_variable( + "capital_gains_behavioural_response" + ) + baseline_branch.set_input( + "capital_gains_before_response", + period, + person("capital_gains_before_response", period), + ) + baseline_mtr = baseline_person( + "marginal_tax_rate_on_capital_gains", period + ) + del simulation.branches["baseline"].branches[ + "baseline_cgr_measurement" + ] + + measurement_branch = simulation.get_branch("cgr_measurement") + measurement_branch.tax_benefit_system.neutralize_variable( + "capital_gains_behavioural_response" + ) + measurement_branch.set_input( + "capital_gains_before_response", + period, + person("capital_gains_before_response", period), + ) + measurement_person = measurement_branch.populations["person"] + reform_mtr = measurement_person( + "marginal_tax_rate_on_capital_gains", period + ) + del simulation.branches["cgr_measurement"] + + # Handle zeros in tax rates to prevent log(0) + min_rate = 0.001 + baseline_mtr_adj = np.maximum(baseline_mtr, min_rate) + reform_mtr_adj = np.maximum(reform_mtr, min_rate) + + # Calculate log difference + return np.log(reform_mtr_adj) - np.log(baseline_mtr_adj) + + +class capital_gains_elasticity(Variable): + value_type = float + entity = Person + label = "elasticity of capital gains realizations" + unit = "/1" + definition_period = YEAR + + def formula(person, period, parameters): + gov = parameters(period).gov + return gov.simulation.capital_gains_responses.elasticity + + +class capital_gains_behavioural_response(Variable): + value_type = float + entity = Person + label = "capital gains behavioral response" + unit = GBP + definition_period = YEAR + + def formula(person, period, parameters): + simulation = person.simulation + if simulation.baseline is None: + return 0 + + capital_gains = person("capital_gains_before_response", period) + tax_rate_change = person("relative_capital_gains_mtr_change", period) + elasticity = person("capital_gains_elasticity", period) + + # Calculate response using log differences + response_factor = np.exp(elasticity * tax_rate_change) - 1 + response = capital_gains * response_factor + + return response + + +class capital_gains_before_response(Variable): + label = "capital gains before responses" + entity = Person + definition_period = YEAR + value_type = float + unit = GBP + uprating = "calibration.programs.capital_gains.total" + + +class adult_index_cg(Variable): + value_type = int + entity = Person + label = "index of adult in household, ranked by capital gains" + definition_period = YEAR + + def formula(person, period, parameters): + return ( + person.get_rank( + person.household, + -person("capital_gains_before_response", period), + condition=person("is_adult", period), + ) + + 1 + ) + + +class marginal_tax_rate_on_capital_gains(Variable): + label = "capital gains marginal tax rate" + documentation = "Percent of marginal capital gains that do not increase household net income." + entity = Person + definition_period = YEAR + value_type = float + unit = "/1" + + def formula(person, period, parameters): + mtr_values = np.zeros(person.count, dtype=np.float32) + simulation = person.simulation + DELTA = 1_000 + adult_index_values = person("adult_index_cg", period) + for adult_index in [1, 2]: + alt_simulation = simulation.get_branch( + f"adult_{adult_index}_cg_rise" + ) + mask = adult_index_values == adult_index + for variable in simulation.tax_benefit_system.variables: + variable_data = simulation.tax_benefit_system.variables[ + variable + ] + if ( + variable not in simulation.input_variables + and not variable_data.is_input_variable() + ): + alt_simulation.delete_arrays(variable) + alt_simulation.set_input( + "capital_gains", + period, + person("capital_gains", period) + mask * DELTA, + ) + alt_person = alt_simulation.person + household_net_income = person.household( + "household_net_income", period + ) + household_net_income_higher_earnings = alt_person.household( + "household_net_income", period + ) + increase = ( + household_net_income_higher_earnings - household_net_income + ) + mtr_values += where(mask, 1 - increase / DELTA, 0) + + del simulation.branches[f"adult_{adult_index}_cg_rise"] + return mtr_values diff --git a/policyengine_uk/variables/household/income/income.py b/policyengine_uk/variables/household/income/income.py index 3d0d2c6dc..f56a7686d 100644 --- a/policyengine_uk/variables/household/income/income.py +++ b/policyengine_uk/variables/household/income/income.py @@ -430,3 +430,7 @@ class capital_gains(Variable): value_type = float unit = GBP uprating = "calibration.programs.capital_gains.total" + adds = [ + "capital_gains_before_response", + "capital_gains_behavioural_response", + ] diff --git a/setup.py b/setup.py index 8c9528bd2..ebbb3f66c 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ setup( name="PolicyEngine-UK", - version="2.11.0", + version="2.12.0", author="PolicyEngine", author_email="nikhil@policyengine.org", classifiers=[