From 1ce28f756a9fc3b3236c00d89ddcdbd6cfd81b7b Mon Sep 17 00:00:00 2001 From: leehengpan Date: Wed, 30 Oct 2024 15:06:25 -0400 Subject: [PATCH] Apply utility functions to rest of reforms, delete dc_ctc --- .../medicare_and_investment_tax_increase.py | 24 +- ...se_taxable_earnings_for_social_security.py | 22 +- .../congress/delauro/american_family_act.py | 31 +- policyengine_us/reforms/reforms.py | 6 - policyengine_us/reforms/states/dc/__init__.py | 3 - policyengine_us/reforms/states/dc/dc_ctc.py | 70 ----- .../reforms/states/dc/dc_ctc_test_microsim.py | 32 --- policyengine_us/reforms/utils.py | 106 ++++++- .../policy/contrib/states/dc/dc_ctc.yaml | 268 ------------------ 9 files changed, 141 insertions(+), 421 deletions(-) delete mode 100644 policyengine_us/reforms/states/dc/__init__.py delete mode 100644 policyengine_us/reforms/states/dc/dc_ctc.py delete mode 100644 policyengine_us/reforms/states/dc/dc_ctc_test_microsim.py delete mode 100644 policyengine_us/tests/policy/contrib/states/dc/dc_ctc.yaml diff --git a/policyengine_us/reforms/biden/budget_2025/medicare_and_investment_tax_increase.py b/policyengine_us/reforms/biden/budget_2025/medicare_and_investment_tax_increase.py index ba3c848c77e..83baa7cd9a0 100644 --- a/policyengine_us/reforms/biden/budget_2025/medicare_and_investment_tax_increase.py +++ b/policyengine_us/reforms/biden/budget_2025/medicare_and_investment_tax_increase.py @@ -1,4 +1,6 @@ from policyengine_us.model_api import * +from policyengine_us.reforms.utils import create_reform_two_threshold_check +import operator def create_medicare_and_investment_tax_increase() -> Reform: @@ -71,15 +73,19 @@ def apply(self): def create_medicare_and_investment_tax_increase_reform( parameters, period, bypass: bool = False ): - if bypass: - return create_medicare_and_investment_tax_increase() - - p = parameters(period).gov.contrib.biden.budget_2025 - - if (p.medicare.rate > 0) | (p.net_investment_income.rate > 0): - return create_medicare_and_investment_tax_increase() - else: - return None + return create_reform_two_threshold_check( + parameters=parameters, + period=period, + parameter_path="gov.contrib.biden.budget_2025", + reform_function=create_medicare_and_investment_tax_increase, + comparison_parameter_path_1="medicare.rate", + comparison_parameter_path_2="net_investment_income.rate", + threshold_check_1=0, + threshold_check_2=0, + comparison_operator_1=operator.gt, + comparison_operator_2=operator.gt, + bypass=bypass, + ) medicare_and_investment_tax_increase = ( diff --git a/policyengine_us/reforms/cbo/payroll/increase_taxable_earnings_for_social_security.py b/policyengine_us/reforms/cbo/payroll/increase_taxable_earnings_for_social_security.py index 1199cf18dbe..f919ccd0dc9 100644 --- a/policyengine_us/reforms/cbo/payroll/increase_taxable_earnings_for_social_security.py +++ b/policyengine_us/reforms/cbo/payroll/increase_taxable_earnings_for_social_security.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.reforms.utils import create_reform_threshold_check +import operator +import numpy as np def create_increase_taxable_earnings_for_social_security() -> Reform: @@ -29,15 +32,16 @@ def apply(self): def create_increase_taxable_earnings_for_social_security_reform( parameters, period, bypass: bool = False ): - if bypass: - return create_increase_taxable_earnings_for_social_security() - - p = parameters(period).gov.contrib.cbo.payroll - - if p.secondary_earnings_threshold < np.inf: - return create_increase_taxable_earnings_for_social_security() - else: - return None + return create_reform_threshold_check( + reform_function=create_increase_taxable_earnings_for_social_security, + parameters=parameters, + period=period, + parameter_path="gov.contrib.cbo.payroll", + comparison_parameter_path="secondary_earnings_threshold", + comparison_operator=operator.lt, + threshold_check=np.inf, + bypass=bypass, + ) increase_taxable_earnings_for_social_security = ( diff --git a/policyengine_us/reforms/congress/delauro/american_family_act.py b/policyengine_us/reforms/congress/delauro/american_family_act.py index 33be681195f..89f3301ac7b 100644 --- a/policyengine_us/reforms/congress/delauro/american_family_act.py +++ b/policyengine_us/reforms/congress/delauro/american_family_act.py @@ -1,5 +1,6 @@ from policyengine_us.model_api import * -from policyengine_core.periods import period as period_ +from policyengine_us.reforms.utils import create_reform_threshold_check +import operator def create_american_family_act_with_baby_bonus() -> Reform: @@ -35,24 +36,16 @@ def apply(self): def create_american_family_act_with_baby_bonus_reform( parameters, period, bypass: bool = False ): - if bypass: - return create_american_family_act_with_baby_bonus() - - p = parameters.gov.contrib.congress.delauro.american_family_act - - reform_active = False - current_period = period_(period) - - for i in range(5): - if p(current_period).baby_bonus > 0: - reform_active = True - break - current_period = current_period.offset(1, "year") - - if reform_active: - return create_american_family_act_with_baby_bonus() - else: - return None + return create_reform_threshold_check( + reform_function=create_american_family_act_with_baby_bonus, + parameters=parameters, + period=period, + parameter_path="gov.contrib.congress.delauro.american_family_act", + comparison_parameter_path="baby_bonus", + comparison_operator=operator.gt, + threshold_check=0, + bypass=bypass, + ) american_family_act = create_american_family_act_with_baby_bonus_reform( diff --git a/policyengine_us/reforms/reforms.py b/policyengine_us/reforms/reforms.py index 7e392e4080f..c13e65713f0 100644 --- a/policyengine_us/reforms/reforms.py +++ b/policyengine_us/reforms/reforms.py @@ -20,9 +20,6 @@ from .biden.budget_2025 import create_capital_gains_tax_increase_reform from .eitc import create_halve_joint_eitc_phase_out_rate_reform from .states.ny.wftc import create_ny_working_families_tax_credit_reform -from .states.dc.dc_ctc import ( - create_dc_ctc_reform, -) from .harris.lift.middle_class_tax_credit import ( create_middle_class_tax_credit_reform, ) @@ -92,8 +89,6 @@ def create_structural_reforms_from_parameters(parameters, period): ) ny_wftc = create_ny_working_families_tax_credit_reform(parameters, period) - dc_ctc = create_dc_ctc_reform(parameters, period) - middle_class_tax_credit = create_middle_class_tax_credit_reform( parameters, period ) @@ -136,7 +131,6 @@ def create_structural_reforms_from_parameters(parameters, period): capital_gains_tax_increase, halve_joint_eitc_phase_out_rate, ny_wftc, - dc_ctc, middle_class_tax_credit, rent_relief_tax_credit, end_child_poverty_act, diff --git a/policyengine_us/reforms/states/dc/__init__.py b/policyengine_us/reforms/states/dc/__init__.py deleted file mode 100644 index 499ba20f994..00000000000 --- a/policyengine_us/reforms/states/dc/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .dc_ctc import ( - create_dc_ctc_reform, -) diff --git a/policyengine_us/reforms/states/dc/dc_ctc.py b/policyengine_us/reforms/states/dc/dc_ctc.py deleted file mode 100644 index f2030e81606..00000000000 --- a/policyengine_us/reforms/states/dc/dc_ctc.py +++ /dev/null @@ -1,70 +0,0 @@ -from policyengine_us.model_api import * -from policyengine_us.reforms.utils import create_reform_if_active - - -def create_dc_ctc() -> Reform: - class dc_ctc(Variable): - value_type = float - entity = TaxUnit - label = "DC Child Tax Credit" - unit = USD - definition_period = YEAR - reference = "https://lims.dccouncil.gov/downloads/LIMS/52461/Introduction/B25-0190-Introduction.pdf" - defined_for = StateCode.DC - - def formula(tax_unit, period, parameters): - p = parameters(period).gov.contrib.states.dc.ctc - person = tax_unit.members - age = person("age", period) - age_eligible = age < p.age_threshold - eligible_children = tax_unit.sum(age_eligible) - capped_children = min_(eligible_children, p.child_cap) - income = tax_unit("adjusted_gross_income", period) - max_amount = p.amount * capped_children - increment = p.reduction.increment - reduction_per_increment = p.reduction.amount - filing_status = tax_unit("filing_status", period) - reduction_start = p.reduction.start[filing_status] - excess = max_(income - reduction_start, 0) - increments = np.ceil(excess / increment) - reduction_amount = increments * reduction_per_increment - return max_(0, max_amount - reduction_amount) - - class dc_refundable_credits(Variable): - value_type = float - entity = TaxUnit - label = "DC refundable credits" - unit = USD - definition_period = YEAR - reference = ( - "https://otr.cfo.dc.gov/sites/default/files/dc/sites/otr/publication/attachments/52926_D-40_12.21.21_Final_Rev011122.pdf#page=63" - "https://otr.cfo.dc.gov/sites/default/files/dc/sites/otr/publication/attachments/2022_D-40_Booklet_Final_blk_01_23_23_Ordc.pdf#page=55" - ) - defined_for = StateCode.DC - - def formula(tax_unit, period, parameters): - p = parameters(period).gov.states.dc.tax.income.credits - previous_credits = add(tax_unit, period, p.refundable) - ctc = tax_unit("dc_ctc", period) - return ctc + previous_credits - - class reform(Reform): - def apply(self): - self.update_variable(dc_ctc) - self.update_variable(dc_refundable_credits) - - return reform - - -def create_dc_ctc_reform(parameters, period, bypass: bool = False): - return create_reform_if_active( - parameters, - period, - "gov.contrib.states.dc.ctc", - "in_effect", - create_dc_ctc, - bypass, - ) - - -dc_ctc = create_dc_ctc_reform(None, None, bypass=True) diff --git a/policyengine_us/reforms/states/dc/dc_ctc_test_microsim.py b/policyengine_us/reforms/states/dc/dc_ctc_test_microsim.py deleted file mode 100644 index 389d1563072..00000000000 --- a/policyengine_us/reforms/states/dc/dc_ctc_test_microsim.py +++ /dev/null @@ -1,32 +0,0 @@ -def test_dc_ctc(): - from policyengine_us import Microsimulation - from policyengine_core.reforms import Reform - from policyengine_core.periods import instant - - baseline = Microsimulation() - - baseline_net_income = baseline.calculate( - "household_net_income", period=2025 - ) - - def modify_parameters(parameters): - parameters.gov.contrib.states.dc.ctc.in_effect.update( - start=instant("2025-01-01"), - stop=instant("2028-12-31"), - value=True, - ) - return parameters - - class parameter_reform(Reform): - def apply(self): - self.modify_parameters(modify_parameters) - - dc_ctc_reformed = Microsimulation(reform=parameter_reform) - dc_ctc_reform_net_income = dc_ctc_reformed.calculate( - "household_net_income", period=2025 - ) - - total_loss = (dc_ctc_reform_net_income - baseline_net_income).sum() - - assert total_loss > 5e6 - assert total_loss < 100e6 diff --git a/policyengine_us/reforms/utils.py b/policyengine_us/reforms/utils.py index 9195c341854..b57c275965f 100644 --- a/policyengine_us/reforms/utils.py +++ b/policyengine_us/reforms/utils.py @@ -1,4 +1,13 @@ from policyengine_core.periods import period as period_ +import numpy as np +import operator + + +def get_nested_value(base_param, path): + value = base_param + for part in path.split("."): + value = getattr(value, part) + return value def create_reform_if_active( @@ -25,11 +34,7 @@ def create_reform_if_active( return reform_function() current_period = period_(period) - path_parts = parameter_path.split(".") - - p = parameters - for part in path_parts: - p = getattr(p, part) + p = get_nested_value(parameters, parameter_path) for _ in range(5): if getattr(p(current_period), active_parameter_path): @@ -37,3 +42,94 @@ def create_reform_if_active( current_period = current_period.offset(1, "year") return None + + +def create_reform_threshold_check( + parameters, + period, + parameter_path: str, + comparison_parameter_path: str, + reform_function, + threshold_check: float = np.inf, + comparison_operator=operator.lt, + bypass: bool = False, +): + """ + Create a reform based on a parameter threshold check. + + Args: + parameters: PolicyEngine parameters object + period: Time period for the reform + parameter_path: Dot-separated path to the parameter to check + comparison_parameter_path: Dot-separated path to the parameter to compare with threshold + reform_function: The specific reform creation function to call + bypass: If True, skip parameter checks and return reform + threshold_check: Value to compare parameter against + comparison_operator: Operator to use for comparison (default: less than) + """ + if bypass: + return reform_function() + + # Navigate parameter tree using the path + p = get_nested_value(parameters, parameter_path) + current_period = period_(period) + + for _ in range(5): + if comparison_operator( + getattr(p(current_period), comparison_parameter_path), + threshold_check, + ): + return reform_function() + current_period = current_period.offset(1, "year") + + return None + + +def create_reform_two_threshold_check( + parameters, + period, + parameter_path: str, + reform_function, + comparison_parameter_path_1: str, + comparison_parameter_path_2: str, + threshold_check_1: float = np.inf, + threshold_check_2: float = np.inf, + comparison_operator_1=operator.lt, + comparison_operator_2=operator.lt, + bypass: bool = False, +): + """ + Create a reform based on a parameter two-threshold check. + + Args: + parameters: PolicyEngine parameters object + period: Time period for the reform + parameter_path: Dot-separated path to the parameter to check + reform_function: The specific reform creation function to call + comparison_parameter_path_1: Dot-separated path to the parameter to compare with threshold 1 + comparison_parameter_path_2: Dot-separated path to the parameter to compare with threshold 2 + threshold_check_1: Value to compare parameter against threshold 1 + threshold_check_2: Value to compare parameter against threshold 2 + comparison_operator_1: Operator to use for comparison (default: less than) + comparison_operator_2: Operator to use for comparison (default: less than) + bypass: If True, skip parameter checks and return reform + """ + if bypass: + return reform_function() + + # Navigate parameter tree using the path + p = get_nested_value(parameters, parameter_path) + current_period = period_(period) + + for _ in range(5): + param_at_period = p(current_period) + value1 = get_nested_value(param_at_period, comparison_parameter_path_1) + value2 = get_nested_value(param_at_period, comparison_parameter_path_2) + + if comparison_operator_1( + value1, threshold_check_1 + ) or comparison_operator_2(value2, threshold_check_2): + return reform_function() + current_period = current_period.offset(1, "year") + + return None diff --git a/policyengine_us/tests/policy/contrib/states/dc/dc_ctc.yaml b/policyengine_us/tests/policy/contrib/states/dc/dc_ctc.yaml deleted file mode 100644 index 372aee42002..00000000000 --- a/policyengine_us/tests/policy/contrib/states/dc/dc_ctc.yaml +++ /dev/null @@ -1,268 +0,0 @@ -- name: 5 children and each is accounted for - period: 2025 - reforms: policyengine_us.reforms.states.dc.dc_ctc.dc_ctc - input: - gov.contrib.states.dc.ctc.in_effect: true - gov.contrib.states.dc.ctc.child_cap: 999 - people: - person1: - age: 40 - person2: - age: 5 - person3: - age: 5 - person4: - age: 5 - person5: - age: 5 - person6: - age: 6 - tax_units: - tax_unit: - members: [person1, person2, person3, person4, person5, person6] - adjusted_gross_income: 240_000 - filing_status: JOINT - households: - household: - members: [person1, person2, person3, person4, person5, person6] - state_code: DC - output: - dc_ctc: 1_680 - - -- name: 5 children with one above the age threshold - period: 2025 - reforms: policyengine_us.reforms.states.dc.dc_ctc.dc_ctc - input: - gov.contrib.states.dc.ctc.in_effect: true - gov.contrib.states.dc.ctc.child_cap: 999 - people: - person1: - age: 40 - person2: - age: 5 - person3: - age: 5 - person4: - age: 5 - person5: - age: 5 - person6: - age: 6 - tax_units: - tax_unit: - members: [person1, person2, person3, person4, person5, person6] - adjusted_gross_income: 240_000 - filing_status: JOINT - households: - household: - members: [person1, person2, person3, person4, person5, person6] - state_code: DC - output: - dc_ctc: 1_680 - -- name: One child, reform does not change output - period: 2025 - reforms: policyengine_us.reforms.states.dc.dc_ctc.dc_ctc - input: - gov.contrib.states.dc.ctc.in_effect: true - people: - person1: - age: 40 - person2: - age: 5 - tax_units: - tax_unit: - members: [person1, person2] - adjusted_gross_income: 100_000 - filing_status: SINGLE - households: - household: - members: [person1, person2] - state_code: DC - output: - dc_ctc: 420 - -- name: Four children with reduction applied, joint, uncapped children - period: 2025 - reforms: policyengine_us.reforms.states.dc.dc_ctc.dc_ctc - input: - gov.contrib.states.dc.ctc.in_effect: true - gov.contrib.states.dc.ctc.child_cap: 999 - people: - person1: - age: 40 - person2: - age: 5 - person3: - age: 5 - person4: - age: 5 - person5: - age: 5 - tax_units: - tax_unit: - members: [person1, person2, person3, person4, person5] - adjusted_gross_income: 250_000 - filing_status: JOINT - households: - household: - members: [person1, person2, person3, person4, person5] - state_code: DC - output: - dc_ctc: 1_480 - -- name: Single person with one child, below reduction, cap applies - period: 2025 - reforms: policyengine_us.reforms.states.dc.dc_ctc.dc_ctc - input: - gov.contrib.states.dc.ctc.in_effect: true - people: - person1: - age: 40 - person2: - age: 5 - tax_units: - tax_unit: - members: [person1, person2] - adjusted_gross_income: 160_000 - filing_status: SINGLE - households: - household: - members: [person1, person2] - state_code: DC - output: - dc_ctc: 420 - -- name: Children capped at 3, cap applies - period: 2025 - reforms: policyengine_us.reforms.states.dc.dc_ctc.dc_ctc - input: - gov.contrib.states.dc.ctc.in_effect: true - people: - person1: - age: 40 - person2: - age: 5 - person3: - age: 5 - person4: - age: 5 - person5: - age: 5 - person6: - age: 6 - tax_units: - tax_unit: - members: [person1, person2, person3, person4, person5, person6] - adjusted_gross_income: 240_000 - filing_status: JOINT - households: - household: - members: [person1, person2, person3, person4, person5, person6] - state_code: DC - output: - dc_ctc: 1_260 - -- name: Joint filing, with reduction, cap applies - period: 2025 - reforms: policyengine_us.reforms.states.dc.dc_ctc.dc_ctc - input: - gov.contrib.states.dc.ctc.in_effect: true - people: - person1: - age: 40 - person2: - age: 5 - person3: - age: 5 - person4: - age: 5 - person5: - age: 5 - tax_units: - tax_unit: - members: [person1, person2, person3, person4, person5] - adjusted_gross_income: 255_000 - filing_status: JOINT - dc_eitc: 200 - households: - household: - members: [person1, person2, person3, person4, person5] - state_code: DC - output: - dc_ctc: 960 - dc_refundable_credits: 1_160 - -- name: Joint filing, fully reduced, cap applies - period: 2025 - reforms: policyengine_us.reforms.states.dc.dc_ctc.dc_ctc - input: - gov.contrib.states.dc.ctc.in_effect: true - people: - person1: - age: 40 - person2: - age: 5 - tax_units: - tax_unit: - members: [person1, person2] - adjusted_gross_income: 261_000 - filing_status: JOINT - households: - household: - members: [person1, person2] - state_code: DC - output: - dc_ctc: 0 - -- name: Reform not in effect - period: 2025 - input: - gov.contrib.states.dc.ctc.in_effect: false - people: - person1: - age: 40 - person2: - age: 5 - person3: - age: 5 - person4: - age: 5 - person5: - age: 5 - person6: - age: 5 - tax_units: - tax_unit: - members: [person1, person2, person3, person4, person5, person6] - adjusted_gross_income: 160_000 - filing_status: JOINT - households: - household: - members: [person1, person2, person3, person4, person5, person6] - state_code: DC - output: - dc_refundable_credits: 0 - -- name: Integration test with reform in effect - period: 2025 - reforms: policyengine_us.reforms.states.dc.dc_ctc.dc_ctc - input: - gov.contrib.states.dc.ctc.in_effect: true - dc_ctc: 1_000 - dc_eitc: 2_000 - dc_income_tax_before_refundable_credits: 5_000 - state_code: DC - output: - dc_income_tax: 2_000 - -- name: Integration test with reform not in effect - period: 2025 - input: - gov.contrib.states.dc.ctc.in_effect: false - dc_eitc: 2_000 - dc_income_tax_before_refundable_credits: 5_000 - state_code: DC - output: - dc_income_tax: 3_000