Skip to content

Honor dataset-supplied tax-unit roles - #9617

Merged
MaxGhenis merged 14 commits into
mainfrom
source-tax-unit-roles
Oct 8, 2026
Merged

MaxGhenis merged 14 commits into
mainfrom
source-tax-unit-roles

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Summary

The certified Populace US build ships the tax units its constructor built, with each person's role. policyengine-us has had no variable to read that column, so the loader drops it and re-derives roles by age: the oldest adult heads the unit and the next-oldest is the spouse. This PR adds the role input under the producer's exact name and makes the role variables use it when supplied. Age ordering, with #9630's rule for input dependents, remains the fallback.

Roles only: filing status is computed by the model, not supplied. Max ruled on 2026-10-06 (decision d318) for roles only: filing status is a policy calculation, so the model computes it from the household's facts. Supplied data may set the tax-unit roles (who is head, spouse or dependent) but not the filing status. Earlier versions of this PR also honored a supplied filing_status_input; that part is reverted. filing_status.py is byte-identical to main, and filing_status_input no longer exists. The build still writes a filing_status_input column. No variable has that name, so the loader skips it, as it did before this PR.

  • New input: tax_unit_role_input (person: HEAD / SPOUSE / DEPENDENT). It has no formula and defaults to UNSPECIFIED.
  • is_tax_unit_head and is_tax_unit_spouse use the supplied roles when every member of the unit has one (tax_unit_roles_supplied). is_tax_unit_dependent follows as before, and filing_status is computed from the resulting roles by main's formula.
  • Malformed supplied roles raise instead of falling back. Examples: roles for only some members, no head or two heads, or two spouses.

This round (hub fix, 2026-10-06)

  • Filing-status part reverted. filing_status.py is identical to main (6440b3e). The filing_status_input variable and its test file filing_status_input.yaml are removed rather than left as a dead input.
  • The romney remove_head_of_household.yaml test is back to main. Each of the three cases this PR had added set filing_status_input, so they tested a supplied status, not roles. With no supplied status there is nothing left for them to test.
  • Roles kept, composed with Exclude input dependents from tax unit head and spouse inference #9630 as rebase R2 did: tax_unit_role_input, tax_unit_roles_supplied, is_tax_unit_head and is_tax_unit_spouse. Supplied roles decide when every member of the unit has one; otherwise Exclude input dependents from tax unit head and spouse inference #9630's inference runs, which skips input dependents.
  • Tests updated for roles only:
    • The unit tests check that no variable is named filing_status_input. They also check that a dataset carrying that column gets the computed status, in the data year and in an extended year.
    • In the property tests, the supplied-status invariant becomes "a unit files jointly exactly when it has a spouse", now checked for every unit rather than only supplied ones. The switch-off-locality property, which tested supplied statuses only, is removed. The differential pins roles but not filing status on either side. A new property composes supplied units with is_tax_unit_dependent inputs.
    • The role YAML gains a case where a unit without supplied roles skips an input dependent next to a unit with them.
  • Docs. docs/usage/microsimulation.md states the roles-only contract. It describes the fallback as it stands after Exclude input dependents from tax unit head and spouse inference #9630, which skips adults input as dependents unless every adult is one.
  • Changelog rewritten for roles only. It drops the 2024 aggregates, which were measured with supplied filing status.
  • Memberless tax units no longer raise. This round's review found the defect. The loader declares every tax unit in a dataset's tax_unit_id column, members or not, and core's all() is True over no members. So tax_unit_roles_supplied counted a memberless unit as supplied with no HEAD and raised, where main gives the unit no head. The unit now needs at least one supplied member. The default build has no memberless units (all 79,729 tax-unit rows have members), so its results do not change. Regression test: test_a_tax_unit_without_members_falls_back_rather_than_raising.
  • Main merged (4c900b6, then 6440b3e) with no conflicts.

The problem, with evidence

This section is carried over from earlier rounds of this PR, minus its filing-status parts. This round re-checked only the role counts in the cached H5 and the core loader code.

The build is populace-us-2024-spm-20260915 (populace_us_2024.h5, sha256 6496cc43…aee84). It is the default in policyengine.py bundle 6.1.1. The same bytes, tagged 20260909, are policyengine-us's DEFAULT_DATASET. In the cached copy of that tag, the person table's tax_unit_role_input has 79,729 HEAD, 35,592 SPOUSE and 51,000 DEPENDENT, one HEAD per tax unit (79,729 units).

