Skip to content

Commit

Permalink
Merge pull request PolicyEngine#26 from MaxGhenis/MaxGhenis/issue24
Browse files Browse the repository at this point in the history
Create package
  • Loading branch information
noman404 authored Oct 21, 2024
2 parents e7dea36 + 8ff490f commit 3bc456b
Show file tree
Hide file tree
Showing 25 changed files with 908 additions and 415 deletions.
36 changes: 36 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Run Tests

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: [3.9, "3.10", 3.11, 3.12]

steps:
- uses: actions/checkout@v2

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e .
pip install pytest
- name: Make TAXSIM executables executable (Unix)
if: runner.os != 'Windows'
run: chmod +x resources/taxsim35/taxsim35-*.exe

- name: Run tests
run: pytest tests/
40 changes: 0 additions & 40 deletions .github/workflows/python-package.yml

This file was deleted.

7 changes: 7 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Copyright 2024 PolicyEngine

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
3 changes: 3 additions & 0 deletions output/policyengine_taxsim_output.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
taxsimid,year,state,mstat,page,sage,fiitax,siitax,fica
1,2021,3,1,40,0,[2775.],[1008.87],[3748.5]
1,2021,3,1,40,0,[2535.],[942.07],[3595.5]
3 changes: 3 additions & 0 deletions output/taxsim35_output.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
taxsimid,year,state,fiitax,siitax,fica,frate,srate,ficar,tfica,credits,v10,v11,v12,v13,v14,v15,v16,v17,v18,v19,v20, v21,v22,v23,v24,v25,v26,v27,v28,v29,v30,v31,v32,v33,v34, v35,v36,v37,v38,v39,v40,v41,staxbc,v42,v43,v44,v45
1.,2021,3,2775.00,1008.87,7497.00,12.00,3.34,15.30,3748.50,.00,49000.00,.00,.00,12550.00,.00,.00,.00,.00,36450.00,4175.00,.00,.00,.00,.00,.00,.00,49000.00,.00,4175.00,7497.00,49000.01,.00,49000.01,.00,12550.00,1008.87,36450.01,.00,.00,.00,.00,3.34,.00,.00,.00,.00,1400.00
1.,2021,3,2535.00,942.07,7191.00,12.00,3.34,15.30,3595.50,.00,47000.00,.00,.00,12550.00,.00,.00,.00,.00,34450.00,3935.00,.00,.00,.00,.00,.00,.00,47000.00,.00,3935.00,7191.00,47001.01,.00,47000.01,.00,12550.00,942.07,34450.01,.00,.00,.00,.00,3.34,.00,.00,.00,.00,1400.00
7 changes: 7 additions & 0 deletions policyengine_taxsim/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from .core.input_mapper import import_single_household
from .core.output_mapper import export_single_household
from .cli import main as cli

__all__ = ["import_single_household", "export_single_household", "cli"]

__version__ = "0.1.0" # Make sure this matches the version in pyproject.toml
45 changes: 45 additions & 0 deletions policyengine_taxsim/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import click
import pandas as pd
from pathlib import Path
from policyengine_taxsim.core.input_mapper import import_single_household
from policyengine_taxsim.core.output_mapper import export_single_household


@click.command()
@click.argument("input_file", type=click.Path(exists=True))
@click.option(
"--output",
"-o",
type=click.Path(),
default="output.csv",
help="Output file path",
)
def main(input_file, output):
"""
Process TAXSIM input file and generate PolicyEngine-compatible output.
"""
try:
# Read input file
df = pd.read_csv(input_file)

# Process each row
results = []
for _, row in df.iterrows():
taxsim_input = row.to_dict()
pe_situation = import_single_household(taxsim_input)
taxsim_output = export_single_household(pe_situation)
results.append(taxsim_output)

# Create output dataframe and save to csv
output_df = pd.DataFrame(results)
output_path = Path(output)
output_path.parent.mkdir(parents=True, exist_ok=True)
output_df.to_csv(output_path, index=False)
click.echo(f"Output saved to {output}")
except Exception as e:
click.echo(f"Error processing input: {str(e)}", err=True)
raise


if __name__ == "__main__":
main()
51 changes: 51 additions & 0 deletions policyengine_taxsim/config/variable_mappings.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
taxsim_to_policyengine:
year: get_year
state: get_state_code
fiitax: income_tax
siitax: state_income_tax
fica: get_fica
frate: federal_mtr
srate: state_mtr
ficar: placeholder
tfica: taxsim_tfica
v10: adjusted_gross_income
v11: tax_unit_taxable_unemployment_compensation
v12: tax_unit_taxable_social_security
v13: basic_standard_deduction
v14: exemptions
v15: placeholder
v16: placeholder
v17: taxable_income_deductions
v18: taxable_income
v19: income_tax
v20: placeholder
v21: placeholder
v22: ctc
v23: refundable_ctc
v24: cdcc
v25: eitc
v26: amt_income
v27: alternative_minimum_tax
v28: income_tax_before_refundable_credits
v29: placeholder
v30: household_net_income
v31: placeholder
v32: state_agi
v33: placeholder
v34: state_standard_deduction
v35: state_itemized_deductions
v36: state_taxable_income
v37: property_tax_credit
v38: child_care_credit
v39: placeholder
v40: placeholder
v41: placeholder
v42: self_employment_income
v43: net_investment_income_tax
v44: employee_medicare_tax
v45: rrc_cares

