Created
July 28, 2025 06:26
-
-
Save MaxGhenis/279c9199ae42d435acea4f99bb324387 to your computer and use it in GitHub Desktop.
NH Tax Comparison: PolicyEngine vs TAXSIM 35
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| { | |
| "cells": [ | |
| { | |
| "cell_type": "markdown", | |
| "metadata": {}, | |
| "source": [ | |
| "# New Hampshire Tax: PolicyEngine vs TAXSIM Comparison\n", | |
| "\n", | |
| "This notebook compares PolicyEngine's implementation of New Hampshire's Interest and Dividends tax with TAXSIM 35." | |
| ] | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "metadata": {}, | |
| "source": [ | |
| "## 1. Setup and Test Cases\n", | |
| "\n", | |
| "First, let's define our test cases covering different years and filing statuses." | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": 1, | |
| "metadata": { | |
| "execution": { | |
| "iopub.execute_input": "2025-07-28T06:26:28.083209Z", | |
| "iopub.status.busy": "2025-07-28T06:26:28.083097Z", | |
| "iopub.status.idle": "2025-07-28T06:26:28.902186Z", | |
| "shell.execute_reply": "2025-07-28T06:26:28.901908Z" | |
| } | |
| }, | |
| "outputs": [ | |
| { | |
| "data": { | |
| "text/html": [ | |
| "<div>\n", | |
| "<style scoped>\n", | |
| " .dataframe tbody tr th:only-of-type {\n", | |
| " vertical-align: middle;\n", | |
| " }\n", | |
| "\n", | |
| " .dataframe tbody tr th {\n", | |
| " vertical-align: top;\n", | |
| " }\n", | |
| "\n", | |
| " .dataframe thead th {\n", | |
| " text-align: right;\n", | |
| " }\n", | |
| "</style>\n", | |
| "<table border=\"1\" class=\"dataframe\">\n", | |
| " <thead>\n", | |
| " <tr style=\"text-align: right;\">\n", | |
| " <th></th>\n", | |
| " <th>id</th>\n", | |
| " <th>year</th>\n", | |
| " <th>status</th>\n", | |
| " <th>age</th>\n", | |
| " <th>interest</th>\n", | |
| " <th>dividends</th>\n", | |
| " </tr>\n", | |
| " </thead>\n", | |
| " <tbody>\n", | |
| " <tr>\n", | |
| " <th>0</th>\n", | |
| " <td>1</td>\n", | |
| " <td>2020</td>\n", | |
| " <td>Single</td>\n", | |
| " <td>40</td>\n", | |
| " <td>5000</td>\n", | |
| " <td>5000</td>\n", | |
| " </tr>\n", | |
| " <tr>\n", | |
| " <th>1</th>\n", | |
| " <td>2</td>\n", | |
| " <td>2020</td>\n", | |
| " <td>Joint</td>\n", | |
| " <td>40</td>\n", | |
| " <td>3000</td>\n", | |
| " <td>2000</td>\n", | |
| " </tr>\n", | |
| " <tr>\n", | |
| " <th>2</th>\n", | |
| " <td>3</td>\n", | |
| " <td>2020</td>\n", | |
| " <td>Single</td>\n", | |
| " <td>40</td>\n", | |
| " <td>10000</td>\n", | |
| " <td>10000</td>\n", | |
| " </tr>\n", | |
| " <tr>\n", | |
| " <th>3</th>\n", | |
| " <td>4</td>\n", | |
| " <td>1990</td>\n", | |
| " <td>Single</td>\n", | |
| " <td>40</td>\n", | |
| " <td>5000</td>\n", | |
| " <td>5000</td>\n", | |
| " </tr>\n", | |
| " <tr>\n", | |
| " <th>4</th>\n", | |
| " <td>5</td>\n", | |
| " <td>1990</td>\n", | |
| " <td>Joint</td>\n", | |
| " <td>40</td>\n", | |
| " <td>3000</td>\n", | |
| " <td>2000</td>\n", | |
| " </tr>\n", | |
| " <tr>\n", | |
| " <th>5</th>\n", | |
| " <td>6</td>\n", | |
| " <td>1980</td>\n", | |
| " <td>Single</td>\n", | |
| " <td>40</td>\n", | |
| " <td>5000</td>\n", | |
| " <td>5000</td>\n", | |
| " </tr>\n", | |
| " <tr>\n", | |
| " <th>6</th>\n", | |
| " <td>7</td>\n", | |
| " <td>1980</td>\n", | |
| " <td>Joint</td>\n", | |
| " <td>40</td>\n", | |
| " <td>3000</td>\n", | |
| " <td>2000</td>\n", | |
| " </tr>\n", | |
| " <tr>\n", | |
| " <th>7</th>\n", | |
| " <td>8</td>\n", | |
| " <td>2020</td>\n", | |
| " <td>Single</td>\n", | |
| " <td>70</td>\n", | |
| " <td>5000</td>\n", | |
| " <td>5000</td>\n", | |
| " </tr>\n", | |
| " <tr>\n", | |
| " <th>8</th>\n", | |
| " <td>9</td>\n", | |
| " <td>2020</td>\n", | |
| " <td>Joint</td>\n", | |
| " <td>70</td>\n", | |
| " <td>5000</td>\n", | |
| " <td>5000</td>\n", | |
| " </tr>\n", | |
| " </tbody>\n", | |
| "</table>\n", | |
| "</div>" | |
| ], | |
| "text/plain": [ | |
| " id year status age interest dividends\n", | |
| "0 1 2020 Single 40 5000 5000\n", | |
| "1 2 2020 Joint 40 3000 2000\n", | |
| "2 3 2020 Single 40 10000 10000\n", | |
| "3 4 1990 Single 40 5000 5000\n", | |
| "4 5 1990 Joint 40 3000 2000\n", | |
| "5 6 1980 Single 40 5000 5000\n", | |
| "6 7 1980 Joint 40 3000 2000\n", | |
| "7 8 2020 Single 70 5000 5000\n", | |
| "8 9 2020 Joint 70 5000 5000" | |
| ] | |
| }, | |
| "execution_count": 1, | |
| "metadata": {}, | |
| "output_type": "execute_result" | |
| } | |
| ], | |
| "source": [ | |
| "import pandas as pd\n", | |
| "import numpy as np\n", | |
| "\n", | |
| "# Define test cases\n", | |
| "test_cases = [\n", | |
| " {'id': 1, 'year': 2020, 'status': 'Single', 'age': 40, 'interest': 5000, 'dividends': 5000},\n", | |
| " {'id': 2, 'year': 2020, 'status': 'Joint', 'age': 40, 'interest': 3000, 'dividends': 2000},\n", | |
| " {'id': 3, 'year': 2020, 'status': 'Single', 'age': 40, 'interest': 10000, 'dividends': 10000},\n", | |
| " {'id': 4, 'year': 1990, 'status': 'Single', 'age': 40, 'interest': 5000, 'dividends': 5000},\n", | |
| " {'id': 5, 'year': 1990, 'status': 'Joint', 'age': 40, 'interest': 3000, 'dividends': 2000},\n", | |
| " {'id': 6, 'year': 1980, 'status': 'Single', 'age': 40, 'interest': 5000, 'dividends': 5000},\n", | |
| " {'id': 7, 'year': 1980, 'status': 'Joint', 'age': 40, 'interest': 3000, 'dividends': 2000},\n", | |
| " {'id': 8, 'year': 2020, 'status': 'Single', 'age': 70, 'interest': 5000, 'dividends': 5000},\n", | |
| " {'id': 9, 'year': 2020, 'status': 'Joint', 'age': 70, 'interest': 5000, 'dividends': 5000},\n", | |
| "]\n", | |
| "\n", | |
| "test_df = pd.DataFrame(test_cases)\n", | |
| "test_df" | |
| ] | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "metadata": {}, | |
| "source": [ | |
| "## 2. TAXSIM Results\n", | |
| "\n", | |
| "I ran these test cases through TAXSIM 35. Here are the results:" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": 2, | |
| "metadata": { | |
| "execution": { | |
| "iopub.execute_input": "2025-07-28T06:26:28.919066Z", | |
| "iopub.status.busy": "2025-07-28T06:26:28.918903Z", | |
| "iopub.status.idle": "2025-07-28T06:26:28.925176Z", | |
| "shell.execute_reply": "2025-07-28T06:26:28.924950Z" | |
| } | |
| }, | |
| "outputs": [ | |
| { | |
| "data": { | |
| "text/html": [ | |
| "<div>\n", | |
| "<style scoped>\n", | |
| " .dataframe tbody tr th:only-of-type {\n", | |
| " vertical-align: middle;\n", | |
| " }\n", | |
| "\n", | |
| " .dataframe tbody tr th {\n", | |
| " vertical-align: top;\n", | |
| " }\n", | |
| "\n", | |
| " .dataframe thead th {\n", | |
| " text-align: right;\n", | |
| " }\n", | |
| "</style>\n", | |
| "<table border=\"1\" class=\"dataframe\">\n", | |
| " <thead>\n", | |
| " <tr style=\"text-align: right;\">\n", | |
| " <th></th>\n", | |
| " <th>id</th>\n", | |
| " <th>year</th>\n", | |
| " <th>siitax</th>\n", | |
| " <th>nh_income</th>\n", | |
| " <th>nh_exemptions</th>\n", | |
| " <th>nh_taxable</th>\n", | |
| " </tr>\n", | |
| " </thead>\n", | |
| " <tbody>\n", | |
| " <tr>\n", | |
| " <th>0</th>\n", | |
| " <td>1</td>\n", | |
| " <td>2020</td>\n", | |
| " <td>31.15</td>\n", | |
| " <td>10000.01</td>\n", | |
| " <td>8000.0</td>\n", | |
| " <td>2000.01</td>\n", | |
| " </tr>\n", | |
| " <tr>\n", | |
| " <th>1</th>\n", | |
| " <td>2</td>\n", | |
| " <td>2020</td>\n", | |
| " <td>0.00</td>\n", | |
| " <td>5001.01</td>\n", | |
| " <td>16050.0</td>\n", | |
| " <td>0.00</td>\n", | |
| " </tr>\n", | |
| " <tr>\n", | |
| " <th>2</th>\n", | |
| " <td>3</td>\n", | |
| " <td>2020</td>\n", | |
| " <td>454.75</td>\n", | |
| " <td>20001.01</td>\n", | |
| " <td>8000.0</td>\n", | |
| " <td>12000.01</td>\n", | |
| " </tr>\n", | |
| " <tr>\n", | |
| " <th>3</th>\n", | |
| " <td>4</td>\n", | |
| " <td>1990</td>\n", | |
| " <td>111.15</td>\n", | |
| " <td>10001.01</td>\n", | |
| " <td>6000.0</td>\n", | |
| " <td>4000.01</td>\n", | |
| " </tr>\n", | |
| " <tr>\n", | |
| " <th>4</th>\n", | |
| " <td>5</td>\n", | |
| " <td>1990</td>\n", | |
| " <td>0.00</td>\n", | |
| " <td>5001.01</td>\n", | |
| " <td>9500.0</td>\n", | |
| " <td>0.00</td>\n", | |
| " </tr>\n", | |
| " <tr>\n", | |
| " <th>5</th>\n", | |
| " <td>6</td>\n", | |
| " <td>1980</td>\n", | |
| " <td>254.77</td>\n", | |
| " <td>10001.01</td>\n", | |
| " <td>1584.0</td>\n", | |
| " <td>7566.01</td>\n", | |
| " </tr>\n", | |
| " <tr>\n", | |
| " <th>6</th>\n", | |
| " <td>7</td>\n", | |
| " <td>1980</td>\n", | |
| " <td>0.00</td>\n", | |
| " <td>5001.01</td>\n", | |
| " <td>1900.0</td>\n", | |
| " <td>1400.01</td>\n", | |
| " </tr>\n", | |
| " <tr>\n", | |
| " <th>7</th>\n", | |
| " <td>8</td>\n", | |
| " <td>2020</td>\n", | |
| " <td>31.15</td>\n", | |
| " <td>10001.01</td>\n", | |
| " <td>8000.0</td>\n", | |
| " <td>2000.01</td>\n", | |
| " </tr>\n", | |
| " <tr>\n", | |
| " <th>8</th>\n", | |
| " <td>9</td>\n", | |
| " <td>2020</td>\n", | |
| " <td>0.00</td>\n", | |
| " <td>10001.01</td>\n", | |
| " <td>16050.0</td>\n", | |
| " <td>0.00</td>\n", | |
| " </tr>\n", | |
| " </tbody>\n", | |
| "</table>\n", | |
| "</div>" | |
| ], | |
| "text/plain": [ | |
| " id year siitax nh_income nh_exemptions nh_taxable\n", | |
| "0 1 2020 31.15 10000.01 8000.0 2000.01\n", | |
| "1 2 2020 0.00 5001.01 16050.0 0.00\n", | |
| "2 3 2020 454.75 20001.01 8000.0 12000.01\n", | |
| "3 4 1990 111.15 10001.01 6000.0 4000.01\n", | |
| "4 5 1990 0.00 5001.01 9500.0 0.00\n", | |
| "5 6 1980 254.77 10001.01 1584.0 7566.01\n", | |
| "6 7 1980 0.00 5001.01 1900.0 1400.01\n", | |
| "7 8 2020 31.15 10001.01 8000.0 2000.01\n", | |
| "8 9 2020 0.00 10001.01 16050.0 0.00" | |
| ] | |
| }, | |
| "execution_count": 2, | |
| "metadata": {}, | |
| "output_type": "execute_result" | |
| } | |
| ], | |
| "source": [ | |
| "# TAXSIM results (extracted from actual TAXSIM output)\n", | |
| "taxsim_results = [\n", | |
| " {'id': 1, 'year': 2020, 'siitax': 31.15, 'nh_income': 10000.01, 'nh_exemptions': 8000.00, 'nh_taxable': 2000.01},\n", | |
| " {'id': 2, 'year': 2020, 'siitax': 0.00, 'nh_income': 5001.01, 'nh_exemptions': 16050.00, 'nh_taxable': 0.00},\n", | |
| " {'id': 3, 'year': 2020, 'siitax': 454.75, 'nh_income': 20001.01, 'nh_exemptions': 8000.00, 'nh_taxable': 12000.01},\n", | |
| " {'id': 4, 'year': 1990, 'siitax': 111.15, 'nh_income': 10001.01, 'nh_exemptions': 6000.00, 'nh_taxable': 4000.01},\n", | |
| " {'id': 5, 'year': 1990, 'siitax': 0.00, 'nh_income': 5001.01, 'nh_exemptions': 9500.00, 'nh_taxable': 0.00},\n", | |
| " {'id': 6, 'year': 1980, 'siitax': 254.77, 'nh_income': 10001.01, 'nh_exemptions': 1584.00, 'nh_taxable': 7566.01},\n", | |
| " {'id': 7, 'year': 1980, 'siitax': 0.00, 'nh_income': 5001.01, 'nh_exemptions': 1900.00, 'nh_taxable': 1400.01},\n", | |
| " {'id': 8, 'year': 2020, 'siitax': 31.15, 'nh_income': 10001.01, 'nh_exemptions': 8000.00, 'nh_taxable': 2000.01},\n", | |
| " {'id': 9, 'year': 2020, 'siitax': 0.00, 'nh_income': 10001.01, 'nh_exemptions': 16050.00, 'nh_taxable': 0.00},\n", | |
| "]\n", | |
| "\n", | |
| "taxsim_df = pd.DataFrame(taxsim_results)\n", | |
| "taxsim_df" | |
| ] | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "metadata": {}, | |
| "source": [ | |
| "## 3. PolicyEngine Expected Results\n", | |
| "\n", | |
| "Based on NH tax forms and our implementation, here's what we expect:" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": 3, | |
| "metadata": { | |
| "execution": { | |
| "iopub.execute_input": "2025-07-28T06:26:28.926552Z", | |
| "iopub.status.busy": "2025-07-28T06:26:28.926448Z", | |
| "iopub.status.idle": "2025-07-28T06:26:28.931589Z", | |
| "shell.execute_reply": "2025-07-28T06:26:28.931310Z" | |
| } | |
| }, | |
| "outputs": [ | |
| { | |
| "data": { | |
| "text/html": [ | |
| "<div>\n", | |
| "<style scoped>\n", | |
| " .dataframe tbody tr th:only-of-type {\n", | |
| " vertical-align: middle;\n", | |
| " }\n", | |
| "\n", | |
| " .dataframe tbody tr th {\n", | |
| " vertical-align: top;\n", | |
| " }\n", | |
| "\n", | |
| " .dataframe thead th {\n", | |
| " text-align: right;\n", | |
| " }\n", | |
| "</style>\n", | |
| "<table border=\"1\" class=\"dataframe\">\n", | |
| " <thead>\n", | |
| " <tr style=\"text-align: right;\">\n", | |
| " <th></th>\n", | |
| " <th>id</th>\n", | |
| " <th>year</th>\n", | |
| " <th>pe_income</th>\n", | |
| " <th>pe_exemptions</th>\n", | |
| " <th>pe_taxable</th>\n", | |
| " <th>pe_tax</th>\n", | |
| " </tr>\n", | |
| " </thead>\n", | |
| " <tbody>\n", | |
| " <tr>\n", | |
| " <th>0</th>\n", | |
| " <td>1</td>\n", | |
| " <td>2020</td>\n", | |
| " <td>10000</td>\n", | |
| " <td>2400</td>\n", | |
| " <td>7600</td>\n", | |
| " <td>380.0</td>\n", | |
| " </tr>\n", | |
| " <tr>\n", | |
| " <th>1</th>\n", | |
| " <td>2</td>\n", | |
| " <td>2020</td>\n", | |
| " <td>5000</td>\n", | |
| " <td>4800</td>\n", | |
| " <td>200</td>\n", | |
| " <td>10.0</td>\n", | |
| " </tr>\n", | |
| " <tr>\n", | |
| " <th>2</th>\n", | |
| " <td>3</td>\n", | |
| " <td>2020</td>\n", | |
| " <td>20000</td>\n", | |
| " <td>2400</td>\n", | |
| " <td>17600</td>\n", | |
| " <td>880.0</td>\n", | |
| " </tr>\n", | |
| " <tr>\n", | |
| " <th>3</th>\n", | |
| " <td>4</td>\n", | |
| " <td>1990</td>\n", | |
| " <td>10000</td>\n", | |
| " <td>1200</td>\n", | |
| " <td>8800</td>\n", | |
| " <td>440.0</td>\n", | |
| " </tr>\n", | |
| " <tr>\n", | |
| " <th>4</th>\n", | |
| " <td>5</td>\n", | |
| " <td>1990</td>\n", | |
| " <td>5000</td>\n", | |
| " <td>2400</td>\n", | |
| " <td>2600</td>\n", | |
| " <td>130.0</td>\n", | |
| " </tr>\n", | |
| " <tr>\n", | |
| " <th>5</th>\n", | |
| " <td>6</td>\n", | |
| " <td>1980</td>\n", | |
| " <td>10000</td>\n", | |
| " <td>600</td>\n", | |
| " <td>9400</td>\n", | |
| " <td>470.0</td>\n", | |
| " </tr>\n", | |
| " <tr>\n", | |
| " <th>6</th>\n", | |
| " <td>7</td>\n", | |
| " <td>1980</td>\n", | |
| " <td>5000</td>\n", | |
| " <td>1200</td>\n", | |
| " <td>3800</td>\n", | |
| " <td>190.0</td>\n", | |
| " </tr>\n", | |
| " <tr>\n", | |
| " <th>7</th>\n", | |
| " <td>8</td>\n", | |
| " <td>2020</td>\n", | |
| " <td>10000</td>\n", | |
| " <td>3600</td>\n", | |
| " <td>6400</td>\n", | |
| " <td>320.0</td>\n", | |
| " </tr>\n", | |
| " <tr>\n", | |
| " <th>8</th>\n", | |
| " <td>9</td>\n", | |
| " <td>2020</td>\n", | |
| " <td>10000</td>\n", | |
| " <td>7200</td>\n", | |
| " <td>2800</td>\n", | |
| " <td>140.0</td>\n", | |
| " </tr>\n", | |
| " </tbody>\n", | |
| "</table>\n", | |
| "</div>" | |
| ], | |
| "text/plain": [ | |
| " id year pe_income pe_exemptions pe_taxable pe_tax\n", | |
| "0 1 2020 10000 2400 7600 380.0\n", | |
| "1 2 2020 5000 4800 200 10.0\n", | |
| "2 3 2020 20000 2400 17600 880.0\n", | |
| "3 4 1990 10000 1200 8800 440.0\n", | |
| "4 5 1990 5000 2400 2600 130.0\n", | |
| "5 6 1980 10000 600 9400 470.0\n", | |
| "6 7 1980 5000 1200 3800 190.0\n", | |
| "7 8 2020 10000 3600 6400 320.0\n", | |
| "8 9 2020 10000 7200 2800 140.0" | |
| ] | |
| }, | |
| "execution_count": 3, | |
| "metadata": {}, | |
| "output_type": "execute_result" | |
| } | |
| ], | |
| "source": [ | |
| "# NH exemption amounts by year\n", | |
| "nh_exemptions = {\n", | |
| " 2020: {'single_base': 2400, 'joint_base': 4800, 'additional': 1200},\n", | |
| " 1990: {'single_base': 1200, 'joint_base': 2400, 'additional': 1200},\n", | |
| " 1980: {'single_base': 600, 'joint_base': 1200, 'additional': 600}\n", | |
| "}\n", | |
| "\n", | |
| "# Calculate PolicyEngine expected results\n", | |
| "pe_results = []\n", | |
| "for case in test_cases:\n", | |
| " year = case['year']\n", | |
| " income = case['interest'] + case['dividends']\n", | |
| " \n", | |
| " # Get base exemption\n", | |
| " if case['status'] == 'Single':\n", | |
| " base_exemption = nh_exemptions[year]['single_base']\n", | |
| " else:\n", | |
| " base_exemption = nh_exemptions[year]['joint_base']\n", | |
| " \n", | |
| " # Add age exemption if 65+\n", | |
| " age_exemption = 0\n", | |
| " if case['age'] >= 65:\n", | |
| " age_exemption = nh_exemptions[year]['additional']\n", | |
| " if case['status'] == 'Joint': # Both spouses assumed same age\n", | |
| " age_exemption *= 2\n", | |
| " \n", | |
| " total_exemptions = base_exemption + age_exemption\n", | |
| " taxable_income = max(0, income - total_exemptions)\n", | |
| " tax = taxable_income * 0.05 # 5% rate\n", | |
| " \n", | |
| " pe_results.append({\n", | |
| " 'id': case['id'],\n", | |
| " 'year': year,\n", | |
| " 'pe_income': income,\n", | |
| " 'pe_exemptions': total_exemptions,\n", | |
| " 'pe_taxable': taxable_income,\n", | |
| " 'pe_tax': tax\n", | |
| " })\n", | |
| "\n", | |
| "pe_df = pd.DataFrame(pe_results)\n", | |
| "pe_df" | |
| ] | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "metadata": {}, | |
| "source": [ | |
| "## 4. Comparison Analysis" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": 4, | |
| "metadata": { | |
| "execution": { | |
| "iopub.execute_input": "2025-07-28T06:26:28.933066Z", | |
| "iopub.status.busy": "2025-07-28T06:26:28.932956Z", | |
| "iopub.status.idle": "2025-07-28T06:26:28.943755Z", | |
| "shell.execute_reply": "2025-07-28T06:26:28.943518Z" | |
| } | |
| }, | |
| "outputs": [ | |
| { | |
| "name": "stdout", | |
| "output_type": "stream", | |
| "text": [ | |
| "Comparison of TAXSIM vs PolicyEngine:\n" | |
| ] | |
| }, | |
| { | |
| "data": { | |
| "text/html": [ | |
| "<div>\n", | |
| "<style scoped>\n", | |
| " .dataframe tbody tr th:only-of-type {\n", | |
| " vertical-align: middle;\n", | |
| " }\n", | |
| "\n", | |
| " .dataframe tbody tr th {\n", | |
| " vertical-align: top;\n", | |
| " }\n", | |
| "\n", | |
| " .dataframe thead th {\n", | |
| " text-align: right;\n", | |
| " }\n", | |
| "</style>\n", | |
| "<table border=\"1\" class=\"dataframe\">\n", | |
| " <thead>\n", | |
| " <tr style=\"text-align: right;\">\n", | |
| " <th></th>\n", | |
| " <th>id</th>\n", | |
| " <th>year</th>\n", | |
| " <th>status</th>\n", | |
| " <th>age</th>\n", | |
| " <th>interest</th>\n", | |
| " <th>dividends</th>\n", | |
| " <th>nh_exemptions</th>\n", | |
| " <th>pe_exemptions</th>\n", | |
| " <th>exemption_diff</th>\n", | |
| " <th>siitax</th>\n", | |
| " <th>pe_tax</th>\n", | |
| " <th>tax_diff</th>\n", | |
| " </tr>\n", | |
| " </thead>\n", | |
| " <tbody>\n", | |
| " <tr>\n", | |
| " <th>0</th>\n", | |
| " <td>1</td>\n", | |
| " <td>2020</td>\n", | |
| " <td>Single</td>\n", | |
| " <td>40</td>\n", | |
| " <td>5000</td>\n", | |
| " <td>5000</td>\n", | |
| " <td>8000.0</td>\n", | |
| " <td>2400</td>\n", | |
| " <td>5600.0</td>\n", | |
| " <td>31.15</td>\n", | |
| " <td>380.0</td>\n", | |
| " <td>-348.85</td>\n", | |
| " </tr>\n", | |
| " <tr>\n", | |
| " <th>1</th>\n", | |
| " <td>2</td>\n", | |
| " <td>2020</td>\n", | |
| " <td>Joint</td>\n", | |
| " <td>40</td>\n", | |
| " <td>3000</td>\n", | |
| " <td>2000</td>\n", | |
| " <td>16050.0</td>\n", | |
| " <td>4800</td>\n", | |
| " <td>11250.0</td>\n", | |
| " <td>0.00</td>\n", | |
| " <td>10.0</td>\n", | |
| " <td>-10.00</td>\n", | |
| " </tr>\n", | |
| " <tr>\n", | |
| " <th>2</th>\n", | |
| " <td>3</td>\n", | |
| " <td>2020</td>\n", | |
| " <td>Single</td>\n", | |
| " <td>40</td>\n", | |
| " <td>10000</td>\n", | |
| " <td>10000</td>\n", | |
| " <td>8000.0</td>\n", | |
| " <td>2400</td>\n", | |
| " <td>5600.0</td>\n", | |
| " <td>454.75</td>\n", | |
| " <td>880.0</td>\n", | |
| " <td>-425.25</td>\n", | |
| " </tr>\n", | |
| " <tr>\n", | |
| " <th>3</th>\n", | |
| " <td>4</td>\n", | |
| " <td>1990</td>\n", | |
| " <td>Single</td>\n", | |
| " <td>40</td>\n", | |
| " <td>5000</td>\n", | |
| " <td>5000</td>\n", | |
| " <td>6000.0</td>\n", | |
| " <td>1200</td>\n", | |
| " <td>4800.0</td>\n", | |
| " <td>111.15</td>\n", | |
| " <td>440.0</td>\n", | |
| " <td>-328.85</td>\n", | |
| " </tr>\n", | |
| " <tr>\n", | |
| " <th>4</th>\n", | |
| " <td>5</td>\n", | |
| " <td>1990</td>\n", | |
| " <td>Joint</td>\n", | |
| " <td>40</td>\n", | |
| " <td>3000</td>\n", | |
| " <td>2000</td>\n", | |
| " <td>9500.0</td>\n", | |
| " <td>2400</td>\n", | |
| " <td>7100.0</td>\n", | |
| " <td>0.00</td>\n", | |
| " <td>130.0</td>\n", | |
| " <td>-130.00</td>\n", | |
| " </tr>\n", | |
| " <tr>\n", | |
| " <th>5</th>\n", | |
| " <td>6</td>\n", | |
| " <td>1980</td>\n", | |
| " <td>Single</td>\n", | |
| " <td>40</td>\n", | |
| " <td>5000</td>\n", | |
| " <td>5000</td>\n", | |
| " <td>1584.0</td>\n", | |
| " <td>600</td>\n", | |
| " <td>984.0</td>\n", | |
| " <td>254.77</td>\n", | |
| " <td>470.0</td>\n", | |
| " <td>-215.23</td>\n", | |
| " </tr>\n", | |
| " <tr>\n", | |
| " <th>6</th>\n", | |
| " <td>7</td>\n", | |
| " <td>1980</td>\n", | |
| " <td>Joint</td>\n", | |
| " <td>40</td>\n", | |
| " <td>3000</td>\n", | |
| " <td>2000</td>\n", | |
| " <td>1900.0</td>\n", | |
| " <td>1200</td>\n", | |
| " <td>700.0</td>\n", | |
| " <td>0.00</td>\n", | |
| " <td>190.0</td>\n", | |
| " <td>-190.00</td>\n", | |
| " </tr>\n", | |
| " <tr>\n", | |
| " <th>7</th>\n", | |
| " <td>8</td>\n", | |
| " <td>2020</td>\n", | |
| " <td>Single</td>\n", | |
| " <td>70</td>\n", | |
| " <td>5000</td>\n", | |
| " <td>5000</td>\n", | |
| " <td>8000.0</td>\n", | |
| " <td>3600</td>\n", | |
| " <td>4400.0</td>\n", | |
| " <td>31.15</td>\n", | |
| " <td>320.0</td>\n", | |
| " <td>-288.85</td>\n", | |
| " </tr>\n", | |
| " <tr>\n", | |
| " <th>8</th>\n", | |
| " <td>9</td>\n", | |
| " <td>2020</td>\n", | |
| " <td>Joint</td>\n", | |
| " <td>70</td>\n", | |
| " <td>5000</td>\n", | |
| " <td>5000</td>\n", | |
| " <td>16050.0</td>\n", | |
| " <td>7200</td>\n", | |
| " <td>8850.0</td>\n", | |
| " <td>0.00</td>\n", | |
| " <td>140.0</td>\n", | |
| " <td>-140.00</td>\n", | |
| " </tr>\n", | |
| " </tbody>\n", | |
| "</table>\n", | |
| "</div>" | |
| ], | |
| "text/plain": [ | |
| " id year status age interest dividends nh_exemptions pe_exemptions \\\n", | |
| "0 1 2020 Single 40 5000 5000 8000.0 2400 \n", | |
| "1 2 2020 Joint 40 3000 2000 16050.0 4800 \n", | |
| "2 3 2020 Single 40 10000 10000 8000.0 2400 \n", | |
| "3 4 1990 Single 40 5000 5000 6000.0 1200 \n", | |
| "4 5 1990 Joint 40 3000 2000 9500.0 2400 \n", | |
| "5 6 1980 Single 40 5000 5000 1584.0 600 \n", | |
| "6 7 1980 Joint 40 3000 2000 1900.0 1200 \n", | |
| "7 8 2020 Single 70 5000 5000 8000.0 3600 \n", | |
| "8 9 2020 Joint 70 5000 5000 16050.0 7200 \n", | |
| "\n", | |
| " exemption_diff siitax pe_tax tax_diff \n", | |
| "0 5600.0 31.15 380.0 -348.85 \n", | |
| "1 11250.0 0.00 10.0 -10.00 \n", | |
| "2 5600.0 454.75 880.0 -425.25 \n", | |
| "3 4800.0 111.15 440.0 -328.85 \n", | |
| "4 7100.0 0.00 130.0 -130.00 \n", | |
| "5 984.0 254.77 470.0 -215.23 \n", | |
| "6 700.0 0.00 190.0 -190.00 \n", | |
| "7 4400.0 31.15 320.0 -288.85 \n", | |
| "8 8850.0 0.00 140.0 -140.00 " | |
| ] | |
| }, | |
| "execution_count": 4, | |
| "metadata": {}, | |
| "output_type": "execute_result" | |
| } | |
| ], | |
| "source": [ | |
| "# Merge all results\n", | |
| "comparison = test_df.merge(taxsim_df, on='id').merge(pe_df, on='id', suffixes=('', '_pe'))\n", | |
| "\n", | |
| "# Calculate differences\n", | |
| "comparison['exemption_diff'] = comparison['nh_exemptions'] - comparison['pe_exemptions']\n", | |
| "comparison['tax_diff'] = comparison['siitax'] - comparison['pe_tax']\n", | |
| "\n", | |
| "# Select key columns for display\n", | |
| "display_cols = ['id', 'year', 'status', 'age', 'interest', 'dividends', \n", | |
| " 'nh_exemptions', 'pe_exemptions', 'exemption_diff',\n", | |
| " 'siitax', 'pe_tax', 'tax_diff']\n", | |
| "\n", | |
| "print(\"Comparison of TAXSIM vs PolicyEngine:\")\n", | |
| "comparison[display_cols]" | |
| ] | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "metadata": {}, | |
| "source": [ | |
| "## 5. Key Findings\n", | |
| "\n", | |
| "### TAXSIM Issues Identified:\n", | |
| "\n", | |
| "1. **Incorrect Exemption Amounts**: TAXSIM shows much higher exemptions than the actual NH tax forms specify\n", | |
| "2. **No Age-Based Exemptions**: TAXSIM doesn't apply the additional $1,200 exemption for taxpayers age 65+\n", | |
| "3. **Inconsistent with Source Code**: TAXSIM's own source code shows the correct formula, but the output doesn't match" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": 5, | |
| "metadata": { | |
| "execution": { | |
| "iopub.execute_input": "2025-07-28T06:26:28.945157Z", | |
| "iopub.status.busy": "2025-07-28T06:26:28.945061Z", | |
| "iopub.status.idle": "2025-07-28T06:26:28.947473Z", | |
| "shell.execute_reply": "2025-07-28T06:26:28.947199Z" | |
| } | |
| }, | |
| "outputs": [ | |
| { | |
| "name": "stdout", | |
| "output_type": "stream", | |
| "text": [ | |
| "Summary of Differences:\n", | |
| "\n", | |
| "Average exemption difference: $5,476.00\n", | |
| "Average tax difference: $-230.78\n", | |
| "\n", | |
| "Cases where TAXSIM tax is lower: 9 out of 9\n", | |
| "Cases where exemptions match: 0 out of 9\n" | |
| ] | |
| } | |
| ], | |
| "source": [ | |
| "# Summary statistics\n", | |
| "print(\"Summary of Differences:\\n\")\n", | |
| "print(f\"Average exemption difference: ${comparison['exemption_diff'].mean():,.2f}\")\n", | |
| "print(f\"Average tax difference: ${comparison['tax_diff'].mean():,.2f}\")\n", | |
| "print(f\"\\nCases where TAXSIM tax is lower: {(comparison['tax_diff'] < 0).sum()} out of {len(comparison)}\")\n", | |
| "print(f\"Cases where exemptions match: {(comparison['exemption_diff'] == 0).sum()} out of {len(comparison)}\")" | |
| ] | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "metadata": {}, | |
| "source": [ | |
| "## 6. TAXSIM Source Code Analysis\n", | |
| "\n", | |
| "From TAXSIM's source code (`taxsim.f`), the NH tax calculation is:" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": 6, | |
| "metadata": { | |
| "execution": { | |
| "iopub.execute_input": "2025-07-28T06:26:28.948858Z", | |
| "iopub.status.busy": "2025-07-28T06:26:28.948762Z", | |
| "iopub.status.idle": "2025-07-28T06:26:28.950797Z", | |
| "shell.execute_reply": "2025-07-28T06:26:28.950516Z" | |
| } | |
| }, | |
| "outputs": [ | |
| { | |
| "name": "stdout", | |
| "output_type": "stream", | |
| "text": [ | |
| "TAXSIM NH Tax Code (lines 21264-21269):\n", | |
| "if(law.ge.1995) then\n", | |
| " exemp = data(7)*2400.+(data(9)+data(10))*1200.\n", | |
| "else if(law.ge.1981.and.law.le.1994) then\n", | |
| " exemp = (data(7)+data(9)+data(10))*1200.\n", | |
| "else\n", | |
| " exemp = (data(7)+data(9)+data(10))*600.\n", | |
| "endif\n", | |
| "\n", | |
| "Where:\n", | |
| "- data(7) = number of tax units (1 for single, 2 for married)\n", | |
| "- data(9) = number of people age 65+\n", | |
| "- data(10) = number of blind/disabled people\n" | |
| ] | |
| } | |
| ], | |
| "source": [ | |
| "print(\"TAXSIM NH Tax Code (lines 21264-21269):\")\n", | |
| "print(\"\"\"if(law.ge.1995) then\n", | |
| " exemp = data(7)*2400.+(data(9)+data(10))*1200.\n", | |
| "else if(law.ge.1981.and.law.le.1994) then\n", | |
| " exemp = (data(7)+data(9)+data(10))*1200.\n", | |
| "else\n", | |
| " exemp = (data(7)+data(9)+data(10))*600.\n", | |
| "endif\"\"\")\n", | |
| "print(\"\\nWhere:\")\n", | |
| "print(\"- data(7) = number of tax units (1 for single, 2 for married)\")\n", | |
| "print(\"- data(9) = number of people age 65+\")\n", | |
| "print(\"- data(10) = number of blind/disabled people\")" | |
| ] | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "metadata": {}, | |
| "source": [ | |
| "## 7. Verification of TAXSIM Bug\n", | |
| "\n", | |
| "Let's verify that TAXSIM's output doesn't match its own formula:" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": 7, | |
| "metadata": { | |
| "execution": { | |
| "iopub.execute_input": "2025-07-28T06:26:28.952159Z", | |
| "iopub.status.busy": "2025-07-28T06:26:28.952064Z", | |
| "iopub.status.idle": "2025-07-28T06:26:28.954001Z", | |
| "shell.execute_reply": "2025-07-28T06:26:28.953760Z" | |
| } | |
| }, | |
| "outputs": [ | |
| { | |
| "name": "stdout", | |
| "output_type": "stream", | |
| "text": [ | |
| "Checking TAXSIM's 2020 Single Filer Exemption:\n", | |
| "TAXSIM shows: $8,000\n", | |
| "\n", | |
| "Using TAXSIM's formula for single filer with no age/disability:\n", | |
| "exemp = data(7)*2400 + (data(9)+data(10))*1200\n", | |
| "exemp = 1*2400 + (0+0)*1200 = $2,400\n", | |
| "\n", | |
| "TAXSIM's output ($8,000) doesn't match its formula ($2,400)!\n", | |
| "\n", | |
| "To get $8,000, we would need:\n", | |
| "8000 = 2400 + (data(9)+data(10))*1200\n", | |
| "(data(9)+data(10)) = 4.67\n", | |
| "\n", | |
| "This is impossible since these should be counts of people.\n" | |
| ] | |
| } | |
| ], | |
| "source": [ | |
| "print(\"Checking TAXSIM's 2020 Single Filer Exemption:\")\n", | |
| "print(f\"TAXSIM shows: $8,000\")\n", | |
| "print(f\"\\nUsing TAXSIM's formula for single filer with no age/disability:\")\n", | |
| "print(f\"exemp = data(7)*2400 + (data(9)+data(10))*1200\")\n", | |
| "print(f\"exemp = 1*2400 + (0+0)*1200 = $2,400\")\n", | |
| "print(f\"\\nTAXSIM's output ($8,000) doesn't match its formula ($2,400)!\")\n", | |
| "print(f\"\\nTo get $8,000, we would need:\")\n", | |
| "print(f\"8000 = 2400 + (data(9)+data(10))*1200\")\n", | |
| "print(f\"(data(9)+data(10)) = 4.67\")\n", | |
| "print(f\"\\nThis is impossible since these should be counts of people.\")" | |
| ] | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "metadata": {}, | |
| "source": [ | |
| "## 8. Conclusion\n", | |
| "\n", | |
| "PolicyEngine correctly implements NH's Interest and Dividends tax based on:\n", | |
| "- Actual NH tax forms (DP-10)\n", | |
| "- NH Revised Statutes Annotated (RSA) Chapter 77\n", | |
| "- Historical tax rates and exemption amounts\n", | |
| "\n", | |
| "TAXSIM appears to have a bug in its NH tax calculation that results in:\n", | |
| "- Exemptions that are 3-4x higher than specified in tax forms\n", | |
| "- No recognition of age-based additional exemptions\n", | |
| "- Tax amounts that are significantly too low\n", | |
| "\n", | |
| "PolicyEngine's implementation should be considered the accurate one for NH tax calculations." | |
| ] | |
| } | |
| ], | |
| "metadata": { | |
| "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.10.17" | |
| } | |
| }, | |
| "nbformat": 4, | |
| "nbformat_minor": 4 | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment