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
1 change: 1 addition & 0 deletions changelog.d/add-decomposed-marginal-tax-rates.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add decomposed marginal tax rate variables: federal_marginal_tax_rate, state_marginal_tax_rate, and fica_marginal_tax_rate.
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
- name: Federal MTR in 12% bracket
absolute_error_margin: 0.001
period: 2026
input:
age: 40
employment_income: 30_000
state_fips: 48 # TX
output:
federal_marginal_tax_rate: 0.12

- name: Federal MTR in 22% bracket
absolute_error_margin: 0.001
period: 2026
input:
age: 40
employment_income: 100_000
state_fips: 48 # TX
output:
federal_marginal_tax_rate: 0.22

- name: Federal MTR in 32% bracket
absolute_error_margin: 0.001
period: 2026
input:
age: 40
employment_income: 250_000
state_fips: 48 # TX
output:
federal_marginal_tax_rate: 0.32

- name: Federal MTR for married couple filing jointly
absolute_error_margin: 0.001
period: 2026
input:
people:
filer:
age: 40
employment_income: 120_000
spouse:
age: 38
employment_income: 50_000
tax_units:
tax_unit:
members: [filer, spouse]
filing_status: JOINT
spm_units:
spm_unit:
members: [filer, spouse]
households:
household:
members: [filer, spouse]
state_fips: 48 # TX
output:
# Joint filers: $170k combined, 22% bracket; default marginal_tax_rate_adults=2
federal_marginal_tax_rate: [0.22, 0.22]

- name: Federal MTR in two tax-unit household
absolute_error_margin: 0.001
period: 2026
input:
simulation.marginal_tax_rate_adults: 2
people:
person1:
age: 43
employment_income: 100_000
person2:
age: 30
employment_income: 40_000
tax_units:
tax_unit1:
members: [person1]
tax_unit2:
members: [person2]
spm_units:
spm_unit1:
members: [person1]
spm_unit2:
members: [person2]
households:
household:
members: [person1, person2]
state_fips: 48 # TX
output:
# person1 at $100k in 22% bracket, person2 at $40k in 12% bracket
federal_marginal_tax_rate: [0.22, 0.12]
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
- name: FICA MTR for single filer below SS wage base
absolute_error_margin: 0.001
period: 2026
input:
age: 40
employment_income: 100_000
state_fips: 48 # TX
output:
# 6.2% SS + 1.45% Medicare
fica_marginal_tax_rate: 0.0765

- name: FICA MTR above SS wage base but below Additional Medicare threshold
absolute_error_margin: 0.001
period: 2026
input:
age: 40
employment_income: 190_000
state_fips: 48 # TX
output:
# SS wage base is $186k in 2026, so only 1.45% Medicare applies
fica_marginal_tax_rate: 0.0145

- name: FICA MTR above Additional Medicare Tax threshold
absolute_error_margin: 0.001
period: 2026
input:
age: 40
employment_income: 250_000
state_fips: 48 # TX
output:
# 1.45% Medicare + 0.9% Additional Medicare Tax
fica_marginal_tax_rate: 0.0235

- name: FICA MTR for married couple filing jointly
absolute_error_margin: 0.001
period: 2026
input:
people:
filer:
age: 40
employment_income: 80_000
spouse:
age: 38
employment_income: 50_000
tax_units:
tax_unit:
members: [filer, spouse]
filing_status: JOINT
spm_units:
spm_unit:
members: [filer, spouse]
households:
household:
members: [filer, spouse]
state_fips: 48 # TX
output:
# Both below SS wage base ($186k), 7.65%; default marginal_tax_rate_adults=2
fica_marginal_tax_rate: [0.0765, 0.0765]

- name: FICA MTR in two tax-unit household with different thresholds
absolute_error_margin: 0.001
period: 2026
input:
simulation.marginal_tax_rate_adults: 2
people:
person1:
age: 43
employment_income: 100_000
person2:
age: 30
employment_income: 190_000
tax_units:
tax_unit1:
members: [person1]
tax_unit2:
members: [person2]
spm_units:
spm_unit1:
members: [person1]
spm_unit2:
members: [person2]
households:
household:
members: [person1, person2]
state_fips: 48 # TX
output:
# person1 at $100k: below SS wage base, 7.65%
# person2 at $190k: above SS wage base ($186k), only Medicare 1.45%
fica_marginal_tax_rate: [0.0765, 0.0145]
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
- name: State MTR is zero in TX (no state income tax)
absolute_error_margin: 0.001
period: 2026
input:
age: 40
employment_income: 100_000
state_fips: 48 # TX
output:
state_marginal_tax_rate: 0

- name: State MTR for CA filer in 9.3% bracket
absolute_error_margin: 0.01
period: 2026
input:
age: 40
employment_income: 100_000
state_fips: 6 # CA
output:
state_marginal_tax_rate: 0.093

- name: State MTR for NY filer in 5.9% bracket
absolute_error_margin: 0.01
period: 2026
input:
age: 40
employment_income: 100_000
state_fips: 36 # NY
output:
state_marginal_tax_rate: 0.059

