{ "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", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
Tax rateSingleMarried, filing jointlyMarried, filing separatelyHead of household
010%$0 to $9,700$0 to $19,400$0 to $9,700$0 to $13,850
112%$9,701 to $39,475$19,401 to $78,950$9,701 to $39,475$13,851 to $52,850
222%$39,476 to $84,200$78,951 to $168,400$39,476 to $84,200$52,851 to $84,200
324%$84,201 to $160,725$168,401 to $321,450$84,201 to $160,725$84,201 to $160,700
432%$160,726 to $204,100$321,451 to $408,200$160,726 to $204,100$160,701 to $204,100
535%$204,101 to $510,300$408,201 to $612,350$204,101 to $306,175$204,101 to $510,300
637%$510,301 or more$612,351 or more$306,176 or more$510,301 or more
\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", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
tax ratesinglemarried, filing jointlymarried, filing separatelyhead of householdsingle minsingle maxmarried, filing jointly minmarried, filing jointly maxmarried, filing separately minmarried, filing separately maxhead of household minhead of household max
00.100 to 97000 to 194000 to 97000 to 138500970001940009700013850
10.129701 to 3947519401 to 789509701 to 3947513851 to 5285097013947519401789509701394751385152850
20.2239476 to 8420078951 to 16840039476 to 8420052851 to 8420039476842007895116840039476842005285184200
30.2484201 to 160725168401 to 32145084201 to 16072584201 to 160700842011607251684013214508420116072584201160700
40.32160726 to 204100321451 to 408200160726 to 204100160701 to 204100160726204100321451408200160726204100160701204100
50.35204101 to 510300408201 to 612350204101 to 306175204101 to 510300204101510300408201612350204101306175204101510300
60.37510301 or more612351 or more306176 or more510301 or more510301-1612351-1306176-1510301-1
\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", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
Filing status2019 tax year2020 tax year
0Single$12,200$12,400
1Married, filing jointly$24,400$24,800
2Married, filing separately$12,200$12,400
3Head of household$18,350$18,650
\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", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
singlemarried, filing jointlymarried, filing separatelyhead of household
201912200244001220018350
202012400248001240018650
\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 }