Skip to content

taxable_unemployment_compensation collapses all UI to head, breaking per-person state AGI #8494

@PavelMakarchuk

Description

@PavelMakarchuk

Summary

taxable_unemployment_compensation assigns the entire tax unit's taxable UI to the head and zero to the spouse, regardless of who actually received the UI. The variable's docstring claims this "will not affect overall tax liability," but it does affect state liability in any state whose AGI flows through adjusted_gross_income_person — including KY, AR, DE, IA, MS, MT (combined-return states), and likely others (e.g., WV, possibly MI).

Originating TAXSIM issue: PolicyEngine/policyengine-taxsim#921 (KY $446 vs TaxAct $524 = $78 off). A parallel WV case (PolicyEngine/policyengine-taxsim#920) likely shares this root cause.

Reproduction

Joint couple, KY, 2025. Primary has no income; spouse has $10K wages and $19,124 UI.

from policyengine_us import Simulation

situation = {
    "people": {
        "you": {"age": {"2025": 75}, "is_tax_unit_head": {"2025": True},
                "unemployment_compensation": {"2025": 1.13}},
        "your partner": {"age": {"2025": 40}, "is_tax_unit_spouse": {"2025": True},
                          "employment_income": {"2025": 10000},
                          "unemployment_compensation": {"2025": 19123.87}},
    },
    "tax_units": {"tu": {"members": ["you", "your partner"]}},
    "households": {"hh": {"members": ["you", "your partner"], "state_fips": {"2025": 21}}},
    "marital_units": {"m": {"members": ["you", "your partner"]}},
    "families": {"f": {"members": ["you", "your partner"]}},
    "spm_units": {"s": {"members": ["you", "your partner"]}},
}
sim = Simulation(situation=situation)
print(sim.calculate("unemployment_compensation", 2025))          # [1.13, 19123.87]  ✓
print(sim.calculate("taxable_unemployment_compensation", 2025))  # [19125., 0.]      ✗
print(sim.calculate("adjusted_gross_income_person", 2025))       # [19125., 10000.] (sum OK, split wrong)
print(sim.calculate("ky_income_tax", 2025))                      # 446.04, should be ~524

Root cause

taxable_unemployment_insurance.py:14-19:

is_tax_unit_head = person("is_tax_unit_head", period)
tax_unit_taxable_uc = person.tax_unit("tax_unit_taxable_unemployment_compensation", period)
return where(is_tax_unit_head, tax_unit_taxable_uc, 0)

This was a deliberate shortcut so taxable UI (a tax-unit-level concept under §85) could be summed into irs_gross_income (person-level) without double counting. The comment "will not affect overall tax liability" holds for federal AGI (tax_unit sum is unchanged) but breaks per-person AGI used by state code:

  • adjusted_gross_income_personirs_gross_incometaxable_unemployment_compensation
  • States with combined-return optimization (KY's ky_files_separately, similar in AR/DE/IA/MS/MT) compare per-spouse tax. When all UI is placed on one spouse, the other spouse appears to have AGI it doesn't (here, $8,500), which lets them claim a separate state standard deduction.

For #921: spouse with no real income gets $8,500 of artificial AGI, claims $3,270 KY std deduction it shouldn't have → $3,270 × 4% ≈ $131 KY tax shortfall (modulo family-size credit).

Suggested fix

Allocate tax_unit_taxable_unemployment_compensation across persons proportionally to each person's unemployment_compensation:

def formula(person, period, parameters):
    tax_unit_taxable_uc = person.tax_unit("tax_unit_taxable_unemployment_compensation", period)
    person_uc = person("unemployment_compensation", period)
    tax_unit_uc = person.tax_unit.sum(person_uc)
    share = where(tax_unit_uc > 0, person_uc / tax_unit_uc, 0)
    return tax_unit_taxable_uc * share

The federal sum is preserved (shares sum to 1) and per-person AGI reflects the actual recipient.

Integration test

- name: taxable_unemployment_compensation preserves per-person allocation
  period: 2025
  input:
    people:
      head:
        age: 75
        is_tax_unit_head: true
        unemployment_compensation: 1_000
      spouse:
        age: 40
        is_tax_unit_spouse: true
        unemployment_compensation: 19_000
    tax_units:
      tax_unit:
        members: [head, spouse]
    households:
      household:
        members: [head, spouse]
        state_code: KY
  output:
    taxable_unemployment_compensation: [1_000, 19_000]

And a KY-level integration test reproducing #921's expected output (PE = $524, was $446).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions