SNAP#

The Supplemental Nutrition Assistance Program (SNAP) is a program that provides food assistance to low-income families. Eligibility depends on household size, state, and income, before and after a series of deductions involving earnings and various costs such as shelter and childcare.

Examples#

California currently provides emergency allotments for the Covid-19 pandemic. This tops up every eligible household to the maximum allotment for their household size, or $95 per month, whichever is larger.

These examples show SNAP allotments for a sample household, depending on their characteristics, both with (Full) and without (Normal) the emergency allotment.

How earnings affect a one-person household’s SNAP allotments#

Consider a single person household in California with $1,000 monthly earned income and $600 monthly rent. They would be eligible for $145 per month in SNAP allotments, or a top-up to the full $250 with the emergency allotment. Since the top-up is $105, less than the $95 emergency allotment minimum, they would not receive more than the maximum allotment.

from policyengine_us import IndividualSim
import pandas as pd
import plotly.express as px

sim_emp = IndividualSim(year=2022)
sim_emp.add_person(name="person", employment_income=1000 * 12)
sim_emp.add_spm_unit(
    name="spm_unit", members=["person"], housing_cost=600 * 12
)

print(
    "SNAP normal allotment: ",
    round(sim_emp.calc("snap_normal_allotment")[0] / 12),
)
print(
    "SNAP emergency allotment: ",
    round(sim_emp.calc("snap_emergency_allotment")[0] / 12),
)
print("Total SNAP: ", round(sim_emp.calc("snap")[0] / 12))
SNAP normal allotment:  159
SNAP emergency allotment:  99
Total SNAP:  258

What if their earnings change? They receive the maximum allotment of $250 per month until they reach $720 in monthly earnings, plus the $95 emergency allotment, coming to $345. When their income hits $980 per month, their normal allotment falls to $155, at which point their total allotment stabilizes at $250. The normal allotment then continues to fall until their earnings reach $1,350, at which point their continue to receive the $20 minimum monthly allotment until they reach $2,150 per month, 200% of the poverty line. At 200% of the poverty line, both their normal and full allotment fall to $0.

sim_emp.vary("employment_income", max=2500 * 12, step=120)

import plotly.express as px

LABELS = dict(
    employment_income="Monthly employment income",
    dividend_income="Monthly dividend income",
    income="Monthly income",
    income_source="Income source",
    housing_cost="Monthly housing cost",
    snap="Monthly SNAP allotment",
    mtr="Marginal tax rate from SNAP",
    housing_subsidy_rate="Housing subsidy rate from SNAP",
    allotment="SNAP allotment",
)

emp_df_full = pd.DataFrame(
    dict(
        employment_income=sim_emp.calc("employment_income")[0],
        allotment="Full",
        snap=sim_emp.calc("snap")[0],
        mtr=-sim_emp.deriv("snap", "employment_income"),
    )
)
emp_df_normal = pd.DataFrame(
    dict(
        employment_income=sim_emp.calc("employment_income")[0],
        allotment="Normal",
        snap=sim_emp.calc("snap_normal_allotment")[0],
        mtr=-sim_emp.deriv("snap_normal_allotment", "employment_income"),
    )
)
emp_df = pd.concat([emp_df_full, emp_df_normal])
emp_df[["employment_income", "snap"]] = (
    emp_df[["employment_income", "snap"]] / 12
).round()

fig = px.line(
    emp_df,
    "employment_income",
    "snap",
    color="allotment",
    labels=LABELS,
    title="SNAP allotment for a one-person household in California with $600 monthly housing costs",
)
fig.update_layout(xaxis_tickformat="$,", yaxis_tickformat="$,")
fig.show()

We can also view their marginal tax rate from the program, revealing that SNAP phases out at 36 cents on the dollar through the phase-out region. The emergency allotment shrinks this region. The person also experiences a cliff (infinite marginal tax rate) when they hit 200% of the poverty line.

fig = px.line(
    emp_df,
    "employment_income",
    "mtr",
    color="allotment",
    labels=LABELS,
    title="SNAP marginal tax rate for a one-person household in California with $600 monthly housing costs",
)
fig.update_layout(
    xaxis_tickformat="$,", yaxis_tickformat=".0%", yaxis_range=[0, 1]
)
fig.show()

How housing costs affect a four-person household’s SNAP allotments#

We can also visualize how other household characteristics affect SNAP allotments. For example, what if we vary the housing costs of a four-person household with $2,000 monthly income?

Their normal SNAP allotment is $409 per month if their housing costs are less than $720 per month. If they spend $1,310 per month on housing, their normal allotment rises 44% to $588 per month.

However, because their maximum allotment exceeds their normal allotment by at least $95 regardless of their housing costs, they will receive the maximum allotment regardless of housing costs as a result of the emergency allotment.

