Skip to content

Count Ohio's unreimbursed medical care deduction once per return - #9925

Merged
MaxGhenis merged 2 commits into
mainfrom
fix-oh-medical-deduction-per-person
Oct 9, 2026
Merged

MaxGhenis merged 2 commits into
mainfrom
fix-oh-medical-deduction-per-person

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Ohio's unreimbursed medical and health care expense deduction (R.C. 5747.01(A)(10)) was counted once per tax-unit member, so a couple deducted it twice and a couple with a child three times.

Cause. oh_unreimbursed_medical_care_expense_deduction_person (Person) listed the TaxUnit variable oh_insured_unreimbursed_medical_care_expenses (worksheet line 8) in its adds. policyengine-core evaluates adds with calculate(..., map_to="person"), and map_result projects a group value onto every member. Each member therefore got the full return amount. It flowed through oh_deductions (Person) into oh_agi_person and then oh_agi, the sum over members, so Ohio AGI fell by n × line 8. Each spouse's oh_joint_filing_credit_qualifying_income also lost the whole amount, which could deny the joint filing credit.

Fix.

  1. Use the existing person-level oh_insured_unreimbursed_medical_care_expenses_person in the adds. It was tested but had no consumer. It gives each member line 8 × (their line 5 expenses / the return's line 5), so the shares sum to line 8.
  2. Floor federal AGI at zero on worksheet line 6. The worksheet says "If less than zero, enter zero" in every year: 2022 p. 27, 2023 p. 40, 2024 p. 41 and 2025 p. 41. Without the floor, negative AGI made line 7 negative, so line 8 exceeded the expenses paid. R.C. 5747.01(A)(10)(b) allows only "the amount the taxpayer paid … to the extent the expenses exceed seven and one-half per cent of the taxpayer's federal adjusted gross income". The floor is also what makes the proration conserve the total: with line 5 at zero, line 8 is now zero, where before it was 7.5% of |AGI| with no expenses to prorate it over.

The changed variables now cite live dam.assets.ohio.gov copies of the instructions. The tax.ohio.gov/static/... 2022 URL returns 404.

Example (2025, joint couple plus a child; head wages 40,000 and medical expenses 6,000; spouse wages 10,000). Line 8 = 6,000 − 7.5% × 50,000 = 2,250.

main this PR
oh_unreimbursed_medical_care_expense_deduction_person [2,250, 2,250, 2,250] [2,250, 0, 0]
oh_unreimbursed_medical_care_expense_deduction 6,750 2,250
oh_agi 43,250 47,750

Joint filing credit (2025; head wages 60,000 and medical expenses 10,000; spouse wages 2,000). On main the spouse's qualifying income is 2,000 − 5,350 = −3,350, so the credit is denied. With this PR it is 2,000, above the $500 threshold, and the credit is 106.53.

Tests

New YAML cases. All six fail on main (checked by restoring main's two variable files):

  • deductions/medical_expense/oh_unreimbursed_medical_care_expense_deduction.yaml, four 2025 multi-person cases:
    • the example above;
    • expenses on every member, prorated 3/6, 1/6 and 2/6;
    • line 1 and line 2 premiums staying with the person who paid them (line 9 = 1,200 + 500 + 2,250);
    • the joint filing credit case.
  • oh_insured_unreimbursed_medical_care_expenses.yaml, two cases with federal AGI −20,000: expenses 1,000 give 1,000, where main gave 2,500; expenses 0 give 0, where main gave 1,500.

Expectations come from the worksheet lines, the 2025 exemption table (p. 17) and brackets (p. 18), and are derived in each test's comments.

Changed expectation. integration_2025.yaml, "2026 - Retired couple receives Ohio retirement income credit", had locked in the double count. Each spouse's modeled Medicare Part B premium (2,434.80) is a line 3 amount, so line 5 = 4,869.60. With federal AGI 63,850 (pension 40,000 plus taxable Social Security 23,850), line 8 = 4,869.60 − 4,788.75 = 80.85. main deducted it once per spouse (161.70). Corrected values:

  • taxable income 35,119.15, was 35,038.30;
  • tax 332.00 + 2.75% × 9,069.15 = 581.40 (R.C. 5747.02(A)(3)(c)), was 579.18;
  • state income tax 331.40, was 329.18.

Property-based test, tests/core/test_oh_unreimbursed_medical_deduction_invariants.py. Hypothesis draws batches of 5–30 tax units: single, joint, head of household, and joint with up to three dependents. Each person has wages, medical expenses, uninsured premiums and long-term care premiums, and some heads have a business loss that makes AGI negative. A seeded population of 200 units adds breadth. Each batch runs as one vectorized simulation, and again with every medical amount zeroed.

  • Why Python. YAML cannot draw random household compositions or compare two runs. The zeroed run is how the test checks that each amount is counted once and only for its own person.
  • Both bugs fail it. Restoring either of main's two variable files fails both tests.
  • Cost (local macOS, not CI). The test file takes 15.0 s of test time and 41.5 s wall with model load, at 1.75 GB peak RSS. It runs 10 simulations: Hypothesis on 2022 and 2025 with 4 examples, and the seeded population on 2024. A first draft with 18 simulations took about 50 s, and was cut because nothing here varies by year.
  • CI group. The file runs in the Rest job's core group, which the Makefile times at 142 s. I have not measured it on Linux CI.

Run locally, one file at a time with policyengine-core test; all pass:

  • Ohio: integration.yaml, integration_2025.yaml, oh_taxable_income.yaml, oh_educator_expense_deduction.yaml, every joint filing credit file, and every file under deductions/medical_expense/.
  • Ohio households outside states/oh/: salt_refund_integration.yaml, state_sales_tax.yaml, taxsim_state_cdcc.yaml, reform/oh_refundable_eitc.yaml, and the three contrib/states/oh files.

The rest is left to CI.

Invariants

These hold for every tax unit and are checked by the property test:

  1. Conservation. Σ members' line 8 shares = line 8.
  2. Differential. Line 8 = max(0, line 5 − 0.075 × max(federal AGI, 0)), with 0.075 taken from the instructions rather than the parameter. Each share = line 8 × own line 5 / return line 5, or 0 when line 5 is 0.
  3. Bounds. 0 ≤ line 8 ≤ line 5, and 0 ≤ each share ≤ the person's own line 5 amount.
  4. Accounting.
    • The tax unit deduction = line 8 + Σ line 1 + Σ line 2, counted once.
    • Zeroing every medical amount raises oh_agi by exactly that deduction. It raises each person's oh_agi_person and joint filing credit qualifying income by exactly their own share.
    • So a spouse never loses qualifying income for the other spouse's expenses.

Microsim impact

A real microsimulation on the default dataset, main at the merge base 6b80a8e447 against this PR's head 949dd8d728, for 2026. The runs went one at a time under the shared heavy-job lock. Sums are weighted.

Output 2026 change 2026 records changed (weighted)
oh_income_tax +75.59M 128 (368,535)
oh_agi +9.33bn 268 (1,114,128)
oh_taxable_income +3.99bn 187 (577,946)
oh_unreimbursed_medical_care_expense_deduction -9.33bn 268 (1,114,128)
oh_insured_unreimbursed_medical_care_expenses -66.23M 15 (72,830)
oh_joint_filing_credit +4.44M 61 (222,143)
oh_joint_filing_credit_eligible +61,397 11 (61,397)
state_income_tax +75.59M 128 (368,535)
income_tax +0 0 (0)
household_net_income -75.59M 128 (368,535)
household_benefits +0 0 (0)

What the numbers show:

  • Deduction. The deduction falls from $18.90bn to $9.57bn: main claimed about twice the once-per-return amount. Ohio AGI rises by the same $9.33bn (to within $1,012, from float32 weighted sums).
  • Tax. Ohio income tax rises $75.6M, on 128 records (368,535 weighted tax units).
  • Taxable income. It rises less than AGI ($3.99bn), because taxable income is floored at zero: 81 of the 268 changed records stay at zero.
  • Line 6 floor. It removes $66.2M of tax unit line 8 on 15 records (72,830 weighted units) with negative federal AGI.
  • Joint filing credit. 61,397 weighted units (11 records) become eligible, and the credit rises $4.44M in all.
  • Nothing else moves. Federal income tax and benefits are unchanged, and state_income_tax moves exactly as oh_income_tax.

The run finished after the merge (2026-10-13). It is the impact the merge audit refers to.

Comparison files are in fixes/9925-impact/ on the hub host.

Methodology note

  • Splitting line 8 between members. Ohio computes line 8 once per return. The model needs a per-person amount only because it builds Ohio AGI per person for the joint filing credit, whose R.C. 5747.05(E) test is each spouse's qualifying income. The law does not say how to divide a medical deduction between spouses. This PR uses the existing proportional share, by each person's line 5 expenses. A dependent's share sits on the dependent's oh_agi_person, so it lowers the return's Ohio AGI but neither spouse's qualifying income.
    • Another reading. The 2025 instructions (p. 29) define qualifying income as "any amount included in Ohio adjusted gross income" other than interest, dividends, capital gains, rents and royalties. Their examples of excluded amounts are all income items deducted on the Schedule of Adjustments. Under that reading, a deduction that is not an income item, like this one, would reduce neither spouse's qualifying income. The model already reduces each person's qualifying income by every person-level Ohio deduction, so that question predates this PR.
  • Worksheet line 3 is limited to Medicare-eligible people whose employer paid none of the premiums. The worksheet also puts there the premiums of anyone eligible for an employer-paid plan. That gap predates this PR; a follow-up is filed separately.

Coordination

axiom: R.C. 5747.01(A)(10) TheAxiomFoundation/rulespec-us#1523 queued

🤖 Generated with Claude Code

MaxGhenis and others added 2 commits October 6, 2026 14:20
oh_unreimbursed_medical_care_expense_deduction_person listed the TaxUnit
variable oh_insured_unreimbursed_medical_care_expenses in its adds.
policyengine-core evaluates adds with map_to="person", which projects a
group value onto every member, so each member deducted the full worksheet
line 8 amount and Ohio AGI (the sum over members) fell by n times it. Each
spouse's joint filing credit qualifying income also lost the whole amount.

Use the existing person-level oh_insured_unreimbursed_medical_care_expenses_person,
which prorates line 8 by each member's share of line 5 and sums to the
return amount. Also enter federal AGI below zero as zero on worksheet
line 6, as every year's instructions say, so line 8 never exceeds line 5.

Adds YAML tests that fail on main (person split, total once, Ohio AGI,
joint filing credit, negative AGI) and Hypothesis invariants (conservation,
an independent worksheet, bounds, once-only accounting). Updates the 2026
retired-couple case, whose Medicare Part B line 8 had been deducted twice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The invariants do not vary by year, so run Hypothesis on 2022 and 2025
with four examples and the seeded population on 2024 only. Test time
falls from about 50 s to 15 s locally; both bugs still fail it.

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

codecov Bot commented Oct 7, 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 (949dd8d).
⚠️ Report is 259 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff            @@
##              main     #9925   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files            4         2    -2     
  Lines           76        25   -51     
  Branches         2         0    -2     
=========================================
- Hits            76        25   -51     
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.

@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Handing off to the US + core hub.

  • Head: 949dd8d728c89b6e5ef12a0c8f9b69bfa751f9b4
  • Independent review: approve (Opus 5.5 lane, Subfleet job 20261008-101900-pe-us-adversary-review; verdicts in /Users/maxghenis/reviews/policybench-reference-adversary-2026-10/engine_review.md).
  • CI: gh pr checks exits 0. GitHub reports MERGEABLE, blocked only on the required approving review.
  • Context: found by PolicyBench's reference adversary (Add the reference adversary and record its first run (evidence only; no score changes) policybench#200, decision d1022).
  • Notes: Ohio unreimbursed medical deduction: lands as the once-per-return and negative-AGI fix only. The scenario_025 defect (worksheet line 3 reads Medicare AND no employer plan where the law says OR; line 1 reads an unfilled input) needs a separate follow-up PR, which a new session is opening.

Nothing is pending from the opening session.

@MaxGhenis
MaxGhenis merged commit f9f3887 into main Oct 9, 2026
37 of 63 checks passed
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

US + core hub merge audit, us#9925 at 949dd8d728c89b6e5ef12a0c8f9b69bfa751f9b4 (squash): Count Ohio's unreimbursed medical care deduction once per return.

  • Origin: PolicyBench's reference adversary (Add the reference adversary and record its first run (evidence only; no score changes) policybench#200) found the engine defect. Max ruled d1022 on 2026-10-06 to exclude the affected cells until a fixed policyengine-us release lands.
  • Review: independent Opus 5.5 review APPROVE (Subfleet), verdict in ~/reviews/policybench-reference-adversary-2026-10/. The PR's last commit predates the review, so the reviewed head is this head.
  • Hand-off: from local_3dd3d124. Nothing was pending from the opening session.
  • Gates checked live at merge:
    • gh pr checks exits 0;
    • the latest Pull request run at the head concluded success;
    • MERGEABLE;
    • not a draft;
    • no CHANGES_REQUESTED review;
    • the semantic-overlap guard;
    • head pinned.
  • Impact: as stated in the PR body's impact section. This is a bug fix that corrects the model to the cited law.
  • --admin: used only because a required approving review is the sole block.

@MaxGhenis
MaxGhenis deleted the fix-oh-medical-deduction-per-person branch October 9, 2026 00:22
MaxGhenis added a commit that referenced this pull request Oct 9, 2026
#9925 squash-merged as f9f3887. Its two test files conflicted add/add
with this branch's copies; main's versions equal #9925's head, so this
branch's extended versions are kept.

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

Copy link
Copy Markdown
Contributor Author

Microsim impact (filled in after merge): the 2026 run on the default dataset finished. It compares main at the merge base 6b80a8e447 with the merged head 949dd8d728.

  • The Ohio unreimbursed medical deduction falls by $9.33bn, from $18.90bn to $9.57bn: main had claimed it about twice.
  • Ohio AGI rises by the same $9.33bn.
  • Ohio income tax rises $75.6M on 368,535 weighted tax units.
  • 61,397 weighted units become eligible for the joint filing credit (+$4.44M).
  • Federal tax and benefits are unchanged.

The PR body's "Microsim impact" section has the full table. Comparison files: ~/reviews/us-hub/fixes/9925-impact/.

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