Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
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.
39 changes: 35 additions & 4 deletions docs/usage/microsimulation.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,10 +153,41 @@ 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 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 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
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 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 Microcosm build carries its constructor's filing status in a
`filing_status_input` column. No variable has that name, so the column is
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
19 changes: 11 additions & 8 deletions policyengine_us/tests/core/test_tax_unit_role_input.py
Original file line number Diff line number Diff line change
Expand Up @@ -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],
}
Expand All @@ -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,
)

Expand Down Expand Up @@ -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 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)
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"]
21 changes: 12 additions & 9 deletions policyengine_us/tests/core/test_tax_unit_role_input_properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 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
Expand Down Expand Up @@ -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]]
Expand Down
20 changes: 12 additions & 8 deletions policyengine_us/tests/microsimulation/test_microsim.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,14 +243,18 @@ 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. 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
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -2058,7 +2060,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:
Expand Down Expand Up @@ -2096,10 +2098,12 @@
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 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.
medicaid_uses_non_filer_rules: [false, false, true]
medicaid_household_size: [2, 2, 2]
medicaid_household_income: [40_000, 40_000, 0]
Expand Down Expand Up @@ -2228,3 +2232,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]
Original file line number Diff line number Diff line change
Expand Up @@ -254,3 +254,54 @@
# 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 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:
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]
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -229,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
Loading
Loading