policyengine-core sets a dataset column only if a variable has its name. In policyengine_core/simulations/simulation.py (3.32.15, the version this round tested with), the loader collects any other column as unknown_columns and only logs that they "do not match any variable in the tax-benefit system and were ignored".

Re-running microunit==0.1.0 construct_tax_units(mode="policyengine") on the H5's own raw ASEC columns reproduces every role in the ASEC half. The PUF-clone half copies each person's role from its source person. The legacy CPS pipeline never shipped this column. policyengine-us-data #824 (merged 2026-04-25) says so:

Keeps only constructed TAX_IDs in the staged CPS data path; PE-US infers tax-unit roles and filing statuses from the resulting graph.

What the age rule does to the certified build in 2024, measured earlier on this PR (main 2.11.2):

  • There are 36,701 spouses against the source's 35,592.
  • 1,161 source dependents become spouses, which turns their parent's return joint. Most are adult children aged 18–23, 187 of them full-time college students.
  • 13,217 units get a different head. In 12,961 of them the source spouse becomes head: microunit heads a couple with the CPS reference person, while the age rule takes the older partner.
  • 252 lone minors whom the source makes heads have no head. Each files as head of household with itself as the qualifying child.
  • 45 source couples lose their spouse, because the age rule blocks every spouse in a unit with any separated member, dependents included.

What #9467 has to do with it

The release manifest's compatibility claim reads:

Max Ghenis via Claude, 2026-09-15: same H5 bytes as populace-us-2024-spm-20260909; native loader path, role variable and dataset pin unchanged across 2.0.x–2.2.x; see #9467

The "role variable" there is the SPM independence role is_spm_independent_minor_role, not a tax-unit role. No tax-unit role mechanism was ever intended or shipped. This PR follows #9467's pattern, a source-owned role with a fallback, but uses a formula-free input rather than DATASET_SOURCE_INPUTS. Two reasons:

  • Carry-over: core carries an input forward to later years only when the variable has no formula. A dataset override of is_tax_unit_head itself would revert to the age rule after the extension's last year.
  • Export gate: microcosm's export gate rejects formula-owned columns.

Design

Which side owns the contract. The producer already emits the role column with the right name and values, so no rebuild is needed. policyengine-us is the consumer that dropped it, so the fix lives here, and the certified H5 bytes flow through unchanged.

Per-unit fallback. A unit uses supplied roles only when every member has one. Otherwise is_tax_unit_head and is_tax_unit_spouse run main's inference: the oldest adult heads the unit, skipping adults input as tax-unit dependents unless every adult is one (#9630), and the next such adult is the spouse unless any member is separated (#9620). Datasets without the column, and household situations that don't set it, get main's roles: no role is supplied, so tax_unit_roles_supplied is false and the role variables evaluate main's expression. That includes a tax-unit row no person belongs to (see "Memberless tax units no longer raise" above).

Composition with #9630. When a unit's roles are supplied, they decide; #9630's input-dependent skipping applies only to units without them. tax_unit_roles_supplied is all-or-none per unit, so where evaluating both branches cannot change either side's result.

Filing status. The model computes it with main's formula from the resulting roles. Because a supplied role can make an adult a dependent rather than a spouse, the computed status moves for those units (for example, a parent whose adult student was age-ordered as their spouse files as head of household rather than jointly).

Invariants

tests/core/test_tax_unit_role_input_properties.py checks each of these with Hypothesis (derandomized). Invariants 1–4 run over generated households of up to four tax units with up to four members each, the differential (5) over up to three supplied units, and the error contract (6) samples 40 examples from the 340 possible ordered role lists for a single unit of one to four members:

  1. Partition: every person is exactly one of head, spouse and dependent.
  2. Supplied roles hold: in a unit whose roles are supplied, the role variables equal the supplied roles, so it has exactly one head and at most one spouse.
  3. Fallback (differential): in a unit without supplied roles, the roles equal an independent implementation of age ordering. It skips adults input as tax-unit dependents unless every adult is one (Exclude input dependents from tax unit head and spouse inference #9630), and reproduces today's unit-wide separation gate, labelled as tracking the known defect A separated dependent unpairs the couple under age-ordered tax-unit roles #9620. A separate property composes supplied units with units whose members carry is_tax_unit_dependent inputs.
  4. Filing status is computed: a unit files jointly exactly when it has a spouse, supplied or inferred.
  5. Workaround equivalence (differential): supplying roles through tax_unit_role_input gives the same filing status and income tax as pinning is_tax_unit_head/spouse/dependent directly. Neither side pins filing_status.
  6. Error contract: supplied roles raise exactly when malformed, meaning supplied for only some members, or not exactly one HEAD and at most one SPOUSE.

Microsimulation impact

The hub compared merge base 6440b3e604 with roles-only branch f99b4e04da on populace_us_2024.h5@populace-us-2024-spm-20260909, for 2025 and 2026. Source was checked at current PR head f0cb844fe3. Its only subsequent model change is the memberless-unit fallback; all 79,729 units in these arrays have members, so that fallback does not affect this dataset. Roles are supplied; filing status remains computed. No microsimulation was rerun for this writeup.

Weighted aggregate changes, in $ millions (PR minus baseline):

Variable 2025 2026
Federal income tax −130.21 −120.80
Household net income +520.23 +578.37
Adjusted gross income −36,868.31 −38,877.29
Standard deduction −25,943.16 −27,164.63
CTC, including other-dependent credit +1,388.91 +1,398.25
Refundable CTC −124.37 −128.41
EITC +38.52 +33.38
State income tax −604.61 −626.80

Role shifts: swaps versus reclassifications

The arrays contain 166,321 people, 79,729 tax units and 57,240 households. Tax-unit IDs, person-to-tax-unit membership and unit sizes are unchanged. The role transitions are identical in both years; annual weights differ. Counts below are person records and weighted people, not tax units.

Individual transition Records Weighted people, 2025 Weighted people, 2026
Head → spouse 12,961 30,002,305 30,216,554
Head → dependent 4 39,311 39,592
Spouse → head 12,948 29,973,952 30,187,999
Spouse → dependent 1,161 3,295,832 3,319,368
Dependent → head 269 162,432 163,592
Dependent → spouse 39 173,335 174,573

In particular, 1,165 people become dependents (3,335,143 weighted in 2025; 3,358,960 in 2026), while 308 cease being dependents (335,767; 338,165). The 1,473 dependent-flag changes count both directions. Spouse status changes for 27,109 people: 14,109 lose it and 13,000 gain it (63,445,425 weighted people changing in 2025; 63,898,495 in 2026).

Any role changes for 27,382 people (63,647,168 weighted in 2025; 64,101,678 in 2026), across 14,412 units (33,565,319; 33,805,012 weighted units). 12,944 units simply exchange head and spouse, keeping every dependent unchanged. These represent 29,934,642 weighted units in 2025 and 30,148,408 in 2026. Their aggregate AGI changes by only −$356 / +$604 and federal tax by −$95 / +$125, consistent with numerical precision. Standard deduction, CTC and EITC are unchanged. State tax has a material exception below.

The other 1,468 units change who is a dependent. These include 1,160 straightforward spouse-to-dependent reclassifications, 250 headless singleton dependents becoming heads, and two headless pairs becoming head/spouse. Three units retain the same head/spouse/dependent counts while changing dependent identities: two (H,S,D) → (D,H,S) cycles and one spouse/dependent exchange. Grouping by role counts alone would mistakenly treat these as harmless swaps. Every person has exactly one role on both sides; the PR removes all 252 formerly headless units and leaves exactly one head and at most one spouse per unit.

The fallback orders eligible adults by age; the supplied roles replace that ordering. These arrays contain no ages, relationships or person-level income components, so they establish which roles change, without establishing whether each newly recognized dependent satisfies dependency requirements.

Tax-unit reconciliation

These disjoint groups reconcile the 2025 totals. Weighted counts here are tax units. Dollar totals include all signed differences; displayed near-zero swap values round to zero.

Unit group Records Weighted units AGI change, $bn Federal tax change, $M State tax change, $M
Lose one spouse; gain one dependent 1,162 3,295,832 −45.026 −1,663.467 −690.995
Gain one spouse; lose one dependent 51 200,766 +8.171 +1,284.743 +202.390
Headless singleton gains a head 250 93,846 +2.484 +420.054 +36.043
Headless pair gains head and spouse 2 922 +0.403 +84.662 +21.623
Same role counts; different dependent 3 39,311 −2.901 −256.199 −164.735
Pure head/spouse exchange 12,944 29,934,642 −0.000 −0.000 −8.931
Roles unchanged 65,317 130,644,555 0.000 0.000 0.000

The first group includes the 1,160 straightforward spouse-to-dependent units and two units whose old head becomes dependent while the old spouse becomes head. Conversely, 303 units gain a filer from a previous dependent: the 51 spouse gains, 250 singleton heads and two formerly headless pairs. Together they restore $11.059bn AGI and $1.789bn federal tax in 2025 ($11.686bn / $1.920bn in 2026). The two three-person cycles alone remove $2.901bn AGI and $256.199M federal tax in 2025. All seven saved tax-unit money variables are exactly unchanged in the 65,317 units whose roles are unchanged.

The code's gross-income consumer excludes dependents' income from the claiming return; above-the-line deductions also exclude dependents' own amounts. Thus changing who is a dependent changes whose income and deductions enter AGI. EITC earned income likewise sums nondependents. Changing only which of the same two adults is head generally preserves those federal sums.

AGI is not taxable income. Computed filing status determines basic standard deduction and tax brackets. Losing a spouse ends joint status, with the replacement status computed from eligibility. The standard-deduction decline goes in the opposite direction: AGI minus standard deduction falls $10.925bn. This difference is not measured taxable income: actual deductions, exemptions and taxable-income arrays were not exported.

The small federal net change is substantial cancellation: $5.301bn in decreases versus $5.170bn in increases in 2025; $5.608bn versus $5.487bn in 2026. In 2025, 239 units have lower AGI yet higher federal tax: AGI −$3.527bn, standard deduction −$4.844bn, refundable CTC −$88.336M, EITC −$333.598M and federal tax +$861.049M. The arrays establish these concurrent changes, rather than separately assigning each dollar to a consumer.

CTC here includes the other-dependent component: its maximum can add $500 for a newly recognized dependent, while phaseout depends on AGI and computed filing status. ctc is entitlement after phaseout, not necessarily realized tax relief; nonrefundable credits are capped at liability and refundable CTC has its own earnings and refund limits. The spouse-loss group adds $1.560bn CTC but loses $78.163M refundable CTC; the spouse-gain group loses $123.307M / $46.211M respectively. The latter also loses $48.676M EITC, offsetting the former's $87.196M gain. Federal income_tax already includes refundable credits, so these credit changes must not be added again to the tax reduction.

Anonymized record examples

2025 dollar amounts are rounded to the nearest dollar. H = head, S = spouse, D = dependent; member order is held fixed within each example. Labels identify examples only, not dataset IDs.

Example Unit size Member roles before → after AGI before → after Federal tax before → after
A 4 (S,H,D,D) → (H,S,D,D) $88,305 → $88,305 $1,754 → $1,754
B 3 (H,S,D) → (H,D,D) $167,064 → $163,101 $19,152 → $23,582
C 2 (H,S) → (H,D) $112,468 → $79,406 $8,995 → $7,970
D 1 (D) → (H) $0 → $350,369 $0 → $83,133
E 3 (H,S,D) → (D,H,S) $133,751 → $55,298 $7,226 → $188
F 2 (H,S) → (H,D) $1,176,936 → $33,270 $346,690 → $1,124

B illustrates tax rising despite lower AGI: standard deduction falls $31,500 → $23,625 and CTC rises $500 → $1,000, but tax rises $4,430. E preserves role counts while replacing a taxable adult. F loses $1.144M AGI and has a weight of 7,169.889 units, making a single record a large aggregate contributor; the saved arrays cannot validate that dependency claim.

Household net income reconciliation

Federal and state income-tax reductions sum to +$734.813M in 2025 and +$747.597M in 2026. Observed household net income rises $520.227M / $578.368M, leaving an aggregate residual of −$214.586M / −$169.229M.

Household net income includes market income, benefits and refundable credits, less taxes before refunds and health costs. The saved arrays lack those remaining components and household membership IDs, so they cannot allocate this residual to programs, households, other taxes or aggregation weights. No particular benefit program is credited with it. The tax-unit reconciliation above is complete for the saved tax-unit outcomes; the household reconciliation remains component-incomplete.

Possible defects found by the impact

  • Missing dependent own returns (#9618). Income and own deductions removed from the claiming return are not computed on a dependent's separate return inside that unit. Honoring supplied dependency roles therefore exposes an existing tax-base omission. The AGI decline and simulated tax savings should not all be interpreted as a complete correction of total liability.
  • Material state-tax changes from pure head/spouse exchanges. State tax falls $8.931M in 2025 / $8.551M in 2026, with 33 / 34 pure-swap units changing by more than $0.005. A's federal outcomes stay fixed but its state tax falls $3,984.59 → $3,204.01. Arkansas and Delaware allocate dependent state income to the head before individual tax calculations, making them candidate asymmetric consumers. State codes and component taxes were not saved, so neither attribution nor a defect is established. Review the material responses before treating every swap as neutral.
  • Minor heads can be mistaken for minor parents (#9619). The fallback excludes children from heading units; supplied roles make the 252 formerly headless units headed. GA CAPS uses head/spouse status and age for its minor-parent waiver without checking parenthood. This is a source-supported downstream risk; the saved arrays do not measure childcare effects or prove ages/parenthood for these records.
  • Dependency claims requiring producer review. Four former heads become dependents; the two materially weighted three-person cycles account for most of their weighted population and a $2.901bn AGI reduction. F is another high-impact claim. Membership is unchanged, so there is no observed movement into a different tax unit. Roles alone cannot establish that these claims are wrong or right; relationships, ages, support and person-income data are required. No wrong-unit assignment is demonstrated by this impact.

No possible defect was fixed as part of this writeup.

Verification

Each test file was run on its own, in the foreground, under the hub's shared heavy lock. The environment was Python 3.13, policyengine-core 3.32.15 and hypothesis 6.168.3.

  • "Before" runs this round's test files on an earlier head: b77b56f (the PR head before this round), or 9d233a2 (this round's head before the memberless fix).
  • "After" runs them on this head's tree, f0cb844. The unit, property and role-YAML runs used that tree's files before they were committed.
Test file Before After (f0cb844)
tests/core/test_tax_unit_role_input.py b77b56f: 3 failed, 8 passed (without the memberless test)
9d233a2: 1 failed, 11 passed
12 passed
tests/core/test_tax_unit_role_input_properties.py b77b56f: 4 passed 4 passed
tests/policy/baseline/household/demographic/tax_unit/tax_unit_role_input.yaml b77b56f: 7 passed 7 passed
tests/policy/contrib/congress/romney/family_security_act/remove_head_of_household.yaml (main's version) n/a 2 passed

The property tests and role YAML also pass on the old head. They test role behaviour that the old head already had, and none of their cases supply a filing status. This round's regressions are the unit tests below.

Regression: a dataset's filing status is no longer read. These three unit tests fail on b77b56f and pass on this head:

  • test_a_supplied_filing_status_column_is_not_read[2024] and [2026]. The dataset's filing_status_input column says SURVIVING_SPOUSE for a lone 16-year-old and SINGLE for the parent of a 21-year-old full-time student. On the old head the model returns ['JOINT', 'SURVIVING_SPOUSE', 'SINGLE']. On this head it computes ['JOINT', 'SINGLE', 'HEAD_OF_HOUSEHOLD'] (26 USC 152(c)(3)(A)(ii), 2(b)(1)(A)(i)). The loader logs that filing_status_input matches no variable and was ignored.
  • test_supplied_input_carries_the_producers_column_name_and_values. On the old head, filing_status_input is still a variable.

Regression: a tax unit with no members. The same dataset as above, plus a fourth tax-unit row that no person belongs to:

  • On main (6440b3e), the four tax units load and the people get age-ordered roles.
  • At 9d233a2, test_a_tax_unit_without_members_falls_back_rather_than_raising fails with ValueError: tax_unit_role_input gives 1 tax units something other than exactly one HEAD and at most one SPOUSE.
  • At this head the test passes: tax_unit_roles_supplied is [True, True, True, False], and the supplied roles hold.

ruff format --check and ruff check pass on the seven changed Python files. The new case in tests/microsimulation/test_microsim.py was not run locally. It loads the full default dataset, and the host's run rule leaves microsimulations to the hub and CI.

Recertification

This section is carried over from earlier rounds; this round did not re-check the policyengine.py or microcosm references.

policyengine.py bundle 6.1.1, data_releases.us, has to be recertified before it can ship a policyengine-us with this change. Its bundle_id is us-6.1.1, its build is populace-us-2024-spm-20260915, and it is certified for policyengine-us 2.2.1.

  • The bytes do not change, but how the model reads them does: tax, credit and filing-status outputs over the build move as above.
  • The release manifest's compatibility claim (>=2.0.0,<2.3, basis publisher_claim) rests on how those bytes are read, so it must not be widened to cover this change.
  • policyengine.py's gate already refuses any policyengine-us from 2.3 on against this release. The path is a new Populace release tag over the same bytes that declares the new model version, followed by scripts/bundle.py certify-data.
  • UK is unaffected.

Also affected:

  • policyengine-us's own default (DEFAULT_DATASET, tag 20260909, same sha256). Its standalone results change on release; its hash pin stays valid.
  • policyengine.py per-year dataset caches built under an older policyengine-us lack the column and keep the age rule until rebuilt. Draft policyengine.py Add WIC category for multiples #524 adds a cache identity keyed on the model runtime.
  • microcosm, when it moves its engine pin past 2.2.1: tax_unit_role_input enters its pinned input registry and should be registered in _POOL_SIMULATION_PRESERVED_ENGINE_INPUTS alongside is_related_to_head_or_spouse. Register tax_unit_role_input and filing_status_input as preserved engine inputs when moving past pe-us 2.2.1 microcosm#1027 still names filing_status_input too; with roles only, that input no longer exists, so only tax_unit_role_input needs registering.

Follow-ups (not in this PR)

axiom: n/a: data-contract change; no provision's rule changes

🤖 Generated with Claude Code

MaxGhenis and others added 2 commits September 24, 2026 08:27
Populace's tax-unit constructor (microunit) writes each person's role
(tax_unit_role_input: HEAD, SPOUSE, DEPENDENT) and each unit's filing
status (filing_status_input) into the certified build, but no variable
read them, so the loader dropped both and the role variables fell back to
age ordering. Add the two input variables under the producer's exact
column and member names, with an UNSPECIFIED default, and make
is_tax_unit_head, is_tax_unit_spouse and filing_status defer to them when
supplied. Malformed supplied values raise instead of falling back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review found that neutralizing filing_status_input in
remove_head_of_household re-derived every supplied status, moving 158
non-head-of-household units on the certified build, and that the
gov.abolitions parameters for head_of_household_eligible and
surviving_spouse_eligible did nothing to supplied statuses. filing_status
now re-derives a supplied HEAD_OF_HOUSEHOLD or SURVIVING_SPOUSE only when
that rule is neutralized or abolished, using core's own switch-off test,
and the reform is back to its original single neutralization.

Also: check a supplied JOINT status against the supplied roles so
abolishing a role variable cannot raise; keep an explicit head out of the
supplied spouse role; document precedence, the fallback's separation gate,
and that supplied adult dependents' income counts toward no return.

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

codecov Bot commented Sep 25, 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 (1ab8e63).
⚠️ Report is 159 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff            @@
##              main     #9617   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files            4         6    +2     
  Lines           76       120   +44     
  Branches         2         3    +1     
=========================================
+ Hits            76       120   +44     
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 and others added 7 commits October 3, 2026 16:18
State seven properties of supplied tax-unit roles and filing statuses
(partition, supplied roles hold, age-ordering fallback, supplied statuses
hold, equivalence with the explicit-pin workaround, switch-off locality,
error contract) and check each over generated households. Adds hypothesis
to the dev extra; uv.lock regenerated with CI's uv 0.12.13 so it changes
only by that package.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Independent review of d8b02c1 found the switch-off-locality property
could generate no unit supplied with the abolished status, so it never
exercised the re-derivation it claims to check. Each example now holds
one forced target unit beside other supplied units.

Also narrow the docs and changelog: a dependent's income is left out of
the claiming return's gross income, but the deductible share of a
dependent's self-employment tax still lowers the filer's AGI through
self_employment_tax_ald.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts:
- pyproject.toml, uv.lock: main already added hypothesis>=6.100.0 to the
  dev group, this branch's only new dependency, so both files are main's;
  `uv lock` leaves uv.lock unchanged.
- is_tax_unit_head.py, is_tax_unit_spouse.py: #9630 narrowed the
  age-ordered fallback to head_or_spouse_candidates (skipping input
  dependents); this branch wraps that fallback in
  where(tax_unit_roles_supplied, <supplied role>, <fallback>). Kept both:
  supplied roles still decide when every member has one, and otherwise the
  fallback uses main's candidates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Filing status is a policy calculation, so the model computes it from the
household's facts, as on main. Supplied data may set who is head, spouse
or dependent, but not the filing status.

- filing_status.py returns to main's formula; no supplied status is read.
- Remove filing_status_input and its YAML tests. The Populace build's
  filing_status_input column now matches no variable, so the loader skips it.
- Revert the romney remove_head_of_household cases, which tested only how
  the repeal re-derived a supplied status.
- Keep tax_unit_role_input, tax_unit_roles_supplied and the head/spouse
  formulas composed with #9630's inference.
- Unit tests: drop the supplied-status tests; add a dataset test showing a
  filing_status_input column that disagrees with the rules is not read, in
  the data year and an extended year.
- Property tests: roles only. Filing status is joint exactly when there is
  a spouse; the role input matches pinning the role flags (neither side
  pins filing_status); a new property composes supplied roles with input
  dependents, against an oracle that skips them as #9630 does.
- Microsim test: check the build's roles only.
- Docs: roles only, the fallback as it stands after #9630, and dependents'
  above-the-line deductions after #9801.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis and others added 3 commits October 6, 2026 12:45
The age-ordered fallback skips adults input as tax unit dependents unless
every adult in the unit is one, in which case every adult stays a
candidate. Say so in tax_unit_role_input's documentation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Per Max's ruling d318, the dataset supplies tax-unit roles but not filing
status, so the fragment no longer says filing_status_input sets
filing_status. It drops the 2024 aggregates measured with supplied filing
status, which no longer describe this change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis MaxGhenis changed the title Honor dataset-supplied tax-unit roles and filing status Honor dataset-supplied tax-unit roles Oct 6, 2026
MaxGhenis and others added 2 commits October 6, 2026 18:05
The loader declares every tax unit in a dataset's tax_unit_id column,
members or not, and all() is True over no members. So
tax_unit_roles_supplied counted a memberless unit as supplied with no
HEAD and raised, where main gives the unit no head. It now needs at least
one supplied member. The certified default build has no memberless units,
so its results do not change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis
MaxGhenis merged commit 7cac939 into main Oct 8, 2026
37 checks passed
@MaxGhenis
MaxGhenis deleted the source-tax-unit-roles branch October 8, 2026 14:22
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

US + core hub merge audit, us#9617 at 7df3acc1e25354f0a4539d0c563220691223729d (squash)

  • Max's rulings:

  • Reviews:

    • Independent review (Subfleet) APPROVE at f0cb844, with no P1.
    • Delta review APPROVE at 7df3acc: docs and body only, no new findings.
  • Gates checked live at merge:

    • gh pr checks exits 0;
    • MERGEABLE;
    • not a draft;
    • no CHANGES_REQUESTED review;
    • head pinned with --match-head-commit.
  • Impact: a real microsim, merge base 6440b3e604 against the roles-only branch, on the default dataset. The only later model change is a memberless-unit fallback, and the dataset has no memberless units. 2025 figures:

    • federal income tax −$130M;
    • household net income +$520M;
    • AGI −$36.9bn;
    • standard deduction −$25.9bn;
    • CTC +$1.39bn;
    • state income tax −$605M.

    Most of this comes from 1,162 units (3.3M weighted) whose supplied roles make a former "spouse" a dependent: about −$45bn of AGI. Without dependent returns (Compute a tax-unit dependent's own income tax return #9618), that income is untaxed. The other 12,944 units are pure head/spouse swaps, which are federally neutral. Full record-level write-up is in the PR body.

  • Follow-ups:

  • --admin: used only because a required approving review is the sole block.

MaxGhenis added a commit that referenced this pull request Oct 8, 2026
…uses

The #9617 cross-check found one Mississippi pure head/spouse swap that still
changed state tax: the microdata supply the health savings account deduction
at the tax-unit level, and person_share_of_tax_unit_amount gave a deduction
with no per-person amounts to the head's column. It now takes an opt-in
split_between_spouses flag, which the Mississippi health savings account,
self-employed health insurance and self-employed retirement adjustments use:
the head and spouse each take half. Medicaid AGI keeps the head fallback.

The property test now draws tax-unit deductions with no per-person split,
and fails on the previous head for this case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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