sim_rent = IndividualSim(year=2022)
sim_rent.add_person(name="parent1", employment_income=1000 * 12)
sim_rent.add_person(name="parent2", employment_income=1000 * 12)
sim_rent.add_person(name="child1")
sim_rent.add_person(name="child2")
sim_rent.add_spm_unit(
    name="spm_unit",
    members=["parent1", "parent2", "child1", "child2"],
)

sim_rent.vary("housing_cost", max=2000 * 12, step=120)

rent_df_full = pd.DataFrame(
    dict(
        allotment="Full",
        housing_cost=sim_rent.calc("housing_cost")[0],
        snap=sim_rent.calc("snap")[0],
        housing_subsidy_rate=sim_rent.deriv("snap", "housing_cost"),
    )
)
rent_df_normal = pd.DataFrame(
    dict(
        allotment="Normal",
        housing_cost=sim_rent.calc("housing_cost")[0],
        snap=sim_rent.calc("snap_normal_allotment")[0],
        housing_subsidy_rate=sim_rent.deriv(
            "snap_normal_allotment", "housing_cost"
        ),
    )
)
rent_df = pd.concat([rent_df_full, rent_df_normal])
rent_df[["housing_cost", "snap"]] = (
    rent_df[["housing_cost", "snap"]] / 12
).round()

fig = px.line(
    rent_df,
    "housing_cost",
    "snap",
    color="allotment",
    labels=LABELS,
    title="SNAP allotment for a four-person household in California with $1,000 monthly earnings",
)
fig.update_layout(
    xaxis_tickformat="$,",
    yaxis_tickformat="$,",
    yaxis_range=[0, rent_df.snap.max() * 1.1],
)
fig.show()

As before, we can also see the rate at which SNAP increases with housing costs. For this household, the normal SNAP allotment subsidizes housing costs at a rate of 30% for housing costs ranging from $720 to $1,310 per month. The emergency allotment nullifies this effect.

fig = px.line(
    rent_df,
    "housing_cost",
    "housing_subsidy_rate",
    color="allotment",
    labels=LABELS,
    title="SNAP housing subsidy rate for a one-person household in California with $1,000 monthly earnings",
)
fig.update_layout(xaxis_tickformat="$,", yaxis_tickformat=".0%")
fig.show()

How unearned income compares to earned income#

SNAP exempts 20% of earned income from net income calculations. This earned income deduction does not apply to unearned income, such as Social Security, unemployment allotments, interest, and dividends.

To illustrate the effect of this provision, let’s return to the first example of a single person with $1,000 monthly income and $600 monthly rent. When their income was earned, they received a normal allotment of $145, plus a $105 emergency allotment to bring them to the one-person maximum of $250. If their income is unearned, for example if it’s from dividends, their normal allotment falls to $55, though the emergency allotment fills the gap, bringing their total allotment again to $250.

sim_div = IndividualSim(year=2022)
sim_div.add_person(name="person", dividend_income=1000 * 12)
sim_div.add_spm_unit(
    name="spm_unit", members=["person"], housing_cost=600 * 12
)

print(
    "SNAP normal allotment: ",
    round(sim_div.calc("snap_normal_allotment")[0] / 12),
)
print(
    "SNAP emergency allotment: ",
    round(sim_div.calc("snap_emergency_allotment")[0] / 12),
)
print("Total SNAP: ", round(sim_div.calc("snap")[0] / 12))
SNAP normal allotment:  69
SNAP emergency allotment:  188
Total SNAP:  258

As a result of the earned income deduction, this household will have a larger SNAP benefit if their income comes from employment, if their monthly income is between $570 and $980.

sim_div.vary("dividend_income", max=2500 * 12, step=120)

div_df_full = pd.DataFrame(
    dict(
        dividend_income=sim_div.calc("dividend_income")[0],
        allotment="Full",
        snap=sim_div.calc("snap")[0],
        mtr=-sim_div.deriv("snap", "dividend_income"),
    )
)
div_df_normal = pd.DataFrame(
    dict(
        dividend_income=sim_div.calc("dividend_income")[0],
        allotment="Normal",
        snap=sim_div.calc("snap_normal_allotment")[0],
        mtr=-sim_div.deriv("snap_normal_allotment", "dividend_income"),
    )
)
div_df = pd.concat([div_df_full, div_df_normal])
div_df[["dividend_income", "snap"]] = (
    div_df[["dividend_income", "snap"]] / 12
).round()

# Combine employment and dividend income DataFrames.
emp_df["income_source"] = "Earned"
div_df["income_source"] = "Unearned"
emp_div_df = pd.concat(
    [
        emp_df.rename(columns=dict(employment_income="income")),
        div_df.rename(columns=dict(dividend_income="income")),
    ]
)

