{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Income Taxes\n",
"- https://en.wikipedia.org/wiki/Income_tax\n",
"- https://en.wikipedia.org/wiki/Income_tax_in_the_United_States\n",
"- https://en.wikipedia.org/wiki/Adjusted_gross_income\n",
"- https://en.wikipedia.org/wiki/Tax_bracket#Tax_brackets_in_the_United_States\n",
"- https://en.wikipedia.org/wiki/Rate_schedule_(federal_income_tax)\n",
" - https://www.nerdwallet.com/blog/taxes/federal-income-tax-brackets/\n",
"- https://en.wikipedia.org/wiki/Tax_deduction\n",
" - https://en.wikipedia.org/wiki/Itemized_deduction\n",
" - https://www.nerdwallet.com/blog/taxes/tax-deductions-tax-breaks/\n",
" - https://en.wikipedia.org/wiki/Tax_credit#United_States\n",
"- https://github.com/PSLmodels/Tax-Calculator/blob/master/taxcalc/calcfunctions.py\n",
"\n",
"## Self-employment Tax\n",
"- https://www.irs.gov/taxtopics/tc554\n",
"- https://www.irs.gov/help/ita/do-i-have-income-subject-to-self-employment-tax\n",
"- https://www.nerdwallet.com/blog/taxes/self-employment-tax/\n",
"- https://www.irs.gov/businesses/small-businesses-self-employed/questions-and-answers-for-the-additional-medicare-tax\n",
"\n",
"## Small-business Taxes\n",
"- \"Publication 334 (2018), Tax Guide for Small Business \n",
" (For Individuals Who Use Schedule C or C-EZ)\" \n",
" https://www.irs.gov/publications/p334"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"# RATE_SCHEDULE = {'single': [\n",
"# (max_amount, rate)\n",
"#]}"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"import bs4, requests, pandas as pd\n",
"import json\n",
"import pprint\n",
"import typing\n",
"from typing import Dict, Union\n",
"from numbers import Real\n",
"\n",
"# Create a simple ~structlog\n",
"import logging\n",
"log = logging.getLogger('tax.rateschedule')\n",
"log.setLevel(logging.DEBUG)\n",
"\n",
"def logg(*args, **kwargs):\n",
" output = None\n",
" if args and kwargs:\n",
" output = (args, kwargs)\n",
" elif args:\n",
" output = args if len(args) > 1 else args[0]\n",
" elif kwargs:\n",
" output = kwargs\n",
" #log.debug(json.dumps(output, indent=2)) # int64 is unserializable\n",
" # log.debug(pprint.pformat(output, sort_dicts=False)) # < py3.8 sorts dicts\n",
" log.debug(repr(output))\n",
"\n",
"# Cache HTTP requests\n",
"import requests_cache\n",
"requests_cache.install_cache('income_tax')\n",
"\n",
"import pytest\n",
"try:\n",
" get_ipython()\n",
" import ipytest\n",
" ipytest.config(rewrite_asserts=True, magics=True)\n",
" __file__ = \"income_taxes.ipynb\"\n",
"except:\n",
" pass"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"
\n",
"\n",
"
\n",
" \n",
" \n",
" | \n",
" Tax rate | \n",
" Single | \n",
" Married, filing jointly | \n",
" Married, filing separately | \n",
" Head of household | \n",
"
\n",
" \n",
" \n",
" \n",
" 0 | \n",
" 10% | \n",
" $0 to $9,700 | \n",
" $0 to $19,400 | \n",
" $0 to $9,700 | \n",
" $0 to $13,850 | \n",
"
\n",
" \n",
" 1 | \n",
" 12% | \n",
" $9,701 to $39,475 | \n",
" $19,401 to $78,950 | \n",
" $9,701 to $39,475 | \n",
" $13,851 to $52,850 | \n",
"
\n",
" \n",
" 2 | \n",
" 22% | \n",
" $39,476 to $84,200 | \n",
" $78,951 to $168,400 | \n",
" $39,476 to $84,200 | \n",
" $52,851 to $84,200 | \n",
"
\n",
" \n",
" 3 | \n",
" 24% | \n",
" $84,201 to $160,725 | \n",
" $168,401 to $321,450 | \n",
" $84,201 to $160,725 | \n",
" $84,201 to $160,700 | \n",
"
\n",
" \n",
" 4 | \n",
" 32% | \n",
" $160,726 to $204,100 | \n",
" $321,451 to $408,200 | \n",
" $160,726 to $204,100 | \n",
" $160,701 to $204,100 | \n",
"
\n",
" \n",
" 5 | \n",
" 35% | \n",
" $204,101 to $510,300 | \n",
" $408,201 to $612,350 | \n",
" $204,101 to $306,175 | \n",
" $204,101 to $510,300 | \n",
"
\n",
" \n",
" 6 | \n",
" 37% | \n",
" $510,301 or more | \n",
" $612,351 or more | \n",
" $306,176 or more | \n",
" $510,301 or more | \n",
"
\n",
" \n",
"
\n",
"
"
],
"text/plain": [
" Tax rate Single Married, filing jointly \\\n",
"0 10% $0 to $9,700 $0 to $19,400 \n",
"1 12% $9,701 to $39,475 $19,401 to $78,950 \n",
"2 22% $39,476 to $84,200 $78,951 to $168,400 \n",
"3 24% $84,201 to $160,725 $168,401 to $321,450 \n",
"4 32% $160,726 to $204,100 $321,451 to $408,200 \n",
"5 35% $204,101 to $510,300 $408,201 to $612,350 \n",
"6 37% $510,301 or more $612,351 or more \n",
"\n",
" Married, filing separately Head of household \n",
"0 $0 to $9,700 $0 to $13,850 \n",
"1 $9,701 to $39,475 $13,851 to $52,850 \n",
"2 $39,476 to $84,200 $52,851 to $84,200 \n",
"3 $84,201 to $160,725 $84,201 to $160,700 \n",
"4 $160,726 to $204,100 $160,701 to $204,100 \n",
"5 $204,101 to $306,175 $204,101 to $510,300 \n",
"6 $306,176 or more $510,301 or more "
]
},
"execution_count": 3,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"def get_tax_rate_table_from_nerdwallet(year=2019):\n",
" \"\"\"Parse the table from \n",
" \"\"\"\n",
" url = 'https://www.nerdwallet.com/blog/taxes/federal-income-tax-brackets/'\n",
" if year != 2019:\n",
" raise ValueError(f\"Year requested not supported. Check: {url}\")\n",
" resp = requests.get(url)\n",
" #bs = bs4.BeautifulSoup(resp.text)\n",
" #tables = bs.findAll('table', {'class': \"tablepress\"})\n",
" #tbl = tables[0]\n",
" df = pd.read_html(resp.text)[0]\n",
" return df\n",
"tax_rate_html_df = get_tax_rate_table_from_nerdwallet(year=2019)\n",
"tax_rate_html_df"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"\n",
"\n",
"
\n",
" \n",
" \n",
" | \n",
" tax rate | \n",
" single | \n",
" married, filing jointly | \n",
" married, filing separately | \n",
" head of household | \n",
" single min | \n",
" single max | \n",
" married, filing jointly min | \n",
" married, filing jointly max | \n",
" married, filing separately min | \n",
" married, filing separately max | \n",
" head of household min | \n",
" head of household max | \n",
"
\n",
" \n",
" \n",
" \n",
" 0 | \n",
" 0.10 | \n",
" 0 to 9700 | \n",
" 0 to 19400 | \n",
" 0 to 9700 | \n",
" 0 to 13850 | \n",
" 0 | \n",
" 9700 | \n",
" 0 | \n",
" 19400 | \n",
" 0 | \n",
" 9700 | \n",
" 0 | \n",
" 13850 | \n",
"
\n",
" \n",
" 1 | \n",
" 0.12 | \n",
" 9701 to 39475 | \n",
" 19401 to 78950 | \n",
" 9701 to 39475 | \n",
" 13851 to 52850 | \n",
" 9701 | \n",
" 39475 | \n",
" 19401 | \n",
" 78950 | \n",
" 9701 | \n",
" 39475 | \n",
" 13851 | \n",
" 52850 | \n",
"
\n",
" \n",
" 2 | \n",
" 0.22 | \n",
" 39476 to 84200 | \n",
" 78951 to 168400 | \n",
" 39476 to 84200 | \n",
" 52851 to 84200 | \n",
" 39476 | \n",
" 84200 | \n",
" 78951 | \n",
" 168400 | \n",
" 39476 | \n",
" 84200 | \n",
" 52851 | \n",
" 84200 | \n",
"
\n",
" \n",
" 3 | \n",
" 0.24 | \n",
" 84201 to 160725 | \n",
" 168401 to 321450 | \n",
" 84201 to 160725 | \n",
" 84201 to 160700 | \n",
" 84201 | \n",
" 160725 | \n",
" 168401 | \n",
" 321450 | \n",
" 84201 | \n",
" 160725 | \n",
" 84201 | \n",
" 160700 | \n",
"
\n",
" \n",
" 4 | \n",
" 0.32 | \n",
" 160726 to 204100 | \n",
" 321451 to 408200 | \n",
" 160726 to 204100 | \n",
" 160701 to 204100 | \n",
" 160726 | \n",
" 204100 | \n",
" 321451 | \n",
" 408200 | \n",
" 160726 | \n",
" 204100 | \n",
" 160701 | \n",
" 204100 | \n",
"
\n",
" \n",
" 5 | \n",
" 0.35 | \n",
" 204101 to 510300 | \n",
" 408201 to 612350 | \n",
" 204101 to 306175 | \n",
" 204101 to 510300 | \n",
" 204101 | \n",
" 510300 | \n",
" 408201 | \n",
" 612350 | \n",
" 204101 | \n",
" 306175 | \n",
" 204101 | \n",
" 510300 | \n",
"
\n",
" \n",
" 6 | \n",
" 0.37 | \n",
" 510301 or more | \n",
" 612351 or more | \n",
" 306176 or more | \n",
" 510301 or more | \n",
" 510301 | \n",
" -1 | \n",
" 612351 | \n",
" -1 | \n",
" 306176 | \n",
" -1 | \n",
" 510301 | \n",
" -1 | \n",
"
\n",
" \n",
"
\n",
"
"
],
"text/plain": [
" tax rate single married, filing jointly \\\n",
"0 0.10 0 to 9700 0 to 19400 \n",
"1 0.12 9701 to 39475 19401 to 78950 \n",
"2 0.22 39476 to 84200 78951 to 168400 \n",
"3 0.24 84201 to 160725 168401 to 321450 \n",
"4 0.32 160726 to 204100 321451 to 408200 \n",
"5 0.35 204101 to 510300 408201 to 612350 \n",
"6 0.37 510301 or more 612351 or more \n",
"\n",
" married, filing separately head of household single min single max \\\n",
"0 0 to 9700 0 to 13850 0 9700 \n",
"1 9701 to 39475 13851 to 52850 9701 39475 \n",
"2 39476 to 84200 52851 to 84200 39476 84200 \n",
"3 84201 to 160725 84201 to 160700 84201 160725 \n",
"4 160726 to 204100 160701 to 204100 160726 204100 \n",
"5 204101 to 306175 204101 to 510300 204101 510300 \n",
"6 306176 or more 510301 or more 510301 -1 \n",
"\n",
" married, filing jointly min married, filing jointly max \\\n",
"0 0 19400 \n",
"1 19401 78950 \n",
"2 78951 168400 \n",
"3 168401 321450 \n",
"4 321451 408200 \n",
"5 408201 612350 \n",
"6 612351 -1 \n",
"\n",
" married, filing separately min married, filing separately max \\\n",
"0 0 9700 \n",
"1 9701 39475 \n",
"2 39476 84200 \n",
"3 84201 160725 \n",
"4 160726 204100 \n",
"5 204101 306175 \n",
"6 306176 -1 \n",
"\n",
" head of household min head of household max \n",
"0 0 13850 \n",
"1 13851 52850 \n",
"2 52851 84200 \n",
"3 84201 160700 \n",
"4 160701 204100 \n",
"5 204101 510300 \n",
"6 510301 -1 "
]
},
"execution_count": 4,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"def clean_and_split_column(row):\n",
" x = str(row)\n",
" values = None\n",
" if \" to \" in x:\n",
" values = x.split(\" to \", 1)\n",
" elif \" or \" in x:\n",
" values = [x.split(\" or \", 1)[0], \"-1\"]\n",
" else:\n",
" raise ValueError((row, row.name, values))\n",
" if len(values) == 1:\n",
" raise ValueError((x, values))\n",
" return pd.Series(int(x) for x in values)\n",
"\n",
"def reshape_tax_rate_table_from_nerdwallet(df):\n",
" df.columns = [col.lower() for col in df.columns]\n",
"\n",
" colnames = ['single', 'married, filing jointly',\n",
" 'married, filing separately', 'head of household']\n",
" for col in [c for c in colnames if c in df.columns]:\n",
" df[col] = df[col].apply(lambda x: x.replace(\"$\", \"\").replace(\",\",\"\"))\n",
" cols = [f\"{col} min\", f\"{col} max\"]\n",
" output = df[col].apply(clean_and_split_column)\n",
" df[cols] = output\n",
" #print(df[cols])\n",
" df['tax rate'] = pd.to_numeric(df['tax rate'].str.rstrip('%')) / 100.0\n",
" return df\n",
"\n",
"tax_rate_df = reshape_tax_rate_table_from_nerdwallet(tax_rate_html_df)\n",
"tax_rate_df"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'taxable_income': 1000000,\n",
" 'taxes_due': 303747.0,\n",
" 'tax_bracket': 0.35,\n",
" 'after_tax_income': None,\n",
" 'effective_tax_rate': None,\n",
" 'levels': [(9700.0, 0.1),\n",
" (39475.0, 0.12),\n",
" (84200.0, 0.22),\n",
" (160725.0, 0.24),\n",
" (204100.0, 0.32),\n",
" (501800.0, 0.35)],\n",
" 'remaining': 0.0}"
]
},
"execution_count": 5,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"log.setLevel(logging.INFO)\n",
"class RateSchedule_USA:\n",
" COUNTRY = 'USA'\n",
" FILING_STATUSES = dict.fromkeys((\n",
" 'single', 'married, filing jointly',\n",
" 'married, filing separately', 'head of household'\n",
" ))\n",
" def __init__(self, df: pd.DataFrame):\n",
" self.df = df\n",
" \n",
" def get_rate(self, taxable_income: Real, filingstatus: str) -> dict:\n",
" \"\"\"\n",
" Args:\n",
" taxable_income (Number): Taxable Income\n",
" filing_status (str): Filing Status\n",
" Returns:\n",
" dict: ``{taxes_due, tax_bracket, after_tax_income, effective_tax_rate}``\n",
" \"\"\"\n",
" _filingstatus = filingstatus.lower()\n",
" if _filingstatus not in self.FILING_STATUSES:\n",
" raise ValueError(f\"{filingstatus} is not in {self.FILING_STATUSES}\")\n",
" \n",
" ctx = dict()\n",
" ctx['taxable_income']: Number = taxable_income\n",
" ctx['taxes_due']: Number = None\n",
" ctx['tax_bracket']: Number = None\n",
" ctx['after_tax_income']: Number = None\n",
" ctx['effective_tax_rate']: Number = None\n",
" \n",
" ctx['levels']: List[amount: Number, tax_rate: Number] = []\n",
" remaining: Number = ctx['taxable_income']\n",
" thislevel: Union[Number,None] = None\n",
" \n",
" cols = self.df[\n",
" [\"tax rate\", f\"{filingstatus} min\", f\"{filingstatus} max\"]] \n",
" for idx, (rate, min_, max_) in cols.iterrows():\n",
" logg(remaining=remaining, thislevel=thislevel, thisrate=rate, ctx=ctx)\n",
" # TODO: is this correct?\n",
" if remaining >= 0:\n",
" if remaining <= max_:\n",
" thislevel = remaining\n",
" elif remaining > max_:\n",
" thislevel = max_\n",
" remaining = remaining - thislevel\n",
" ctx['levels'].append((thislevel, rate))\n",
" ctx['tax_bracket'] = rate\n",
" ctx['taxes_due'] = (\n",
" (ctx['taxes_due'] if ctx['taxes_due'] is not None else 0) +\n",
" thislevel * rate)\n",
" if remaining == 0:\n",
" break\n",
" logg(remaining=remaining, thislevel=thislevel, thisrate=rate, ctx=ctx)\n",
" ctx['remaining'] = remaining\n",
" if remaining:\n",
" raise Exception(\"Remaining is > 0\", remaining)\n",
" return ctx\n",
"\n",
"def calculate_etr_aft(data: Dict) -> Dict:\n",
" ctx = {}\n",
" ctx['effective_tax_rate']: Number = (\n",
" data['taxes_due'] / data['income'] if data['income'] else 0)\n",
" ctx['after_tax_income']: Number = data['income'] - data['taxes_due']\n",
" return ctx\n",
"\n",
"\n",
"rate_schedules = dict()\n",
"rate_schedules['USA'] = {}\n",
"rate_schedules['USA'][2019] = RateSchedule_USA(tax_rate_df) # ()\n",
"\n",
"RATE_SCHEDULE = rate_schedules['USA'][2019]\n",
"RATE_SCHEDULE.get_rate(1_000_000, 'single')"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"\n",
"\n",
"
\n",
" \n",
" \n",
" | \n",
" Filing status | \n",
" 2019 tax year | \n",
" 2020 tax year | \n",
"
\n",
" \n",
" \n",
" \n",
" 0 | \n",
" Single | \n",
" $12,200 | \n",
" $12,400 | \n",
"
\n",
" \n",
" 1 | \n",
" Married, filing jointly | \n",
" $24,400 | \n",
" $24,800 | \n",
"
\n",
" \n",
" 2 | \n",
" Married, filing separately | \n",
" $12,200 | \n",
" $12,400 | \n",
"
\n",
" \n",
" 3 | \n",
" Head of household | \n",
" $18,350 | \n",
" $18,650 | \n",
"
\n",
" \n",
"
\n",
"
"
],
"text/plain": [
" Filing status 2019 tax year 2020 tax year\n",
"0 Single $12,200 $12,400\n",
"1 Married, filing jointly $24,400 $24,800\n",
"2 Married, filing separately $12,200 $12,400\n",
"3 Head of household $18,350 $18,650"
]
},
"execution_count": 6,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"url = 'https://www.nerdwallet.com/blog/taxes/tax-deductions-tax-breaks/'\n",
"resp = requests.get(url)\n",
"std_deduction_html_df = pd.read_html(resp.text)[1]\n",
"std_deduction_html_df"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"\n",
"\n",
"
\n",
" \n",
" \n",
" | \n",
" single | \n",
" married, filing jointly | \n",
" married, filing separately | \n",
" head of household | \n",
"
\n",
" \n",
" \n",
" \n",
" 2019 | \n",
" 12200 | \n",
" 24400 | \n",
" 12200 | \n",
" 18350 | \n",
"
\n",
" \n",
" 2020 | \n",
" 12400 | \n",
" 24800 | \n",
" 12400 | \n",
" 18650 | \n",
"
\n",
" \n",
"
\n",
"
"
],
"text/plain": [
" single married, filing jointly married, filing separately \\\n",
"2019 12200 24400 12200 \n",
"2020 12400 24800 12400 \n",
"\n",
" head of household \n",
"2019 18350 \n",
"2020 18650 "
]
},
"execution_count": 7,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"def reshape_std_deduction_tbl(df):\n",
" df.columns = [col.split()[0] for col in df.columns]\n",
" df = df.T\n",
" df.columns = [col.lower() for col in df.iloc[0]]\n",
" df = df.iloc[1:]\n",
" for col in df.columns:\n",
" df[col] = pd.to_numeric(df[col].apply(lambda x: x.replace(\",\",\"\").lstrip(\"$\")))\n",
" df.index = pd.to_numeric(df.index)\n",
" return df\n",
" \n",
"std_deduction_df = reshape_std_deduction_tbl(std_deduction_html_df)\n",
"std_deduction_df"
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'income': 0,\n",
" 'year': 2019,\n",
" 'filingstatus': 'single',\n",
" 'standard_deduction': 12200,\n",
" 'agi': 0}"
]
},
"execution_count": 8,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"def calculate_agi(income: Real, filingstatus: str, year=2019,\n",
" std_deduction_df: pd.DataFrame=std_deduction_df) -> Real:\n",
" ctx = {}\n",
" ctx['income']: Real = income\n",
" ctx['year']: int = year\n",
" ctx['filingstatus']: str = filingstatus\n",
"\n",
" try:\n",
" ctx['standard_deduction']: Real = std_deduction_df[filingstatus][year]\n",
" except ValueError as e:\n",
" raise\n",
" ctx['agi']: Real = income\n",
" if income <= ctx['standard_deduction']:\n",
" ctx['agi'] = 0\n",
" else:\n",
" ctx['agi'] = income - ctx['standard_deduction']\n",
" return ctx\n",
"\n",
"calculate_agi(100_000, filingstatus='single', std_deduction_df=std_deduction_df)\n",
"calculate_agi(0, filingstatus='single', std_deduction_df=std_deduction_df)"
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {},
"outputs": [],
"source": [
"def calculate_taxes(income: Real, filingstatus: str=None, rate_schedule=RATE_SCHEDULE):\n",
" ctx = dict()\n",
" ctx['income'] = income\n",
" ctx.update(calculate_agi(income, filingstatus=filingstatus))\n",
" ctx['deductions'] = 0\n",
" ctx['taxable_income'] = ctx['agi'] - ctx['deductions']\n",
" ctx.update(rate_schedule.get_rate(ctx['taxable_income'], filingstatus=filingstatus))\n",
" ctx.update(calculate_etr_aft(ctx))\n",
" assert ctx['income'] == income\n",
" return ctx"
]
},
{
"cell_type": "code",
"execution_count": 17,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"============================ test session starts =============================\n",
"platform linux -- Python 3.7.3, pytest-5.2.2, py-1.8.0, pluggy-0.13.0\n",
"rootdir: /home/wturner/-wrk/-ce36/math/src/notebooks/personalfinance\n",
"plugins: cov-2.8.1\n",
"collected 3 items\n",
"\n",
"income_taxes.py "
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"DEBUG:tax.rateschedule:{'remaining': 0, 'thislevel': None, 'thisrate': 0.1, 'ctx': {'taxable_income': 0, 'taxes_due': None, 'tax_bracket': None, 'after_tax_income': None, 'effective_tax_rate': None, 'levels': []}}\n",
"DEBUG:tax.rateschedule:{'remaining': 0, 'thislevel': 0, 'thisrate': 0.1, 'ctx': {'taxable_income': 0, 'taxes_due': 0.0, 'tax_bracket': 0.1, 'after_tax_income': None, 'effective_tax_rate': None, 'levels': [(0, 0.1)]}}\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"."
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"DEBUG:tax.rateschedule:{'remaining': 0, 'thislevel': None, 'thisrate': 0.1, 'ctx': {'taxable_income': 0, 'taxes_due': None, 'tax_bracket': None, 'after_tax_income': None, 'effective_tax_rate': None, 'levels': []}}\n",
"DEBUG:tax.rateschedule:{'remaining': 0, 'thislevel': 0, 'thisrate': 0.1, 'ctx': {'taxable_income': 0, 'taxes_due': 0.0, 'tax_bracket': 0.1, 'after_tax_income': None, 'effective_tax_rate': None, 'levels': [(0, 0.1)]}}\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"."
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"DEBUG:tax.rateschedule:{'remaining': 87800, 'thislevel': None, 'thisrate': 0.1, 'ctx': {'taxable_income': 87800, 'taxes_due': None, 'tax_bracket': None, 'after_tax_income': None, 'effective_tax_rate': None, 'levels': []}}\n",
"DEBUG:tax.rateschedule:{'remaining': 78100.0, 'thislevel': 9700.0, 'thisrate': 0.12, 'ctx': {'taxable_income': 87800, 'taxes_due': 970.0, 'tax_bracket': 0.1, 'after_tax_income': None, 'effective_tax_rate': None, 'levels': [(9700.0, 0.1)]}}\n",
"DEBUG:tax.rateschedule:{'remaining': 38625.0, 'thislevel': 39475.0, 'thisrate': 0.22, 'ctx': {'taxable_income': 87800, 'taxes_due': 5707.0, 'tax_bracket': 0.12, 'after_tax_income': None, 'effective_tax_rate': None, 'levels': [(9700.0, 0.1), (39475.0, 0.12)]}}\n",
"DEBUG:tax.rateschedule:{'remaining': 0.0, 'thislevel': 38625.0, 'thisrate': 0.22, 'ctx': {'taxable_income': 87800, 'taxes_due': 14204.5, 'tax_bracket': 0.22, 'after_tax_income': None, 'effective_tax_rate': None, 'levels': [(9700.0, 0.1), (39475.0, 0.12), (38625.0, 0.22)]}}\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"F [100%]\n",
"\n",
"================================== FAILURES ==================================\n",
"________________ test_calculate_taxes[args2-expected_output2] ________________\n",
"\n",
"args = (100000, 'single')\n",
"expected_output = {'after_tax_income': 1, 'agi': 87800, 'effective_tax_rate': 0, 'income': 100000, ...}\n",
"\n",
" @pytest.mark.parametrize('args,expected_output',[\n",
" ((0, 'single'), dict(income=0, agi=0, taxes_due=0, tax_bracket=0.1, after_tax_income=0, effective_tax_rate=0)),\n",
" ((1, 'single'), dict(income=1, agi=0, taxes_due=0, tax_bracket=0.1, after_tax_income=1, effective_tax_rate=0)),\n",
" ((100_000, 'single'), dict(income=100_000, agi=100_000-stdded_single, taxes_due=15_246, tax_bracket=0.1, after_tax_income=1, effective_tax_rate=0)),\n",
" \n",
" ])\n",
" def test_calculate_taxes(args, expected_output):\n",
" output = calculate_taxes(*args)\n",
" assert 'income' in output\n",
" assert output['income'] == args[0]\n",
" assert 'filingstatus' in output\n",
" assert output['filingstatus'] == args[1]\n",
" assert 'agi' in output\n",
" assert 'deductions' in output\n",
" assert 'taxable_income' in output\n",
" assert 'tax_bracket' in output\n",
" assert 'taxes_due' in output\n",
" assert 'after_tax_income' in output\n",
" assert 'effective_tax_rate' in output\n",
" print(output['standard_deduction'])\n",
" assert sum(l[0] for l in output['levels']) == output['taxable_income']\n",
" assert output['levels'][-1][1] == output['tax_bracket']\n",
"> assert {key:output[key] for key in expected_output} == expected_output\n",
"E AssertionError: assert {'after_tax_i...: 100000, ...} == {'after_tax_i...: 100000, ...}\n",
"E Omitting 2 identical items, use -vv to show\n",
"E Differing items:\n",
"E {'taxes_due': 14204.5} != {'taxes_due': 15246}\n",
"E {'tax_bracket': 0.22} != {'tax_bracket': 0.1}\n",
"E {'after_tax_income': 85795.5} != {'after_tax_income': 1}\n",
"E {'effective_tax_rate': 0.142045} != {'effective_tax_rate': 0}\n",
"E Use -v to get the full diff\n",
"\n",
":25: AssertionError\n",
"---------------------------- Captured stdout call ----------------------------\n",
"12200\n",
"----------------------------- Captured log call ------------------------------\n",
"DEBUG tax.rateschedule::23 {'remaining': 87800, 'thislevel': None, 'thisrate': 0.1, 'ctx': {'taxable_income': 87800, 'taxes_due': None, 'tax_bracket': None, 'after_tax_income': None, 'effective_tax_rate': None, 'levels': []}}\n",
"DEBUG tax.rateschedule::23 {'remaining': 78100.0, 'thislevel': 9700.0, 'thisrate': 0.12, 'ctx': {'taxable_income': 87800, 'taxes_due': 970.0, 'tax_bracket': 0.1, 'after_tax_income': None, 'effective_tax_rate': None, 'levels': [(9700.0, 0.1)]}}\n",
"DEBUG tax.rateschedule::23 {'remaining': 38625.0, 'thislevel': 39475.0, 'thisrate': 0.22, 'ctx': {'taxable_income': 87800, 'taxes_due': 5707.0, 'tax_bracket': 0.12, 'after_tax_income': None, 'effective_tax_rate': None, 'levels': [(9700.0, 0.1), (39475.0, 0.12)]}}\n",
"DEBUG tax.rateschedule::23 {'remaining': 0.0, 'thislevel': 38625.0, 'thisrate': 0.22, 'ctx': {'taxable_income': 87800, 'taxes_due': 14204.5, 'tax_bracket': 0.22, 'after_tax_income': None, 'effective_tax_rate': None, 'levels': [(9700.0, 0.1), (39475.0, 0.12), (38625.0, 0.22)]}}\n",
"======================== 1 failed, 2 passed in 0.20s =========================\n"
]
}
],
"source": [
"%%run_pytest[clean]\n",
"log.setLevel(logging.DEBUG)\n",
"stdded_single = std_deduction_df['single'][2019]\n",
"@pytest.mark.parametrize('args,expected_output',[\n",
" ((0, 'single'), dict(income=0, agi=0, taxes_due=0, tax_bracket=0.1, after_tax_income=0, effective_tax_rate=0)),\n",
" ((1, 'single'), dict(income=1, agi=0, taxes_due=0, tax_bracket=0.1, after_tax_income=1, effective_tax_rate=0)),\n",
" ((100_000, 'single'), dict(income=100_000, agi=100_000-stdded_single, taxes_due=15_246, tax_bracket=0.1, after_tax_income=1, effective_tax_rate=0)),\n",
"\n",
"])\n",
"def test_calculate_taxes(args, expected_output):\n",
" output = calculate_taxes(*args)\n",
" assert 'income' in output\n",
" assert output['income'] == args[0]\n",
" assert 'filingstatus' in output\n",
" assert output['filingstatus'] == args[1]\n",
" assert 'agi' in output\n",
" assert 'deductions' in output\n",
" assert 'taxable_income' in output\n",
" assert 'tax_bracket' in output\n",
" assert 'taxes_due' in output\n",
" assert 'after_tax_income' in output\n",
" assert 'effective_tax_rate' in output\n",
" print(output['standard_deduction'])\n",
" assert sum(l[0] for l in output['levels']) == output['taxable_income']\n",
" assert output['levels'][-1][1] == output['tax_bracket']\n",
" assert {key:output[key] for key in expected_output} == expected_output"
]
},
{
"cell_type": "code",
"execution_count": 14,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"0.1751480637813212"
]
},
"execution_count": 14,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"15_378 / 87_800"
]
}
],
"metadata": {
"jupytext": {
"formats": "ipynb,py,md"
},
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.7.3"
}
},
"nbformat": 4,
"nbformat_minor": 4
}