Skip to content

Standardize local income tax aggregation across states (#9690) - #9694

Open
jindal-git wants to merge 3 commits into
PolicyEngine:mainfrom
jindal-git:fix/standardize-local-income-tax-rollups
Open

jindal-git wants to merge 3 commits into
PolicyEngine:mainfrom
jindal-git:fix/standardize-local-income-tax-rollups

Conversation

@jindal-git

@jindal-git jindal-git commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Resolves #9690.

This PR standardizes how local income and occupational taxes are aggregated into state, local, household, and SPM totals across NYC, Maryland, Indiana, and Multnomah County (OR):

  1. New York City (NYC):

    • Extracted nyc_income_tax_before_refundable_credits from state_income_tax_before_refundable_credits.yaml into local_income_tax_before_refundable_credits.
    • Extracted nyc_refundable_credits from state_refundable_credits.yaml and household_refundable_state_credits.yaml.
    • Extracted nyc_non_refundable_credits from state_non_refundable_credits.yaml.
    • Routed nyc_refundable_credits through household_refundable_local_tax_credits and local_tax.
    • Result: state_income_tax and local_income_tax can now be summed without double-counting NYC tax. household_tax and household_net_income remain invariant.
  2. Maryland Counties:

    • Extracted md_local_income_tax_before_refundable_credits from state_income_tax_before_refundable_credits.yaml into local_income_tax_before_refundable_credits.
    • Moved Montgomery County refundable EITC (md_montgomery_eitc) out of gov.states.md.tax.income.credits.refundable into household_refundable_local_tax_credits and local_tax.
    • Result: MD county income tax and credits are properly classified under local aggregates rather than state aggregates, with household_tax remaining invariant.
  3. Indiana & Multnomah County (OR):

    • Added in_county_tax to both local_income_tax (SALT-deductible measure) and local_income_tax_before_refundable_credits.
    • Added a safe county fallback (ADAMS_COUNTY_IN guard) in in_county_tax.py matching the Maryland pattern (md_withheld_income_tax: ParameterNotFoundError for MD household with county=UNKNOWN (no UNKNOWN guard) #8975), ensuring county_str: UNKNOWN produces 0 tax without raising ParameterNotFoundError.
    • Added max_(0, in_agi) clamp supported by Indiana Schedule CT-40, line 3 (zero floor).
    • Added or_multnomah_pfa_tax to local_income_tax_before_refundable_credits. Following the existing Yonkers convention, excluded it from local_income_tax because Multnomah PFA tax depends on OR taxable income which subtracts federal tax liability, preventing circular dependency cycles through federal SALT.
  4. Supplemental Poverty Measure (SPM) Alignment:

    • Created local_tax at the TaxUnit level:
      40958\text{local_tax} = \text{local_income_tax_before_refundable_credits} + \text{local_occupational_tax} - \text{nyc_refundable_credits} - \text{md_montgomery_eitc}40958
    • Created spm_unit_local_tax on SPMUnit using sum_contained_tax_units("local_tax", spm_unit, period) and added it to spm_unit_taxes.adds.
    • Result: Aligns with Census SPM methodology (Census P60-283, p. 20, subtracting income and payroll taxes generally). Restores NYC and Maryland local taxes/credits to spm_unit_taxes and spm_unit_net_income, while properly including all modeled local income and occupational taxes (Philadelphia, Kansas City, St. Louis, Yonkers, Wilmington, Indiana counties, Denver, etc.) in SPM poverty metrics.

Behavior Changes Worth Noting

  • Every Indiana tax unit with positive in_agi now carries county tax in household_tax (defaulting to Adams County, 1.6%, if no county is specified).
  • state_marginal_tax_rate, federal_state_income_tax, and taxsim_siitax lose NYC and MD county components, as they are now strictly pure state measures.

Verification

  • All partner contract tests passed (620 / 620 tests in policyengine_us/tests/policy/baseline/partners/**).
  • Full microsimulation suite passed (test_microsim.py and test_or_ctc_sunset.py).
  • Core unit tests passed (11 / 11 tests in policyengine_us/tests/core/test_local_employee_taxes.py).
  • Added tests:
    • spm_unit_local_tax.yaml: verifies NYC income tax and refundable credits, MD county tax, and Montgomery County EITC flows into SPM.
    • local_tax_rollup.yaml: added end-to-end Indiana earnings and SALT itemizer tests, and Multnomah County earnings test.
    • in_county_tax.yaml: pinned negative in_agi floor, UNKNOWN county guard, and default county behavior.
    • md_local_income_tax_integration.yaml: pinned local tax and SPM outputs.
  • Code style and formatting verified via make format.

@codecov

codecov Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (909176a) to head (e4a8c2d).
⚠️ Report is 118 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff            @@
##              main     #9694   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files            4         8    +4     
  Lines           76        80    +4     
  Branches         2         0    -2     
=========================================
+ Hits            76        80    +4     
Flag Coverage Δ
unittests 100.00% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@hua7450 hua7450 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: request changes

Reviewed head d48d631a. Findings come from code traces of the PR head against the merge base; I did not run tests locally (CI is green at this head).

The reclassification itself holds up:

  • household_tax and household_net_income are invariant for NYC, Maryland and Yonkers residents, as the description says.
  • Maryland county tax still reaches the SALT deduction through md_withheld_income_tax, and NYC through nyc_income_tax, with no double count.
  • Adding in_county_tax to local_income_tax is right (it is a deductible local income tax under IRC §164(a)(3) and (b)(2)) and creates no cycle. in_withheld_income_tax uses the state rate only, so nothing is counted twice.
  • Excluding or_multnomah_pfa_tax from local_income_tax is needed: it would otherwise loop through or_federal_tax_liability_subtraction to federal income_tax and back via SALT.
  • The 2025 Multnomah case is right: 1.5% over $125,000 for single filers gives 1,125.

Must fix

1. NYC and Maryland county tax drop out of SPM taxes and poverty outputs

  • policyengine_us/variables/household/expense/tax/spm_unit_state_tax.py:18 reads only state_income_tax plus state_use_tax. spm_unit_taxes has no local term, and nothing on the SPM path reads local_income_tax_before_refundable_credits or the new household_refundable_local_tax_credits.
  • Before this PR, NYC income tax net of NYC refundable credits and Maryland county tax reached spm_unit_taxes through state_income_tax. After it they do not, so spm_unit_net_income, spm_unit_is_in_spm_poverty, spm_unit_is_in_deep_spm_poverty, poverty_gap and deep_poverty_gap change for NYC and Maryland residents. The PR re-routed the household path but not the SPM path.
  • Size: spm_unit_net_income rises by 2,897.28 for the household in md_local_income_tax_integration.yaml and by about 3,175 for the $100k NYC filer in #9690. Low-income NYC filers move the other way, because refundable NYC credits (NYC EITC, school tax credit, CDCC) no longer add to SPM resources.
  • CI cannot see this: tests/policy/baseline/household/income/spm_unit/spm_unit_net_income.yaml supplies spm_unit_taxes directly.
  • The SPM subtracts income taxes generally, not only state ones. Census P60-283, page 20: "The SPM subtracts amounts paid for child support, income and payroll taxes" (https://www2.census.gov/library/publications/2024/demo/p60-283.pdf#page=20).

Other local taxes (Philadelphia, Kansas City, St. Louis, Yonkers, Wilmington) were already absent from the SPM path, so there is a design choice here. Either add a local term to the SPM tax path (local income tax before refundable credits, net of local refundable credits), or keep NYC and Maryland county tax there explicitly. If the exclusion is intended, please say so in the description and changelog. Whichever route, an SPM-path test with an NYC and a Maryland household that does not force spm_unit_taxes would lock it in.

Should address

2. in_county_tax has an unguarded county lookup and now sits on the federal tax path

  • policyengine_us/variables/gov/states/in/tax/income/in_county_tax.py:19 is rate[in_in] = rates[county[in_in]]. county_rates.yaml has 92 Indiana keys and no UNKNOWN.
  • An Indiana household with county_str: UNKNOWN or a non-Indiana county raises ParameterNotFoundError. Before this PR nothing aggregated in_county_tax; now the error surfaces in income_tax (through local_income_tax and SALT) and in household_tax.
  • Maryland had the same crash fixed in #8975. md_flat_rate_county_tax.py:24 carries the guard (safe_county = where(in_md & (county != "UNKNOWN"), county, "ALLEGANY_COUNTY_MD")) plus a county_str: UNKNOWN test case. The same pattern would work here.
  • I did not measure how often this triggers. The default county is valid and the microsimulation checks pass, which is why I have not rated it a blocker.

3. Test coverage for the new flows

  • The new cases in local_tax_rollup.yaml and local_income_tax_salt_asymmetry.yaml supply in_agi and or_taxable_income directly. No Indiana or Multnomah case runs from real income to household_tax or household_net_income, and none asserts the Indiana SALT effect on federal income_tax.
  • There is no negative-in_agi case in in_county_tax.yaml for the new max_(0, in_agi) clamp.
  • No case pins the default-county behavior (see the note below).
  • The Multnomah case is duplicated almost verbatim across the two files.

Suggestions

  • Leftover local entries in state aggregates. state_non_refundable_credits.yaml:34 still lists nyc_non_refundable_credits, and md_montgomery_eitc (a county credit) is still inside md_refundable_credits, so state_income_tax still nets one local credit.
  • Reference for the zero floor. The clamp is supported by Indiana Schedule CT-40, line 3: "Multiply line 1 by the rate on line 2 (leave blank if less than zero)" (https://forms.in.gov/Download.aspx?id=16337#page=1). The variable's only reference is the IC Title 6 page, which does not show the floor; adding the CT-40 link would corroborate the changed line.
  • Multnomah and SALT. The exclusion is fine as a documented limitation: it understates SALT and never understates federal tax. The county says employers must withhold the tax, so a withheld-style estimate could carry it into SALT later without a cycle.

Behavior changes worth stating in the description

  • Every Indiana tax unit with positive in_agi now carries county tax in household_tax. With no county supplied the model uses the first Indiana county, Adams (1.6% from 2024).
  • state_marginal_tax_rate, federal_state_income_tax and taxsim_siitax lose the NYC and Maryland county components, with no local counterpart.

@jindal-git

Copy link
Copy Markdown
Contributor Author

Thank you for the thorough and insightful review, @hua7450! All feedback has been addressed in commit e4a8c2d1:

1. SPM Taxes & Poverty Outputs (Must Fix)

  • Created local_tax at the TaxUnit level, netting local income and occupational taxes against local refundable credits (nyc_refundable_credits and md_montgomery_eitc).
  • Created spm_unit_local_tax on SPMUnit using sum_contained_tax_units("local_tax", spm_unit, period) and added it to spm_unit_taxes.adds.
  • This conforms with Census SPM methodology (Census P60-283, p. 20), restores NYC and Maryland county taxes/credits to spm_unit_taxes and spm_unit_net_income, and also brings all other modeled local income and occupational taxes (Philadelphia, St. Louis, Kansas City, Yonkers, Wilmington, Indiana, Denver) into SPM.
  • Added dedicated SPM test cases in spm_unit_local_tax.yaml (NYC tax, NYC refundable credits, MD county tax, Montgomery County EITC) and pinned SPM outputs in md_local_income_tax_integration.yaml.

2. Indiana County Guard (Should Address)

  • Added the Maryland-style fallback guard in in_county_tax.py (safe_county = where(in_in & (county != "UNKNOWN"), county, "ADAMS_COUNTY_IN")), setting rate to 0.0 for UNKNOWN county to prevent ParameterNotFoundError on the federal tax path.
  • Added a test case in in_county_tax.yaml verifying that county_str: UNKNOWN produces 0 tax without raising an error.

3. Test Coverage for New Flows (Should Address)

  • Added end-to-end tests from employment_income to household_tax, household_net_income, and SPM totals for both Indiana and Multnomah County in local_tax_rollup.yaml.
  • Added an Indiana itemizer test in local_tax_rollup.yaml verifying that Indiana county tax expands the federal SALT deduction and reduces federal taxable income.
  • Added a negative-in_agi test case in in_county_tax.yaml confirming the zero clamp.
  • Added a default-county test case in in_county_tax.yaml confirming unsupplied county defaults to Adams County.

4. Suggestions & Cleanups

  • Removed nyc_non_refundable_credits from state_non_refundable_credits.yaml.
  • Moved Montgomery County refundable EITC (md_montgomery_eitc) out of parameters/gov/states/md/tax/income/credits/refundable.yaml into household_refundable_local_tax_credits and local_tax.
  • Added the citation for Indiana Schedule CT-40, line 3 (zero floor) to in_county_tax.py.
  • Updated the PR description with the noted behavior changes (Indiana default county tax and pure state scope of state_marginal_tax_rate, etc.).

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Standardize local income tax aggregation across states (NYC, MD, IN, Multnomah County)

2 participants