Skip to content

Keep AddMed out of fiitax in every year, add the addmed column, and fix single-household frate - #1239

Merged
MaxGhenis merged 2 commits into
mainfrom
addmed-fiitax-convention
Sep 25, 2026
Merged

MaxGhenis merged 2 commits into
mainfrom
addmed-fiitax-convention

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Summary

For tax years 2013–2023, the bundled taxsimtest (cd2026081819) puts the Additional Medicare Tax (AddMed) in fiitax. From 2024 it leaves it out. In every year it also counts AddMed in tfica.

I checked whether the emulator should copy that year split. It should not:

The Microsimulation runner already leaves AddMed out of fiitax (since #963). This PR:

  • documents the convention with the actual evidence and pins it with tests. The old comments cited form lines that were never extracted. They also presented the emulator's layout as taxsimtest's ("AddMed … flows through v44"), but taxsimtest's line 44 is AddMed alone, and the emulator had no addmed column.
  • adds the missing addmed output column.
  • fixes the single-household path's frate. It still added AddMed in every year, which contradicted that path's own fiitax and the runner.

Evidence

Binary behavior (engine output, cd2026081819). Single filer, TX, $400K wages, idtl 2:

year fiitax v28 addmed tfica
2021 111,951.75 110,151.75 1,800.00 16,453.60
2023 108,847.00 107,047.00 1,800.00 17,532.40
2024 105,264.75 105,264.75 1,800.00 18,053.20
2025 104,034.75 104,034.75 1,800.00 18,518.20

Probe sweeps across wages and self-employment income, several filing statuses, all 52 state codes, AMT and credit cases, and idtl 0/2/5 found that fiitax − (v28 + v27 + niit − credits − cares) equals addmed for every year 2013–2023 and is 0 from 2024. 2012 has no AddMed. The net investment income tax is in fiitax in every year. The older resources/taxsim35 build (cdate-2025Aug23) still puts AddMed in 2024 fiitax and leaves it out of tfica.

TAXSIM's author:

That is what the emulator does:

  • PE-US income_tax includes NIIT through income_tax_before_refundable_credits.
  • taxsim_tfica includes AddMed once, through employee_payroll_tax, whose components include additional_medicare_tax. That variable is computed on wages plus self-employment income. self_employment_tax holds only regular SECA.
  • The emulator's tfica matches the binary's to the cent in the table above.

Documentation is stale on this point. https://taxsim.nber.org/taxsimtest/, "Output Results" (Last-Modified 2026-09-04):

fiitax = Federal income tax liability including capital gains rates, surtaxes, Maximum Tax, NIIT, AMT, Additional Medicare Tax and refundable and non-refundable credits including CTC, ACTC and EIC etc, but not including self-employment or FICA taxes.

Where the split likely comes from. This is a pointer only; the binary was built from newer, unpublished source. In the amalgamated taxsim.f in public feenberg/taxsim at 359c71f (2026-04-29):

  • nlaw (lines 39383–39387) sends in(lawyr,1987,2023) to law87 and later years to law24.
  • law87 ends with tax=tax+addmtx (line 39214). law24 has no such line.
  • sstax adds addmtx to c(75) and c(190) (lines 40002–40003).

The standalone nlaw.for and sstax.for in the same commit are older and differ. The public code also computes 1,524.60 of AddMed on $400K of wages, where the binary gives 1,800.

Changes

  • variable_mappings.yaml: new addmed → additional_medicare_tax output, idtl 2 only. taxsimtest prints an addmed column only at idtl 2.
  • core/marginal_rates.py: single-household frate no longer adds AddMed. At $400K wages it was 35.9 and is now 35.0. That matches the runner and the binary's own +$100 difference on the same basis (2024: 105,299.75 − 105,264.75).
  • runners/policyengine_runner.py:
    • comments now state the verified basis;
    • mtr_vars is an ordered tuple instead of a set, so the frate/srate output column order no longer changes with hash randomization;
    • the fiitax computation itself is unchanged.
  • README: an "Additional Medicare Tax and fiitax" section and an addmed row. The section scopes the convention to PolicyEngine-computed rows: the default CLI's StitchedRunner takes pre-2021 rows from the bundled binary, and those carry the binary's inclusion until it is refreshed.
  • Dashboard docs: the fiitax description no longer says it includes AddMed, and addmed is marked implemented.
  • output/*.csv: the e2e artifacts are regenerated. They add the addmed column and fix the frate/srate order; no values change.
  • tests/test_addmed_excluded_from_fiitax.py, rewritten. The old docstring gave addmed = 2,355.75 for the 2024 $500K case; the binary prints 2,700.00. The tests:
  • tests/test_performance.py: the fake-sim unit test now runs for 2021, 2023 and 2024.

Testing

  • The full pytest tests/ suite passes locally: Python 3.14, PE-US 2.11.3, macOS. The only non-pass is the intended xfail.
  • A reviewer agent also ran the AddMed module in a Python 3.10 environment matching CI's 3.10 pins (PE-US 1.783.0, pandas 2.3.3): 14 passed, 1 xfailed.
  • CI showed that the bundled Windows binary (cd2026081318) reports addmed 1,524.60 on $400K of wages, where the macOS and Linux builds (cd2026081819) report 1,800.00. That is 0.009 × (0.9235 × 400,000 − 200,000): the self-employment factor applied to wages. The binary guard therefore checks only the convention (from 2024, fiitax == v28 with addmed > 0) and does not pin the amount. The emulator tests pin the macOS/Linux values and don't run the binary. The Windows build still xfails the 2013–2023 check.
  • The first CI run's three Python 3.11 cells were cancelled by the matrix's fail-fast after the Windows 3.10 failure.

Not in this PR

  • v43/v44 differ from taxsimtest's idtl 5 lines 43 and 44 (NIIT; AddMed alone). The emulator's v44 is employee Medicare tax plus AddMed. Queued as a separate task.
  • The emulator appears to compute mstat 6 as joint. Queued as a separate task.
  • Three other binary observations are drafted as questions for NBER, not posted:

🤖 Generated with Claude Code

…e-household frate

The bundled taxsimtest (cd2026081819) adds the Additional Medicare Tax
to fiitax for 2013-2023 and not from 2024, while counting it in tfica
in every year. The emulator does not copy that split: TAXSIM's author
said AddMed belongs with FICA (#416) and on #1225 (2026-09-24) called
its inclusion in fiitax "an error that was present in 2000-2023" that
he believes he has corrected. The bundled binary predates that.

- Add an `addmed` output column (idtl=2), as taxsimtest prints.
- Stop adding AddMed to frate on the single-household export_household
  path (it did so in every year, unlike its own fiitax and the runner).
- Replace comments that cited unextracted form lines and attributed the
  emulator's v44/addmed layout to taxsimtest; document the convention
  and the stitched pre-2021 caveat in the README and dashboard docs.
- Make frate/srate column order deterministic and regenerate the e2e
  output artifacts (new addmed column; no value changes).
- Rewrite tests/test_addmed_excluded_from_fiitax.py with expected values
  from taxsimtest output, a 2024+ binary guard, and a strict xfail that
  flips once resources/taxsimtest carries the #1225 correction.

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

vercel Bot commented Sep 24, 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 24, 2026 9:39pm UTC

Request Review

The bundled Windows taxsimtest (cd2026081318) reports addmed 1524.60 on
$400K of wages where the macOS/Linux builds (cd2026081819) report
1800.00, so CI failed on windows-latest. The guard now checks only the
convention: from 2024, fiitax == v28 with addmed > 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis
MaxGhenis merged commit a80b8c1 into main Sep 25, 2026
18 of 22 checks passed
@MaxGhenis
MaxGhenis deleted the addmed-fiitax-convention branch September 27, 2026 19:30
MaxGhenis added a commit that referenced this pull request Oct 11, 2026
…order across hash seeds (#1427)

#1239 made the frate/srate columns an ordered tuple, but nothing tested it: within one process a set of strings iterates in a stable order, so a regression only shows up across processes with different PYTHONHASHSEED. Add a test that runs PolicyEngineRunner in two subprocesses whose seeds iterate {"frate", "srate"} in opposite orders and requires identical output.

Fix three other places where a set's order reached output:

- api._validate_csv named whichever bad numeric column came first in hash order; it now names the first one in the uploaded header.
- BaseTaxRunner.validate_required_columns returned its missing and extra lists in hash order; they now follow required_columns and the input columns, each name once.
- compare's consolidated CSV listed records in set order of their IDs; it now follows the frame TaxComparator sorted by taxsimid.

Each has example tests and a Hypothesis property test.

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

This branch was successfully deployed

1 active deployment
Preview — 62b32a68 Deployed Sep 24, 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