policyengine_to_taxsim:
# This section would be the inverse of the above mapping
# It's left empty for brevity, but you should populate it
# with the inverse relationships for bidirectional conversion
Empty file.
35 changes: 35 additions & 0 deletions policyengine_taxsim/core/input_mapper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from policyengine_taxsim.core.utils import (
load_variable_mappings,
get_state_code,
)


def import_single_household(taxsim_vars):
"""
Convert TAXSIM input variables to a PolicyEngine situation.
Args:
taxsim_vars (dict): Dictionary of TAXSIM input variables
Returns:
dict: PolicyEngine situation dictionary
"""
mappings = load_variable_mappings()["taxsim_to_policyengine"]

year = str(int(taxsim_vars["year"])) # Ensure year is an integer string
state = get_state_code(taxsim_vars["state"])

situation = {
"people": {
"you": {
"age": {year: int(taxsim_vars.get("page", 40))},
"employment_income": {year: int(taxsim_vars.get("pwages", 0))},
}
},
"households": {
"your household": {"members": ["you"], "state_name": {year: state}}
},
"tax_units": {"your tax unit": {"members": ["you"]}},
}

return situation
53 changes: 53 additions & 0 deletions policyengine_taxsim/core/output_mapper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from policyengine_taxsim.core.utils import (
load_variable_mappings,
get_state_number,
)
from policyengine_us import Simulation


def export_single_household(policyengine_situation):
"""
Convert a PolicyEngine situation to TAXSIM output variables.
Args:
policyengine_situation (dict): PolicyEngine situation dictionary
Returns:
dict: Dictionary of TAXSIM output variables
"""
mappings = load_variable_mappings()["policyengine_to_taxsim"]

simulation = Simulation(situation=policyengine_situation)

year = list(
policyengine_situation["households"]["your household"][
"state_name"
].keys()
)[0]
state_name = policyengine_situation["households"]["your household"][
"state_name"
][year]

taxsim_output = {
"taxsimid": policyengine_situation.get("taxsimid", 1),
"year": int(year),
"state": get_state_number(state_name),
"mstat": policyengine_situation["tax_units"]["your tax unit"]
.get("marital_status", {})
.get(year, 1),
"page": policyengine_situation["people"]["you"]["age"][year],
"sage": policyengine_situation["people"]
.get("your spouse", {})
.get("age", {})
.get(year, 0),
"fiitax": simulation.calculate("income_tax", period=year),
"siitax": simulation.calculate("state_income_tax", period=year),
"fica": simulation.calculate(
"employee_social_security_tax", period=year
)
+ simulation.calculate("employee_medicare_tax", period=year),
}

# Add more variables as needed to match TAXSIM output

return taxsim_output
90 changes: 90 additions & 0 deletions policyengine_taxsim/core/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import yaml
from pathlib import Path


def load_variable_mappings():
"""Load variable mappings from YAML file."""
config_path = (
Path(__file__).parent.parent / "config" / "variable_mappings.yaml"
)
with open(config_path, "r") as f:
return yaml.safe_load(f)


STATE_MAPPING = {
1: "AL",
2: "AK",
3: "AZ",
4: "AR",
5: "CA",
6: "CO",
7: "CT",
8: "DE",
9: "DC",
10: "FL",
11: "GA",
12: "HI",
13: "ID",
14: "IL",
15: "IN",
16: "IA",
17: "KS",
18: "KY",
19: "LA",
20: "ME",
21: "MD",
22: "MA",
23: "MI",
24: "MN",
25: "MS",
26: "MO",
27: "MT",
28: "NE",
29: "NV",
30: "NH",
31: "NJ",
32: "NM",
33: "NY",
34: "NC",
35: "ND",
36: "OH",
37: "OK",
38: "OR",
39: "PA",
40: "RI",
41: "SC",
42: "SD",
43: "TN",
44: "TX",
45: "UT",
46: "VT",
47: "VA",
48: "WA",
49: "WV",
50: "WI",
51: "WY",
}


def get_state_code(state_number):
"""Convert state number to state code."""
return STATE_MAPPING.get(state_number, "Invalid state number")


def get_state_number(state_code):
"""Convert state code to state number."""
state_mapping_reverse = {v: k for k, v in STATE_MAPPING.items()}
return state_mapping_reverse.get(
state_code, 0
) # Return 0 for invalid state codes


def is_date(string):
"""Check if a string represents a valid year."""
try:
year = int(string)
return (
1900 <= year <= 2100
) # Assuming years between 1900 and 2100 are valid
except ValueError:
return False
Loading

0 comments on commit 3bc456b

Please sign in to comment.