Utah State income tax#
PolicyEngine models Utah’s State income tax, including the following components:
The main rate applied to taxable income (currently 4.85%)
The (nonrefundable) taxpayer credit
The income tax exemption
The chart below shows the absolute tax amount by employment income, for each of four household types.
Show code cell source
from policyengine_us import Simulation
import plotly.express as px
import pandas as pd
def create_situation(num_dependents, has_spouse):
situation = {
"people": {
"person": {
"age": 30,
},
},
"tax_units": {
"tax_unit": {
"members": ["person"],
},
},
"households": {
"household": {
"state_code": "UT",
"members": ["person"],
},
},
"axes": [
[
{
"name": "employment_income",
"min": 0,
"max": 200_000,
"count": 1000,
}
]
],
}
for i in range(num_dependents):
situation["people"][f"person{i}"] = {"age": 0}
situation["tax_units"]["tax_unit"]["members"].append(f"person{i}")
situation["households"]["household"]["members"].append(f"person{i}")
if has_spouse:
situation["people"]["spouse"] = {"age": 30}
situation["tax_units"]["tax_unit"]["members"].append("spouse")
situation["households"]["household"]["members"].append("spouse")
return situation
def create_simulation(situation):
return Simulation(
situation=situation,
)
single_no_dependents = create_simulation(create_situation(0, False))
single_with_dependents = create_simulation(create_situation(1, False))
married_no_dependents = create_simulation(create_situation(0, True))
married_with_dependents = create_simulation(create_situation(1, True))
employment_income = single_no_dependents.calculate("employment_income")
# Create a dataframe with
df = pd.concat(
[
pd.DataFrame(
{
"Employment income": employment_income,
"Utah income tax": simulation.calculate("ut_income_tax"),
"Household type": household_type,
}
)
for simulation, household_type in [
(single_no_dependents, "Single, no dependents"),
(single_with_dependents, "Single, with 1 dependent under 1"),
(married_no_dependents, "Married, no dependents"),
(married_with_dependents, "Married, with 1 dependent under 1"),
]
]
)
GRAY = "#BDBDBD"
BLUE = "#5091cc"
LIGHT_BLUE = "lightblue"
DARK_BLUE = "darkblue"
px.line(
df,
x="Employment income",
y="Utah income tax",
color="Household type",
color_discrete_map={
"Single, no dependents": GRAY,
"Single, with 1 dependent under 1": BLUE,
"Married, no dependents": LIGHT_BLUE,
"Married, with 1 dependent under 1": DARK_BLUE,
},
template="plotly_white",
).update_layout(
title="Utah income tax, by household type",
xaxis_title="Employment income",
yaxis_title="Utah income tax",
xaxis_tickformat="$,.0f",
yaxis_tickformat="$,.0f",
width=800,
height=600,
)
Marginal tax rates#
Show code cell source
df = pd.concat(
[
pd.DataFrame(
{
"Employment income": employment_income[1:],
"Utah income tax MTR": (
(
simulation.calculate("ut_income_tax")[1:]
- simulation.calculate("ut_income_tax")[:-1]
)
/ (employment_income[1:] - employment_income[:-1])
),
"Household type": household_type,
}
)
for simulation, household_type in [
(single_no_dependents, "Single, no dependents"),
(single_with_dependents, "Single, with 1 dependent under 1"),
(married_no_dependents, "Married, no dependents"),
(married_with_dependents, "Married, with 1 dependent under 1"),
]
]
)
fig = px.line(
df,
x="Employment income",
y="Utah income tax MTR",
color="Household type",
color_discrete_map={
"Single, no dependents": GRAY,
"Single, with 1 dependent under 1": BLUE,
"Married, no dependents": LIGHT_BLUE,
"Married, with 1 dependent under 1": DARK_BLUE,
},
template="plotly_white",
line_shape="vh",
).update_layout(
title="Utah income tax marginal rate, by household type",
xaxis_title="Employment income",
yaxis_title="Marginal tax rate",
xaxis_tickformat="$,.0f",
yaxis_tickformat=".2%",
width=800,
height=600,
yaxis_range=[0, 0.12],
)
# The y axis should only have markers for 0, 4.85 and 6.15 percent
fig.update_yaxes(tickvals=[0, 0.0485, 0.0615, 0.0865, 0.1105])
from policyengine_us import Microsimulation
from policyengine_core.reforms import Reform
from policyengine_core.periods import instant
import pandas as pd
baseline = Microsimulation()
def score_rate_reform(new_tax_rate: float) -> dict:
def modify_parameters(parameters):
parameters.gov.states.ut.tax.income.rate.update(
period="2023", value=new_tax_rate
)
return parameters
class reform(Reform):
def apply(self):
self.modify_parameters(modify_parameters)
reformed = Microsimulation(reform=reform)
net_cost = (
reformed.calculate("ut_income_tax", 2023).sum()
- baseline.calculate("ut_income_tax", 2023).sum()
)
gain_per_household = net_cost / baseline.calculate("UT").sum()
in_utah = baseline.calculate("UT", map_to="person")
poverty_change = (
reformed.calculate("person_in_poverty", 2023)[in_utah].mean()
/ baseline.calculate("person_in_poverty", 2023)[in_utah].mean()
) - 1
return {
"net_cost": net_cost,
"gain_per_household": gain_per_household,
"poverty_impact": poverty_change,
}
tax_rate_changes = [-0.01, -0.005, -0.001, 0, 0.001, 0.005, 0.01]
tax_rates = [0.0485 + x for x in tax_rate_changes]
costs = []
gains = []
poverty_changes = []
for tax_rate in tax_rates:
result = score_rate_reform(tax_rate)
costs.append(result["net_cost"])
gains.append(result["gain_per_household"])
poverty_changes.append(result["poverty_impact"])
df = pd.DataFrame(
{
"Tax rate": tax_rates,
"Tax rate change": tax_rate_changes,
"Net cost": costs,
"Gain per household": gains,
}
)
df
Tax rate | Tax rate change | Net cost | Gain per household | |
---|---|---|---|---|
0 | 0.0385 | -0.010 | -1.001593e+09 | -904.465417 |
1 | 0.0435 | -0.005 | -3.761084e+08 | -339.636049 |
2 | 0.0475 | -0.001 | 1.255614e+08 | 113.385365 |
3 | 0.0485 | 0.000 | 2.512052e+08 | 226.845076 |
4 | 0.0495 | 0.001 | 3.770251e+08 | 340.463794 |
5 | 0.0535 | 0.005 | 8.811724e+08 | 795.722476 |
6 | 0.0585 | 0.010 | 1.512878e+09 | 1366.169957 |
References#
Most of the logic in PolicyEngine was developed in line with the 2022 tax form (TC-40). For historical parameters, the legislation is the primary source: specifically, Title 59 (Revenue and Taxation), Chapter 10 (Individual Income Tax Act) of the Utah Code. Utah has a historical legislation viewer which allows you to see the text of any section at any particular date after 2014.