Written by Douglas Issichopoulos dougissi.com in Nov 2022
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.ticker import FuncFormatter, PercentFormatter
# source: https://sites.google.com/view/incometaxspreadsheet/home
tax_table = pd.read_csv('2021_1040_tax_table.csv')
tax_table
At Least | But Less Than | Single | Married filing jointly | Married filing separately | Head of household | |
---|---|---|---|---|---|---|
0 | 0 | 5 | 0 | 0 | 0 | 0 |
1 | 5 | 15 | 1 | 1 | 1 | 1 |
2 | 15 | 25 | 2 | 2 | 2 | 2 |
3 | 25 | 50 | 4 | 4 | 4 | 4 |
4 | 50 | 75 | 6 | 6 | 6 | 6 |
... | ... | ... | ... | ... | ... | ... |
2057 | 99750 | 99800 | 17967 | 13448 | 17967 | 16515 |
2058 | 99800 | 99850 | 17979 | 13459 | 17979 | 16527 |
2059 | 99850 | 99900 | 17991 | 13470 | 17991 | 16539 |
2060 | 99900 | 99950 | 18003 | 13481 | 18003 | 16551 |
2061 | 99950 | 100000 | 18015 | 13492 | 18015 | 16563 |
2062 rows × 6 columns
def format_limited_tax_table(tax_table: pd.DataFrame, category: str, step: int = 10000) -> pd.DataFrame:
limited_tax_table = tax_table[tax_table["At Least"] % step == 0]
limited_tax_table = limited_tax_table.set_index("At Least", drop=True)
limited_tax_table.index.name = "Taxable Income"
limited_tax_table["Tax Owed"] = limited_tax_table[category]
limited_tax_table["Tax Owed as % of Taxable Income"] = limited_tax_table["Tax Owed"] / limited_tax_table.index
limited_tax_table["Remaining Post-Tax"] = limited_tax_table.index - limited_tax_table["Tax Owed"]
return limited_tax_table[["Tax Owed", "Tax Owed as % of Taxable Income", "Remaining Post-Tax"]]
indiv_limited_tax_table = format_limited_tax_table(tax_table, "Single")
indiv_limited_tax_table
Tax Owed | Tax Owed as % of Taxable Income | Remaining Post-Tax | |
---|---|---|---|
Taxable Income | |||
0 | 0 | NaN | 0 |
10000 | 1004 | 0.100400 | 8996 |
20000 | 2204 | 0.110200 | 17796 |
30000 | 3404 | 0.113467 | 26596 |
40000 | 4604 | 0.115100 | 35396 |
50000 | 6754 | 0.135080 | 43246 |
60000 | 8954 | 0.149233 | 51046 |
70000 | 11154 | 0.159343 | 58846 |
80000 | 13354 | 0.166925 | 66646 |
90000 | 15627 | 0.173633 | 74373 |
mfj_limited_tax_table = format_limited_tax_table(tax_table, "Married filing jointly")
mfj_limited_tax_table
Tax Owed | Tax Owed as % of Taxable Income | Remaining Post-Tax | |
---|---|---|---|
Taxable Income | |||
0 | 0 | NaN | 0 |
10000 | 1003 | 0.100300 | 8997 |
20000 | 2005 | 0.100250 | 17995 |
30000 | 3205 | 0.106833 | 26795 |
40000 | 4405 | 0.110125 | 35595 |
50000 | 5605 | 0.112100 | 44395 |
60000 | 6805 | 0.113417 | 53195 |
70000 | 8005 | 0.114357 | 61995 |
80000 | 9205 | 0.115062 | 70795 |
90000 | 11303 | 0.125589 | 78697 |
columns = [
"At Least",
"But Less Than",
"Rate",
"Subtraction Amount"
]
data = [
[100000, 164926, .24, 5797],
[164926, 209425, .32, 19173],
[209426, 523601, .35, 25455.75],
[523601, float("inf"), .37, 35927.75]
]
indiv_tax_brackets = pd.DataFrame(data, columns=columns)
indiv_tax_brackets
At Least | But Less Than | Rate | Subtraction Amount | |
---|---|---|---|---|
0 | 100000 | 164926.0 | 0.24 | 5797.00 |
1 | 164926 | 209425.0 | 0.32 | 19173.00 |
2 | 209426 | 523601.0 | 0.35 | 25455.75 |
3 | 523601 | inf | 0.37 | 35927.75 |
columns = [
"At Least",
"But Less Than",
"Rate",
"Subtraction Amount"
]
data = [
[100000, 172751, .22, 8503],
[172751, 329851, .24, 11958],
[329851, 418851, .32, 38346],
[418851, 628301, .35, 50911.50],
[628301, float("inf"), .37, 63477.50]
]
mfj_tax_brackets = pd.DataFrame(data, columns=columns)
mfj_tax_brackets
At Least | But Less Than | Rate | Subtraction Amount | |
---|---|---|---|---|
0 | 100000 | 172751.0 | 0.22 | 8503.0 |
1 | 172751 | 329851.0 | 0.24 | 11958.0 |
2 | 329851 | 418851.0 | 0.32 | 38346.0 |
3 | 418851 | 628301.0 | 0.35 | 50911.5 |
4 | 628301 | inf | 0.37 | 63477.5 |
def add_tax_columns(brackets_df: pd.DataFrame, income_start=100000, income_stop=1000000, income_step=10000) -> pd.DataFrame:
def calculate_tax(taxable_income: int) -> float:
filtered = brackets_df[(brackets_df["At Least"] <= taxable_income) & (taxable_income < brackets_df["But Less Than"])]
if filtered.empty:
return
bracket = filtered.iloc[0]
return (taxable_income * bracket["Rate"]) - bracket["Subtraction Amount"]
taxable_incomes = pd.RangeIndex(income_start, income_stop+income_step, income_step).to_frame(name="Taxable Income")
taxable_incomes["Tax Owed"] = taxable_incomes["Taxable Income"].map(calculate_tax)
taxable_incomes["Tax Owed as % of Taxable Income"] = taxable_incomes["Tax Owed"] / taxable_incomes["Taxable Income"]
taxable_incomes["Remaining Post-Tax"] = taxable_incomes["Taxable Income"] - taxable_incomes["Tax Owed"]
taxable_incomes.set_index("Taxable Income", drop=True, inplace=True)
return taxable_incomes
indiv_incomes_w_tax = add_tax_columns(indiv_tax_brackets)
indiv_incomes_w_tax
Tax Owed | Tax Owed as % of Taxable Income | Remaining Post-Tax | |
---|---|---|---|
Taxable Income | |||
100000 | 18203.00 | 0.182030 | 81797.00 |
110000 | 20603.00 | 0.187300 | 89397.00 |
120000 | 23003.00 | 0.191692 | 96997.00 |
130000 | 25403.00 | 0.195408 | 104597.00 |
140000 | 27803.00 | 0.198593 | 112197.00 |
... | ... | ... | ... |
960000 | 319272.25 | 0.332575 | 640727.75 |
970000 | 322972.25 | 0.332961 | 647027.75 |
980000 | 326672.25 | 0.333339 | 653327.75 |
990000 | 330372.25 | 0.333709 | 659627.75 |
1000000 | 334072.25 | 0.334072 | 665927.75 |
91 rows × 3 columns
mfj_incomes_w_tax = add_tax_columns(mfj_tax_brackets)
mfj_incomes_w_tax
Tax Owed | Tax Owed as % of Taxable Income | Remaining Post-Tax | |
---|---|---|---|
Taxable Income | |||
100000 | 13497.0 | 0.134970 | 86503.0 |
110000 | 15697.0 | 0.142700 | 94303.0 |
120000 | 17897.0 | 0.149142 | 102103.0 |
130000 | 20097.0 | 0.154592 | 109903.0 |
140000 | 22297.0 | 0.159264 | 117703.0 |
... | ... | ... | ... |
960000 | 291722.5 | 0.303878 | 668277.5 |
970000 | 295422.5 | 0.304559 | 674577.5 |
980000 | 299122.5 | 0.305227 | 680877.5 |
990000 | 302822.5 | 0.305881 | 687177.5 |
1000000 | 306522.5 | 0.306522 | 693477.5 |
91 rows × 3 columns
indiv_tax = pd.concat([indiv_limited_tax_table, indiv_incomes_w_tax])
indiv_tax
Tax Owed | Tax Owed as % of Taxable Income | Remaining Post-Tax | |
---|---|---|---|
Taxable Income | |||
0 | 0.00 | NaN | 0.00 |
10000 | 1004.00 | 0.100400 | 8996.00 |
20000 | 2204.00 | 0.110200 | 17796.00 |
30000 | 3404.00 | 0.113467 | 26596.00 |
40000 | 4604.00 | 0.115100 | 35396.00 |
... | ... | ... | ... |
960000 | 319272.25 | 0.332575 | 640727.75 |
970000 | 322972.25 | 0.332961 | 647027.75 |
980000 | 326672.25 | 0.333339 | 653327.75 |
990000 | 330372.25 | 0.333709 | 659627.75 |
1000000 | 334072.25 | 0.334072 | 665927.75 |
101 rows × 3 columns
mfj_tax = pd.concat([mfj_limited_tax_table, mfj_incomes_w_tax])
mfj_tax
Tax Owed | Tax Owed as % of Taxable Income | Remaining Post-Tax | |
---|---|---|---|
Taxable Income | |||
0 | 0.0 | NaN | 0.0 |
10000 | 1003.0 | 0.100300 | 8997.0 |
20000 | 2005.0 | 0.100250 | 17995.0 |
30000 | 3205.0 | 0.106833 | 26795.0 |
40000 | 4405.0 | 0.110125 | 35595.0 |
... | ... | ... | ... |
960000 | 291722.5 | 0.303878 | 668277.5 |
970000 | 295422.5 | 0.304559 | 674577.5 |
980000 | 299122.5 | 0.305227 | 680877.5 |
990000 | 302822.5 | 0.305881 | 687177.5 |
1000000 | 306522.5 | 0.306522 | 693477.5 |
101 rows × 3 columns
def plot_tax_details(incomes_df: pd.DataFrame, filing_status: str, brackets_df: pd.DataFrame = None) -> None:
def add_vertical_lines(color: str = "red") -> None:
for ub in brackets_df["At Least"].to_list():
if ub < float("inf"):
plt.axvline(ub, color=color, linestyle="dotted")
def plot_tax_graph(col: str, ylim: tuple = None):
ax = incomes_df[col].plot(
xlabel="Taxable Income",
ylabel=col,
title=f"2021 IRS Federal Form 1040\nTaxable Income vs {col.replace('_', ' ')}\nAcross Tax Brackets (Red Dotted Lines)\nFiling Status: {filing_status.title()}",
ylim=ylim,
)
if brackets_df is not None:
add_vertical_lines()
ax.get_xaxis().set_major_formatter(FuncFormatter(lambda x, p: f"${format(int(x), ',')}"))
if col == 'Tax Owed as % of Taxable Income':
ax.get_yaxis().set_major_formatter(PercentFormatter(xmax=1, decimals=0))
else:
ax.get_yaxis().set_major_formatter(FuncFormatter(lambda x, p: f"${format(int(x), ',')}"))
ax.legend(loc='lower right')
plt.xticks(range(0, 1100000, 100000), rotation=-90)
fig = plt.gcf()
plt.show()
return fig
figs = []
for col in incomes_df:
if col == "Tax Owed":
ylim = (0, 350000)
elif col == "Tax Owed as % of Taxable Income":
ylim = (0, .35)
elif col == "Remaining Post-Tax":
ylim = (0, 750000)
else:
ylim = None
fig = plot_tax_graph(col, ylim)
figs.append(fig)
return figs
indiv_figs = plot_tax_details(indiv_tax, "single", indiv_tax_brackets)
# ## save figs
# for i, fig in enumerate(indiv_figs):
# fig.savefig(f"figs/single/fig{i}.png", bbox_inches="tight")
mfj_figs = plot_tax_details(mfj_tax, "married filing jointly", mfj_tax_brackets)