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 logic to enforce policy parameter 'validations' #1502

Merged
merged 3 commits into from
Aug 8, 2017
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
4 changes: 3 additions & 1 deletion RELEASES.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ Release 0.9.3 on 2017-??-??
- None

**New Features**
- None
- Add validation checking of policy parameters involved in a reform
[[#1502](https://github.com/open-source-economics/Tax-Calculator/pull/1502)
by Martin Holmer]

**Bug Fixes**
- None
Expand Down
18 changes: 8 additions & 10 deletions taxcalc/current_law_policy.json
Original file line number Diff line number Diff line change
Expand Up @@ -567,8 +567,7 @@
1000,
1050,
1050,
1050],
"validations": {"min": "default"}
1050]
},

"_STD_Aged": {
Expand All @@ -592,8 +591,7 @@
[1550, 1200, 1200, 1550, 1550],
[1550, 1250, 1250, 1550, 1550],
[1550, 1250, 1250, 1550, 1550],
[1550, 1250, 1250, 1550, 1550]],
"validations": {"min": "default"}
[1550, 1250, 1250, 1550, 1550]]
},

"_STD_allow_charity_ded_nonitemizers": {
Expand Down Expand Up @@ -757,7 +755,7 @@
"col_var": "",
"col_label": "",
"value": [0.1],
"validations": {"min": "default"}
"validations": {"min": 0.1}
},

"_ID_Medical_frt_add4aged": {
Expand Down Expand Up @@ -959,7 +957,7 @@
"col_var": "",
"col_label": "",
"value": [0.5],
"validations": {"max": "default"}
"validations": {"max": 0.5}
},

"_ID_Charity_crt_noncash": {
Expand All @@ -976,7 +974,7 @@
"col_var": "",
"col_label": "",
"value": [0.3],
"validations": {"max": "default"}
"validations": {"max": 0.3}
},

"_ID_Charity_frt": {
Expand All @@ -993,7 +991,7 @@
"col_var": "",
"col_label": "",
"value": [0.0],
"validations": {"min": "default"}
"validations": {"min": 0.0}
},


Expand Down Expand Up @@ -1051,7 +1049,7 @@
"col_var": "",
"col_label": "",
"value": [0.1],
"validations": {"min": "default"}
"validations": {"min": 0.1}
},

"_ID_Casualty_hc": {
Expand Down Expand Up @@ -1108,7 +1106,7 @@
"col_var": "",
"col_label": "",
"value": [0.02],
"validations": {"min": "default"}
"validations": {"min": 0.02}
},