- name: State MTR for married couple filing jointly in CA
absolute_error_margin: 0.01
period: 2026
input:
people:
filer:
age: 40
employment_income: 120_000
spouse:
age: 38
employment_income: 50_000
tax_units:
tax_unit:
members: [filer, spouse]
filing_status: JOINT
spm_units:
spm_unit:
members: [filer, spouse]
households:
household:
members: [filer, spouse]
state_fips: 6 # CA
output:
# CA joint filers at $170k combined — 9.3% bracket; default marginal_tax_rate_adults=2
state_marginal_tax_rate: [0.093, 0.093]

- name: State MTR in two tax-unit household in TX
absolute_error_margin: 0.001
period: 2026
input:
simulation.marginal_tax_rate_adults: 2
people:
person1:
age: 43
employment_income: 100_000
person2:
age: 30
employment_income: 40_000
tax_units:
tax_unit1:
members: [person1]
tax_unit2:
members: [person2]
spm_units:
spm_unit1:
members: [person1]
spm_unit2:
members: [person2]
households:
household:
members: [person1, person2]
state_fips: 48 # TX
output:
# TX has no state income tax — both should be 0
state_marginal_tax_rate: [0, 0]
20 changes: 20 additions & 0 deletions policyengine_us/variables/household/federal_marginal_tax_rate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from policyengine_us.model_api import *
from policyengine_us.variables.household.marginal_tax_rate_helpers import (
compute_component_mtr,
)


class federal_marginal_tax_rate(Variable):
label = "federal marginal tax rate"
documentation = (
"Marginal change in federal income tax per dollar of additional earnings."
)
entity = Person
definition_period = YEAR
value_type = float
unit = "/1"

def formula(person, period, parameters):
return compute_component_mtr(
person, period, parameters, "income_tax", "federal_mtr"
)
20 changes: 20 additions & 0 deletions policyengine_us/variables/household/fica_marginal_tax_rate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from policyengine_us.model_api import *
from policyengine_us.variables.household.marginal_tax_rate_helpers import (
compute_component_mtr,
)


class fica_marginal_tax_rate(Variable):
label = "FICA marginal tax rate"
documentation = (
"Marginal change in employee payroll tax per dollar of additional earnings."
)
entity = Person
definition_period = YEAR
value_type = float
unit = "/1"

def formula(person, period, parameters):
return compute_component_mtr(
person, period, parameters, "employee_payroll_tax", "fica_mtr"
)
59 changes: 59 additions & 0 deletions policyengine_us/variables/household/marginal_tax_rate_helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from policyengine_us.model_api import *


def compute_component_mtr(person, period, parameters, tax_variable, branch_prefix):
"""Compute the marginal tax rate for a specific tax component.

Uses the same counterfactual branch pattern as marginal_tax_rate:
perturbs earnings by delta and measures the change in the given
tax_unit-level tax variable, aggregated to the household level
for consistency with how marginal_tax_rate uses household_net_income.

Args:
person: The person entity.
period: The simulation period.
parameters: The parameter tree.
tax_variable: Name of the tax_unit variable to measure
(e.g. "income_tax", "state_income_tax", "employee_payroll_tax").
branch_prefix: Unique prefix for branch names to avoid conflicts.

Returns:
Array of marginal tax rates per person.
"""
is_head = person("is_tax_unit_head", period)
base_tax = person.household.sum(person.tax_unit(tax_variable, period) * is_head)
delta = parameters(period).simulation.marginal_tax_rate_delta
adult_count = parameters(period).simulation.marginal_tax_rate_adults
sim = person.simulation
mtr_values = np.zeros(person.count, dtype=np.float32)
adult_indexes = person("adult_earnings_index", period)
employment_income = person("employment_income", period)
self_employment_income = person("self_employment_income", period)
emp_self_emp_ratio = person("emp_self_emp_ratio", period)

for adult_index in range(1, 1 + adult_count):
branch_name = f"{branch_prefix}_for_adult_{adult_index}"
alt_sim = sim.get_branch(branch_name)
for variable in sim.tax_benefit_system.variables:
if variable not in sim.input_variables or variable == "employment_income":
alt_sim.delete_arrays(variable)
mask = adult_index == adult_indexes
alt_sim.set_input(
"employment_income",
period,
employment_income + mask * delta * emp_self_emp_ratio,
)
alt_sim.set_input(
"self_employment_income",
period,
self_employment_income + mask * delta * (1 - emp_self_emp_ratio),
)
alt_person = alt_sim.person
alt_is_head = alt_person("is_tax_unit_head", period)
alt_tax = alt_person.household.sum(
alt_person.tax_unit(tax_variable, period) * alt_is_head
)
increase = alt_tax - base_tax
mtr_values += where(mask, increase / delta, 0)
del sim.branches[branch_name]
return mtr_values
20 changes: 20 additions & 0 deletions policyengine_us/variables/household/state_marginal_tax_rate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from policyengine_us.model_api import *
from policyengine_us.variables.household.marginal_tax_rate_helpers import (
compute_component_mtr,
)


class state_marginal_tax_rate(Variable):
label = "state marginal tax rate"
documentation = (
"Marginal change in state income tax per dollar of additional earnings."
)
entity = Person
definition_period = YEAR
value_type = float
unit = "/1"

def formula(person, period, parameters):
return compute_component_mtr(
person, period, parameters, "state_income_tax", "state_mtr"
)
Loading