Skip to content

Treat TAXSIM mstat 6 as a married-filing-separately return - #1246

Merged
MaxGhenis merged 4 commits into
mainfrom
fix/mstat6-married-filing-separately
Sep 28, 2026
Merged

MaxGhenis merged 4 commits into
mainfrom
fix/mstat6-married-filing-separately

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Summary

TAXSIM mstat 6 is a married-filing-separately return. Neither emulator path handled it:

  • Batch/CLI path (TaxsimMicrosimDataset._process_person_data_for_year): has_spouse = np.isin(mstat, [2, 6]) put a spouse into the tax unit, so PolicyEngine computed a joint return. An mstat 6 row with $400k of 2023 wages was taxed with MFJ brackets.
  • Single-household path (form_household_situation): built a plain one-person unit, so PolicyEngine computed single.

Both paths now build mstat 6 as a single-person tax unit plus dependents, never a spouse. Two inputs are set:

  • is_separated on the head. PE-US's filing_status formula yields SEPARATE, or HEAD_OF_HOUSEHOLD when a qualifying child lets IRC 7703(b) treat the filer as unmarried.
  • cohabitating_spouses on the tax unit. This selects the IRC 86(c)(1)(C) zero Social Security base, which taxsimtest applies to mstat 6. The zero base applies to a separate filer who "does not live apart from his spouse at all times during the taxable year"; a TAXSIM row cannot say the filer lived apart all year, so the emulator assumes they did not, as taxsimtest does.

MSTAT_MARRIED_SEPARATE = 6 is exported from policyengine_taxsim.core.input_mapper; an in-progress axiom-oracles change (not yet pushed) plans to gate its emulator leg on that name. The README, R docs, dashboard marital-status codes, the variable_mappings.yaml mstat type, and the text-output label now list mstat 6.

