diff --git a/changelog.d/remove-uprating-from-formula-variables.changed.md b/changelog.d/remove-uprating-from-formula-variables.changed.md new file mode 100644 index 00000000000..1fb233348af --- /dev/null +++ b/changelog.d/remove-uprating-from-formula-variables.changed.md @@ -0,0 +1 @@ +Remove redundant uprating metadata and class-level aggregation metadata from variables that already define their computation explicitly. diff --git a/policyengine_us/data/economic_assumptions.py b/policyengine_us/data/economic_assumptions.py index 75d03e1a791..66b4fc3eca8 100644 --- a/policyengine_us/data/economic_assumptions.py +++ b/policyengine_us/data/economic_assumptions.py @@ -12,6 +12,38 @@ # is updated with new projection years, datasets will automatically # extend to match โ€” no hardcoded year constant to maintain. CPI_U_PARAM_PATH = "gov.bls.cpi.cpi_u" +DEFAULT_MICRODATA_UPRATING = ( + "calibration.gov.cbo.income_by_source.adjusted_gross_income" +) + +MICRODATA_UPRATING_OVERRIDES = { + "american_opportunity_credit": DEFAULT_MICRODATA_UPRATING, + "cdcc_relevant_expenses": DEFAULT_MICRODATA_UPRATING, + "employment_income": "calibration.gov.irs.soi.employment_income", + "employment_income_last_year": "calibration.gov.irs.soi.employment_income", + "energy_efficient_home_improvement_credit": DEFAULT_MICRODATA_UPRATING, + "foreign_tax_credit": DEFAULT_MICRODATA_UPRATING, + "interest_deduction": DEFAULT_MICRODATA_UPRATING, + "long_term_capital_gains": "calibration.gov.irs.soi.long_term_capital_gains", + "misc_deduction": DEFAULT_MICRODATA_UPRATING, + "person_weight": "calibration.gov.census.populations.total", + "pre_tax_contributions": DEFAULT_MICRODATA_UPRATING, + "rent": "gov.bls.cpi.cpi_u", + "savers_credit": DEFAULT_MICRODATA_UPRATING, + "self_employment_income": "calibration.gov.irs.soi.self_employment_income", + "self_employed_health_insurance_ald": DEFAULT_MICRODATA_UPRATING, + "self_employed_pension_contribution_ald": DEFAULT_MICRODATA_UPRATING, + "social_security": "calibration.gov.irs.soi.social_security", + "spm_unit_weight": "calibration.gov.census.populations.total", + "spm_unit_spm_threshold": DEFAULT_MICRODATA_UPRATING, + "state_and_local_sales_or_income_tax": DEFAULT_MICRODATA_UPRATING, + "sstb_self_employment_income": "calibration.gov.irs.soi.self_employment_income", + "taxable_pension_income": "calibration.gov.irs.soi.taxable_pension_income", + "taxable_unemployment_compensation": DEFAULT_MICRODATA_UPRATING, + "tax_unit_weight": "calibration.gov.census.populations.total", + "tax_exempt_pension_income": DEFAULT_MICRODATA_UPRATING, + "total_self_employment_income": "calibration.gov.irs.soi.self_employment_income", +} def get_parameter_last_year(parameter) -> int: @@ -100,11 +132,10 @@ def _apply_uprating(dataset: USMultiYearDataset, system=None) -> USMultiYearData def _apply_single_year_uprating(current, previous, system): """Apply multiplicative uprating from previous year to current year. - For each variable column in each entity DataFrame, looks up the - variable's uprating parameter path in ``system.variables``. If the - variable has an uprating parameter, computes the growth factor as - ``param(current_year) / param(previous_year)`` and multiplies the - column by that factor. + For each variable column in each entity DataFrame, looks up its + dataset-extension uprating parameter path. Formula and adds/subtracts + variables cannot use Core variable-level uprating, so their dataset-only + upraters live in ``MICRODATA_UPRATING_OVERRIDES`` instead. Variables without an uprating parameter (or whose uprating parameter evaluates to 0 for the previous year) are left unchanged โ€” they were @@ -122,7 +153,9 @@ def _apply_single_year_uprating(current, previous, system): if col not in system.variables: continue var = system.variables[col] - uprating_path = getattr(var, "uprating", None) + uprating_path = MICRODATA_UPRATING_OVERRIDES.get(col) or getattr( + var, "uprating", None + ) if uprating_path is None: continue diff --git a/policyengine_us/tests/microsimulation/data/test_extend_single_year_dataset.py b/policyengine_us/tests/microsimulation/data/test_extend_single_year_dataset.py index 632aa0ad2e9..16027926ac9 100644 --- a/policyengine_us/tests/microsimulation/data/test_extend_single_year_dataset.py +++ b/policyengine_us/tests/microsimulation/data/test_extend_single_year_dataset.py @@ -125,6 +125,30 @@ def test_given_variable_without_uprating_then_values_unchanged( # Then โ€” age has no uprating, should be unchanged np.testing.assert_array_equal(current.person["age"].values, AGE_BASE) + def test_given_computed_variable_with_microdata_override_then_values_scaled( + self, base_dataset + ): + # Given + current = base_dataset.copy() + current.time_period = str(BASE_YEAR + 1) + previous = base_dataset + variables = { + "employment_income": MockVariable("employment_income", uprating=None) + } + system = MockSystem( + variables=variables, + parameters=build_mock_parameters( + {EMPLOYMENT_INCOME_UPRATING: EMPLOYMENT_INCOME_PARAM_VALUES} + ), + ) + + # When + _apply_single_year_uprating(current, previous, system) + + # Then + expected = EMPLOYMENT_INCOME_BASE * EMPLOYMENT_INCOME_GROWTH_FACTOR_2024_TO_2025 + np.testing.assert_allclose(current.person["employment_income"].values, expected) + def test_given_household_variable_with_uprating_then_values_scaled( self, base_dataset, mock_system ): diff --git a/policyengine_us/tests/test_system_import.py b/policyengine_us/tests/test_system_import.py index 7cd9df70008..df73dc9ef18 100644 --- a/policyengine_us/tests/test_system_import.py +++ b/policyengine_us/tests/test_system_import.py @@ -36,3 +36,49 @@ def test_package_import_does_not_raise(): import policyengine_us importlib.reload(policyengine_us) + + +def test_variables_use_at_most_one_computation_mode(): + """Variables should choose exactly one computation mode after all + system-level mutations, including default uprating assignment. + """ + from policyengine_us.system import CountryTaxBenefitSystem + + system = CountryTaxBenefitSystem() + conflicts = [] + for name, variable in system.variables.items(): + modes = [] + if variable.formulas: + modes.append("formula") + if variable.adds is not None or variable.subtracts is not None: + modes.append("adds/subtracts") + if variable.uprating is not None: + modes.append("uprating") + if len(modes) > 1: + conflicts.append(f"{name}: {', '.join(modes)}") + + assert conflicts == [] + + +def test_computed_default_uprated_variables_have_microdata_overrides(): + """Default uprating can only be assigned to input variables at runtime. + Computed columns that previously received the default uprater keep that + behavior through microdata-only overrides. + """ + from policyengine_us.data.economic_assumptions import MICRODATA_UPRATING_OVERRIDES + from policyengine_us.system import CountryTaxBenefitSystem + from policyengine_us.tools.default_uprating import INPUT_VARIABLES + + system = CountryTaxBenefitSystem() + missing_overrides = [] + for name in INPUT_VARIABLES: + variable = system.variables.get(name) + if variable is None: + continue + if ( + not variable.is_input_variable() + and name not in MICRODATA_UPRATING_OVERRIDES + ): + missing_overrides.append(name) + + assert missing_overrides == [] diff --git a/policyengine_us/tools/default_uprating.py b/policyengine_us/tools/default_uprating.py index 8f7b3d8ba90..915dc8ddf14 100644 --- a/policyengine_us/tools/default_uprating.py +++ b/policyengine_us/tools/default_uprating.py @@ -98,7 +98,11 @@ def add_default_uprating(system): for variable in system.variables.values(): - if (variable.name in INPUT_VARIABLES) and (variable.uprating is None): + if ( + (variable.name in INPUT_VARIABLES) + and (variable.uprating is None) + and variable.is_input_variable() + ): variable.uprating = ( "calibration.gov.cbo.income_by_source.adjusted_gross_income" ) diff --git a/policyengine_us/variables/gov/ssa/ss/social_security.py b/policyengine_us/variables/gov/ssa/ss/social_security.py index 163abcabb77..003f9946929 100644 --- a/policyengine_us/variables/gov/ssa/ss/social_security.py +++ b/policyengine_us/variables/gov/ssa/ss/social_security.py @@ -12,4 +12,3 @@ class social_security(Variable): "social_security_" + i for i in ["dependents", "disability", "retirement", "survivors"] ] - uprating = "calibration.gov.irs.soi.social_security" diff --git a/policyengine_us/variables/gov/states/tax/income/state_itemized_deductions.py b/policyengine_us/variables/gov/states/tax/income/state_itemized_deductions.py index 7c7c584b279..086420bef56 100644 --- a/policyengine_us/variables/gov/states/tax/income/state_itemized_deductions.py +++ b/policyengine_us/variables/gov/states/tax/income/state_itemized_deductions.py @@ -7,7 +7,6 @@ class state_itemized_deductions(Variable): label = "State itemized deductions" unit = USD definition_period = YEAR - adds = "gov.states.household.state_itemized_deductions" def formula(tax_unit, period, parameters): # States that adopt the federal itemized deductions diff --git a/policyengine_us/variables/gov/states/tax/income/taxsim_state_agi.py b/policyengine_us/variables/gov/states/tax/income/taxsim_state_agi.py index b91941ca0c3..dcd31c2df2d 100644 --- a/policyengine_us/variables/gov/states/tax/income/taxsim_state_agi.py +++ b/policyengine_us/variables/gov/states/tax/income/taxsim_state_agi.py @@ -7,7 +7,6 @@ class taxsim_state_agi(Variable): label = "State adjusted gross income" unit = USD definition_period = YEAR - adds = "gov.states.household.state_agis" def formula(tax_unit, period, parameters): # States that adopt the federal AGI diff --git a/policyengine_us/variables/household/demographic/weights/person_weight.py b/policyengine_us/variables/household/demographic/weights/person_weight.py index 27553ab00ac..fc465f66c1d 100644 --- a/policyengine_us/variables/household/demographic/weights/person_weight.py +++ b/policyengine_us/variables/household/demographic/weights/person_weight.py @@ -6,7 +6,6 @@ class person_weight(Variable): entity = Person label = "Person weight" definition_period = YEAR - uprating = "calibration.gov.census.populations.total" def formula(person, period, parameters): return person.household("household_weight", period) diff --git a/policyengine_us/variables/household/demographic/weights/spm_unit_weight.py b/policyengine_us/variables/household/demographic/weights/spm_unit_weight.py index 5d8ed9aba82..7bd9e182198 100644 --- a/policyengine_us/variables/household/demographic/weights/spm_unit_weight.py +++ b/policyengine_us/variables/household/demographic/weights/spm_unit_weight.py @@ -6,7 +6,6 @@ class spm_unit_weight(Variable): entity = SPMUnit label = "SPM unit weight" definition_period = YEAR - uprating = "calibration.gov.census.populations.total" def formula(spm_unit, period, parameters): # Use household weights if not provided diff --git a/policyengine_us/variables/household/demographic/weights/tax_unit_weight.py b/policyengine_us/variables/household/demographic/weights/tax_unit_weight.py index 01c5e4e2536..8c471fb8fe8 100644 --- a/policyengine_us/variables/household/demographic/weights/tax_unit_weight.py +++ b/policyengine_us/variables/household/demographic/weights/tax_unit_weight.py @@ -6,7 +6,6 @@ class tax_unit_weight(Variable): entity = TaxUnit label = "Tax unit weight" definition_period = YEAR - uprating = "calibration.gov.census.populations.total" def formula(tax_unit, period, parameters): return tax_unit.household("household_weight", period) diff --git a/policyengine_us/variables/household/expense/housing/rent.py b/policyengine_us/variables/household/expense/housing/rent.py index 47d560e14a4..081a286054d 100644 --- a/policyengine_us/variables/household/expense/housing/rent.py +++ b/policyengine_us/variables/household/expense/housing/rent.py @@ -7,7 +7,6 @@ class rent(Variable): label = "Rent" unit = USD definition_period = YEAR - uprating = "gov.bls.cpi.cpi_u" def formula(person, period, parameters): pre_subsidy_rent = person("pre_subsidy_rent", period) diff --git a/policyengine_us/variables/household/income/person/capital_gains/long_term_capital_gains.py b/policyengine_us/variables/household/income/person/capital_gains/long_term_capital_gains.py index aab51871699..86d1e3e9d59 100644 --- a/policyengine_us/variables/household/income/person/capital_gains/long_term_capital_gains.py +++ b/policyengine_us/variables/household/income/person/capital_gains/long_term_capital_gains.py @@ -12,7 +12,6 @@ class long_term_capital_gains(Variable): title="26 U.S. Code ยง 1222(3)", href="https://www.law.cornell.edu/uscode/text/26/1222#3", ) - uprating = "calibration.gov.irs.soi.long_term_capital_gains" adds = [ "long_term_capital_gains_before_response", "capital_gains_behavioral_response", diff --git a/policyengine_us/variables/household/income/person/employment_income_last_year.py b/policyengine_us/variables/household/income/person/employment_income_last_year.py index 6e9d21ed647..4220cb92231 100644 --- a/policyengine_us/variables/household/income/person/employment_income_last_year.py +++ b/policyengine_us/variables/household/income/person/employment_income_last_year.py @@ -8,7 +8,6 @@ class employment_income_last_year(Variable): documentation = "Wages and salaries in prior year, including tips and commissions." unit = USD definition_period = YEAR - uprating = "calibration.gov.irs.soi.employment_income" def formula_2024(person, period, parameters): employment_income_target = parameters.calibration.gov.irs.soi.employment_income diff --git a/policyengine_us/variables/household/income/person/retirement/taxable_pension_income.py b/policyengine_us/variables/household/income/person/retirement/taxable_pension_income.py index b1f36a0d6fe..ea9d48c12ca 100644 --- a/policyengine_us/variables/household/income/person/retirement/taxable_pension_income.py +++ b/policyengine_us/variables/household/income/person/retirement/taxable_pension_income.py @@ -7,6 +7,5 @@ class taxable_pension_income(Variable): label = "taxable pension income" unit = USD definition_period = YEAR - uprating = "calibration.gov.irs.soi.taxable_pension_income" adds = ["taxable_public_pension_income", "taxable_private_pension_income"] diff --git a/policyengine_us/variables/household/income/person/self_employment/total_self_employment_income.py b/policyengine_us/variables/household/income/person/self_employment/total_self_employment_income.py index c5699055e0d..6851e3b795e 100644 --- a/policyengine_us/variables/household/income/person/self_employment/total_self_employment_income.py +++ b/policyengine_us/variables/household/income/person/self_employment/total_self_employment_income.py @@ -13,4 +13,3 @@ class total_self_employment_income(Variable): definition_period = YEAR adds = ["self_employment_income", "sstb_self_employment_income"] reference = "https://www.law.cornell.edu/uscode/text/26/1402#a" - uprating = "calibration.gov.irs.soi.self_employment_income" diff --git a/policyengine_us/variables/input/employment_income.py b/policyengine_us/variables/input/employment_income.py index feab9b621d5..499dd2c3ab3 100644 --- a/policyengine_us/variables/input/employment_income.py +++ b/policyengine_us/variables/input/employment_income.py @@ -13,4 +13,3 @@ class employment_income(Variable): "employment_income_behavioral_response", ] reference = "https://www.law.cornell.edu/uscode/text/26/3401#a" - uprating = "calibration.gov.irs.soi.employment_income" diff --git a/policyengine_us/variables/input/self_employment_income.py b/policyengine_us/variables/input/self_employment_income.py index 8f4db220852..708d7fc6b0f 100644 --- a/policyengine_us/variables/input/self_employment_income.py +++ b/policyengine_us/variables/input/self_employment_income.py @@ -13,4 +13,3 @@ class self_employment_income(Variable): "self_employment_income_behavioral_response", ] reference = "https://www.law.cornell.edu/uscode/text/26/1402#a" - uprating = "calibration.gov.irs.soi.self_employment_income" diff --git a/policyengine_us/variables/input/sstb_self_employment_income.py b/policyengine_us/variables/input/sstb_self_employment_income.py index c0cab37c3ad..f135d901cf0 100644 --- a/policyengine_us/variables/input/sstb_self_employment_income.py +++ b/policyengine_us/variables/input/sstb_self_employment_income.py @@ -23,4 +23,3 @@ class sstb_self_employment_income(Variable): "sstb_self_employment_income_before_lsr", "sstb_self_employment_income_behavioral_response", ] - uprating = "calibration.gov.irs.soi.self_employment_income"