Standardize local income tax aggregation across states (#9690) - #9694
jindal-git wants to merge 3 commits into
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. 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
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
hua7450
left a comment
There was a problem hiding this comment.
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_taxandhousehold_net_incomeare 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 throughnyc_income_tax, with no double count. - Adding
in_county_taxtolocal_income_taxis right (it is a deductible local income tax under IRC §164(a)(3) and (b)(2)) and creates no cycle.in_withheld_income_taxuses the state rate only, so nothing is counted twice. - Excluding
or_multnomah_pfa_taxfromlocal_income_taxis needed: it would otherwise loop throughor_federal_tax_liability_subtractionto federalincome_taxand 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:18reads onlystate_income_taxplusstate_use_tax.spm_unit_taxeshas no local term, and nothing on the SPM path readslocal_income_tax_before_refundable_creditsor the newhousehold_refundable_local_tax_credits.- Before this PR, NYC income tax net of NYC refundable credits and Maryland county tax reached
spm_unit_taxesthroughstate_income_tax. After it they do not, sospm_unit_net_income,spm_unit_is_in_spm_poverty,spm_unit_is_in_deep_spm_poverty,poverty_gapanddeep_poverty_gapchange for NYC and Maryland residents. The PR re-routed the household path but not the SPM path. - Size:
spm_unit_net_incomerises by 2,897.28 for the household inmd_local_income_tax_integration.yamland 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.yamlsuppliesspm_unit_taxesdirectly. - 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:19israte[in_in] = rates[county[in_in]].county_rates.yamlhas 92 Indiana keys and noUNKNOWN.- An Indiana household with
county_str: UNKNOWNor a non-Indiana county raisesParameterNotFoundError. Before this PR nothing aggregatedin_county_tax; now the error surfaces inincome_tax(throughlocal_income_taxand SALT) and inhousehold_tax. - Maryland had the same crash fixed in #8975.
md_flat_rate_county_tax.py:24carries the guard (safe_county = where(in_md & (county != "UNKNOWN"), county, "ALLEGANY_COUNTY_MD")) plus acounty_str: UNKNOWNtest 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.yamlandlocal_income_tax_salt_asymmetry.yamlsupplyin_agiandor_taxable_incomedirectly. No Indiana or Multnomah case runs from real income tohousehold_taxorhousehold_net_income, and none asserts the Indiana SALT effect on federalincome_tax. - There is no negative-
in_agicase inin_county_tax.yamlfor the newmax_(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:34still listsnyc_non_refundable_credits, andmd_montgomery_eitc(a county credit) is still insidemd_refundable_credits, sostate_income_taxstill 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_aginow carries county tax inhousehold_tax. With no county supplied the model uses the first Indiana county, Adams (1.6% from 2024). state_marginal_tax_rate,federal_state_income_taxandtaxsim_siitaxlose the NYC and Maryland county components, with no local counterpart.
|
Thank you for the thorough and insightful review, @hua7450! All feedback has been addressed in commit 1. SPM Taxes & Poverty Outputs (Must Fix)
2. Indiana County Guard (Should Address)
3. Test Coverage for New Flows (Should Address)
4. Suggestions & Cleanups
|
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):
New York City (NYC):
nyc_income_tax_before_refundable_creditsfromstate_income_tax_before_refundable_credits.yamlintolocal_income_tax_before_refundable_credits.nyc_refundable_creditsfromstate_refundable_credits.yamlandhousehold_refundable_state_credits.yaml.nyc_non_refundable_creditsfromstate_non_refundable_credits.yaml.nyc_refundable_creditsthroughhousehold_refundable_local_tax_creditsandlocal_tax.state_income_taxandlocal_income_taxcan now be summed without double-counting NYC tax.household_taxandhousehold_net_incomeremain invariant.Maryland Counties:
md_local_income_tax_before_refundable_creditsfromstate_income_tax_before_refundable_credits.yamlintolocal_income_tax_before_refundable_credits.md_montgomery_eitc) out ofgov.states.md.tax.income.credits.refundableintohousehold_refundable_local_tax_creditsandlocal_tax.household_taxremaining invariant.Indiana & Multnomah County (OR):
in_county_taxto bothlocal_income_tax(SALT-deductible measure) andlocal_income_tax_before_refundable_credits.ADAMS_COUNTY_INguard) inin_county_tax.pymatching the Maryland pattern (md_withheld_income_tax: ParameterNotFoundError for MD household with county=UNKNOWN (no UNKNOWN guard) #8975), ensuringcounty_str: UNKNOWNproduces 0 tax without raisingParameterNotFoundError.max_(0, in_agi)clamp supported by Indiana Schedule CT-40, line 3 (zero floor).or_multnomah_pfa_taxtolocal_income_tax_before_refundable_credits. Following the existing Yonkers convention, excluded it fromlocal_income_taxbecause Multnomah PFA tax depends on OR taxable income which subtracts federal tax liability, preventing circular dependency cycles through federal SALT.Supplemental Poverty Measure (SPM) Alignment:
local_taxat theTaxUnitlevel:40958\text{local_tax} = \text{local_income_tax_before_refundable_credits} + \text{local_occupational_tax} - \text{nyc_refundable_credits} - \text{md_montgomery_eitc}40958
spm_unit_local_taxonSPMUnitusingsum_contained_tax_units("local_tax", spm_unit, period)and added it tospm_unit_taxes.adds.spm_unit_taxesandspm_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
in_aginow carries county tax inhousehold_tax(defaulting to Adams County, 1.6%, if no county is specified).state_marginal_tax_rate,federal_state_income_tax, andtaxsim_siitaxlose NYC and MD county components, as they are now strictly pure state measures.Verification
policyengine_us/tests/policy/baseline/partners/**).test_microsim.pyandtest_or_ctc_sunset.py).policyengine_us/tests/core/test_local_employee_taxes.py).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 negativein_agifloor,UNKNOWNcounty guard, and default county behavior.md_local_income_tax_integration.yaml: pinned local tax and SPM outputs.make format.