diff --git a/changelog_entry.yaml b/changelog_entry.yaml index e69de29bb2d..a8f81acf597 100644 --- a/changelog_entry.yaml +++ b/changelog_entry.yaml @@ -0,0 +1,5 @@ +- bump: patch + changes: + added: + - Add Census SPM weekly work-expense parameters and formulas for work and childcare expense caps. + date: 2026-04-09 00:00:00 diff --git a/policyengine_us/parameters/gov/census/index.yaml b/policyengine_us/parameters/gov/census/index.yaml new file mode 100644 index 00000000000..2852e302ad0 --- /dev/null +++ b/policyengine_us/parameters/gov/census/index.yaml @@ -0,0 +1,4 @@ +metadata: + propagate_metadata_to_children: true + economy: false + household: false diff --git a/policyengine_us/parameters/gov/census/spm/index.yaml b/policyengine_us/parameters/gov/census/spm/index.yaml new file mode 100644 index 00000000000..2852e302ad0 --- /dev/null +++ b/policyengine_us/parameters/gov/census/spm/index.yaml @@ -0,0 +1,4 @@ +metadata: + propagate_metadata_to_children: true + economy: false + household: false diff --git a/policyengine_us/parameters/gov/census/spm/work_expense/index.yaml b/policyengine_us/parameters/gov/census/spm/work_expense/index.yaml new file mode 100644 index 00000000000..2852e302ad0 --- /dev/null +++ b/policyengine_us/parameters/gov/census/spm/work_expense/index.yaml @@ -0,0 +1,4 @@ +metadata: + propagate_metadata_to_children: true + economy: false + household: false diff --git a/policyengine_us/parameters/gov/census/spm/work_expense/weekly_amount.yaml b/policyengine_us/parameters/gov/census/spm/work_expense/weekly_amount.yaml new file mode 100644 index 00000000000..e54dbe89dc1 --- /dev/null +++ b/policyengine_us/parameters/gov/census/spm/work_expense/weekly_amount.yaml @@ -0,0 +1,31 @@ +description: Weekly work-expense deduction amount used in Census Supplemental Poverty Measure calculations. +values: + 2009-01-01: 28.0500 + 2010-01-01: 25.5000 + 2011-01-01: 27.1575 + 2012-01-01: 33.0225 + 2013-01-01: 39.3975 + 2014-01-01: 39.2530 + 2015-01-01: 40.0945 + 2016-01-01: 38.4710 + 2017-01-01: 36.3460 + 2018-01-01: 37.1025 + 2019-01-01: 39.7205 + 2020-01-01: 39.6100 + 2021-01-01: 38.0630 + 2022-01-01: 31.0080 + 2023-01-01: 33.4050 + 2024-01-01: 34.9945 + +metadata: + unit: currency-USD + period: year + uprating: gov.bls.cpi.cpi_u + label: Census SPM weekly work-expense deduction amount + reference: + - title: Supplemental Poverty Measure (SPM) Technical Documentation + href: https://www2.census.gov/programs-surveys/supplemental-poverty-measure/technical-documentation/spm_techdoc.pdf + - title: Poverty in the United States 2023 + href: https://www2.census.gov/library/publications/2024/demo/p60-283.pdf + - title: Poverty in the United States 2024 + href: https://www2.census.gov/library/publications/2025/demo/p60-287.pdf diff --git a/policyengine_us/tests/policy/baseline/household/expense/childcare/spm_unit_capped_work_childcare_expenses.yaml b/policyengine_us/tests/policy/baseline/household/expense/childcare/spm_unit_capped_work_childcare_expenses.yaml new file mode 100644 index 00000000000..d9bbafe41ea --- /dev/null +++ b/policyengine_us/tests/policy/baseline/household/expense/childcare/spm_unit_capped_work_childcare_expenses.yaml @@ -0,0 +1,107 @@ +- name: Combined work and childcare expenses are capped at the lower earner + period: 2024 + absolute_error_margin: 0.001 + input: + people: + head: + age: 35 + weeks_worked: 10 + employment_income: 40_000 + is_household_head: true + is_tax_unit_head: true + spouse: + age: 33 + weeks_worked: 20 + employment_income: 2_000 + is_tax_unit_spouse: true + child: + age: 8 + tax_units: + tax_unit: + members: [head, spouse, child] + spm_units: + spm_unit: + members: [head, spouse, child] + spm_unit_pre_subsidy_childcare_expenses: 2_000 + output: + spm_unit_capped_work_childcare_expenses: 2_000 + +- name: Work expenses are preserved when they already exceed the earnings cap + period: 2024 + absolute_error_margin: 0.001 + input: + people: + head: + age: 41 + weeks_worked: 52 + employment_income: 1_500 + is_household_head: true + is_tax_unit_head: true + child: + age: 5 + tax_units: + tax_unit: + members: [head, child] + spm_units: + spm_unit: + members: [head, child] + spm_unit_pre_subsidy_childcare_expenses: 3_000 + output: + spm_unit_capped_work_childcare_expenses: 1_819.714 + +- name: Childcare is capped by the remaining lower-earner earnings after work expenses + period: 2024 + absolute_error_margin: 0.001 + input: + people: + head: + age: 46 + weeks_worked: 52 + employment_income: 9_098.57 + is_household_head: true + is_tax_unit_head: true + spouse: + age: 46 + weeks_worked: 52 + employment_income: 30_000 + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + spm_units: + spm_unit: + members: [head, spouse] + spm_unit_pre_subsidy_childcare_expenses: 21_580 + output: + spm_unit_capped_work_childcare_expenses: 9_098.57 + +- name: Unmarried partner counts toward the reference person's remaining childcare cap + period: 2024 + absolute_error_margin: 0.001 + input: + people: + head: + age: 35 + weeks_worked: 10 + employment_income: 40_000 + is_household_head: true + is_tax_unit_head: true + partner: + age: 33 + weeks_worked: 20 + employment_income: 2_000 + is_unmarried_partner_of_household_head: true + is_tax_unit_head: true + child: + age: 8 + tax_units: + reference_unit: + members: [head, child] + partner_unit: + members: [partner] + spm_units: + spm_unit: + members: [head, partner, child] + spm_unit_pre_subsidy_childcare_expenses: 2_000 + output: + spm_unit_capped_work_childcare_expenses: 2_000 diff --git a/policyengine_us/tests/policy/baseline/household/expense/childcare/spm_unit_head_spouse_earned_cap.yaml b/policyengine_us/tests/policy/baseline/household/expense/childcare/spm_unit_head_spouse_earned_cap.yaml new file mode 100644 index 00000000000..966a3e49b63 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/household/expense/childcare/spm_unit_head_spouse_earned_cap.yaml @@ -0,0 +1,163 @@ +- name: Household head and spouse determine the SPM childcare earnings cap + period: 2024 + absolute_error_margin: 0.001 + input: + people: + reference_person: + age: 35 + employment_income: 40_000 + is_household_head: true + is_tax_unit_spouse: true + spouse: + age: 45 + employment_income: 8_000 + is_tax_unit_head: true + child: + age: 8 + tax_units: + tax_unit: + members: [reference_person, spouse, child] + spm_units: + spm_unit: + members: [reference_person, spouse, child] + output: + spm_unit_head_spouse_earned_cap: 8_000 + +- name: Household head and unmarried partner determine the SPM childcare earnings cap + period: 2024 + absolute_error_margin: 0.001 + input: + people: + reference_person: + age: 35 + employment_income: 40_000 + is_household_head: true + is_tax_unit_head: true + partner: + age: 33 + employment_income: 2_500 + is_unmarried_partner_of_household_head: true + is_tax_unit_head: true + child: + age: 8 + tax_units: + reference_unit: + members: [reference_person, child] + partner_unit: + members: [partner] + spm_units: + spm_unit: + members: [reference_person, partner, child] + output: + spm_unit_head_spouse_earned_cap: 2_500 + +- name: Farm income counts toward the Census SPM childcare earnings cap + period: 2024 + absolute_error_margin: 0.001 + input: + people: + reference_person: + age: 35 + employment_income: 40_000 + is_household_head: true + is_tax_unit_head: true + spouse: + age: 33 + farm_income: 3_000 + is_tax_unit_spouse: true + child: + age: 8 + tax_units: + tax_unit: + members: [reference_person, spouse, child] + spm_units: + spm_unit: + members: [reference_person, spouse, child] + output: + spm_unit_head_spouse_earned_cap: 3_000 + +- name: Extra adults in the SPM unit do not expand the reference person's childcare cap + period: 2024 + absolute_error_margin: 0.001 + input: + people: + reference_person: + age: 35 + employment_income: 40_000 + is_household_head: true + is_tax_unit_head: true + partner: + age: 33 + employment_income: 5_000 + is_unmarried_partner_of_household_head: true + is_tax_unit_head: true + unrelated_adult: + age: 55 + employment_income: 1_000 + is_tax_unit_head: true + child: + age: 8 + tax_units: + reference_unit: + members: [reference_person, child] + partner_unit: + members: [partner] + unrelated_unit: + members: [unrelated_adult] + spm_units: + spm_unit: + members: [reference_person, partner, unrelated_adult, child] + output: + spm_unit_head_spouse_earned_cap: 5_000 + +- name: Spouse takes precedence over unmarried partner in the reference person's childcare cap + period: 2024 + absolute_error_margin: 0.001 + input: + people: + reference_person: + age: 35 + employment_income: 40_000 + is_household_head: true + is_tax_unit_head: true + spouse: + age: 33 + employment_income: 6_000 + is_tax_unit_spouse: true + partner: + age: 31 + employment_income: 2_500 + is_unmarried_partner_of_household_head: true + is_tax_unit_head: true + tax_units: + reference_unit: + members: [reference_person, spouse] + partner_unit: + members: [partner] + spm_units: + spm_unit: + members: [reference_person, spouse, partner] + output: + spm_unit_head_spouse_earned_cap: 6_000 + +- name: The childcare cap falls back to tax-unit head/spouse when no reference person is provided + period: 2024 + absolute_error_margin: 0.001 + input: + people: + head: + age: 35 + employment_income: 40_000 + is_tax_unit_head: true + spouse: + age: 33 + employment_income: 4_000 + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + spm_units: + spm_unit: + members: [head, spouse] + output: + spm_unit_head_spouse_earned_cap: 4_000 diff --git a/policyengine_us/tests/policy/baseline/household/expense/childcare/spm_unit_work_expenses.yaml b/policyengine_us/tests/policy/baseline/household/expense/childcare/spm_unit_work_expenses.yaml new file mode 100644 index 00000000000..d3c7c74d672 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/household/expense/childcare/spm_unit_work_expenses.yaml @@ -0,0 +1,41 @@ +- name: Work expenses apply only to adult earners in the SPM unit + period: 2024 + absolute_error_margin: 0.001 + input: + people: + adult_worker: + age: 40 + weeks_worked: 10 + employment_income: 20_000 + adult_nonworker: + age: 38 + weeks_worked: 30 + employment_income: 0 + child: + age: 12 + weeks_worked: 52 + employment_income: 500 + spm_units: + spm_unit: + members: [adult_worker, adult_nonworker, child] + output: + spm_unit_work_expenses: 349.945 + +- name: Farm-income-only adults still incur Census SPM work expenses + period: 2024 + absolute_error_margin: 0.001 + input: + people: + adult_farmer: + age: 40 + weeks_worked: 10 + farm_income: 20_000 + adult_nonworker: + age: 38 + weeks_worked: 30 + farm_income: 0 + spm_units: + spm_unit: + members: [adult_farmer, adult_nonworker] + output: + spm_unit_work_expenses: 349.945 diff --git a/policyengine_us/tests/policy/baseline/household/income/person/weeks_worked.yaml b/policyengine_us/tests/policy/baseline/household/income/person/weeks_worked.yaml new file mode 100644 index 00000000000..ea615f3cea6 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/household/income/person/weeks_worked.yaml @@ -0,0 +1,16 @@ +- name: Weeks worked is an input variable + period: 2024 + input: + weeks_worked: 40 + output: + weeks_worked: 40 + +- name: Weeks worked carries forward when future data is missing + period: 2025 + input: + people: + person: + weeks_worked: + 2024: 37 + output: + weeks_worked: 37 diff --git a/policyengine_us/tools/default_uprating.py b/policyengine_us/tools/default_uprating.py index a69922ba8fe..eec084bc4c3 100644 --- a/policyengine_us/tools/default_uprating.py +++ b/policyengine_us/tools/default_uprating.py @@ -60,7 +60,6 @@ "spm_unit_spm_threshold", "non_sch_d_capital_gains", "spm_unit_state_tax_reported", - "spm_unit_capped_work_childcare_expenses", "farm_income", "taxable_403b_distributions", "qualified_tuition_expenses", diff --git a/policyengine_us/variables/household/demographic/person/is_unmarried_partner_of_household_head.py b/policyengine_us/variables/household/demographic/person/is_unmarried_partner_of_household_head.py new file mode 100644 index 00000000000..e8d0f44cebc --- /dev/null +++ b/policyengine_us/variables/household/demographic/person/is_unmarried_partner_of_household_head.py @@ -0,0 +1,8 @@ +from policyengine_us.model_api import * + + +class is_unmarried_partner_of_household_head(Variable): + value_type = bool + entity = Person + label = "is unmarried partner of household head" + definition_period = YEAR diff --git a/policyengine_us/variables/household/expense/childcare/spm_unit_capped_work_childcare_expenses.py b/policyengine_us/variables/household/expense/childcare/spm_unit_capped_work_childcare_expenses.py index a1dfaf5d349..619b7f76673 100644 --- a/policyengine_us/variables/household/expense/childcare/spm_unit_capped_work_childcare_expenses.py +++ b/policyengine_us/variables/household/expense/childcare/spm_unit_capped_work_childcare_expenses.py @@ -7,4 +7,14 @@ class spm_unit_capped_work_childcare_expenses(Variable): label = "SPM unit work and childcare expenses" definition_period = YEAR unit = USD - uprating = "gov.bls.cpi.cpi_u" + + def formula_2024(spm_unit, period, parameters): + work_expenses = spm_unit("spm_unit_work_expenses", period) + childcare_expenses = spm_unit( + "spm_unit_pre_subsidy_childcare_expenses", period + ) + earned_cap = spm_unit("spm_unit_head_spouse_earned_cap", period) + remaining_childcare_cap = np.maximum(earned_cap - work_expenses, 0) + return work_expenses + min_( + np.maximum(childcare_expenses, 0), remaining_childcare_cap + ) diff --git a/policyengine_us/variables/household/expense/childcare/spm_unit_head_spouse_earned_cap.py b/policyengine_us/variables/household/expense/childcare/spm_unit_head_spouse_earned_cap.py new file mode 100644 index 00000000000..3117292a487 --- /dev/null +++ b/policyengine_us/variables/household/expense/childcare/spm_unit_head_spouse_earned_cap.py @@ -0,0 +1,42 @@ +from policyengine_us.model_api import * + + +class spm_unit_head_spouse_earned_cap(Variable): + value_type = float + entity = SPMUnit + label = "SPM unit lower-earner cap for the reference person and spouse/partner" + definition_period = YEAR + unit = USD + + def formula(spm_unit, period, parameters): + person = spm_unit.members + is_reference_person = person("is_household_head", period) + has_reference_person = spm_unit.any(is_reference_person) + is_head_or_spouse = person("is_tax_unit_head_or_spouse", period) + is_reference_person_spouse = ( + person.tax_unit.any(is_reference_person) + & is_head_or_spouse + & ~is_reference_person + ) + is_reference_person_partner = person( + "is_unmarried_partner_of_household_head", period + ) + has_spouse = spm_unit.any(is_reference_person_spouse) + eligible_reference_person_or_partner = ( + is_reference_person + | is_reference_person_spouse + | (~has_spouse & is_reference_person_partner) + ) + eligible_people = where( + has_reference_person, + eligible_reference_person_or_partner, + is_head_or_spouse, + ) + earned_income = person("spm_work_childcare_earnings", period) + eligible_earnings = eligible_people * np.maximum(earned_income, 0) + + count_head_or_spouse = spm_unit.sum(eligible_people) + total_earned = spm_unit.sum(eligible_earnings) + max_earned = spm_unit.max(eligible_earnings) + + return where(count_head_or_spouse > 1, total_earned - max_earned, total_earned) diff --git a/policyengine_us/variables/household/expense/childcare/spm_unit_work_expenses.py b/policyengine_us/variables/household/expense/childcare/spm_unit_work_expenses.py new file mode 100644 index 00000000000..6ed82434f9c --- /dev/null +++ b/policyengine_us/variables/household/expense/childcare/spm_unit_work_expenses.py @@ -0,0 +1,21 @@ +from policyengine_us.model_api import * + + +class spm_unit_work_expenses(Variable): + value_type = float + entity = SPMUnit + label = "SPM unit work expenses" + definition_period = YEAR + unit = USD + + def formula(spm_unit, period, parameters): + person = spm_unit.members + weeks_worked = person("weeks_worked", period) + is_adult = person("is_adult", period) + earned_income = person("spm_work_childcare_earnings", period) + weekly_amount = parameters(period).gov.census.spm.work_expense.weekly_amount + + eligible_weeks = is_adult * (earned_income > 0) * np.clip( + weeks_worked, 0, 52 + ) + return spm_unit.sum(eligible_weeks) * weekly_amount diff --git a/policyengine_us/variables/household/expense/childcare/spm_work_childcare_earnings.py b/policyengine_us/variables/household/expense/childcare/spm_work_childcare_earnings.py new file mode 100644 index 00000000000..382d3cb015d --- /dev/null +++ b/policyengine_us/variables/household/expense/childcare/spm_work_childcare_earnings.py @@ -0,0 +1,15 @@ +from policyengine_us.model_api import * + + +class spm_work_childcare_earnings(Variable): + value_type = float + entity = Person + label = "earnings relevant to Census SPM work and childcare expense caps" + definition_period = YEAR + unit = USD + + adds = [ + "employment_income", + "self_employment_income", + "farm_income", + ] diff --git a/policyengine_us/variables/input/weeks_worked.py b/policyengine_us/variables/input/weeks_worked.py new file mode 100644 index 00000000000..14d40b279bf --- /dev/null +++ b/policyengine_us/variables/input/weeks_worked.py @@ -0,0 +1,12 @@ +from policyengine_us.model_api import * + + +class weeks_worked(Variable): + value_type = int + entity = Person + label = "Weeks worked during the year" + definition_period = YEAR + documentation = "Number of weeks worked during the year." + + def formula_2025(person, period, parameters): + return person("weeks_worked", period.last_year)