Skip to content
Merged
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
1 change: 1 addition & 0 deletions changelog.d/mtr-uncomputed-adults.added.md
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions changelog.d/mtr-uncomputed-adults.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix `cliff_evaluated` to rank adults by market income, as the marginal tax rate loop does, instead of by age.
Original file line number Diff line number Diff line change
@@ -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 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:
Expand Down
195 changes: 195 additions & 0 deletions policyengine_us/tests/core/test_marginal_tax_rate_coverage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
"""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, 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. 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

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",
)
# 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,),
(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),
(-20_000, 30_000),
(10_000, -5_000, 0, 60_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}"
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 = []
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])


# 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)
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -88,3 +88,49 @@
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. 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
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]
federal_marginal_tax_rate: [0.22, 0.12, 0]
fica_marginal_tax_rate: [0.0765, 0.0765, 0]
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# 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
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: Children are never simulated, even with earnings and room under the cap
period: 2024
input:
people:
person1:
age: 40
employment_income: 60_000
person2:
age: 16
employment_income: 8_000
person3:
age: 10
households:
household:
members: [person1, person2, person3]
state_code: TX
output:
adult_earnings_index: [1, 0, 0]
marginal_tax_rate_computed: [true, false, false]
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 3 additions & 4 deletions policyengine_us/variables/household/cliff.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: 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
Expand Down
3 changes: 3 additions & 0 deletions policyengine_us/variables/household/fica_marginal_tax_rate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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: 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
Expand Down
3 changes: 3 additions & 0 deletions policyengine_us/variables/household/marginal_tax_rate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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: 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
Expand Down
Loading
Loading