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.

Hide 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#

Hide 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.