fig = px.line(
    emp_div_df[emp_div_df.allotment == "Full"],
    "income",
    "snap",
    color="income_source",
    labels=LABELS,
    title="Full SNAP allotment for a one-person household in California with $600 monthly housing costs",
)
fig.update_layout(xaxis_tickformat="$,", yaxis_tickformat="$,")
fig.show()

While the SNAP marginal tax rate is 36% for employment income between $710 and $960, it is 45% for dividend income between $570 and $760.

fig = px.line(
    emp_div_df[emp_div_df.allotment == "Full"],
    "income",
    "mtr",
    color="income_source",
    labels=LABELS,
    title="Full SNAP marginal tax rate for a one-person household in California with $600 monthly housing costs",
)
fig.update_layout(
    xaxis_tickformat="$,", yaxis_tickformat=".0%", yaxis_range=[0, 1]
)
fig.show()

In the absence of the emergency allotment, SNAP allotments differ between earned and unearned income over larger income ranges.

fig = px.line(
    emp_div_df[emp_div_df.allotment == "Normal"],
    "income",
    "snap",
    color="income_source",
    labels=LABELS,
    title="Normal SNAP allotment for a one-person household in California with $600 monthly housing costs",
)
fig.update_layout(xaxis_tickformat="$,", yaxis_tickformat="$,")
fig.show()

Without emergency allotments, the marginal tax rates remain at 36% and 45% for earned and unearned income, respectively, over larger income ranges.

fig = px.line(
    emp_div_df[emp_div_df.allotment == "Normal"],
    "income",
    "mtr",
    color="income_source",
    labels=LABELS,
    title="Normal SNAP marginal tax rate for a one-person household in California with $600 monthly housing costs",
)
fig.update_layout(
    xaxis_tickformat="$,", yaxis_tickformat=".0%", yaxis_range=[0, 1]
)
fig.show()

Massachusetts#

sim_emp = IndividualSim(year=2022)
sim_emp.add_person(name="person", employment_income=1000 * 12)
sim_emp.add_spm_unit(
    name="spm_unit", members=["person"], housing_cost=600 * 12
)
sim_emp.add_household(name="household", members=["person"], state_code="MA")

sim_emp.vary("employment_income", max=2500 * 12, step=120)

import plotly.express as px

LABELS = dict(
    employment_income="Monthly employment income",
    dividend_income="Monthly dividend income",
    income="Monthly income",
    income_source="Income source",
    housing_cost="Monthly housing cost",
    snap="Monthly SNAP allotment",
    mtr="Marginal tax rate from SNAP",
    housing_subsidy_rate="Housing subsidy rate from SNAP",
    allotment="SNAP allotment",
)

emp_df_full = pd.DataFrame(
    dict(
        employment_income=sim_emp.calc("employment_income")[0],
        allotment="Full",
        snap=sim_emp.calc("snap")[0],
        mtr=-sim_emp.deriv("snap", "employment_income"),
    )
)
emp_df_normal = pd.DataFrame(
    dict(
        employment_income=sim_emp.calc("employment_income")[0],
        allotment="Normal",
        snap=sim_emp.calc("snap_normal_allotment")[0],
        mtr=-sim_emp.deriv("snap_normal_allotment", "employment_income"),
    )
)
emp_df = pd.concat([emp_df_full, emp_df_normal])
emp_df[["employment_income", "snap"]] = (
    emp_df[["employment_income", "snap"]] / 12
).round()

fig = px.line(
    emp_df,
    "employment_income",
    "snap",
    color="allotment",
    labels=LABELS,
    title="SNAP allotment for a one-person household in MA with $600 monthly housing costs",
)
fig.update_layout(xaxis_tickformat="$,", yaxis_tickformat="$,")
fig.show()

SNAP uses a modified version of the federal poverty guideline- the FPG as it was on October 1st of the previous year.

from policyengine_us import Simulation
from policyengine_core.charts import format_fig
import plotly.express as px
import pandas as pd

sim = Simulation(
    situation=dict(
        people={
            "person": {
                "employment_income": 1000 * 12,
            },
        },
    )
)

instants = []
values = []
variables = []

for year in range(2021, 2024):
    for month in range(1, 13):
        period = f"{year}-{month:02d}"
        instants.append(period)
        values.append(sim.calculate("spm_unit_fpg", period)[0])
        variables.append("FPG")
        values.append(sim.calculate("snap_fpg", period)[0])
        variables.append("SNAP FPG")
        instants.append(period)

df = pd.DataFrame(dict(Period=instants, Value=values, Variable=variables))

fig = px.bar(
    df,
    x="Period",
    y="Value",
    color="Variable",
    title="Federal poverty guidelines",
    barmode="group",
)

format_fig(fig)