Written by Douglas Issichopoulos dougissi.com in Nov 2022

Imports¶

In [1]:
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.ticker import FuncFormatter, PercentFormatter

Tax Table Data¶

In [2]:
# source: https://sites.google.com/view/incometaxspreadsheet/home
tax_table = pd.read_csv('2021_1040_tax_table.csv')
tax_table
Out[2]:
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

In [3]:
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"]]
In [4]:
indiv_limited_tax_table = format_limited_tax_table(tax_table, "Single")
indiv_limited_tax_table
Out[4]:
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
In [5]:
mfj_limited_tax_table = format_limited_tax_table(tax_table, "Married filing jointly")
mfj_limited_tax_table
Out[5]:
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

Tax Brackets Data¶

Single Individuals¶

In [6]:
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
Out[6]:
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

Married Filing Jointly¶

In [7]:
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
Out[7]:
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

add tax columns¶

In [8]:
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
In [9]:
indiv_incomes_w_tax = add_tax_columns(indiv_tax_brackets)
indiv_incomes_w_tax
Out[9]:
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

In [10]:
mfj_incomes_w_tax = add_tax_columns(mfj_tax_brackets)
mfj_incomes_w_tax
Out[10]:
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

Combine Tax Table and Tax Bracket Data¶

In [11]:
indiv_tax = pd.concat([indiv_limited_tax_table, indiv_incomes_w_tax])
indiv_tax
Out[11]:
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

In [12]:
mfj_tax = pd.concat([mfj_limited_tax_table, mfj_incomes_w_tax])
mfj_tax
Out[12]:
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

Plots¶

In [13]:
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
In [14]:
indiv_figs = plot_tax_details(indiv_tax, "single", indiv_tax_brackets)
In [15]:
# ## save figs
# for i, fig in enumerate(indiv_figs):
#     fig.savefig(f"figs/single/fig{i}.png", bbox_inches="tight")
In [16]:
mfj_figs = plot_tax_details(mfj_tax, "married filing jointly", mfj_tax_brackets)
In [17]:
# ## save figs
# for i, fig in enumerate(mfj_figs):
#     fig.savefig(f"figs/mfj/fig{i}.png", bbox_inches="tight")