Once this merges, the Populace benchmark converter (#1216, scripts/convert_h5_to_taxsim.py) can emit mstat 6 for PolicyEngine's SEPARATE tax units instead of mstat 1.

TAXSIM's definition

"4. mstat Marital Status 1. single or head of household (unmarried) 2. joint (married) 6. separate (married). Note that Married-separate is not usually desirable under US tax law. 8. Dependent taxpayer. (Typically a child with income). Head of Household status is determined by taxsim from dependent information below. Unmarried taxpayers with dependents are assigned head-of-household."
— https://taxsim.nber.org/taxsimtest/, section "4. mstat Marital Status" (extracted 2026-09-24)

"It is an error to specify a non-zero spouse age for an unmarried taxpayer." … "Note that swages must be zero for non-joint returns."
— same page, sections 5 and 8

The bundled binary enforces both: it aborts on mstat 6 with a nonzero sage or swages.

How taxsimtest taxes mstat 6 (build cd2026081819)

These are paired probes: each record was run as mstat 1 and as mstat 6. The federal set covers 27 profiles × 2021–2025 with state 0; the state set covers 51 states × {2021, 2023, 2025} × 6 profiles. After this change, the emulator reproduces every federal mstat 6 behavior below; the divergences are listed in the next section.

Mechanism taxsimtest mstat 6 Emulator after this PR
Ordinary and capital-gains brackets MFS (2023, $400k wages: v28 107,832.50 vs single 107,047.00) matches
Standard deduction; aged add-on MFS amount; married add-on (2022, age 70: 14,350 vs 14,700) matches
SALT cap $5,000 (2022: itemized 25,000 vs 30,000) matches
Capital-loss limit $1,500 (2024: AGI 48,500 vs 47,000) matches
NIIT threshold $125,000 (2023: 2,280 vs 380) matches
Social Security base zero (2024, $20k SS + $30k pension: taxable SS 17,000 vs 9,600) matches (was 9,600 before cohabitating_spouses)
Dependent child head of household: std. deduction, brackets, EITC, CTC, CDCC matches
Marginal rate (frate) 37% at $400k in 2023 vs 35% single (v28 finite difference) matches; SEPARATE survives the MTR branch because both inputs are dataset inputs

Known divergences (not emulated)

The emulator's job is to map TAXSIM inputs; PolicyEngine then computes the law. Each row quotes the operative text, extracted mechanically from law.cornell.edu or the state instructions on 2026-09-24/25.

Divergence Statute or instruction Which engine follows it
Additional Medicare threshold: taxsimtest uses $200k for mstat 6 IRC 3101(b)(2): "(B) in the case of a married taxpayer (as defined in section 7703) filing a separate return, ½ of the dollar amount determined under subparagraph (A)" PE ($125k)
2025 senior deduction: taxsimtest allows $6,000 to mstat 6 IRC 151(d)(5)(C)(v) "Married individuals": "this subparagraph shall apply only if the taxpayer and the taxpayer's spouse file a joint return for the taxable year" PE
Childless EITC: taxsimtest denies it 2022–2025 but allows it in 2021; PE-US allows it every year from 2021 (gov.irs.credits.eitc.eligibility.separate_filer) IRC 32(d)(1): "In the case of an individual who is married, this section shall apply only if a joint return is filed"; (d)(2)(B)(ii) requires that the filer "resides with a qualifying child of the individual for more than one-half of such taxable year" (ARPA §9623(c): "taxable years beginning after December 31, 2020") neither fully (PE-US in no year, tracked in PolicyEngine/policyengine-us#9607; taxsimtest except 2021)
mstat 6 with a dependent that is not a qualifying child (age 19 non-student, 25): taxsimtest gives head of household, PE-US gives SEPARATE IRC 7703(b)(1): "the principal place of abode of a child (within the meaning of section 152(f)(1)) with respect to whom such individual is entitled to a deduction for the taxable year under section 151" taxsimtest, if the dependent is the filer's child (PE-US's 7703(b) path counts only qualifying children)
New York 2022–2025: taxsimtest returns siitax 0 for every mstat 6 record without dependents (staxbc −99999999999999967232); 2021, and records with dependents, are computed normally — PE
New Jersey property-tax deduction: PE-US halves it for separate filers who share a home; taxsimtest does not 2025 NJ-1040 instructions, line 41: "a Property Tax Deduction of up to $15,000 ($7,500 if you and your spouse file separate returns but maintained the same main home)" PE

Arizona is a case where the same flag moves PE toward taxsimtest. Take a 70-year-old renter with a $3,000 pension and $6,000 rent in 2023. taxsimtest gives mstat 6 $145.00 more property-tax credit than mstat 1 (siitax −393.22 vs −248.22). PE with cohabitating_spouses gives $200 more; PE without the flag gives the same as mstat 1.

State sweep

Four independent research runs root-caused every state where the mstat-6-specific error exceeded $5 on the six profiles swept. The error is measured as (pe6 − pe1) − (ts6 − ts1). Each run used taxsimtest idtl=5 traces, PE-US code, and verbatim state forms.

  • None found an emulator mapping error. For the head-of-household profile (mstat 6 plus a child), both engines give identical results for mstat 1 and mstat 6 in all 153 state-years.
  • The runs classify most residues as taxsimtest applying single-filer rules to mstat 6. Their findings: West Virginia 2023+ uses Schedule I instead of the MFS Schedule II; North Dakota, Minnesota, and Wisconsin use single brackets or deductions; Alabama's MFS standard deduction is too small; Connecticut uses the wrong MFS exemption and add-back. These are the runs' conclusions from state forms, not verified independently here.
  • Candidate PE-US state issues, tracked separately: the Hawaii SALT income gate, the CalEITC MFS gate (2021), Minnesota 2025, and a Wisconsin 2025 bracket threshold.

Invariants (tested)

For every mstat 6 record, tested on 60 seeded random records across 2021–2025 with random incomes, ages, and 0–3 dependents:

  1. Structure. The unit is the filer plus depx dependents, never a spouse; is_separated is set on the head only; cohabitating_spouses is set.
  2. Filing status. SEPARATE when no dependent is under 19, otherwise HEAD_OF_HOUSEHOLD; never JOINT or SINGLE.
  3. Differential. The single-household path computes the same filing status, federal tax before credits (v28) and federal income tax after credits (fiitax) as the batch path.
  4. Monotonicity. A childless separate return never owes less federal tax than the single return with the same inputs. Every MFS amount is at most the single amount.

Tests

tests/test_married_filing_separately.py has 44 tests:

  • 18 taxsimtest cases, each run through both paths: 12 federal and 6 state (WV, WI, NM, KS, UT, NC). In each, taxsimtest's mstat 6 result differs from its mstat 1 result, or equals the head-of-household result. Every expected value is taxsimtest output. A guard test re-runs the bundled binary on all 36 records (mstat 1 and 6) so the hard-coded values cannot drift silently.
  • An frate check.
  • Structural tests for a mixed mstat 1/2/6 chunk and for the single-household situation.
  • The three property tests described under Invariants.
  • Discriminates: on origin/main code, 37 of the first 41 tests fail.
  • Passes on both PE-US lines CI resolves: 2.15.1 (Python 3.11+, the version CI installs) and 1.783.0 (Python 3.10).

axiom: n/a: TAXSIM input mapping in the emulator; no policy rule is changed. The IRC 32(d)(2) separated-spouse EITC encoding debt is tracked separately.

🤖 Generated with Claude Code

@vercel

vercel Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
policyengine-taxsim Ready Ready Preview Sep 27, 2026 11:02am UTC

Request Review

MaxGhenis and others added 4 commits September 27, 2026 07:02
Both emulator paths now build mstat 6 as a single-person tax unit whose head
is is_separated and whose unit has cohabitating_spouses, so PE-US computes
filing status SEPARATE (HEAD_OF_HOUSEHOLD with a qualifying child) and the
IRC 86(c)(1)(C) zero Social Security base, matching taxsimtest. The batch
path previously added a spouse (PE computed JOINT) and the single-household
path computed SINGLE.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Seeded random mstat 6 records check that the unit never has a spouse, the
head alone is is_separated, filing status is SEPARATE or HEAD_OF_HOUSEHOLD by
whether a dependent is under 19, both execution paths agree, and a childless
separate return never owes less federal tax than a single return.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…habiting assumption

PE-US 2.15 nets Oregon's 2023 kicker credit into siitax for every filing
status while taxsimtest does not (taxsim #1071), so the Oregon itemizer case
no longer isolates mstat 6. North Carolina 2023 (taxsimtest 3420.00 single,
3467.50 separate) matches in both paths on PE-US 2.15.1. Also note that a
TAXSIM row cannot express a separate filer who lived apart all year, so the
emulator assumes cohabitation, as taxsimtest does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…path check

- diagnose-issue playbook listed mstat 6 as dependent; it is married filing
  separately (8 is dependent taxpayer).
- The dashboard formatter labels mstat 6 'Married Filing Separately', with a
  Vitest case.
- taxsimtest's NY zero applies only to mstat 6 records without dependents.
- CLAUDE.md notes that cohabitating_spouses also selects AZ's
  property-tax-credit table for claimants living with others.
- The cross-path property test also compares fiitax.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Merge audit (2026-09-28 open-PR sweep): gh pr checks exit 0 at head d6218fc; mergeable=MERGEABLE; not draft; no CHANGES_REQUESTED. Independent review: GPT-6 Astra via Subfleet (job11), verdict APPROVE: Gates pass: 11 passing checks, MERGEABLE, not draft, no change requests; publish/versioning are intentionally skipped on PRs.; Both emulator paths represent mstat 6 without a spouse and retain the separate-filer/cohabitation inputs. Documentation and output labels agree.; Tests cover external TAXSIM expectations, both paths, mixed household structures, marginal rates and randomized invariants. No blocking regression, secret or debug leftover found.; Not superseded: local origin/main still treats. Repo rules: Org requires passing CI and immediate pre-merge recheck; repo requires external-ground-truth tests and towncrier fragments; squash is allowed and matches recent merges.

@MaxGhenis
MaxGhenis merged commit 9cb4bac into main Sep 28, 2026
13 checks passed

This branch was successfully deployed

1 active deployment
Preview — d6218fc5 Deployed Sep 27, 2026 by vercel[bot]
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.

1 participant