From e234f60478f2aab8379ac78673e552e872755cb8 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 27 Sep 2026 18:34:31 -0400 Subject: [PATCH 1/3] Add marginal_tax_rate_computed and fix cliff_evaluated ranking The marginal tax rate variables are simulated only for the top simulation.marginal_tax_rate_adults adults per household by market income; everyone else gets a placeholder zero that looks like a real one. Add a flag for exactly the simulated people, define cliff_evaluated as that flag (it ranked adults by age, while cliff_gap comes from the earnings-ranked marginal_tax_rate), and document the cap on every affected variable and the parameter. Co-Authored-By: Claude Opus 5.5 --- changelog.d/mtr-uncomputed-adults.added.md | 1 + changelog.d/mtr-uncomputed-adults.fixed.md | 1 + .../simulation/marginal_tax_rate_adults.yaml | 2 +- .../core/test_marginal_tax_rate_coverage.py | 153 ++++++++++++++++++ .../baseline/household/cliff_evaluated.yaml | 22 +++ .../baseline/household/marginal_tax_rate.yaml | 79 ++++++++- .../household/marginal_tax_rate_computed.yaml | 91 +++++++++++ .../gov/simulation/capital_gains_responses.py | 3 + policyengine_us/variables/household/cliff.py | 7 +- .../household/federal_marginal_tax_rate.py | 3 + .../household/fica_marginal_tax_rate.py | 3 + .../variables/household/marginal_tax_rate.py | 3 + .../household/marginal_tax_rate_computed.py | 27 ++++ ...inal_tax_rate_including_health_benefits.py | 3 + .../household/state_marginal_tax_rate.py | 3 + 15 files changed, 395 insertions(+), 6 deletions(-) create mode 100644 changelog.d/mtr-uncomputed-adults.added.md create mode 100644 changelog.d/mtr-uncomputed-adults.fixed.md create mode 100644 policyengine_us/tests/core/test_marginal_tax_rate_coverage.py create mode 100644 policyengine_us/tests/policy/baseline/household/marginal_tax_rate_computed.yaml create mode 100644 policyengine_us/variables/household/marginal_tax_rate_computed.py diff --git a/changelog.d/mtr-uncomputed-adults.added.md b/changelog.d/mtr-uncomputed-adults.added.md new file mode 100644 index 00000000000..8a287e66b06 --- /dev/null +++ b/changelog.d/mtr-uncomputed-adults.added.md @@ -0,0 +1 @@ +Add `marginal_tax_rate_computed`, which flags the people whose marginal tax rates are simulated (the top `simulation.marginal_tax_rate_adults` earners among each household's adults); the marginal tax rate variables are a placeholder zero for everyone else. diff --git a/changelog.d/mtr-uncomputed-adults.fixed.md b/changelog.d/mtr-uncomputed-adults.fixed.md new file mode 100644 index 00000000000..7cfb6daf93d --- /dev/null +++ b/changelog.d/mtr-uncomputed-adults.fixed.md @@ -0,0 +1 @@ +Fix `cliff_evaluated` to rank adults by market income, as the marginal tax rate loop does, instead of by age. diff --git a/policyengine_us/parameters/simulation/marginal_tax_rate_adults.yaml b/policyengine_us/parameters/simulation/marginal_tax_rate_adults.yaml index 1f1ae60a4d1..6708f03254b 100644 --- a/policyengine_us/parameters/simulation/marginal_tax_rate_adults.yaml +++ b/policyengine_us/parameters/simulation/marginal_tax_rate_adults.yaml @@ -1,4 +1,4 @@ -description: Number of adults to simulate a marginal tax rate for, in each household. +description: Number of adults in each household, ranked by market income, for whom marginal tax rates are simulated. Everyone else gets a marginal tax rate of zero; marginal_tax_rate_computed flags who is simulated. values: 0000-01-01: 2 metadata: diff --git a/policyengine_us/tests/core/test_marginal_tax_rate_coverage.py b/policyengine_us/tests/core/test_marginal_tax_rate_coverage.py new file mode 100644 index 00000000000..b0b44f5067b --- /dev/null +++ b/policyengine_us/tests/core/test_marginal_tax_rate_coverage.py @@ -0,0 +1,153 @@ +"""Invariants of which people get a simulated marginal tax rate. + +The marginal tax rate variables run one branch simulation per adult rank, from +1 to simulation.marginal_tax_rate_adults, perturbing the adult whose +adult_earnings_index (rank by market income within the household) equals that +rank. Everyone else keeps a placeholder of zero. marginal_tax_rate_computed +flags the simulated people, and cliff_evaluated must agree with it because the +cliff gap is derived from marginal_tax_rate. + +Every household in a grid of adult counts, earnings orders, ties, teen earners +and children shares one simulation per cap, and is checked against that rule, +restated here without adult_earnings_index: + +1. Each household flags min(cap, adults) people, all adults, and every flagged + adult has at least the market income of every unflagged adult. +2. cliff_evaluated equals marginal_tax_rate_computed. +3. Every marginal tax rate variable is exactly zero wherever the flag is false. +4. Raising the cap only adds people: anyone flagged under a lower cap keeps the + same flag and the same marginal tax rates under a higher one. +""" + +from itertools import product + +import numpy as np +import pytest +from policyengine_core.reforms import Reform + +from policyengine_us import Simulation + +YEAR = 2024 +CAP = "simulation.marginal_tax_rate_adults" +# One variable per code path: the net income loop, its health-benefit copy, and +# the component helper shared by the federal, state and FICA variables. +MTR_VARIABLES = ( + "marginal_tax_rate", + "marginal_tax_rate_including_health_benefits", + "federal_marginal_tax_rate", +) +ADULT_EARNINGS = ( + (0,), + (40_000,), + (80_000, 0), + (0, 80_000), + (50_000, 50_000), + (0, 0, 0), + (30_000, 90_000, 60_000), + (20_000, 0, 20_000, 70_000), + (10_000, 45_000, 0, 45_000, 120_000), +) +TEEN_EARNINGS = (None, 0, 8_000) +HAS_CHILD = (False, True) + + +def _situation(): + people, tax_units, spm_units, households = {}, {}, {}, {} + shapes = product(ADULT_EARNINGS, TEEN_EARNINGS, HAS_CHILD) + for h, (adults, teen, child) in enumerate(shapes): + members = [] + for a, earnings in enumerate(adults): + name = f"h{h}_adult{a}" + people[name] = { + "age": {YEAR: 25 + 7 * a}, + "employment_income": {YEAR: earnings}, + } + tax_units[f"{name}_tax_unit"] = {"members": [name]} + members.append(name) + dependents = [] + if teen is not None: + dependents.append(f"h{h}_teen") + people[f"h{h}_teen"] = { + "age": {YEAR: 16}, + "employment_income": {YEAR: teen}, + } + if child: + dependents.append(f"h{h}_child") + people[f"h{h}_child"] = {"age": {YEAR: 6}} + tax_units[f"h{h}_adult0_tax_unit"]["members"] += dependents + members += dependents + spm_units[f"h{h}_spm_unit"] = {"members": members} + households[f"h{h}"] = {"members": members, "state_code": {YEAR: "TX"}} + return { + "people": people, + "tax_units": tax_units, + "spm_units": spm_units, + "households": households, + } + + +def _simulation(cap): + return Simulation( + situation=_situation(), + reform=Reform.from_dict({CAP: {str(YEAR): cap}}, country_id="us"), + ) + + +def _calc(simulation, variable): + return np.asarray(simulation.calculate(variable, YEAR)) + + +@pytest.fixture(scope="module") +def households(): + simulation = _simulation(0) + return { + "household_id": simulation.populations["household"].members_entity_id, + "is_adult": _calc(simulation, "is_adult").astype(bool), + "market_income": _calc(simulation, "market_income"), + } + + +@pytest.mark.parametrize("cap", [0, 1, 2, 3, 5]) +def test_flag_selects_the_top_earning_adults(households, cap): + simulation = _simulation(cap) + flag = _calc(simulation, "marginal_tax_rate_computed").astype(bool) + assert np.array_equal(_calc(simulation, "cliff_evaluated").astype(bool), flag) + assert not flag[~households["is_adult"]].any() + for household_id in np.unique(households["household_id"]): + in_household = households["household_id"] == household_id + adults = in_household & households["is_adult"] + flagged = in_household & flag + assert flagged.sum() == min(cap, adults.sum()) + unflagged_adults = adults & ~flag + if flagged.any() and unflagged_adults.any(): + income = households["market_income"] + assert income[flagged].min() >= income[unflagged_adults].max() + + +@pytest.fixture(scope="module") +def mtr_by_cap(): + results = {} + for cap in (2, 3): + simulation = _simulation(cap) + results[cap] = { + variable: _calc(simulation, variable) + for variable in ("marginal_tax_rate_computed",) + MTR_VARIABLES + } + return results + + +@pytest.mark.parametrize("variable", MTR_VARIABLES) +def test_unsimulated_people_get_a_placeholder_zero(mtr_by_cap, variable): + for results in mtr_by_cap.values(): + flag = results["marginal_tax_rate_computed"].astype(bool) + assert (results[variable][~flag] == 0).all() + + +@pytest.mark.parametrize("variable", MTR_VARIABLES) +def test_raising_the_cap_only_adds_people(mtr_by_cap, variable): + lower, higher = mtr_by_cap[2], mtr_by_cap[3] + lower_flag = lower["marginal_tax_rate_computed"].astype(bool) + higher_flag = higher["marginal_tax_rate_computed"].astype(bool) + assert (higher_flag[lower_flag]).all() + assert higher_flag.sum() > lower_flag.sum() + assert np.array_equal(lower[variable][lower_flag], higher[variable][lower_flag]) diff --git a/policyengine_us/tests/policy/baseline/household/cliff_evaluated.yaml b/policyengine_us/tests/policy/baseline/household/cliff_evaluated.yaml index fe9a4312347..6b4bbb036aa 100644 --- a/policyengine_us/tests/policy/baseline/household/cliff_evaluated.yaml +++ b/policyengine_us/tests/policy/baseline/household/cliff_evaluated.yaml @@ -65,3 +65,25 @@ members: [person1, person2, person3] output: cliff_evaluated: [true, true, true] + +- name: cliff_evaluated follows earnings rank, not age, like marginal_tax_rate + period: 2024 + input: + people: + person1: + age: 70 + person2: + age: 40 + employment_income: 80_000 + person3: + age: 38 + employment_income: 50_000 + person4: + age: 22 + employment_income: 30_000 + households: + household: + members: [person1, person2, person3, person4] + state_code: TX + output: + cliff_evaluated: [false, true, true, false] diff --git a/policyengine_us/tests/policy/baseline/household/marginal_tax_rate.yaml b/policyengine_us/tests/policy/baseline/household/marginal_tax_rate.yaml index c6fc58a09cb..fe682084cc8 100644 --- a/policyengine_us/tests/policy/baseline/household/marginal_tax_rate.yaml +++ b/policyengine_us/tests/policy/baseline/household/marginal_tax_rate.yaml @@ -35,7 +35,7 @@ absolute_error_margin: 0.0001 period: 2021 input: - simulation.marginal_tax_rate_adults: 1 # the default value + simulation.marginal_tax_rate_adults: 1 # below the default of 2 people: person1: age: 43 @@ -88,3 +88,80 @@ state_fips: 48 # TX output: marginal_tax_rate: [0.3165, -0.0765] + +# Three single roommates in Texas in 2024, each filing alone. Taxable income is +# earnings less the $14,600 standard deduction, so the $90,000 earner is in the +# 22% bracket and the $60,000 and $30,000 earners are in the 12% bracket; each +# also pays 7.65% employee payroll tax. +- name: Third earner in a household gets a placeholder zero under the default cap + absolute_error_margin: 0.0001 + period: 2024 + input: + people: + person1: + age: 40 + employment_income: 90_000 + person2: + age: 35 + employment_income: 60_000 + person3: + age: 30 + employment_income: 30_000 + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2] + tax_unit3: + members: [person3] + spm_units: + spm_unit1: + members: [person1] + spm_unit2: + members: [person2] + spm_unit3: + members: [person3] + households: + household: + members: [person1, person2, person3] + state_code: TX + output: + marginal_tax_rate: [0.2965, 0.1965, 0] + marginal_tax_rate_computed: [true, true, false] + +- name: Raising the cap simulates the third earner's marginal tax rate + absolute_error_margin: 0.0001 + period: 2024 + input: + simulation.marginal_tax_rate_adults: 3 + people: + person1: + age: 40 + employment_income: 90_000 + person2: + age: 35 + employment_income: 60_000 + person3: + age: 30 + employment_income: 30_000 + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2] + tax_unit3: + members: [person3] + spm_units: + spm_unit1: + members: [person1] + spm_unit2: + members: [person2] + spm_unit3: + members: [person3] + households: + household: + members: [person1, person2, person3] + state_code: TX + output: + marginal_tax_rate: [0.2965, 0.1965, 0.1965] + marginal_tax_rate_computed: [true, true, true] diff --git a/policyengine_us/tests/policy/baseline/household/marginal_tax_rate_computed.yaml b/policyengine_us/tests/policy/baseline/household/marginal_tax_rate_computed.yaml new file mode 100644 index 00000000000..e0180699d2b --- /dev/null +++ b/policyengine_us/tests/policy/baseline/household/marginal_tax_rate_computed.yaml @@ -0,0 +1,91 @@ +# The marginal tax rate loop simulates adults whose adult_earnings_index (rank +# by market income within the household) is 1 through +# simulation.marginal_tax_rate_adults; everyone else gets a placeholder zero. + +- name: Default cap simulates the top two earners, not the two oldest adults + period: 2024 + input: + people: + person1: + age: 70 + person2: + age: 40 + employment_income: 80_000 + person3: + age: 38 + employment_income: 50_000 + person4: + age: 22 + employment_income: 30_000 + households: + household: + members: [person1, person2, person3, person4] + state_code: TX + output: + adult_earnings_index: [4, 1, 2, 3] + marginal_tax_rate_computed: [false, true, true, false] + +- name: Raising the cap to the number of adults simulates every adult + period: 2024 + input: + simulation.marginal_tax_rate_adults: 4 + people: + person1: + age: 70 + person2: + age: 40 + employment_income: 80_000 + person3: + age: 38 + employment_income: 50_000 + person4: + age: 22 + employment_income: 30_000 + households: + household: + members: [person1, person2, person3, person4] + state_code: TX + output: + marginal_tax_rate_computed: [true, true, true, true] + +- name: A cap of zero simulates nobody + period: 2024 + input: + simulation.marginal_tax_rate_adults: 0 + people: + person1: + age: 40 + employment_income: 80_000 + person2: + age: 38 + employment_income: 50_000 + households: + household: + members: [person1, person2] + state_code: TX + output: + marginal_tax_rate_computed: [false, false] + +- name: Children are never simulated, even with earnings and room under the cap + period: 2024 + input: + simulation.marginal_tax_rate_adults: 5 + people: + person1: + age: 40 + employment_income: 60_000 + person2: + age: 39 + employment_income: 20_000 + person3: + age: 16 + employment_income: 8_000 + person4: + age: 10 + households: + household: + members: [person1, person2, person3, person4] + state_code: TX + output: + adult_earnings_index: [1, 2, 0, 0] + marginal_tax_rate_computed: [true, true, false, false] diff --git a/policyengine_us/variables/gov/simulation/capital_gains_responses.py b/policyengine_us/variables/gov/simulation/capital_gains_responses.py index d39a372f76a..cf19dfcb2e7 100644 --- a/policyengine_us/variables/gov/simulation/capital_gains_responses.py +++ b/policyengine_us/variables/gov/simulation/capital_gains_responses.py @@ -86,6 +86,9 @@ class marginal_tax_rate_on_capital_gains(Variable): label = "capital gains marginal tax rate" documentation = ( "Percent of marginal capital gains that do not increase household net income." + " Simulated only for the two adults in each household with the largest" + " long-term capital gains (adult_index_cg 1 and 2), regardless of" + " simulation.marginal_tax_rate_adults; zero for everyone else." ) entity = Person definition_period = YEAR diff --git a/policyengine_us/variables/household/cliff.py b/policyengine_us/variables/household/cliff.py index 28f24445a93..984ff7f5ca7 100644 --- a/policyengine_us/variables/household/cliff.py +++ b/policyengine_us/variables/household/cliff.py @@ -9,10 +9,9 @@ class cliff_evaluated(Variable): definition_period = YEAR def formula(person, period, parameters): - adult_index_values = person("adult_index", period) - mtr_adult_count = parameters(period).simulation.marginal_tax_rate_adults - is_adult = person("is_adult", period) - return is_adult & (adult_index_values <= mtr_adult_count) + # The cliff gap comes from marginal_tax_rate, so it is simulated + # exactly where the marginal tax rate is. + return person("marginal_tax_rate_computed", period) class cliff_gap(Variable): diff --git a/policyengine_us/variables/household/federal_marginal_tax_rate.py b/policyengine_us/variables/household/federal_marginal_tax_rate.py index 5d6da27865a..b513b71f35c 100644 --- a/policyengine_us/variables/household/federal_marginal_tax_rate.py +++ b/policyengine_us/variables/household/federal_marginal_tax_rate.py @@ -8,6 +8,9 @@ class federal_marginal_tax_rate(Variable): label = "federal marginal tax rate" documentation = ( "Marginal change in federal income tax per dollar of additional earnings." + " Simulated only where marginal_tax_rate_computed is true, that is," + " for the top simulation.marginal_tax_rate_adults earners among the" + " adults in each household; zero for everyone else." ) entity = Person definition_period = YEAR diff --git a/policyengine_us/variables/household/fica_marginal_tax_rate.py b/policyengine_us/variables/household/fica_marginal_tax_rate.py index d434d34cdd3..8d8bb907c4a 100644 --- a/policyengine_us/variables/household/fica_marginal_tax_rate.py +++ b/policyengine_us/variables/household/fica_marginal_tax_rate.py @@ -8,6 +8,9 @@ class fica_marginal_tax_rate(Variable): label = "FICA marginal tax rate" documentation = ( "Marginal change in employee payroll tax per dollar of additional earnings." + " Simulated only where marginal_tax_rate_computed is true, that is," + " for the top simulation.marginal_tax_rate_adults earners among the" + " adults in each household; zero for everyone else." ) entity = Person definition_period = YEAR diff --git a/policyengine_us/variables/household/marginal_tax_rate.py b/policyengine_us/variables/household/marginal_tax_rate.py index a0abd052d58..ec251d9df8b 100644 --- a/policyengine_us/variables/household/marginal_tax_rate.py +++ b/policyengine_us/variables/household/marginal_tax_rate.py @@ -5,6 +5,9 @@ class marginal_tax_rate(Variable): label = "marginal tax rate" documentation = ( "Fraction of marginal income gains that do not increase household net income." + " Simulated only where marginal_tax_rate_computed is true, that is," + " for the top simulation.marginal_tax_rate_adults earners among the" + " adults in each household; zero for everyone else." ) entity = Person definition_period = YEAR diff --git a/policyengine_us/variables/household/marginal_tax_rate_computed.py b/policyengine_us/variables/household/marginal_tax_rate_computed.py new file mode 100644 index 00000000000..fe57f78def7 --- /dev/null +++ b/policyengine_us/variables/household/marginal_tax_rate_computed.py @@ -0,0 +1,27 @@ +from policyengine_us.model_api import * + + +class marginal_tax_rate_computed(Variable): + value_type = bool + entity = Person + label = "marginal tax rate computed" + documentation = ( + "Whether the marginal tax rate variables are simulated for this " + "person. They are simulated only for adults whose " + "adult_earnings_index (rank by market income within the household) " + "is at most simulation.marginal_tax_rate_adults. Everyone else, " + "including children with earnings, gets a marginal tax rate of zero " + "that is a placeholder, not a simulated result. Filter on this flag " + "before averaging or taking quantiles of marginal tax rates across " + "people." + ) + definition_period = YEAR + + def formula(person, period, parameters): + adult_count = parameters(period).simulation.marginal_tax_rate_adults + adult_earnings_index = person("adult_earnings_index", period) + return ( + person("is_adult", period) + & (adult_earnings_index >= 1) + & (adult_earnings_index <= adult_count) + ) diff --git a/policyengine_us/variables/household/marginal_tax_rate_including_health_benefits.py b/policyengine_us/variables/household/marginal_tax_rate_including_health_benefits.py index 02fe206ca49..5f7c68b875a 100644 --- a/policyengine_us/variables/household/marginal_tax_rate_including_health_benefits.py +++ b/policyengine_us/variables/household/marginal_tax_rate_including_health_benefits.py @@ -5,6 +5,9 @@ class marginal_tax_rate_including_health_benefits(Variable): label = "Marginal tax rate including health benefits" documentation = ( "Fraction of marginal income gains that do not increase household net income." + " Simulated only where marginal_tax_rate_computed is true, that is," + " for the top simulation.marginal_tax_rate_adults earners among the" + " adults in each household; zero for everyone else." ) entity = Person definition_period = YEAR diff --git a/policyengine_us/variables/household/state_marginal_tax_rate.py b/policyengine_us/variables/household/state_marginal_tax_rate.py index 68071d3516f..60aae215a02 100644 --- a/policyengine_us/variables/household/state_marginal_tax_rate.py +++ b/policyengine_us/variables/household/state_marginal_tax_rate.py @@ -8,6 +8,9 @@ class state_marginal_tax_rate(Variable): label = "state marginal tax rate" documentation = ( "Marginal change in state income tax per dollar of additional earnings." + " Simulated only where marginal_tax_rate_computed is true, that is," + " for the top simulation.marginal_tax_rate_adults earners among the" + " adults in each household; zero for everyone else." ) entity = Person definition_period = YEAR From dd8db7d2d20c9543fc57bdfc386f38448b630566 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 28 Sep 2026 15:54:32 -0400 Subject: [PATCH 2/3] Tighten MTR coverage docs and tests per review Describe the ranking as market income rather than earnings, name the five variables the flag covers and exclude the separately ranked capital gains MTR, add federal and FICA positive oracles for the component helper, add negative market income households to the property grid, and scope the cap-monotonicity invariant to simulations without behavioral responses. Co-Authored-By: Claude Opus 5.5 --- .../simulation/marginal_tax_rate_adults.yaml | 2 +- .../core/test_marginal_tax_rate_coverage.py | 20 +++++++++++-------- .../baseline/household/marginal_tax_rate.yaml | 7 ++++++- .../household/federal_marginal_tax_rate.py | 6 +++--- .../household/fica_marginal_tax_rate.py | 6 +++--- .../variables/household/marginal_tax_rate.py | 6 +++--- .../household/marginal_tax_rate_computed.py | 18 +++++++++-------- ...inal_tax_rate_including_health_benefits.py | 6 +++--- .../household/state_marginal_tax_rate.py | 6 +++--- 9 files changed, 44 insertions(+), 33 deletions(-) diff --git a/policyengine_us/parameters/simulation/marginal_tax_rate_adults.yaml b/policyengine_us/parameters/simulation/marginal_tax_rate_adults.yaml index 6708f03254b..2d4bb345bc4 100644 --- a/policyengine_us/parameters/simulation/marginal_tax_rate_adults.yaml +++ b/policyengine_us/parameters/simulation/marginal_tax_rate_adults.yaml @@ -1,4 +1,4 @@ -description: Number of adults in each household, ranked by market income, for whom marginal tax rates are simulated. Everyone else gets a marginal tax rate of zero; marginal_tax_rate_computed flags who is simulated. +description: Number of adults in each household, ranked by market income, for whom the earnings marginal tax rates are simulated. Everyone else gets a marginal tax rate of zero; marginal_tax_rate_computed flags who is simulated. values: 0000-01-01: 2 metadata: diff --git a/policyengine_us/tests/core/test_marginal_tax_rate_coverage.py b/policyengine_us/tests/core/test_marginal_tax_rate_coverage.py index b0b44f5067b..157fe1383cc 100644 --- a/policyengine_us/tests/core/test_marginal_tax_rate_coverage.py +++ b/policyengine_us/tests/core/test_marginal_tax_rate_coverage.py @@ -7,16 +7,18 @@ flags the simulated people, and cliff_evaluated must agree with it because the cliff gap is derived from marginal_tax_rate. -Every household in a grid of adult counts, earnings orders, ties, teen earners -and children shares one simulation per cap, and is checked against that rule, -restated here without adult_earnings_index: +Every household in a grid of adult counts, earnings orders, ties, negative +market income, teen earners and children shares one simulation per cap, and is +checked against that rule, restated here without adult_earnings_index: 1. Each household flags min(cap, adults) people, all adults, and every flagged adult has at least the market income of every unflagged adult. 2. cliff_evaluated equals marginal_tax_rate_computed. 3. Every marginal tax rate variable is exactly zero wherever the flag is false. 4. Raising the cap only adds people: anyone flagged under a lower cap keeps the - same flag and the same marginal tax rates under a higher one. + same flag and the same marginal tax rates under a higher one. This holds with + behavioral responses off, as here; with labor supply responses on, the cap + changes measured marginal tax rates and so earnings and ranks. """ from itertools import product @@ -36,6 +38,8 @@ "marginal_tax_rate_including_health_benefits", "federal_marginal_tax_rate", ) +# Employment income per adult; a negative entry is a self-employment loss, which +# makes that adult's market income negative. ADULT_EARNINGS = ( (0,), (40_000,), @@ -46,6 +50,8 @@ (30_000, 90_000, 60_000), (20_000, 0, 20_000, 70_000), (10_000, 45_000, 0, 45_000, 120_000), + (-20_000, 30_000), + (10_000, -5_000, 0, 60_000), ) TEEN_EARNINGS = (None, 0, 8_000) HAS_CHILD = (False, True) @@ -58,10 +64,8 @@ def _situation(): members = [] for a, earnings in enumerate(adults): name = f"h{h}_adult{a}" - people[name] = { - "age": {YEAR: 25 + 7 * a}, - "employment_income": {YEAR: earnings}, - } + income = "employment_income" if earnings >= 0 else "self_employment_income" + people[name] = {"age": {YEAR: 25 + 7 * a}, income: {YEAR: earnings}} tax_units[f"{name}_tax_unit"] = {"members": [name]} members.append(name) dependents = [] diff --git a/policyengine_us/tests/policy/baseline/household/marginal_tax_rate.yaml b/policyengine_us/tests/policy/baseline/household/marginal_tax_rate.yaml index fe682084cc8..cdb1c953ff3 100644 --- a/policyengine_us/tests/policy/baseline/household/marginal_tax_rate.yaml +++ b/policyengine_us/tests/policy/baseline/household/marginal_tax_rate.yaml @@ -92,7 +92,8 @@ # Three single roommates in Texas in 2024, each filing alone. Taxable income is # earnings less the $14,600 standard deduction, so the $90,000 earner is in the # 22% bracket and the $60,000 and $30,000 earners are in the 12% bracket; each -# also pays 7.65% employee payroll tax. +# also pays 7.65% employee payroll tax. The federal and FICA components check the +# shared component helper, which has its own copy of the adult loop. - name: Third earner in a household gets a placeholder zero under the default cap absolute_error_margin: 0.0001 period: 2024 @@ -128,6 +129,8 @@ output: marginal_tax_rate: [0.2965, 0.1965, 0] marginal_tax_rate_computed: [true, true, false] + federal_marginal_tax_rate: [0.22, 0.12, 0] + fica_marginal_tax_rate: [0.0765, 0.0765, 0] - name: Raising the cap simulates the third earner's marginal tax rate absolute_error_margin: 0.0001 @@ -165,3 +168,5 @@ output: marginal_tax_rate: [0.2965, 0.1965, 0.1965] marginal_tax_rate_computed: [true, true, true] + federal_marginal_tax_rate: [0.22, 0.12, 0.12] + fica_marginal_tax_rate: [0.0765, 0.0765, 0.0765] diff --git a/policyengine_us/variables/household/federal_marginal_tax_rate.py b/policyengine_us/variables/household/federal_marginal_tax_rate.py index b513b71f35c..34094a87d78 100644 --- a/policyengine_us/variables/household/federal_marginal_tax_rate.py +++ b/policyengine_us/variables/household/federal_marginal_tax_rate.py @@ -8,9 +8,9 @@ class federal_marginal_tax_rate(Variable): label = "federal marginal tax rate" documentation = ( "Marginal change in federal income tax per dollar of additional earnings." - " Simulated only where marginal_tax_rate_computed is true, that is," - " for the top simulation.marginal_tax_rate_adults earners among the" - " adults in each household; zero for everyone else." + " Simulated only where marginal_tax_rate_computed is true: for up to" + " simulation.marginal_tax_rate_adults adults per household, those with" + " the highest market income. Zero for everyone else." ) entity = Person definition_period = YEAR diff --git a/policyengine_us/variables/household/fica_marginal_tax_rate.py b/policyengine_us/variables/household/fica_marginal_tax_rate.py index 8d8bb907c4a..2dfa8b4655c 100644 --- a/policyengine_us/variables/household/fica_marginal_tax_rate.py +++ b/policyengine_us/variables/household/fica_marginal_tax_rate.py @@ -8,9 +8,9 @@ class fica_marginal_tax_rate(Variable): label = "FICA marginal tax rate" documentation = ( "Marginal change in employee payroll tax per dollar of additional earnings." - " Simulated only where marginal_tax_rate_computed is true, that is," - " for the top simulation.marginal_tax_rate_adults earners among the" - " adults in each household; zero for everyone else." + " Simulated only where marginal_tax_rate_computed is true: for up to" + " simulation.marginal_tax_rate_adults adults per household, those with" + " the highest market income. Zero for everyone else." ) entity = Person definition_period = YEAR diff --git a/policyengine_us/variables/household/marginal_tax_rate.py b/policyengine_us/variables/household/marginal_tax_rate.py index ec251d9df8b..879241a29ab 100644 --- a/policyengine_us/variables/household/marginal_tax_rate.py +++ b/policyengine_us/variables/household/marginal_tax_rate.py @@ -5,9 +5,9 @@ class marginal_tax_rate(Variable): label = "marginal tax rate" documentation = ( "Fraction of marginal income gains that do not increase household net income." - " Simulated only where marginal_tax_rate_computed is true, that is," - " for the top simulation.marginal_tax_rate_adults earners among the" - " adults in each household; zero for everyone else." + " Simulated only where marginal_tax_rate_computed is true: for up to" + " simulation.marginal_tax_rate_adults adults per household, those with" + " the highest market income. Zero for everyone else." ) entity = Person definition_period = YEAR diff --git a/policyengine_us/variables/household/marginal_tax_rate_computed.py b/policyengine_us/variables/household/marginal_tax_rate_computed.py index fe57f78def7..917a754fc2e 100644 --- a/policyengine_us/variables/household/marginal_tax_rate_computed.py +++ b/policyengine_us/variables/household/marginal_tax_rate_computed.py @@ -6,14 +6,16 @@ class marginal_tax_rate_computed(Variable): entity = Person label = "marginal tax rate computed" documentation = ( - "Whether the marginal tax rate variables are simulated for this " - "person. They are simulated only for adults whose " - "adult_earnings_index (rank by market income within the household) " - "is at most simulation.marginal_tax_rate_adults. Everyone else, " - "including children with earnings, gets a marginal tax rate of zero " - "that is a placeholder, not a simulated result. Filter on this flag " - "before averaging or taking quantiles of marginal tax rates across " - "people." + "Whether marginal_tax_rate, marginal_tax_rate_including_health_benefits, " + "federal_marginal_tax_rate, state_marginal_tax_rate and " + "fica_marginal_tax_rate are simulated for this person. They are " + "simulated only for adults whose adult_earnings_index (rank by market " + "income within the household) is at most " + "simulation.marginal_tax_rate_adults. Everyone else, including " + "children with earnings, gets a zero that is a placeholder, not a " + "simulated result. Filter on this flag before summarizing those " + "variables across people. marginal_tax_rate_on_capital_gains ranks " + "adults separately and is not covered by this flag." ) definition_period = YEAR diff --git a/policyengine_us/variables/household/marginal_tax_rate_including_health_benefits.py b/policyengine_us/variables/household/marginal_tax_rate_including_health_benefits.py index 5f7c68b875a..b6b5580fae0 100644 --- a/policyengine_us/variables/household/marginal_tax_rate_including_health_benefits.py +++ b/policyengine_us/variables/household/marginal_tax_rate_including_health_benefits.py @@ -5,9 +5,9 @@ class marginal_tax_rate_including_health_benefits(Variable): label = "Marginal tax rate including health benefits" documentation = ( "Fraction of marginal income gains that do not increase household net income." - " Simulated only where marginal_tax_rate_computed is true, that is," - " for the top simulation.marginal_tax_rate_adults earners among the" - " adults in each household; zero for everyone else." + " Simulated only where marginal_tax_rate_computed is true: for up to" + " simulation.marginal_tax_rate_adults adults per household, those with" + " the highest market income. Zero for everyone else." ) entity = Person definition_period = YEAR diff --git a/policyengine_us/variables/household/state_marginal_tax_rate.py b/policyengine_us/variables/household/state_marginal_tax_rate.py index 60aae215a02..d7f4a20fcd2 100644 --- a/policyengine_us/variables/household/state_marginal_tax_rate.py +++ b/policyengine_us/variables/household/state_marginal_tax_rate.py @@ -8,9 +8,9 @@ class state_marginal_tax_rate(Variable): label = "state marginal tax rate" documentation = ( "Marginal change in state income tax per dollar of additional earnings." - " Simulated only where marginal_tax_rate_computed is true, that is," - " for the top simulation.marginal_tax_rate_adults earners among the" - " adults in each household; zero for everyone else." + " Simulated only where marginal_tax_rate_computed is true: for up to" + " simulation.marginal_tax_rate_adults adults per household, those with" + " the highest market income. Zero for everyone else." ) entity = Person definition_period = YEAR From a335a18575bd449032fa473bcb1a25118b5529dc Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 28 Sep 2026 16:46:51 -0400 Subject: [PATCH 3/3] Move cap-override MTR cases from YAML to pytest The batched YAML runner caches one tax-benefit system copy per distinct parameter override for the life of each subprocess. Four new marginal_tax_rate_adults overrides raised household batch 4's peak RSS from 17.6 GB to 22.7 GB and killed the 16 GB CI runner. Keep YAML cases on the default cap and check the cap-3 roommate rates in pytest, where simulations are freed per test; batch 4 now peaks at 17.7 GB. Co-Authored-By: Claude Opus 5.5 --- .../core/test_marginal_tax_rate_coverage.py | 38 +++++++++++++ .../baseline/household/marginal_tax_rate.yaml | 42 +------------- .../household/marginal_tax_rate_computed.yaml | 56 +++---------------- 3 files changed, 48 insertions(+), 88 deletions(-) diff --git a/policyengine_us/tests/core/test_marginal_tax_rate_coverage.py b/policyengine_us/tests/core/test_marginal_tax_rate_coverage.py index 157fe1383cc..bb75598951f 100644 --- a/policyengine_us/tests/core/test_marginal_tax_rate_coverage.py +++ b/policyengine_us/tests/core/test_marginal_tax_rate_coverage.py @@ -155,3 +155,41 @@ def test_raising_the_cap_only_adds_people(mtr_by_cap, variable): assert (higher_flag[lower_flag]).all() assert higher_flag.sum() > lower_flag.sum() assert np.array_equal(lower[variable][lower_flag], higher[variable][lower_flag]) + + +# Three single roommates in Texas in 2024, each filing alone. Taxable income is +# wages less the $14,600 standard deduction ($75,400, $45,400, $15,400), so a +# $1,000 raise is taxed at 22%, 12% and 12%, plus 7.65% employee payroll tax. +# Nobody crosses a bracket or payroll threshold, and Texas has no income tax. +ROOMMATE_WAGES = (90_000, 60_000, 30_000) +ROOMMATE_RATES = { + "marginal_tax_rate": (0.2965, 0.1965, 0.1965), + "federal_marginal_tax_rate": (0.22, 0.12, 0.12), + "fica_marginal_tax_rate": (0.0765, 0.0765, 0.0765), + "state_marginal_tax_rate": (0, 0, 0), +} + + +@pytest.mark.parametrize("cap", [2, 3]) +def test_roommates_match_hand_computed_rates(cap): + names = [f"roommate{i}" for i in range(len(ROOMMATE_WAGES))] + situation = { + "people": { + name: {"age": {YEAR: 40 - 5 * i}, "employment_income": {YEAR: wages}} + for i, (name, wages) in enumerate(zip(names, ROOMMATE_WAGES)) + }, + "tax_units": {f"{name}_tax_unit": {"members": [name]} for name in names}, + "spm_units": {f"{name}_spm_unit": {"members": [name]} for name in names}, + "households": {"home": {"members": names, "state_code": {YEAR: "TX"}}}, + } + simulation = Simulation( + situation=situation, + reform=Reform.from_dict({CAP: {str(YEAR): cap}}, country_id="us"), + ) + simulated = np.arange(len(names)) < cap + assert np.array_equal( + _calc(simulation, "marginal_tax_rate_computed").astype(bool), simulated + ) + for variable, rates in ROOMMATE_RATES.items(): + expected = np.where(simulated, rates, 0) + assert np.allclose(_calc(simulation, variable), expected, atol=1e-4) diff --git a/policyengine_us/tests/policy/baseline/household/marginal_tax_rate.yaml b/policyengine_us/tests/policy/baseline/household/marginal_tax_rate.yaml index cdb1c953ff3..8ad352eb148 100644 --- a/policyengine_us/tests/policy/baseline/household/marginal_tax_rate.yaml +++ b/policyengine_us/tests/policy/baseline/household/marginal_tax_rate.yaml @@ -94,6 +94,9 @@ # 22% bracket and the $60,000 and $30,000 earners are in the 12% bracket; each # also pays 7.65% employee payroll tax. The federal and FICA components check the # shared component helper, which has its own copy of the adult loop. +# The same household with the cap raised to 3 is in +# tests/core/test_marginal_tax_rate_coverage.py, which avoids another cached +# tax-benefit system in the batched YAML runner. - name: Third earner in a household gets a placeholder zero under the default cap absolute_error_margin: 0.0001 period: 2024 @@ -131,42 +134,3 @@ marginal_tax_rate_computed: [true, true, false] federal_marginal_tax_rate: [0.22, 0.12, 0] fica_marginal_tax_rate: [0.0765, 0.0765, 0] - -- name: Raising the cap simulates the third earner's marginal tax rate - absolute_error_margin: 0.0001 - period: 2024 - input: - simulation.marginal_tax_rate_adults: 3 - people: - person1: - age: 40 - employment_income: 90_000 - person2: - age: 35 - employment_income: 60_000 - person3: - age: 30 - employment_income: 30_000 - tax_units: - tax_unit1: - members: [person1] - tax_unit2: - members: [person2] - tax_unit3: - members: [person3] - spm_units: - spm_unit1: - members: [person1] - spm_unit2: - members: [person2] - spm_unit3: - members: [person3] - households: - household: - members: [person1, person2, person3] - state_code: TX - output: - marginal_tax_rate: [0.2965, 0.1965, 0.1965] - marginal_tax_rate_computed: [true, true, true] - federal_marginal_tax_rate: [0.22, 0.12, 0.12] - fica_marginal_tax_rate: [0.0765, 0.0765, 0.0765] diff --git a/policyengine_us/tests/policy/baseline/household/marginal_tax_rate_computed.yaml b/policyengine_us/tests/policy/baseline/household/marginal_tax_rate_computed.yaml index e0180699d2b..cf66b77d6fe 100644 --- a/policyengine_us/tests/policy/baseline/household/marginal_tax_rate_computed.yaml +++ b/policyengine_us/tests/policy/baseline/household/marginal_tax_rate_computed.yaml @@ -1,6 +1,9 @@ # The marginal tax rate loop simulates adults whose adult_earnings_index (rank # by market income within the household) is 1 through # simulation.marginal_tax_rate_adults; everyone else gets a placeholder zero. +# Cases here use the default cap of 2: every distinct parameter override pins +# another copy of the tax-benefit system in the batched YAML runner, so cases +# that vary the cap live in tests/core/test_marginal_tax_rate_coverage.py. - name: Default cap simulates the top two earners, not the two oldest adults period: 2024 @@ -25,67 +28,22 @@ adult_earnings_index: [4, 1, 2, 3] marginal_tax_rate_computed: [false, true, true, false] -- name: Raising the cap to the number of adults simulates every adult - period: 2024 - input: - simulation.marginal_tax_rate_adults: 4 - people: - person1: - age: 70 - person2: - age: 40 - employment_income: 80_000 - person3: - age: 38 - employment_income: 50_000 - person4: - age: 22 - employment_income: 30_000 - households: - household: - members: [person1, person2, person3, person4] - state_code: TX - output: - marginal_tax_rate_computed: [true, true, true, true] - -- name: A cap of zero simulates nobody - period: 2024 - input: - simulation.marginal_tax_rate_adults: 0 - people: - person1: - age: 40 - employment_income: 80_000 - person2: - age: 38 - employment_income: 50_000 - households: - household: - members: [person1, person2] - state_code: TX - output: - marginal_tax_rate_computed: [false, false] - - name: Children are never simulated, even with earnings and room under the cap period: 2024 input: - simulation.marginal_tax_rate_adults: 5 people: person1: age: 40 employment_income: 60_000 person2: - age: 39 - employment_income: 20_000 - person3: age: 16 employment_income: 8_000 - person4: + person3: age: 10 households: household: - members: [person1, person2, person3, person4] + members: [person1, person2, person3] state_code: TX output: - adult_earnings_index: [1, 2, 0, 0] - marginal_tax_rate_computed: [true, true, false, false] + adult_earnings_index: [1, 0, 0] + marginal_tax_rate_computed: [true, false, false]