Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add Behavior class and move defaukt behavioral assumptions to behavior.json #367

Merged
merged 16 commits into from
Oct 2, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ include taxcalc/_version.py
include taxcalc/StageIFactors.csv
include taxcalc/WEIGHTS.csv
include taxcalc/params.json
include taxcalc/behavior.json
21 changes: 8 additions & 13 deletions docs/notebooks/Behavioral_example.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,11 @@
"# Create a Parameters object for Plan X and Plan Y\n",
"params_x = Parameters()\n",
"params_y = Parameters()\n",
"behavior_y = Behavior()\n",
"\n",
"# Create two Calculators\n",
"calcX = Calculator(params_x, records_x)\n",
"calcY = Calculator(params_y, records_y)\n"
"calcY = Calculator(params_y, records_y, behavior=behavior_y)\n"
]
},
{
Expand Down Expand Up @@ -117,9 +118,12 @@
"outputs": [],
"source": [
"# Call the behavioral effects calculator and create new Plan Y Calculator obects. \n",
"calcY_behavior1 = behavior(calcX, calcY, elast_wrt_atr=0.4, inc_effect=0.15)\n",
"calcY_behavior2 = behavior(calcX, calcY, elast_wrt_atr=0.5, inc_effect=0.15)\n",
"calcY_behavior3 = behavior(calcX, calcY, elast_wrt_atr=0.4, inc_effect=0) "
"behavior_y.update_behavior({2013: {'_BE_inc': [0.15], '_BE_sub': [0.4]}})\n",
"calcY_behavior1 = behavior(calcX, calcY)\n",
"behavior_y.update_behavior({2013: {'_BE_inc': [0.15], '_BE_sub': [0.5]}})\n",
"calcY_behavior2 = behavior(calcX, calcY)\n",
"behavior_y.update_behavior({2013: {'_BE_inc': [0.0], '_BE_sub': [0.4]}})\n",
"calcY_behavior3 = behavior(calcX, calcY) "
]
},
{
Expand Down Expand Up @@ -175,15 +179,6 @@
"print(calcY_behavior2.records._ospctax.sum())\n",
"print(calcY_behavior3.records._ospctax.sum())"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": []
}
],
"metadata": {
Expand Down
25 changes: 25 additions & 0 deletions taxcalc/behavior.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{ "_BE_inc":{
"start_year": 2013,
"long_name": "Income effect",
"description": "Behavior effect",
"row_label": ["2013"],
"col_label": "",
"value": [0.0]
},
"_BE_sub":{
"start_year": 2013,
"long_name": "Substitution effect",
"description": "Behavior effect",
"row_label": ["2013"],
"col_label": "",
"value": [0.0]
},
"_BE_CG_per":{
"start_year": 2013,
"long_name": "Persistent",
"description": "Behavior effect",
"row_label": ["2013"],
"col_label": "",
"value": [0.0]
}
}
207 changes: 175 additions & 32 deletions taxcalc/behavior.py
Original file line number Diff line number Diff line change
@@ -1,74 +1,217 @@
import numpy as np
import copy
import json
import os
import numpy as np
from .parameters import Parameters
from .utils import expand_array


def update_income(behavioral_effect, calcY):
delta_inc = np.where(calcY.records.c00100 > 0, behavioral_effect, 0)
def update_income(behavioral_effect, calc_y):
delta_inc = np.where(calc_y.records.c00100 > 0, behavioral_effect, 0)

# Attribute the behavioral effects across itemized deductions,
# wages, and other income.

_itemized = np.where(calcY.records.c04470 < calcY.records._standard,
_itemized = np.where(calc_y.records.c04470 < calc_y.records._standard,
0,
calcY.records.c04470)
calc_y.records.c04470)

delta_wages = np.where(calcY.records.c00100 + _itemized > 0,
(delta_inc * calcY.records.e00200 /
(calcY.records.c00100 + _itemized)),
delta_wages = np.where(calc_y.records.c00100 + _itemized > 0,
(delta_inc * calc_y.records.e00200 /
(calc_y.records.c00100 + _itemized)),
0)

other_inc = calcY.records.c00100 - calcY.records.e00200
other_inc = calc_y.records.c00100 - calc_y.records.e00200

delta_other_inc = np.where(calcY.records.c00100 + _itemized > 0,
delta_other_inc = np.where(calc_y.records.c00100 + _itemized > 0,
(delta_inc * other_inc /
(calcY.records.c00100 + _itemized)),
(calc_y.records.c00100 + _itemized)),
0)

delta_itemized = np.where(calcY.records.c00100 + _itemized > 0,
delta_itemized = np.where(calc_y.records.c00100 + _itemized > 0,
(delta_inc * _itemized /
(calcY.records.c00100 + _itemized)),
(calc_y.records.c00100 + _itemized)),
0)

calcY.records.e00200 = calcY.records.e00200 + delta_wages
calc_y.records.e00200 = calc_y.records.e00200 + delta_wages

calcY.records.e00300 = calcY.records.e00300 + delta_other_inc
calc_y.records.e00300 = calc_y.records.e00300 + delta_other_inc

calcY.records.e19570 = np.where(_itemized > 0,
calcY.records.e19570 + delta_itemized, 0)
calc_y.records.e19570 = np.where(_itemized > 0,
calc_y.records.e19570 + delta_itemized, 0)
# TODO, we should create a behavioral modification
# variable instead of using e19570

calcY.calc_all()
calc_y.calc_all()

return calcY
return calc_y


def behavior(calcX, calcY, elast_wrt_atr=0.4, inc_effect=0.15,
update_income=update_income):
def behavior(calc_x, calc_y, update_income=update_income):
"""
Modify plan Y records to account for micro-feedback effect that arrise
from moving from plan X to plan Y.
"""

# Calculate marginal tax rates for plan x and plan y.
mtrX = calcX.mtr('e00200')
mtrX = calc_x.mtr('e00200')

mtrY = calcY.mtr('e00200')
mtrY = calc_y.mtr('e00200')

# Calculate the percent change in after-tax rate.
pct_diff_atr = ((1 - mtrY) - (1 - mtrX)) / (1 - mtrX)

# Calculate the magnitude of the substitution and income effects.
substitution_effect = (elast_wrt_atr * pct_diff_atr *
(calcX.records.c04800))
substitution_effect = (calc_y.behavior.BE_sub * pct_diff_atr *
(calc_x.records.c04800))

income_effect = inc_effect * (calcY.records._ospctax -
calcX.records._ospctax)
calcY_behavior = copy.deepcopy(calcY)
income_effect = calc_y.behavior.BE_inc * (calc_y.records._ospctax -
calc_x.records._ospctax)
calc_y_behavior = copy.deepcopy(calc_y)

combined_behavioral_effect = income_effect + substitution_effect

calcY_behavior = update_income(combined_behavioral_effect,
calcY_behavior)

return calcY_behavior
calc_y_behavior = update_income(combined_behavioral_effect,
calc_y_behavior)

return calc_y_behavior


class Behavior(object):

JSON_START_YEAR = Parameters.JSON_START_YEAR
BEHAVIOR_FILENAME = 'behavior.json'
DEFAULT_NUM_YEARS = Parameters.DEFAULT_NUM_YEARS

def __init__(self, behavior_dict=None,
start_year=JSON_START_YEAR,
num_years=DEFAULT_NUM_YEARS,
inflation_rates=None):
if behavior_dict:
if not isinstance(behavior_dict, dict):
raise ValueError('behavior_dict is not a dictionary')
self._vals = behavior_dict
else: # if None, read current-law parameters
self._vals = self._behavior_dict_from_json_file()

if num_years < 1:
raise ValueError('num_years < 1')

self._current_year = start_year
self._start_year = start_year
self._num_years = num_years
self._end_year = start_year + num_years - 1
self.set_default_vals()

def set_default_vals(self):
# extend current-law parameter values into future with _inflation_rates
for name, data in self._vals.items():
values = data['value']
setattr(self, name,
expand_array(values, inflate=False,
inflation_rates=None,
num_years=self._num_years))
self.set_year(self._start_year)

@property
def num_years(self):
return self._num_years

@property
def current_year(self):
return self._current_year

@property
def end_year(self):
return self._end_year

@property
def start_year(self):
return self._start_year

@staticmethod
def _behavior_dict_from_json_file():
"""
Read params.json file and return complete params dictionary.

Parameters
----------
nothing: void

Returns
-------
params: dictionary
containing complete contents of params.json file.
"""
behavior_path = os.path.join(os.path.abspath(
os.path.dirname(__file__)),
Behavior.BEHAVIOR_FILENAME)
if os.path.exists(behavior_path):
with open(behavior_path) as pfile:
behavior = json.load(pfile)
else:
from pkg_resources import resource_stream, Requirement
path_in_egg = os.path.join('taxcalc', Behavior.BEHAVIOR_FILENAME)
buf = resource_stream(Requirement.parse('taxcalc'), path_in_egg)
as_bytes = buf.read()
as_string = as_bytes.decode("utf-8")
behavior = json.loads(as_string)
return behavior

def update_behavior(self, reform):
self.set_default_vals()
if self.current_year != self.start_year:
self.set_year(self.start_year)

for year in reform:
if year != self.start_year:
self.set_year(year)
num_years_to_expand = (self.start_year + self.num_years) - year
for name, values in reform[year].items():
# determine inflation indexing status of parameter with name
cval = getattr(self, name, None)
if cval is None:
# it is a tax law parameter not behavior
continue
nval = expand_array(values,
inflate=False,
inflation_rates=None,
num_years=num_years_to_expand)

cval[(self.current_year - self.start_year):] = nval
self.set_year(self.start_year)

def set_year(self, year):
"""
Set behavior parameters to values for specified calendar year.

Parameters
----------
year: int
calendar year for which to current_year and parameter values

Raises
------
ValueError:
if year is not in [start_year, end_year] range.

Returns
-------
nothing: void

Notes
-----
To increment the current year, use the following statement::

behavior.set_year(behavior.current_year + 1)

where behavior is a policy Behavior object.
"""
if year < self.start_year or year > self.end_year:
msg = 'year passed to set_year() must be in [{},{}] range.'
raise ValueError(msg.format(self.start_year, self.end_year))
self._current_year = year
year_zero_indexed = year - self._start_year
for name in self._vals:
arr = getattr(self, name)
setattr(self, name[1:], arr[year_zero_indexed])
15 changes: 11 additions & 4 deletions taxcalc/calculate.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import pandas as pd
from pandas import DataFrame
import math
import copy
import numpy as np
import pandas as pd
from pandas import DataFrame
from .utils import *
from .functions import *
from .parameters import Parameters
from .records import Records
import copy
from .behavior import Behavior

all_cols = set()

Expand All @@ -24,13 +25,18 @@ def add_df(alldfs, df):

class Calculator(object):

def __init__(self, params=None, records=None, sync_years=True, **kwargs):
def __init__(self, params=None, records=None,
sync_years=True, behavior=None, **kwargs):

if isinstance(params, Parameters):
self._params = params
else:
msg = 'Must supply tax parameters as a Parameters object'
raise ValueError(msg)
if isinstance(behavior, Behavior):
self.behavior = behavior
else:
self.behavior = Behavior(start_year=params.start_year)

if isinstance(records, Records):
self._records = records
Expand Down Expand Up @@ -132,6 +138,7 @@ def calc_all_test(self):
def increment_year(self):
self.records.increment_year()
self.params.set_year(self.params.current_year + 1)
self.behavior.set_year(self.params.current_year)

@property
def current_year(self):
Expand Down
5 changes: 4 additions & 1 deletion taxcalc/parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -477,11 +477,14 @@ def _update(self, year_mods):
default_cpi = False
cpi_inflated = year_mods[year].get(name + '_cpi', default_cpi)
# set post-reform values of parameter with name
cval = getattr(self, name, None)
if cval is None:
# it is a behavior parameter instead
continue
nval = expand_array(values,
inflate=cpi_inflated,
inflation_rates=inf_rates,
num_years=num_years_to_expand)
cval = getattr(self, name)
cval[(self.current_year - self.start_year):] = nval
self.set_year(self._current_year)

Expand Down
Loading