From 9344ddb50f2d53e4bd4c9784eecc2e2a7df1f24b Mon Sep 17 00:00:00 2001 From: Ziming Date: Fri, 30 Oct 2026 18:12:47 -0400 Subject: [PATCH 1/6] Keep an inferred tax unit spouse in the head's marital unit When tax roles are not supplied, the age-ordered fallback could make an unmarried adult child or relative the head's spouse even though supplied marital units separate them. The fallback now considers only people in the head's marital unit. Situations that omit marital units put everyone in one unit, so they keep the existing behavior. The Texas FPP unrelated-adult case now gives that adult their own tax unit, since an unclaimed adult files their own return. Fixes #9868 Co-Authored-By: Claude Opus 5.5 --- ...t-inferred-spouse-to-marital-unit.fixed.md | 1 + .../fpp/tx_fpp_dependent_care_deduction.yaml | 6 ++ .../tax_unit/is_tax_unit_spouse.yaml | 62 +++++++++++++++++++ .../tax_unit/is_tax_unit_spouse.py | 8 +++ 4 files changed, 77 insertions(+) create mode 100644 changelog.d/restrict-inferred-spouse-to-marital-unit.fixed.md diff --git a/changelog.d/restrict-inferred-spouse-to-marital-unit.fixed.md b/changelog.d/restrict-inferred-spouse-to-marital-unit.fixed.md new file mode 100644 index 00000000000..fa28286e49a --- /dev/null +++ b/changelog.d/restrict-inferred-spouse-to-marital-unit.fixed.md @@ -0,0 +1 @@ +Keep an inferred tax unit spouse within the head's supplied marital unit, so an unmarried adult child or other relative is no longer made the head's spouse. diff --git a/policyengine_us/tests/policy/baseline/gov/states/tx/fpp/tx_fpp_dependent_care_deduction.yaml b/policyengine_us/tests/policy/baseline/gov/states/tx/fpp/tx_fpp_dependent_care_deduction.yaml index 4f813a881b3..a0d7883f3ed 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/tx/fpp/tx_fpp_dependent_care_deduction.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/tx/fpp/tx_fpp_dependent_care_deduction.yaml @@ -144,6 +144,12 @@ members: [person1] marital_unit2: members: [person2] + # An unrelated adult who is not claimed files their own return. + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2] spm_units: spm_unit: members: [person1, person2] diff --git a/policyengine_us/tests/policy/baseline/household/demographic/tax_unit/is_tax_unit_spouse.yaml b/policyengine_us/tests/policy/baseline/household/demographic/tax_unit/is_tax_unit_spouse.yaml index 2bc08f8d0ee..22ead1c39e0 100644 --- a/policyengine_us/tests/policy/baseline/household/demographic/tax_unit/is_tax_unit_spouse.yaml +++ b/policyengine_us/tests/policy/baseline/household/demographic/tax_unit/is_tax_unit_spouse.yaml @@ -103,3 +103,65 @@ is_tax_unit_head: [true, false, false, false] is_tax_unit_spouse: [false, true, false, false] tax_unit_dependents: 2 + +- name: Supplied singleton marital units keep an adult child out of the spouse role. + period: 2025 + input: + people: + parent: + age: 45 + employment_income: 20_000 + adult_child: + age: 18 + tax_units: + tax_unit: + members: [parent, adult_child] + marital_units: + parent_unit: + members: [parent] + child_unit: + members: [adult_child] + output: + is_tax_unit_head: [true, false] + is_tax_unit_spouse: [false, false] + is_tax_unit_dependent: [false, true] + filing_status: HEAD_OF_HOUSEHOLD + eitc_child_count: 1 + eitc: 4_328 + +- name: The supplied marital partner takes precedence over an older adult in the same tax unit. + period: 2025 + input: + people: + head: + age: 60 + relative: + age: 55 + spouse: + age: 50 + tax_units: + tax_unit: + members: [head, relative, spouse] + marital_units: + couple: + members: [head, spouse] + relative_unit: + members: [relative] + output: + is_tax_unit_spouse: [false, false, true] + is_tax_unit_dependent: [false, true, false] + +- name: An explicit empty marital mapping gives each person a singleton marital unit. + period: 2025 + input: + people: + parent: + age: 45 + adult_child: + age: 18 + tax_units: + tax_unit: + members: [parent, adult_child] + marital_units: {} + output: + is_tax_unit_spouse: [false, false] diff --git a/policyengine_us/variables/household/demographic/tax_unit/is_tax_unit_spouse.py b/policyengine_us/variables/household/demographic/tax_unit/is_tax_unit_spouse.py index dd950508fd6..46f35a2bb1d 100644 --- a/policyengine_us/variables/household/demographic/tax_unit/is_tax_unit_spouse.py +++ b/policyengine_us/variables/household/demographic/tax_unit/is_tax_unit_spouse.py @@ -21,6 +21,14 @@ def formula(person, period, parameters): candidate = head_or_spouse_candidates(person, period) head = person("is_tax_unit_head", period) eligible = candidate & ~head & ~is_separated + # An inferred spouse must also share the head's marital unit. When a + # situation omits marital units, Core puts everyone in one unit, so + # this only applies when membership is supplied. Compare population + # memberships, not the ID variable: situations can supply groups + # without numeric ID inputs. + marital_unit = person.simulation.populations["marital_unit"].members_entity_id + head_marital_unit = tax_unit.max(where(head, marital_unit, -1)) + eligible = eligible & (marital_unit == head_marital_unit) age = person("age", period) next_oldest_adult = person.get_rank(tax_unit, -age, eligible) == 0 # An explicit is_tax_unit_head input still wins over a supplied role, From 96f9a925e5bd3e750117c3afb711b9ab5a8665c7 Mon Sep 17 00:00:00 2001 From: Ziming Date: Fri, 30 Oct 2026 20:54:13 -0400 Subject: [PATCH 2/6] Document split couples and restore the Medicaid guard test A married couple living apart in separate marital units is not inferred as tax spouses; the formula comment says to supply their roles. Read marital-unit membership with the repository's usual idiom. Rename the Medicaid grandparent case, whose teen is now a dependent, and add a copy without marital units so the parent-link guard is tested again. Update the Texas FPP comment for the new spouse rule. Co-Authored-By: Claude Opus 5.5 --- .../income/parent_ids_composition.yaml | 51 +++++++++++++++++-- .../income/tx_fpp_dependent_care_deduction.py | 5 +- .../tax_unit/is_tax_unit_spouse.py | 15 +++--- 3 files changed, 58 insertions(+), 13 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/income/parent_ids_composition.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/income/parent_ids_composition.yaml index dcc0740c669..941b18c9542 100644 --- a/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/income/parent_ids_composition.yaml +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/income/parent_ids_composition.yaml @@ -2058,7 +2058,7 @@ # Additional linked-household membership regressions from the recovery audit. -- name: A grandparent inferred as the teen parent's tax spouse is not a stepparent +- name: A teen parent in their own marital unit is not the grandparent's spouse, and the grandparent is not a stepparent period: 2026 input: people: @@ -2096,10 +2096,11 @@ members: [grandparent, teen, baby] state_code: OH output: - # The default tax roles label the two adults as head and spouse, but the - # parent links show that they are parent and child. That role inference - # cannot establish a marriage or make the grandparent the baby's step - # parent. The baby uses non-filer rules and counts only baby plus teen. + # The teen has their own marital unit, so the default tax roles make them + # the grandparent's dependent rather than spouse, and the parent links + # show that the two are parent and child. The grandparent is not the + # baby's step parent. The baby uses non-filer rules and counts only baby + # plus teen. medicaid_uses_non_filer_rules: [false, false, true] medicaid_household_size: [2, 2, 2] medicaid_household_income: [40_000, 40_000, 0] @@ -2228,3 +2229,43 @@ ca_medicaid_household_pregnancies: [1, 1] medicaid_household_size: [3, 3] medicaid_household_income: [40_000, 40_000] + +- name: Without marital units, a grandparent inferred as the teen parent's tax spouse is not a stepparent + period: 2026 + input: + people: + grandparent: + person_id: 1 + age: 60 + employment_income: 40_000 + teen: + person_id: 2 + age: 18 + parent_1_id: 1 + baby: + person_id: 3 + age: 1 + parent_1_id: 2 + tax_units: + adult_return: + members: [grandparent, teen] + tax_unit_is_filer: true + baby_unit: + members: [baby] + tax_unit_is_filer: false + families: + family: + members: [grandparent, teen, baby] + households: + home: + members: [grandparent, teen, baby] + state_code: OH + output: + # With marital units omitted, Core puts everyone in one marital unit and + # the default tax roles label the two adults as head and spouse, but the + # parent links show that they are parent and child. That role inference + # cannot establish a marriage or make the grandparent the baby's step + # parent. The baby uses non-filer rules and counts only baby plus teen. + medicaid_uses_non_filer_rules: [false, false, true] + medicaid_household_size: [2, 2, 2] + medicaid_household_income: [40_000, 40_000, 0] diff --git a/policyengine_us/variables/gov/states/tx/fpp/income/tx_fpp_dependent_care_deduction.py b/policyengine_us/variables/gov/states/tx/fpp/income/tx_fpp_dependent_care_deduction.py index aa3c83174e8..314cd80b02a 100644 --- a/policyengine_us/variables/gov/states/tx/fpp/income/tx_fpp_dependent_care_deduction.py +++ b/policyengine_us/variables/gov/states/tx/fpp/income/tx_fpp_dependent_care_deduction.py @@ -26,8 +26,9 @@ def formula(spm_unit, period, parameters): adult_care_expenses = person("care_expenses", period) age = person("age", period) is_child = age < p.child_age_threshold - # is_tax_unit_spouse flags the oldest non-head adult even when - # unmarried, so require an actual two-person marital unit. + # When marital units are omitted, is_tax_unit_spouse can flag the + # oldest non-head adult even if unmarried, so also require an actual + # two-person marital unit. is_spouse = person("is_tax_unit_spouse", period) & ( person.marital_unit.nb_persons() == 2 ) diff --git a/policyengine_us/variables/household/demographic/tax_unit/is_tax_unit_spouse.py b/policyengine_us/variables/household/demographic/tax_unit/is_tax_unit_spouse.py index 46f35a2bb1d..cbc5ef0cee9 100644 --- a/policyengine_us/variables/household/demographic/tax_unit/is_tax_unit_spouse.py +++ b/policyengine_us/variables/household/demographic/tax_unit/is_tax_unit_spouse.py @@ -21,12 +21,15 @@ def formula(person, period, parameters): candidate = head_or_spouse_candidates(person, period) head = person("is_tax_unit_head", period) eligible = candidate & ~head & ~is_separated - # An inferred spouse must also share the head's marital unit. When a - # situation omits marital units, Core puts everyone in one unit, so - # this only applies when membership is supplied. Compare population - # memberships, not the ID variable: situations can supply groups - # without numeric ID inputs. - marital_unit = person.simulation.populations["marital_unit"].members_entity_id + # An inferred spouse must also share the head's marital unit, which + # holds a married, cohabiting couple. When a situation omits marital + # units, Core puts everyone in one unit, so this only applies when + # membership is supplied. A married couple living apart in separate + # marital units, such as one spouse in a care facility, is therefore + # not inferred as spouses; supply is_tax_unit_spouse or + # tax_unit_role_input for them. Compare memberships, not the ID + # variable: situations can supply groups without numeric ID inputs. + marital_unit = person.marital_unit.reference_entity.members_entity_id head_marital_unit = tax_unit.max(where(head, marital_unit, -1)) eligible = eligible & (marital_unit == head_marital_unit) age = person("age", period) From 7ccef8e5c5f84726663aff890997d02c8cc69d23 Mon Sep 17 00:00:00 2001 From: Ziming Date: Fri, 30 Oct 2026 22:55:44 -0400 Subject: [PATCH 3/6] Update Python tests for the marital-unit spouse rule The tax-unit role dataset gave a married couple separate marital units and pinned the old age-ordered spouse; the couple now shares a unit, and the parent's student is a dependent, so that parent files as head of household. The parent-links order test relied on the mother being the grandparent's inferred spouse; she is now the explicit head, which keeps its check that the id names the co-resident mother. Co-Authored-By: Claude Opus 5.5 --- .../tests/core/test_parent_links.py | 8 +++++++- .../tests/core/test_tax_unit_role_input.py | 19 +++++++++++-------- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/policyengine_us/tests/core/test_parent_links.py b/policyengine_us/tests/core/test_parent_links.py index 29756485672..475fb11d2cf 100644 --- a/policyengine_us/tests/core/test_parent_links.py +++ b/policyengine_us/tests/core/test_parent_links.py @@ -318,7 +318,13 @@ def test_duplicate_ids_resolve_the_same_way_in_every_member_order(): def run(tax_unit_order, home_order, shared_home): people = { - "mother": {"person_id": {"2026": 1}, "age": {"2026": 35}}, + # The mother files; with her own marital unit she cannot be the + # grandparent's inferred spouse. + "mother": { + "person_id": {"2026": 1}, + "age": {"2026": 35}, + "is_tax_unit_head": {"2026": True}, + }, "grandparent": {"person_id": {"2026": 1}, "age": {"2026": 60}}, "child": { "person_id": {"2026": 2}, diff --git a/policyengine_us/tests/core/test_tax_unit_role_input.py b/policyengine_us/tests/core/test_tax_unit_role_input.py index a15b26ab69e..1b520a65be4 100644 --- a/policyengine_us/tests/core/test_tax_unit_role_input.py +++ b/policyengine_us/tests/core/test_tax_unit_role_input.py @@ -111,7 +111,8 @@ def _dataset( "person_tax_unit_id": tax_unit_of, "person_spm_unit_id": np.ones(6, dtype=int), "person_family_id": np.ones(6, dtype=int), - "person_marital_unit_id": np.arange(1, 7), + # The couple shares a marital unit; everyone else has their own. + "person_marital_unit_id": [1, 1, 2, 3, 4, 5], "age": np.asarray(ages, dtype=float), "is_full_time_student": [False, False, True, False, False, True], } @@ -131,7 +132,7 @@ def _dataset( tax_unit=tax_unit, spm_unit=pd.DataFrame({"spm_unit_id": [1]}), family=pd.DataFrame({"family_id": [1]}), - marital_unit=pd.DataFrame({"marital_unit_id": np.arange(1, 7)}), + marital_unit=pd.DataFrame({"marital_unit_id": np.arange(1, 6)}), time_period=YEAR, ) @@ -197,13 +198,15 @@ def test_a_tax_unit_without_members_falls_back_rather_than_raising(): def test_dataset_without_supplied_columns_keeps_age_ordering(): """The same people without the columns: the oldest adult heads each unit. - The 60-year-old heads the couple, the 16-year-old heads nothing, and the - student becomes the parent's spouse, so both multi-person units file - jointly - the behaviour the supplied roles exist to correct. + The 60-year-old heads the couple instead of the reference person - the + behaviour the supplied roles exist to correct - and the 16-year-old heads + nothing. The spouse must share the head's marital unit, so the parent's + student, who has their own, is a dependent and the parent files as head + of household. """ simulation = Microsimulation(dataset=_dataset(False), dataset_end_year=YEAR) heads, spouses, dependents, statuses = _roles_and_statuses(simulation, YEAR) assert heads == [False, True, False, False, True, False] - assert spouses == [True, False, False, False, False, True] - assert dependents == [False, False, True, True, False, False] - assert statuses == ["JOINT", "HEAD_OF_HOUSEHOLD", "JOINT"] + assert spouses == [True, False, False, False, False, False] + assert dependents == [False, False, True, True, False, True] + assert statuses == ["JOINT", "HEAD_OF_HOUSEHOLD", "HEAD_OF_HOUSEHOLD"] From 24f8a1a6645ac36c9544ffa5e64e65142a656611 Mon Sep 17 00:00:00 2001 From: Ziming Date: Fri, 6 Nov 2026 13:13:49 -0500 Subject: [PATCH 4/6] Fix issues from review: let a married couple head the tax unit When no tax roles are supplied and two head-or-spouse candidates share a two-person marital unit in the tax unit, the older member of that couple now heads it, ahead of an older adult outside the couple. Before, the couple got no head or spouse role and became that adult's dependents. Also drop Texas FPP's two-person marital-unit check on the spouse, update the fallback docs, add cases for the reviewed households, and pin the parent as head in one Medicaid case that assumed it. Co-Authored-By: Claude Opus 5.5 --- ...t-inferred-spouse-to-marital-unit.fixed.md | 2 +- docs/usage/microsimulation.md | 19 +++++-- .../test_tax_unit_role_input_properties.py | 21 ++++---- .../tests/microsimulation/test_microsim.py | 19 ++++--- .../income/parent_ids_composition.yaml | 2 + ..._income_deemed_from_ineligible_spouse.yaml | 50 +++++++++++++++++++ ..._ssi_spousal_resource_deeming_applies.yaml | 11 ++-- .../fpp/tx_fpp_dependent_care_deduction.yaml | 30 +++++++++++ .../tax_unit/is_tax_unit_head.yaml | 45 +++++++++++++++++ .../income/tx_fpp_dependent_care_deduction.py | 11 ++-- .../demographic/tax_unit/is_tax_unit_head.py | 27 +++++++++- .../tax_unit/is_tax_unit_spouse.py | 4 +- .../tax_unit/tax_unit_role_input.py | 2 +- 13 files changed, 205 insertions(+), 38 deletions(-) diff --git a/changelog.d/restrict-inferred-spouse-to-marital-unit.fixed.md b/changelog.d/restrict-inferred-spouse-to-marital-unit.fixed.md index fa28286e49a..c2ce3c62e9c 100644 --- a/changelog.d/restrict-inferred-spouse-to-marital-unit.fixed.md +++ b/changelog.d/restrict-inferred-spouse-to-marital-unit.fixed.md @@ -1 +1 @@ -Keep an inferred tax unit spouse within the head's supplied marital unit, so an unmarried adult child or other relative is no longer made the head's spouse. +Keep an inferred tax unit spouse within the head's supplied marital unit, so an unmarried adult child or other relative is no longer made the head's spouse. A married couple sharing a marital unit in the tax unit now heads it ahead of an older adult outside the couple. diff --git a/docs/usage/microsimulation.md b/docs/usage/microsimulation.md index 041ff825887..293e80da6dc 100644 --- a/docs/usage/microsimulation.md +++ b/docs/usage/microsimulation.md @@ -153,10 +153,21 @@ still take precedence. Without supplied roles, a unit falls back to age ordering. The oldest adult is the head, skipping adults input as tax-unit dependents unless every adult in -the unit is one. The next-oldest such adult is the spouse, unless any member of -the unit is separated. Without dependent inputs, that pairs an adult student -with a parent as joint filers and leaves a minor living without a parent with -no head. +the unit is one. When two such adults share a two-person marital unit within +the tax unit, the older of that couple is the head instead: a married couple +can file jointly (26 USC 6013(a)), and a couple who files a joint return can't +be claimed as anyone's dependents (26 USC 152(b)(2)). The spouse is the +next-oldest such adult who shares the head's marital unit, unless any member of +the unit is separated. When a situation omits marital units, Core puts everyone +in one marital unit, so plain age order applies. A minor living without a +parent is left with no head. + +This rule has one known limitation. When a tax unit holds a married couple and +an older adult outside their marital unit, the couple heads the unit and the +older adult becomes their dependent, whatever that adult's income. If the older +adult files instead, for example a parent who claims a married child and the +child's spouse who don't file jointly, supply `is_tax_unit_head` or +`tax_unit_role_input`. The Populace build carries its constructor's filing status in a `filing_status_input` column. No variable has that name, so the column is diff --git a/policyengine_us/tests/core/test_tax_unit_role_input_properties.py b/policyengine_us/tests/core/test_tax_unit_role_input_properties.py index b27dfa95508..a7b64dce25c 100644 --- a/policyengine_us/tests/core/test_tax_unit_role_input_properties.py +++ b/policyengine_us/tests/core/test_tax_unit_role_input_properties.py @@ -6,11 +6,13 @@ 2. Supplied roles hold: in a unit whose roles are supplied, the role variables equal the supplied roles, so the unit has exactly one head and at most one spouse. -3. Fallback: in a unit without supplied roles, the roles equal an independent - implementation of age ordering. The oldest adult heads the unit, earliest - member on ties, skipping adults input as tax unit dependents unless every - adult is one; the next such adult is the spouse unless any member of the - unit is separated. +3. Fallback (marital units omitted): in a unit without supplied roles, the + roles equal an independent implementation of age ordering. The oldest adult + heads the unit, earliest member on ties, skipping adults input as tax unit + dependents unless every adult is one; the next such adult is the spouse + unless any member of the unit is separated. The generated situations pass + no marital units, so Core puts everyone in one marital unit and the + couple-first head and shared-marital-unit spouse rules reduce to age order. 4. Filing status is computed: no supplied status is read, so a unit files jointly exactly when it has a spouse, supplied or inferred. 5. Workaround equivalence (differential): supplying roles through @@ -119,10 +121,11 @@ def _situation(units, pin_explicitly=False): def _age_rule(ages, separated, dependent_input=None): - # Mirrors the fallback as it stands. Adults input as tax unit dependents - # are skipped unless every adult is one (#9630). Any separated member - # blocks every spouse; that unit-wide gate is a known defect (#9620), so - # update this reference when it is fixed. + # Mirrors the fallback as it stands, with marital units omitted (see + # invariant 3). Adults input as tax unit dependents are skipped unless + # every adult is one (#9630). Any separated member blocks every spouse; + # that unit-wide gate is a known defect (#9620), so update this reference + # when it is fixed. adults = [i for i, age in enumerate(ages) if age >= 18] if dependent_input is not None: non_dependents = [i for i in adults if not dependent_input[i]] diff --git a/policyengine_us/tests/microsimulation/test_microsim.py b/policyengine_us/tests/microsimulation/test_microsim.py index 9081b6474ad..577e4f21350 100644 --- a/policyengine_us/tests/microsimulation/test_microsim.py +++ b/policyengine_us/tests/microsimulation/test_microsim.py @@ -243,14 +243,17 @@ def test_default_dataset_uses_its_supplied_tax_unit_roles(): """Every tax-unit role on the certified default build is its own. `populace_us_2024` ships its tax-unit constructor's roles - (`tax_unit_role_input`). Without them, age ordering made the oldest adult - the head and the next-oldest the spouse, pairing adult students with their - parents as joint filers, heading couples by the older partner rather than - the reference person, and leaving lone minors with no head. This checks the - whole population, person by person, in the data year and in an extended - year, since the loader carries the column forward rather than the formulas. - Filing status is not checked against the build: the model computes it from - these roles and the filing rules. + (`tax_unit_role_input`). Without supplied roles, the oldest adult heads + the tax unit; when two candidates share a two-person marital unit in the + tax unit, the older of that couple heads instead. The inferred spouse must + share the head's marital unit, so supplied marital units no longer pair an + adult student with a parent; when marital units are omitted, plain age + order applies. The fallback still heads couples by the older partner + rather than the reference person and leaves lone minors with no head. + This checks the whole population, person by person, in the data year and + in an extended year, since the loader carries the column forward rather + than the formulas. Filing status is not checked against the build: the + model computes it from these roles and the filing rules. """ import numpy as np import pandas as pd diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/income/parent_ids_composition.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/income/parent_ids_composition.yaml index 941b18c9542..6eaad3b87c3 100644 --- a/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/income/parent_ids_composition.yaml +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/income/parent_ids_composition.yaml @@ -1239,6 +1239,8 @@ parent: person_id: 1 age: 40 + # Pinned as head: without roles, the married couple would head the unit. + is_tax_unit_head: true adult_child: person_id: 2 age: 19 diff --git a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_spouse/ssi_income_deemed_from_ineligible_spouse.yaml b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_spouse/ssi_income_deemed_from_ineligible_spouse.yaml index e1968e9c591..512576f5e6a 100644 --- a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_spouse/ssi_income_deemed_from_ineligible_spouse.yaml +++ b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_spouse/ssi_income_deemed_from_ineligible_spouse.yaml @@ -254,3 +254,53 @@ # Alone: (500 - 20) x 12 = 5,760. Deemed: 25,370 - 5,760 = 19,610. is_ssi_spousal_deeming_applies: [true, false] ssi_income_deemed_from_ineligible_spouse: [19_610, 0] + +- name: Spouse income is deemed within a couple heading a unit that includes an older parent (2025) + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + # At 78 she is not an ineligible child; her own marital unit keeps + # her from being the inferred spouse. + age: 78 + social_security_retirement: 15_000 + person2: + age: 50 + employment_income_before_lsr: 30_000 + person3: + age: 48 + is_disabled: true + meets_ssi_disability_criteria: true + person4: + # Ineligible child of the couple. + age: 10 + tax_units: + tax_unit: + members: [person1, person2, person3, person4] + marital_units: + parent_marital_unit: + members: [person1] + couple: + members: [person2, person3] + child_marital_unit: + members: [person4] + spm_units: + spm_unit: + members: [person1, person2, person3, person4] + households: + household: + members: [person1, person2, person3, person4] + state_code: TX + output: + # No roles are supplied: the couple heads the tax unit ahead of the older + # parent, so person2 is the ineligible spouse of person3. + is_tax_unit_head: [false, true, false, false] + is_ssi_ineligible_spouse: [false, true, false, false] + is_ssi_ineligible_child: [false, false, false, true] + # Child allocation: (1,450 - 967) x 12 = 5,796. + # Deemed earnings: 30,000 - 5,796 = 24,204 > 5,796, so deeming applies. + is_ssi_spousal_deeming_applies: [false, false, true, false] + # Couple: (24,204 / 12 - 20 - 65) x 0.5 x 12 = (2,017 - 85) x 6 = 11,592. + # Alone: 0. Deemed: 11,592 - 0 = 11,592. + ssi_income_deemed_from_ineligible_spouse: [0, 0, 11_592, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/resources/deemed/is_ssi_spousal_resource_deeming_applies.yaml b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/resources/deemed/is_ssi_spousal_resource_deeming_applies.yaml index 569eef9cce1..a8abfc90849 100644 --- a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/resources/deemed/is_ssi_spousal_resource_deeming_applies.yaml +++ b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/resources/deemed/is_ssi_spousal_resource_deeming_applies.yaml @@ -359,10 +359,11 @@ ssi_resources_deemed_from_ineligible_spouse: [0, 3001, 0] meets_ssi_resource_test: [true, false, false] -# Explicit marital units stand whatever tax roles the model infers: with -# roles omitted the model may treat one spouse as the mother's tax spouse and -# the other as her dependent, but 416.1205(b) still combines the couple's -# resources: 0 + 3,001 > 3,000. +# With roles omitted, the couple now heads the mother's tax unit and makes a +# joint claim. 416.1205(b) combines their resources against the couple limit +# (0 + 3,001 > 3,000), so nothing is deemed from an ineligible spouse. +# Whatever tax roles are inferred, the resource test result stays the same as +# in the preceding case with pinned roles. - name: Married adults in a parent's tax unit with roles omitted count each other's resources period: 2026-01 input: @@ -394,7 +395,7 @@ home: members: [mother, claimant, spouse] output: - ssi_resources_deemed_from_ineligible_spouse: [0, 3001, 0] + ssi_resources_deemed_from_ineligible_spouse: [0, 0, 0] meets_ssi_resource_test: [true, false, false] # 416.1202(a)(2) excludes retroactive military payments of an ineligible diff --git a/policyengine_us/tests/policy/baseline/gov/states/tx/fpp/tx_fpp_dependent_care_deduction.yaml b/policyengine_us/tests/policy/baseline/gov/states/tx/fpp/tx_fpp_dependent_care_deduction.yaml index a0d7883f3ed..46215943017 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/tx/fpp/tx_fpp_dependent_care_deduction.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/tx/fpp/tx_fpp_dependent_care_deduction.yaml @@ -235,3 +235,33 @@ output: # min(1,200, 175 x 12) = 1,200. tx_fpp_dependent_care_deduction: 1_200 + +- name: Case 11, disabled spouse counts when marital units are omitted. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + person2: + age: 38 + is_disabled: true + care_expenses: 3_000 + person3: + age: 8 + # No marital units: Core puts all three in one marital unit. + tax_units: + tax_unit: + members: [person1, person2, person3] + spm_units: + spm_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: TX + output: + # Age order makes person1 the head and person2 the spouse. + is_tax_unit_spouse: [false, true, false] + # min(3,000, 175 x 12) = 2,100 + tx_fpp_dependent_care_deduction: 2_100 diff --git a/policyengine_us/tests/policy/baseline/household/demographic/tax_unit/is_tax_unit_head.yaml b/policyengine_us/tests/policy/baseline/household/demographic/tax_unit/is_tax_unit_head.yaml index 0fec812cc39..a6b832b5709 100644 --- a/policyengine_us/tests/policy/baseline/household/demographic/tax_unit/is_tax_unit_head.yaml +++ b/policyengine_us/tests/policy/baseline/household/demographic/tax_unit/is_tax_unit_head.yaml @@ -145,3 +145,48 @@ output: is_tax_unit_head: [false, true] is_tax_unit_spouse: [true, false] + +- name: A married couple heads the unit ahead of an older parent outside their marital unit. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 78 + social_security_retirement: 15_000 + head: + age: 50 + employment_income_before_lsr: 80_000 + spouse: + age: 48 + employment_income_before_lsr: 40_000 + child: + age: 10 + tax_units: + tax_unit: + members: [parent, head, spouse, child] + marital_units: + parent_marital_unit: + members: [parent] + couple: + members: [head, spouse] + child_marital_unit: + members: [child] + spm_units: + spm_unit: + members: [parent, head, spouse, child] + households: + household: + members: [parent, head, spouse, child] + state_code: TX + output: + # No roles are supplied. The head and spouse share a two-person marital + # unit, so the older of the couple (50) heads the unit instead of the + # parent (78), who is outside it and becomes a dependent. + is_tax_unit_head: [false, true, false, false] + is_tax_unit_spouse: [false, false, true, false] + is_tax_unit_dependent: [true, false, false, true] + filing_status: JOINT + # Wages 80,000 + 40,000 = 120,000. The parent's 15,000 of Social Security + # is a dependent's income, so it is left off the couple's return. + adjusted_gross_income: 120_000 diff --git a/policyengine_us/variables/gov/states/tx/fpp/income/tx_fpp_dependent_care_deduction.py b/policyengine_us/variables/gov/states/tx/fpp/income/tx_fpp_dependent_care_deduction.py index 314cd80b02a..806d21c7d3d 100644 --- a/policyengine_us/variables/gov/states/tx/fpp/income/tx_fpp_dependent_care_deduction.py +++ b/policyengine_us/variables/gov/states/tx/fpp/income/tx_fpp_dependent_care_deduction.py @@ -26,16 +26,13 @@ def formula(spm_unit, period, parameters): adult_care_expenses = person("care_expenses", period) age = person("age", period) is_child = age < p.child_age_threshold - # When marital units are omitted, is_tax_unit_spouse can flag the - # oldest non-head adult even if unmarried, so also require an actual - # two-person marital unit. - is_spouse = person("is_tax_unit_spouse", period) & ( - person.marital_unit.nb_persons() == 2 - ) is_disabled_adult = ( person("is_disabled", period) & ~is_child - & (person("is_tax_unit_dependent", period) | is_spouse) + & ( + person("is_tax_unit_dependent", period) + | person("is_tax_unit_spouse", period) + ) ) eligible_expenses = ( childcare_expenses * is_child + adult_care_expenses * is_disabled_adult diff --git a/policyengine_us/variables/household/demographic/tax_unit/is_tax_unit_head.py b/policyengine_us/variables/household/demographic/tax_unit/is_tax_unit_head.py index 9882c5a7acb..271a5aff46d 100644 --- a/policyengine_us/variables/household/demographic/tax_unit/is_tax_unit_head.py +++ b/policyengine_us/variables/household/demographic/tax_unit/is_tax_unit_head.py @@ -9,6 +9,10 @@ class is_tax_unit_head(Variable): entity = Person label = "Head of tax unit" definition_period = YEAR + reference = ( + "https://www.law.cornell.edu/uscode/text/26/6013", + "https://www.law.cornell.edu/uscode/text/26/152", + ) def formula(person, period, parameters): tax_unit = person.tax_unit @@ -17,6 +21,25 @@ def formula(person, period, parameters): supplied = tax_unit("tax_unit_roles_supplied", period) # Otherwise the oldest adult is the head, skipping input dependents. eligible = head_or_spouse_candidates(person, period) + # A married couple can file a joint return (26 USC 6013(a)), and a + # couple who files a joint return can't be claimed as anyone's + # dependents (26 USC 152(b)(2)). So when two such adults share a + # two-person marital unit within this tax unit, the older of the + # couple heads it, even if another adult is older. When a situation + # omits marital units, Core puts everyone in one unit, so this changes + # nothing there: a two-person unit gives the same head. + # NOTE: the older adult then becomes the couple's dependent whatever + # their income. If that adult files instead, claiming a couple who + # don't file jointly, supply is_tax_unit_head or tax_unit_role_input. + marital_unit = person.marital_unit + tax_unit_id = tax_unit.reference_entity.members_entity_id + couple_candidate = ( + eligible + & (marital_unit.nb_persons() == 2) + & (marital_unit.sum(eligible) == 2) + & (marital_unit.min(tax_unit_id) == marital_unit.max(tax_unit_id)) + ) + eligible = where(tax_unit.any(couple_candidate), couple_candidate, eligible) age = person("age", period) - oldest_adult = person.get_rank(tax_unit, -age, eligible) == 0 - return where(supplied, role == role.possible_values.HEAD, oldest_adult) + oldest_candidate = person.get_rank(tax_unit, -age, eligible) == 0 + return where(supplied, role == role.possible_values.HEAD, oldest_candidate) diff --git a/policyengine_us/variables/household/demographic/tax_unit/is_tax_unit_spouse.py b/policyengine_us/variables/household/demographic/tax_unit/is_tax_unit_spouse.py index cbc5ef0cee9..fe5ba991f55 100644 --- a/policyengine_us/variables/household/demographic/tax_unit/is_tax_unit_spouse.py +++ b/policyengine_us/variables/household/demographic/tax_unit/is_tax_unit_spouse.py @@ -22,7 +22,9 @@ def formula(person, period, parameters): head = person("is_tax_unit_head", period) eligible = candidate & ~head & ~is_separated # An inferred spouse must also share the head's marital unit, which - # holds a married, cohabiting couple. When a situation omits marital + # holds a married, cohabiting couple. is_tax_unit_head takes the head + # from such a couple when the tax unit has one, so the next-oldest + # adult here is the head's partner. When a situation omits marital # units, Core puts everyone in one unit, so this only applies when # membership is supplied. A married couple living apart in separate # marital units, such as one spouse in a care facility, is therefore diff --git a/policyengine_us/variables/household/demographic/tax_unit/tax_unit_role_input.py b/policyengine_us/variables/household/demographic/tax_unit/tax_unit_role_input.py index 74003fcca48..e28019ae6c7 100644 --- a/policyengine_us/variables/household/demographic/tax_unit/tax_unit_role_input.py +++ b/policyengine_us/variables/household/demographic/tax_unit/tax_unit_role_input.py @@ -15,4 +15,4 @@ class tax_unit_role_input(Variable): default_value = TaxUnitRole.UNSPECIFIED definition_period = YEAR label = "Supplied tax unit role" - documentation = "The person's role in their tax unit - head, spouse or dependent - as assigned by the tax-unit constructor that built a dataset, such as the Populace US build. When every member of a tax unit has a supplied role, is_tax_unit_head and is_tax_unit_spouse use it, so the constructor's head, its spouse pairing and its dependency claims all hold: a younger reference person can head a couple, an adult student stays a dependent, and a minor living without a parent can head a return. The roles only say who is head, spouse or dependent; filing_status is still computed from the unit's members and the filing rules. UNSPECIFIED (the default) means the role was not supplied, and the role variables fall back to age ordering: the oldest adult is the head, skipping adults input as tax unit dependents unless every adult in the unit is one, and the next-oldest such adult is the spouse unless any member of the unit is separated." + documentation = "The person's role in their tax unit - head, spouse or dependent - as assigned by the tax-unit constructor that built a dataset, such as the Populace US build. When every member of a tax unit has a supplied role, is_tax_unit_head and is_tax_unit_spouse use it, so the constructor's head, its spouse pairing and its dependency claims all hold: a younger reference person can head a couple, an adult student stays a dependent, and a minor living without a parent can head a return. The roles only say who is head, spouse or dependent; filing_status is still computed from the unit's members and the filing rules. UNSPECIFIED (the default) means the role was not supplied, and the role variables fall back to age ordering: the oldest adult is the head, skipping adults input as tax unit dependents unless every adult in the unit is one, except that the older member of a couple sharing a two-person marital unit in the tax unit heads it, and the next-oldest such adult who shares the head's marital unit is the spouse unless any member of the unit is separated. When marital units are omitted, everyone shares one marital unit, so plain age order applies." From 6706583acba6b027219190f4756abfb828daa73b Mon Sep 17 00:00:00 2001 From: Ziming Date: Fri, 6 Nov 2026 15:57:55 -0500 Subject: [PATCH 5/6] Fix issues from review: pair adults past the qualifying child age Keep the head's marital-unit partner as the inferred spouse. A head without one takes the next-oldest adult unless that adult meets the qualifying child age test against the head (26 USC 152(c)(3)), so an adult earner outside the head's marital unit stays on the return. A separated member no longer forms a head couple. Extend the IRS dependent age limits back to 1954, with the student limit from 1989. Co-Authored-By: Claude Opus 5.5 --- ...t-inferred-spouse-to-marital-unit.fixed.md | 2 +- docs/usage/microsimulation.md | 37 +++-- .../dependent/ineligible_age/non_student.yaml | 7 +- .../irs/dependent/ineligible_age/student.yaml | 10 +- .../tests/core/test_tax_unit_role_input.py | 6 +- .../test_tax_unit_role_input_properties.py | 2 +- .../tests/microsimulation/test_microsim.py | 8 +- .../income/parent_ids_composition.yaml | 5 +- ..._income_deemed_from_ineligible_spouse.yaml | 5 +- .../tax_unit/is_tax_unit_head.yaml | 34 +++++ .../tax_unit/is_tax_unit_spouse.yaml | 135 ++++++++++++++++++ .../demographic/tax_unit/is_tax_unit_head.py | 7 +- .../tax_unit/is_tax_unit_spouse.py | 44 ++++-- .../tax_unit/tax_unit_role_input.py | 2 +- 14 files changed, 263 insertions(+), 41 deletions(-) diff --git a/changelog.d/restrict-inferred-spouse-to-marital-unit.fixed.md b/changelog.d/restrict-inferred-spouse-to-marital-unit.fixed.md index c2ce3c62e9c..b8dcc4ff90e 100644 --- a/changelog.d/restrict-inferred-spouse-to-marital-unit.fixed.md +++ b/changelog.d/restrict-inferred-spouse-to-marital-unit.fixed.md @@ -1 +1 @@ -Keep an inferred tax unit spouse within the head's supplied marital unit, so an unmarried adult child or other relative is no longer made the head's spouse. A married couple sharing a marital unit in the tax unit now heads it ahead of an older adult outside the couple. +Stop inferring an adult outside the head's supplied marital unit as the head's tax unit spouse when that adult meets the qualifying child age test, so an adult child under 19, a student under 24 or a disabled adult child is no longer made the head's spouse. A married couple sharing a marital unit in the tax unit now heads it ahead of an older adult outside the couple. The IRS dependent age limits now start in 1954, with the student limit of 24 from 1989. diff --git a/docs/usage/microsimulation.md b/docs/usage/microsimulation.md index 293e80da6dc..63f798a89ff 100644 --- a/docs/usage/microsimulation.md +++ b/docs/usage/microsimulation.md @@ -156,18 +156,31 @@ the head, skipping adults input as tax-unit dependents unless every adult in the unit is one. When two such adults share a two-person marital unit within the tax unit, the older of that couple is the head instead: a married couple can file jointly (26 USC 6013(a)), and a couple who files a joint return can't -be claimed as anyone's dependents (26 USC 152(b)(2)). The spouse is the -next-oldest such adult who shares the head's marital unit, unless any member of -the unit is separated. When a situation omits marital units, Core puts everyone -in one marital unit, so plain age order applies. A minor living without a -parent is left with no head. - -This rule has one known limitation. When a tax unit holds a married couple and -an older adult outside their marital unit, the couple heads the unit and the -older adult becomes their dependent, whatever that adult's income. If the older -adult files instead, for example a parent who claims a married child and the -child's spouse who don't file jointly, supply `is_tax_unit_head` or -`tax_unit_role_input`. +be claimed as anyone's dependents (26 USC 152(b)(2)). The spouse is the head's +partner in their marital unit. A head without one takes the next-oldest such +adult, except an adult who meets the qualifying child age test against the head +(26 USC 152(c)(3)): under 19, or under 24 if a full-time student, and younger +than the head, or permanently and totally disabled at any age. That adult stays +a dependent. No spouse is inferred when any member of the unit is separated, +and a separated member doesn't count toward a couple. When a situation omits +marital units, Core puts everyone in one marital unit, so plain age order +applies. A minor living without a parent is left with no head. + +This rule has three known limitations. Supply `is_tax_unit_head`, +`is_tax_unit_spouse` or `tax_unit_role_input` for these households: + +- When a tax unit holds a married couple and an older adult outside their + marital unit, the couple heads the unit and the older adult becomes their + dependent, whatever that adult's income. The older adult may file instead, + for example a parent who claims a married child and the child's spouse who + don't file jointly. +- An adult outside the head's marital unit who fails the age test, such as a + 45-year-old child with wages, is still paired with the head, and the unit + files jointly. Under the law each would file their own return, or the adult + could be a qualifying relative, but the pairing keeps the adult's income on + the one return the model computes for the unit. +- A disabled spouse living apart in their own marital unit, such as in a care + facility, becomes the head's dependent. The Populace build carries its constructor's filing status in a `filing_status_input` column. No variable has that name, so the column is diff --git a/policyengine_us/parameters/gov/irs/dependent/ineligible_age/non_student.yaml b/policyengine_us/parameters/gov/irs/dependent/ineligible_age/non_student.yaml index 3558f57b5ad..e30b9b03213 100644 --- a/policyengine_us/parameters/gov/irs/dependent/ineligible_age/non_student.yaml +++ b/policyengine_us/parameters/gov/irs/dependent/ineligible_age/non_student.yaml @@ -6,5 +6,10 @@ metadata: reference: - title: 26 U.S. Code § 152 - Dependent defined href: https://www.law.cornell.edu/uscode/text/26/152#c_3_A_i + # The section's amendment notes record no change to the age since 1954. + - title: 26 U.S. Code § 151(c)(1)(B)(i) (2000 edition) + href: https://www.govinfo.gov/content/pkg/USCODE-2000-title26/html/USCODE-2000-title26-subtitleA-chap1-subchapB-partV-sec151.htm + - title: 26 U.S. Code § 7851(a)(1)(A) - Applicability of revenue laws + href: https://www.law.cornell.edu/uscode/text/26/7851#a_1_A values: - 2018-01-01: 19 + 1954-01-01: 19 diff --git a/policyengine_us/parameters/gov/irs/dependent/ineligible_age/student.yaml b/policyengine_us/parameters/gov/irs/dependent/ineligible_age/student.yaml index 0381eaab9a6..2bc3436f4dd 100644 --- a/policyengine_us/parameters/gov/irs/dependent/ineligible_age/student.yaml +++ b/policyengine_us/parameters/gov/irs/dependent/ineligible_age/student.yaml @@ -6,5 +6,13 @@ metadata: reference: - title: 26 U.S. Code § 152 - Dependent defined href: https://www.law.cornell.edu/uscode/text/26/152#c_3_A_ii + # The 1988 amendment and its effective date are in the section's notes. + - title: 26 U.S. Code § 151(c)(1)(B)(ii) (2000 edition), with Pub. L. 100-647 § 6010 + href: https://www.govinfo.gov/content/pkg/USCODE-2000-title26/html/USCODE-2000-title26-subtitleA-chap1-subchapB-partV-sec151.htm + - title: 26 U.S. Code § 7851(a)(1)(A) - Applicability of revenue laws + href: https://www.law.cornell.edu/uscode/text/26/7851#a_1_A values: - 2018-01-01: 24 + # The 1954 Code allowed a student of any age; Pub. L. 100-647 added the + # age limit for taxable years beginning after December 31, 1988. + 1954-01-01: .inf + 1989-01-01: 24 diff --git a/policyengine_us/tests/core/test_tax_unit_role_input.py b/policyengine_us/tests/core/test_tax_unit_role_input.py index 1b520a65be4..901d1a81d46 100644 --- a/policyengine_us/tests/core/test_tax_unit_role_input.py +++ b/policyengine_us/tests/core/test_tax_unit_role_input.py @@ -200,9 +200,9 @@ def test_dataset_without_supplied_columns_keeps_age_ordering(): The 60-year-old heads the couple instead of the reference person - the behaviour the supplied roles exist to correct - and the 16-year-old heads - nothing. The spouse must share the head's marital unit, so the parent's - student, who has their own, is a dependent and the parent files as head - of household. + nothing. The parent's 21-year-old student has their own marital unit and + meets the qualifying child age test, so they are a dependent and the + parent files as head of household. """ simulation = Microsimulation(dataset=_dataset(False), dataset_end_year=YEAR) heads, spouses, dependents, statuses = _roles_and_statuses(simulation, YEAR) diff --git a/policyengine_us/tests/core/test_tax_unit_role_input_properties.py b/policyengine_us/tests/core/test_tax_unit_role_input_properties.py index a7b64dce25c..a1e27e688f5 100644 --- a/policyengine_us/tests/core/test_tax_unit_role_input_properties.py +++ b/policyengine_us/tests/core/test_tax_unit_role_input_properties.py @@ -12,7 +12,7 @@ dependents unless every adult is one; the next such adult is the spouse unless any member of the unit is separated. The generated situations pass no marital units, so Core puts everyone in one marital unit and the - couple-first head and shared-marital-unit spouse rules reduce to age order. + couple-first head and partner-first spouse rules reduce to age order. 4. Filing status is computed: no supplied status is read, so a unit files jointly exactly when it has a spouse, supplied or inferred. 5. Workaround equivalence (differential): supplying roles through diff --git a/policyengine_us/tests/microsimulation/test_microsim.py b/policyengine_us/tests/microsimulation/test_microsim.py index 577e4f21350..6bfc7e0457a 100644 --- a/policyengine_us/tests/microsimulation/test_microsim.py +++ b/policyengine_us/tests/microsimulation/test_microsim.py @@ -245,10 +245,10 @@ def test_default_dataset_uses_its_supplied_tax_unit_roles(): `populace_us_2024` ships its tax-unit constructor's roles (`tax_unit_role_input`). Without supplied roles, the oldest adult heads the tax unit; when two candidates share a two-person marital unit in the - tax unit, the older of that couple heads instead. The inferred spouse must - share the head's marital unit, so supplied marital units no longer pair an - adult student with a parent; when marital units are omitted, plain age - order applies. The fallback still heads couples by the older partner + tax unit, the older of that couple heads instead. An adult outside the + head's marital unit who meets the qualifying child age test is not the + inferred spouse, so supplied marital units no longer pair an adult student + with a parent; when marital units are omitted, plain age order applies. The fallback still heads couples by the older partner rather than the reference person and leaves lone minors with no head. This checks the whole population, person by person, in the data year and in an extended year, since the loader carries the column forward rather diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/income/parent_ids_composition.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/income/parent_ids_composition.yaml index 6eaad3b87c3..ba1cca34267 100644 --- a/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/income/parent_ids_composition.yaml +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/income/parent_ids_composition.yaml @@ -2098,8 +2098,9 @@ members: [grandparent, teen, baby] state_code: OH output: - # The teen has their own marital unit, so the default tax roles make them - # the grandparent's dependent rather than spouse, and the parent links + # The teen has their own marital unit and, at 18, meets the qualifying + # child age test, so the default tax roles make them the grandparent's + # dependent rather than spouse, and the parent links # show that the two are parent and child. The grandparent is not the # baby's step parent. The baby uses non-filer rules and counts only baby # plus teen. diff --git a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_spouse/ssi_income_deemed_from_ineligible_spouse.yaml b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_spouse/ssi_income_deemed_from_ineligible_spouse.yaml index 512576f5e6a..9b620bcd8d0 100644 --- a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_spouse/ssi_income_deemed_from_ineligible_spouse.yaml +++ b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_spouse/ssi_income_deemed_from_ineligible_spouse.yaml @@ -261,8 +261,9 @@ input: people: person1: - # At 78 she is not an ineligible child; her own marital unit keeps - # her from being the inferred spouse. + # At 78 she is not an ineligible child. Her son heads the unit with + # his wife, his partner in their marital unit, so she isn't the + # inferred spouse. age: 78 social_security_retirement: 15_000 person2: diff --git a/policyengine_us/tests/policy/baseline/household/demographic/tax_unit/is_tax_unit_head.yaml b/policyengine_us/tests/policy/baseline/household/demographic/tax_unit/is_tax_unit_head.yaml index a6b832b5709..d9adee9979d 100644 --- a/policyengine_us/tests/policy/baseline/household/demographic/tax_unit/is_tax_unit_head.yaml +++ b/policyengine_us/tests/policy/baseline/household/demographic/tax_unit/is_tax_unit_head.yaml @@ -190,3 +190,37 @@ # Wages 80,000 + 40,000 = 120,000. The parent's 15,000 of Social Security # is a dependent's income, so it is left off the couple's return. adjusted_gross_income: 120_000 + +- name: A separated member of a two-person marital unit does not head the unit ahead of an older parent. + period: 2025 + input: + people: + parent: + age: 78 + social_security_retirement: 15_000 + separated_adult: + age: 40 + is_separated: true + employment_income_before_lsr: 30_000 + partner: + age: 38 + employment_income_before_lsr: 20_000 + tax_units: + tax_unit: + members: [parent, separated_adult, partner] + marital_units: + parent_marital_unit: + members: [parent] + couple: + members: [separated_adult, partner] + households: + household: + members: [parent, separated_adult, partner] + state_code: TX + output: + # A unit with a separated member infers no spouse, so the two-person + # marital unit is not a couple that can file jointly. The oldest adult + # heads the unit. + is_tax_unit_head: [true, false, false] + is_tax_unit_spouse: [false, false, false] + filing_status: SINGLE diff --git a/policyengine_us/tests/policy/baseline/household/demographic/tax_unit/is_tax_unit_spouse.yaml b/policyengine_us/tests/policy/baseline/household/demographic/tax_unit/is_tax_unit_spouse.yaml index 22ead1c39e0..5f4e34fa8a4 100644 --- a/policyengine_us/tests/policy/baseline/household/demographic/tax_unit/is_tax_unit_spouse.yaml +++ b/policyengine_us/tests/policy/baseline/household/demographic/tax_unit/is_tax_unit_spouse.yaml @@ -165,3 +165,138 @@ marital_units: {} output: is_tax_unit_spouse: [false, false] + +- name: An adult past the qualifying child age in their own marital unit stays the head's spouse. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 75 + social_security_retirement: 15_000 + adult_child: + age: 45 + employment_income_before_lsr: 50_000 + tax_units: + tax_unit: + members: [parent, adult_child] + marital_units: + parent_marital_unit: + members: [parent] + child_marital_unit: + members: [adult_child] + households: + household: + members: [parent, adult_child] + state_code: TX + output: + # No roles are supplied, and the parent has no partner in their marital + # unit. At 45 the adult child fails the qualifying child age test + # (26 USC 152(c)(3)), so they can't be the parent's dependent. The age + # order pairs them with the parent, which keeps their wages on the one + # return the model computes for the unit. + is_tax_unit_head: [true, false] + is_tax_unit_spouse: [false, true] + filing_status: JOINT + # Provisional income 50,000 + 15,000 x 0.5 = 57,500 is over the 44,000 + # joint adjusted base. Taxable Social Security is the lesser of + # 15,000 x 0.85 = 12,750 and (57,500 - 44,000) x 0.85 + # + (44,000 - 32,000) x 0.5 = 17,475, so 12,750. + # AGI = 50,000 + 12,750 = 62,750. + adjusted_gross_income: 62_750 + +- name: A 19-year-old non-student in their own marital unit stays the head's spouse. + period: 2025 + input: + people: + parent: + age: 45 + adult_child: + age: 19 + tax_units: + tax_unit: + members: [parent, adult_child] + marital_units: + parent_marital_unit: + members: [parent] + child_marital_unit: + members: [adult_child] + output: + # The non-student limit is under 19, so a 19-year-old fails the age test. + is_tax_unit_spouse: [false, true] + +- name: A 23-year-old full-time student in their own marital unit is a dependent. + period: 2025 + input: + people: + parent: + age: 50 + student: + age: 23 + is_full_time_student: true + tax_units: + tax_unit: + members: [parent, student] + marital_units: + parent_marital_unit: + members: [parent] + student_marital_unit: + members: [student] + output: + # The student limit is under 24, and the student is younger than the + # parent, so they meet the age test and are not paired as the spouse. + is_tax_unit_spouse: [false, false] + is_tax_unit_dependent: [false, true] + +- name: A 26-year-old full-time student in their own marital unit is a dependent before 1989. + period: 1988 + input: + people: + parent: + age: 60 + student: + age: 26 + is_full_time_student: true + tax_units: + tax_unit: + members: [parent, student] + marital_units: + parent_marital_unit: + members: [parent] + student_marital_unit: + members: [student] + output: + # Before Pub. L. 100-647 added the under-24 limit for 1989, a student of + # any age met the age test. + is_tax_unit_spouse: [false, false] + is_tax_unit_dependent: [false, true] + +- name: A permanently and totally disabled adult child in their own marital unit is a dependent. + period: 2025 + input: + people: + parent: + age: 70 + employment_income_before_lsr: 30_000 + adult_child: + age: 40 + is_disabled: true + is_permanently_and_totally_disabled: true + tax_units: + tax_unit: + members: [parent, adult_child] + marital_units: + parent_marital_unit: + members: [parent] + child_marital_unit: + members: [adult_child] + households: + household: + members: [parent, adult_child] + state_code: TX + output: + # A permanently and totally disabled child meets the age test at any age + # (26 USC 152(c)(3)(B)), so the parent files as head of household. + is_tax_unit_spouse: [false, false] + is_tax_unit_dependent: [false, true] + filing_status: HEAD_OF_HOUSEHOLD diff --git a/policyengine_us/variables/household/demographic/tax_unit/is_tax_unit_head.py b/policyengine_us/variables/household/demographic/tax_unit/is_tax_unit_head.py index 271a5aff46d..098e2d7cdea 100644 --- a/policyengine_us/variables/household/demographic/tax_unit/is_tax_unit_head.py +++ b/policyengine_us/variables/household/demographic/tax_unit/is_tax_unit_head.py @@ -28,15 +28,18 @@ def formula(person, period, parameters): # couple heads it, even if another adult is older. When a situation # omits marital units, Core puts everyone in one unit, so this changes # nothing there: a two-person unit gives the same head. + # A separated member doesn't form such a couple: is_tax_unit_spouse + # gives no one the spouse role in a unit with a separated member. # NOTE: the older adult then becomes the couple's dependent whatever # their income. If that adult files instead, claiming a couple who # don't file jointly, supply is_tax_unit_head or tax_unit_role_input. marital_unit = person.marital_unit tax_unit_id = tax_unit.reference_entity.members_entity_id + couple_eligible = eligible & ~person("is_separated", period) couple_candidate = ( - eligible + couple_eligible & (marital_unit.nb_persons() == 2) - & (marital_unit.sum(eligible) == 2) + & (marital_unit.sum(couple_eligible) == 2) & (marital_unit.min(tax_unit_id) == marital_unit.max(tax_unit_id)) ) eligible = where(tax_unit.any(couple_candidate), couple_candidate, eligible) diff --git a/policyengine_us/variables/household/demographic/tax_unit/is_tax_unit_spouse.py b/policyengine_us/variables/household/demographic/tax_unit/is_tax_unit_spouse.py index fe5ba991f55..a6ed2c60375 100644 --- a/policyengine_us/variables/household/demographic/tax_unit/is_tax_unit_spouse.py +++ b/policyengine_us/variables/household/demographic/tax_unit/is_tax_unit_spouse.py @@ -9,6 +9,7 @@ class is_tax_unit_spouse(Variable): entity = Person label = "Spouse of tax unit" definition_period = YEAR + reference = "https://www.law.cornell.edu/uscode/text/26/152#c_3" def formula(person, period, parameters): tax_unit = person.tax_unit @@ -21,20 +22,41 @@ def formula(person, period, parameters): candidate = head_or_spouse_candidates(person, period) head = person("is_tax_unit_head", period) eligible = candidate & ~head & ~is_separated - # An inferred spouse must also share the head's marital unit, which - # holds a married, cohabiting couple. is_tax_unit_head takes the head - # from such a couple when the tax unit has one, so the next-oldest - # adult here is the head's partner. When a situation omits marital - # units, Core puts everyone in one unit, so this only applies when - # membership is supplied. A married couple living apart in separate - # marital units, such as one spouse in a care facility, is therefore - # not inferred as spouses; supply is_tax_unit_spouse or - # tax_unit_role_input for them. Compare memberships, not the ID - # variable: situations can supply groups without numeric ID inputs. + # A marital unit holds a married, cohabiting couple, and + # is_tax_unit_head takes the head from such a couple when the tax + # unit has one, so the head's partner in their marital unit is the + # spouse. A head without one takes the next-oldest adult, except an + # adult who meets the qualifying child age test against the head: + # under 19, or under 24 if a full-time student, and younger than + # the head, or permanently and totally disabled (26 USC 152(c)(3)). + # That adult can be the head's dependent, so an adult child isn't + # paired with their parent, while an older adult's income stays on + # the return. When a situation omits marital units, Core puts + # everyone in one unit, so the age order applies unchanged. Compare + # memberships, not the ID variable: situations can supply groups + # without numeric ID inputs. + # NOTE: an adult outside the head's marital unit who fails the age + # test is still paired with the head, even if they could be a + # qualifying relative. A disabled spouse living apart in their own + # marital unit becomes a dependent. Supply is_tax_unit_spouse or + # tax_unit_role_input for either. marital_unit = person.marital_unit.reference_entity.members_entity_id head_marital_unit = tax_unit.max(where(head, marital_unit, -1)) - eligible = eligible & (marital_unit == head_marital_unit) + in_head_marital_unit = marital_unit == head_marital_unit age = person("age", period) + p = parameters(period).gov.irs.dependent.ineligible_age + student = person("is_full_time_student", period) + age_limit = where(student, p.student, p.non_student) + head_age = tax_unit.max(where(head, age, 0)) + meets_age_test = ((age < age_limit) & (age < head_age)) | person( + "is_permanently_and_totally_disabled", period + ) + has_partner = tax_unit.any(eligible & in_head_marital_unit) + eligible = where( + has_partner, + eligible & in_head_marital_unit, + eligible & ~meets_age_test, + ) next_oldest_adult = person.get_rank(tax_unit, -age, eligible) == 0 # An explicit is_tax_unit_head input still wins over a supplied role, # so keep the head out of the spouse role either way. diff --git a/policyengine_us/variables/household/demographic/tax_unit/tax_unit_role_input.py b/policyengine_us/variables/household/demographic/tax_unit/tax_unit_role_input.py index e28019ae6c7..7f1be2ce92c 100644 --- a/policyengine_us/variables/household/demographic/tax_unit/tax_unit_role_input.py +++ b/policyengine_us/variables/household/demographic/tax_unit/tax_unit_role_input.py @@ -15,4 +15,4 @@ class tax_unit_role_input(Variable): default_value = TaxUnitRole.UNSPECIFIED definition_period = YEAR label = "Supplied tax unit role" - documentation = "The person's role in their tax unit - head, spouse or dependent - as assigned by the tax-unit constructor that built a dataset, such as the Populace US build. When every member of a tax unit has a supplied role, is_tax_unit_head and is_tax_unit_spouse use it, so the constructor's head, its spouse pairing and its dependency claims all hold: a younger reference person can head a couple, an adult student stays a dependent, and a minor living without a parent can head a return. The roles only say who is head, spouse or dependent; filing_status is still computed from the unit's members and the filing rules. UNSPECIFIED (the default) means the role was not supplied, and the role variables fall back to age ordering: the oldest adult is the head, skipping adults input as tax unit dependents unless every adult in the unit is one, except that the older member of a couple sharing a two-person marital unit in the tax unit heads it, and the next-oldest such adult who shares the head's marital unit is the spouse unless any member of the unit is separated. When marital units are omitted, everyone shares one marital unit, so plain age order applies." + documentation = "The person's role in their tax unit - head, spouse or dependent - as assigned by the tax-unit constructor that built a dataset, such as the Populace US build. When every member of a tax unit has a supplied role, is_tax_unit_head and is_tax_unit_spouse use it, so the constructor's head, its spouse pairing and its dependency claims all hold: a younger reference person can head a couple, an adult student stays a dependent, and a minor living without a parent can head a return. The roles only say who is head, spouse or dependent; filing_status is still computed from the unit's members and the filing rules. UNSPECIFIED (the default) means the role was not supplied, and the role variables fall back to age ordering: the oldest adult is the head, skipping adults input as tax unit dependents unless every adult in the unit is one, except that the older member of a couple sharing a two-person marital unit in the tax unit heads it. The spouse is the head's partner in their marital unit; a head without one takes the next-oldest such adult who doesn't meet the qualifying child age test against the head (under 19, under 24 if a full-time student, or permanently and totally disabled). No spouse is inferred when any member of the unit is separated. When marital units are omitted, everyone shares one marital unit, so plain age order applies." From 57191b15205ab8b5ecded12044576a3049f59651 Mon Sep 17 00:00:00 2001 From: Ziming Date: Sun, 8 Nov 2026 21:34:02 -0500 Subject: [PATCH 6/6] Address second-round review of the tax-role fallback Add cases for an 18-year-old the same age as the head and for a separated adult living with two adults when marital units are omitted. Document the disabled-adult and separated-adult limitations, add the "younger than the head" test to tax_unit_role_input's documentation, restore main's parent-links test, and rewrap a long docstring line. Co-Authored-By: Claude Opus 5.5 --- docs/usage/microsimulation.md | 13 ++++++-- .../tests/core/test_parent_links.py | 8 +---- .../tests/microsimulation/test_microsim.py | 5 ++-- .../tax_unit/is_tax_unit_head.yaml | 29 ++++++++++++++++++ .../tax_unit/is_tax_unit_spouse.yaml | 30 +++++++++++++++++++ .../tax_unit/is_tax_unit_spouse.py | 8 +++-- .../tax_unit/tax_unit_role_input.py | 2 +- 7 files changed, 79 insertions(+), 16 deletions(-) diff --git a/docs/usage/microsimulation.md b/docs/usage/microsimulation.md index 63f798a89ff..f700f76203d 100644 --- a/docs/usage/microsimulation.md +++ b/docs/usage/microsimulation.md @@ -166,7 +166,7 @@ and a separated member doesn't count toward a couple. When a situation omits marital units, Core puts everyone in one marital unit, so plain age order applies. A minor living without a parent is left with no head. -This rule has three known limitations. Supply `is_tax_unit_head`, +This rule has four known limitations. Supply `is_tax_unit_head`, `is_tax_unit_spouse` or `tax_unit_role_input` for these households: - When a tax unit holds a married couple and an older adult outside their @@ -179,8 +179,15 @@ This rule has three known limitations. Supply `is_tax_unit_head`, files jointly. Under the law each would file their own return, or the adult could be a qualifying relative, but the pairing keeps the adult's income on the one return the model computes for the unit. -- A disabled spouse living apart in their own marital unit, such as in a care - facility, becomes the head's dependent. +- A permanently and totally disabled adult outside the head's marital unit + becomes the head's dependent, whatever their income. Examples are a spouse + living apart in a care facility, or a self-supporting sibling, whose income + then leaves the return. +- When a separated adult outside a married couple's marital unit shares their + tax unit, the couple heads the unit but no spouse is inferred, since a unit + with a separated member infers none. The other member of the couple becomes + a dependent. Under the law a spouse can't be the other spouse's dependent, + and the couple could file jointly. The Populace build carries its constructor's filing status in a `filing_status_input` column. No variable has that name, so the column is diff --git a/policyengine_us/tests/core/test_parent_links.py b/policyengine_us/tests/core/test_parent_links.py index 475fb11d2cf..29756485672 100644 --- a/policyengine_us/tests/core/test_parent_links.py +++ b/policyengine_us/tests/core/test_parent_links.py @@ -318,13 +318,7 @@ def test_duplicate_ids_resolve_the_same_way_in_every_member_order(): def run(tax_unit_order, home_order, shared_home): people = { - # The mother files; with her own marital unit she cannot be the - # grandparent's inferred spouse. - "mother": { - "person_id": {"2026": 1}, - "age": {"2026": 35}, - "is_tax_unit_head": {"2026": True}, - }, + "mother": {"person_id": {"2026": 1}, "age": {"2026": 35}}, "grandparent": {"person_id": {"2026": 1}, "age": {"2026": 60}}, "child": { "person_id": {"2026": 2}, diff --git a/policyengine_us/tests/microsimulation/test_microsim.py b/policyengine_us/tests/microsimulation/test_microsim.py index 6bfc7e0457a..e171f5d73fc 100644 --- a/policyengine_us/tests/microsimulation/test_microsim.py +++ b/policyengine_us/tests/microsimulation/test_microsim.py @@ -248,8 +248,9 @@ def test_default_dataset_uses_its_supplied_tax_unit_roles(): tax unit, the older of that couple heads instead. An adult outside the head's marital unit who meets the qualifying child age test is not the inferred spouse, so supplied marital units no longer pair an adult student - with a parent; when marital units are omitted, plain age order applies. The fallback still heads couples by the older partner - rather than the reference person and leaves lone minors with no head. + with a parent; when marital units are omitted, plain age order applies. + The fallback still heads couples by the older partner rather than the + reference person and leaves lone minors with no head. This checks the whole population, person by person, in the data year and in an extended year, since the loader carries the column forward rather than the formulas. Filing status is not checked against the build: the diff --git a/policyengine_us/tests/policy/baseline/household/demographic/tax_unit/is_tax_unit_head.yaml b/policyengine_us/tests/policy/baseline/household/demographic/tax_unit/is_tax_unit_head.yaml index d9adee9979d..50ff322a476 100644 --- a/policyengine_us/tests/policy/baseline/household/demographic/tax_unit/is_tax_unit_head.yaml +++ b/policyengine_us/tests/policy/baseline/household/demographic/tax_unit/is_tax_unit_head.yaml @@ -224,3 +224,32 @@ is_tax_unit_head: [true, false, false] is_tax_unit_spouse: [false, false, false] filing_status: SINGLE + +- name: With marital units omitted, two adults living with a separated older adult do not head the unit as a couple. + period: 2025 + input: + people: + parent: + age: 78 + is_separated: true + social_security_retirement: 15_000 + adult_1: + age: 50 + employment_income_before_lsr: 80_000 + adult_2: + age: 48 + employment_income_before_lsr: 40_000 + tax_units: + tax_unit: + members: [parent, adult_1, adult_2] + households: + household: + members: [parent, adult_1, adult_2] + state_code: TX + output: + # With marital units omitted, Core puts all three adults in one marital + # unit. It isn't a two-person unit, so the two non-separated adults aren't + # a couple, and the oldest adult heads the unit. A unit with a separated + # member infers no spouse. + is_tax_unit_head: [true, false, false] + is_tax_unit_spouse: [false, false, false] diff --git a/policyengine_us/tests/policy/baseline/household/demographic/tax_unit/is_tax_unit_spouse.yaml b/policyengine_us/tests/policy/baseline/household/demographic/tax_unit/is_tax_unit_spouse.yaml index 5f4e34fa8a4..ceef0f946c5 100644 --- a/policyengine_us/tests/policy/baseline/household/demographic/tax_unit/is_tax_unit_spouse.yaml +++ b/policyengine_us/tests/policy/baseline/household/demographic/tax_unit/is_tax_unit_spouse.yaml @@ -225,6 +225,36 @@ # The non-student limit is under 19, so a 19-year-old fails the age test. is_tax_unit_spouse: [false, true] +- name: An 18-year-old the same age as the head stays the head's spouse. + period: 2025 + input: + people: + adult_1: + age: 18 + employment_income_before_lsr: 10_000 + adult_2: + age: 18 + employment_income_before_lsr: 8_000 + tax_units: + tax_unit: + members: [adult_1, adult_2] + marital_units: + marital_unit_1: + members: [adult_1] + marital_unit_2: + members: [adult_2] + households: + household: + members: [adult_1, adult_2] + state_code: TX + output: + # A qualifying child must be younger than the taxpayer unless permanently + # and totally disabled (26 USC 152(c)(3)(A)). adult_2 is under 19 but not + # younger than the head, so they fail the age test and are paired with the + # head. + is_tax_unit_spouse: [false, true] + filing_status: JOINT + - name: A 23-year-old full-time student in their own marital unit is a dependent. period: 2025 input: diff --git a/policyengine_us/variables/household/demographic/tax_unit/is_tax_unit_spouse.py b/policyengine_us/variables/household/demographic/tax_unit/is_tax_unit_spouse.py index a6ed2c60375..1408fa7e122 100644 --- a/policyengine_us/variables/household/demographic/tax_unit/is_tax_unit_spouse.py +++ b/policyengine_us/variables/household/demographic/tax_unit/is_tax_unit_spouse.py @@ -37,9 +37,11 @@ def formula(person, period, parameters): # without numeric ID inputs. # NOTE: an adult outside the head's marital unit who fails the age # test is still paired with the head, even if they could be a - # qualifying relative. A disabled spouse living apart in their own - # marital unit becomes a dependent. Supply is_tax_unit_spouse or - # tax_unit_role_input for either. + # qualifying relative. A permanently and totally disabled adult + # outside the head's marital unit becomes a dependent whatever their + # income. And when a separated adult shares the unit with a couple who + # heads it, the head's partner isn't inferred as the spouse. Supply + # is_tax_unit_spouse or tax_unit_role_input for these households. marital_unit = person.marital_unit.reference_entity.members_entity_id head_marital_unit = tax_unit.max(where(head, marital_unit, -1)) in_head_marital_unit = marital_unit == head_marital_unit diff --git a/policyengine_us/variables/household/demographic/tax_unit/tax_unit_role_input.py b/policyengine_us/variables/household/demographic/tax_unit/tax_unit_role_input.py index 7f1be2ce92c..613873695c7 100644 --- a/policyengine_us/variables/household/demographic/tax_unit/tax_unit_role_input.py +++ b/policyengine_us/variables/household/demographic/tax_unit/tax_unit_role_input.py @@ -15,4 +15,4 @@ class tax_unit_role_input(Variable): default_value = TaxUnitRole.UNSPECIFIED definition_period = YEAR label = "Supplied tax unit role" - documentation = "The person's role in their tax unit - head, spouse or dependent - as assigned by the tax-unit constructor that built a dataset, such as the Populace US build. When every member of a tax unit has a supplied role, is_tax_unit_head and is_tax_unit_spouse use it, so the constructor's head, its spouse pairing and its dependency claims all hold: a younger reference person can head a couple, an adult student stays a dependent, and a minor living without a parent can head a return. The roles only say who is head, spouse or dependent; filing_status is still computed from the unit's members and the filing rules. UNSPECIFIED (the default) means the role was not supplied, and the role variables fall back to age ordering: the oldest adult is the head, skipping adults input as tax unit dependents unless every adult in the unit is one, except that the older member of a couple sharing a two-person marital unit in the tax unit heads it. The spouse is the head's partner in their marital unit; a head without one takes the next-oldest such adult who doesn't meet the qualifying child age test against the head (under 19, under 24 if a full-time student, or permanently and totally disabled). No spouse is inferred when any member of the unit is separated. When marital units are omitted, everyone shares one marital unit, so plain age order applies." + documentation = "The person's role in their tax unit - head, spouse or dependent - as assigned by the tax-unit constructor that built a dataset, such as the Microcosm US build. When every member of a tax unit has a supplied role, is_tax_unit_head and is_tax_unit_spouse use it, so the constructor's head, its spouse pairing and its dependency claims all hold: a younger reference person can head a couple, an adult student stays a dependent, and a minor living without a parent can head a return. The roles only say who is head, spouse or dependent; filing_status is still computed from the unit's members and the filing rules. UNSPECIFIED (the default) means the role was not supplied, and the role variables fall back to age ordering: the oldest adult is the head, skipping adults input as tax unit dependents unless every adult in the unit is one, except that the older member of a couple sharing a two-person marital unit in the tax unit heads it. The spouse is the head's partner in their marital unit; a head without one takes the next-oldest such adult who doesn't meet the qualifying child age test against the head (under 19, or under 24 if a full-time student, and younger than the head, or permanently and totally disabled). No spouse is inferred when any member of the unit is separated. When marital units are omitted, everyone shares one marital unit, so plain age order applies."