"_ID_Miscellaneous_hc": {
Expand Down
65 changes: 65 additions & 0 deletions taxcalc/policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
# pep8 --ignore=E402 policy.py
# pylint --disable=locally-disabled policy.py

import six
import numpy as np
from taxcalc.parameters import ParametersBase
from taxcalc.growfactors import Growfactors

Expand Down Expand Up @@ -187,6 +189,7 @@ def implement_reform(self, reform):
self.set_year(year)
self._update({year: reform[year]})
self.set_year(precall_current_year)
self._validate_parameter_values()

def current_law_version(self):
"""
Expand Down Expand Up @@ -215,3 +218,65 @@ def translate_json_reform_suffixes(jsonstr):
and return the consolidated JSON string
"""
return jsonstr # TODO temporary code

# ----- begin private methods of Policy class -----

VALIDATED_PARAMETERS = set([
'_II_credit_prt',
'_II_credit_nr_prt',
'_ID_Medical_frt_add4aged',
'_II_brk1',
'_II_brk2',
'_II_brk3',
'_II_brk4',
'_II_brk5',
'_II_brk6',
'_II_brk7',
'_PT_brk1',
'_PT_brk2',
'_PT_brk3',
'_PT_brk4',
'_PT_brk5',
'_PT_brk6',
'_PT_brk7',
'_CTC_new_refund_limit_payroll_rt',
'_FST_AGI_trt',
'_FST_AGI_thd_lo',
'_FST_AGI_thd_hi',
'_AGI_surtax_trt',
'_STD',
'_ID_Casualty_frt',
'_ID_Charity_crt_all',
'_ID_Charity_crt_noncash',
'_ID_Charity_frt',
'_ID_Medical_frt',
'_ID_Miscellaneous_frt'
])

def _validate_parameter_values(self):
"""
Check policy parameter values using validations information from
the current_law_policy.json file.
"""
clp = self.current_law_version()
syr = Policy.JSON_START_YEAR
for pname in Policy.VALIDATED_PARAMETERS:
pvalue = getattr(self, pname)
for vop, vval in self._vals[pname]['validations'].items():
if isinstance(vval, six.string_types):
if vval == 'default':
vvalue = getattr(clp, pname)
else:
vvalue = getattr(self, vval)
else:
vvalue = np.full(pvalue.shape, vval)
assert pvalue.shape == vvalue.shape
for idx in np.ndindex(pvalue.shape):
if vop == 'min' and pvalue[idx] < vvalue[idx]:
msg = '{} {} value {} < min value {}'
raise ValueError(msg.format(idx[0] + syr, pname,
pvalue[idx], vvalue[idx]))
if vop == 'max' and pvalue[idx] > vvalue[idx]:
msg = '{} {} value {} > max value {}'
raise ValueError(msg.format(idx[0] + syr, pname,
pvalue[idx], vvalue[idx]))
62 changes: 58 additions & 4 deletions taxcalc/tests/test_policy.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import os
import sys
import six
import json
import tempfile
import numpy as np
Expand Down Expand Up @@ -52,7 +53,7 @@ def test_correct_Policy_instantiation():
def test_policy_json_content():
ppo = Policy()
policy = getattr(ppo, '_vals')
for name, data in policy.items():
for _, data in policy.items():
start_year = data.get('start_year')
assert isinstance(start_year, int)
assert start_year == Policy.JSON_START_YEAR
Expand Down Expand Up @@ -364,10 +365,10 @@ def test_implement_reform_Policy_raises_on_no_year():

def test_Policy_reform_in_start_year():
ppo = Policy(start_year=2013)
reform = {2013: {'_STD_Aged': [[1400, 1100, 1100, 1400, 1400]]}}
reform = {2013: {'_STD': [[16000, 13000, 13000, 16000, 16000]]}}
ppo.implement_reform(reform)
assert_allclose(ppo.STD_Aged,
np.array([1400, 1100, 1100, 1400, 1400]),
assert_allclose(ppo.STD,
np.array([16000, 13000, 13000, 16000, 16000]),
atol=0.01, rtol=0.0)


Expand Down Expand Up @@ -747,3 +748,56 @@ def test_json_reform_suffixes(tests_path):
unmatched = suffixes ^ Policy.JSON_REFORM_SUFFIXES
if len(unmatched) != 0:
assert unmatched == 'UNMATCHED SUFFIXES'


def test_validated_parameters_set(tests_path):
"""
Check Policy.VALIDATED_PARAMETERS against current_law_policy.json info.
"""
# read current_law_policy.json file into a dictionary
path = os.path.join(tests_path, '..', 'current_law_policy.json')
clpfile = open(path, 'r')
clpdict = json.load(clpfile)
clpfile.close()
# construct set of parameter names with "validations" field in clpdict
json_validated_params = set()
for pname in clpdict:
param = clpdict[pname]
assert isinstance(param, dict)
valid = param.get('validations', None)
if valid:
json_validated_params.add(pname)
for vop, vval in valid.items():
assert vop in ['min', 'max']
if isinstance(vval, six.string_types):
if vval == 'default':
continue
elif vval in clpdict:
continue
else:
assert vval == 'ILLEGAL VALIDATION STRING VALUE'
else:
if isinstance(vval, int):
continue
elif isinstance(vval, float):
continue
else:
assert vval == 'ILLEGAL VALIDATION NUMERIC VALUE'
# compare contents of Policy.VALIDATED_PARAMETERS and json_validated_params
unmatched = Policy.VALIDATED_PARAMETERS ^ json_validated_params
if len(unmatched) != 0:
assert unmatched == 'UNMATCHED VALIDATED PARAMETERS'


def test_validate_param_values_errors():
"""
Check detection of failures of min and max validations.
"""
pol1 = Policy()
ref1 = {2020: {'_ID_Medical_frt': [0.05]}}
with pytest.raises(ValueError):
pol1.implement_reform(ref1)
pol2 = Policy()
ref2 = {2021: {'_ID_Charity_crt_all': [0.60]}}
with pytest.raises(ValueError):
pol2.implement_reform(ref2)
8 changes: 4 additions & 4 deletions taxcalc/tests/test_reforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -612,15 +612,15 @@ def test_r25(puf_subsample):
reform_json = """
{
"start_year": 2015,
"value": {"_ID_Miscellaneous_frt": [0.01]},
"name": "Lower AGI floor for miscellaneous itemded by 1 pts",
"value": {"_ID_Miscellaneous_frt": [0.03]},
"name": "Increase AGI floor for miscellaneous itemded by 1 pts",
"output_type": "iitax",
"compare_with": {}
}
"""
expected_res = ('Lower AGI floor for miscellaneous itemded by 1 pts'
expected_res = ('Increase AGI floor for miscellaneous itemded by 1 pts'
'\n'
'Tax-Calculator,-2.9,-3.0,-3.1,-3.3')
'Tax-Calculator,2.1,2.2,2.3,2.4')
actual_res = reform_results(json.loads(reform_json), puf_subsample)
assert actual_res == expected_res

Expand Down