def __init__(self, policy=None, records=None, verbose=True, sync_years=True, consumption=None, behavior=None): # pylint: disable=too-many-arguments,too-many-branches if isinstance(policy, Policy): self.policy = policy else: raise ValueError('must specify policy as a Policy object') if isinstance(records, Records): self.records = records else: raise ValueError('must specify records as a Records object') if self.policy.current_year < self.records.data_year: self.policy.set_year(self.records.data_year) if consumption is None: self.consumption = Consumption(start_year=policy.start_year) elif isinstance(consumption, Consumption): self.consumption = consumption while self.consumption.current_year < self.policy.current_year: next_year = self.consumption.current_year + 1 self.consumption.set_year(next_year) else: raise ValueError('consumption must be None or Consumption object') if behavior is None: self.behavior = Behavior(start_year=policy.start_year) elif isinstance(behavior, Behavior): self.behavior = behavior while self.behavior.current_year < self.policy.current_year: next_year = self.behavior.current_year + 1 self.behavior.set_year(next_year) else: raise ValueError('behavior must be None or Behavior object') if sync_years and self.records.current_year == self.records.data_year: if verbose: print('You loaded data for ' + str(self.records.data_year) + '.') if len(self.records.IGNORED_VARS) > 0: print('Your data include the following unused ' + 'variables that will be ignored:') for var in self.records.IGNORED_VARS: print(' ' + var) while self.records.current_year < self.policy.current_year: self.records.increment_year() if verbose: print('Tax-Calculator startup automatically ' + 'extrapolated your data to ' + str(self.records.current_year) + '.') assert self.policy.current_year == self.records.current_year
class Calculator(object): """ Constructor for the Calculator class. Parameters ---------- policy: Policy class object this argument must be specified IMPORTANT NOTE: never pass the same Policy object to more than one Calculator. In other words, when specifying more than one Calculator object, do this:: pol1 = Policy() rec1 = Records() calc1 = Calculator(policy=pol1, records=rec1) pol2 = Policy() rec2 = Records() calc2 = Calculator(policy=pol2, records=rec2) records: Records class object this argument must be specified IMPORTANT NOTE: never pass the same Records object to more than one Calculator. In other words, when specifying more than one Calculator object, do this:: pol1 = Policy() rec1 = Records() calc1 = Calculator(policy=pol1, records=rec1) pol2 = Policy() rec2 = Records() calc2 = Calculator(policy=pol2, records=rec2) verbose: boolean specifies whether or not to write to stdout data-loaded and data-extrapolated progress reports; default value is true. sync_years: boolean specifies whether or not to syncronize policy year and records year; default value is true. consumption: Consumption class object specifies consumption response assumptions used to calculate "effective" marginal tax rates; default is None, which implies no consumption responses assumed in marginal tax rate calculations. behavior: Behavior class object specifies behaviorial responses used by Calculator; default is None, which implies no behavioral responses to policy reform. Raises ------ ValueError: if parameters are not the appropriate type. Returns ------- class instance: Calculator """ def __init__(self, policy=None, records=None, verbose=True, sync_years=True, consumption=None, behavior=None): # pylint: disable=too-many-arguments,too-many-branches if isinstance(policy, Policy): self.policy = policy else: raise ValueError('must specify policy as a Policy object') if isinstance(records, Records): self.records = records else: raise ValueError('must specify records as a Records object') if consumption is None: self.consumption = Consumption(start_year=policy.start_year) elif isinstance(consumption, Consumption): self.consumption = consumption while self.consumption.current_year < self.policy.current_year: next_year = self.consumption.current_year + 1 self.consumption.set_year(next_year) else: raise ValueError('consumption must be None or Consumption object') if behavior is None: self.behavior = Behavior(start_year=policy.start_year) elif isinstance(behavior, Behavior): self.behavior = behavior while self.behavior.current_year < self.policy.current_year: next_year = self.behavior.current_year + 1 self.behavior.set_year(next_year) else: raise ValueError('behavior must be None or Behavior object') if sync_years and self.records.current_year == Records.PUF_YEAR: if verbose: print('You loaded data for ' + str(self.records.current_year) + '.') if len(self.records.IGNORED_VARS) > 0: print('Your data include the following unused ' + 'variables that will be ignored:') for var in self.records.IGNORED_VARS: print(' ' + var) while self.records.current_year < self.policy.current_year: self.records.increment_year() if verbose: print('Tax-Calculator startup automatically ' + 'extrapolated your data to ' + str(self.records.current_year) + '.') assert self.policy.current_year == self.records.current_year def calc_all(self, zero_out_calc_vars=False): """ Call all tax-calculation functions. """ # conducts static analysis of Calculator object for current_year self._calc_one_year(zero_out_calc_vars) BenefitSurtax(self) BenefitLimitation(self) FairShareTax(self.policy, self.records) LumpSumTax(self.policy, self.records) ExpandIncome(self.policy, self.records) AfterTaxIncome(self.policy, self.records) def increment_year(self): """ Advance all objects to next year. """ next_year = self.policy.current_year + 1 self.records.increment_year() self.policy.set_year(next_year) self.consumption.set_year(next_year) self.behavior.set_year(next_year) def advance_to_year(self, year): """ The advance_to_year function gives an optional way of implementing increment year functionality by immediately specifying the year as input. New year must be at least the current year. """ iteration = year - self.records.current_year if iteration < 0: raise ValueError('New current year must be ' + 'greater than current year!') for _ in range(iteration): self.increment_year() assert self.records.current_year == year @property def current_year(self): """ Calculator class current calendar year property. """ return self.policy.current_year MTR_VALID_VARIABLES = [ 'e00200p', 'e00200s', 'e00900p', 'e00300', 'e00400', 'e00600', 'e00650', 'e01400', 'e01700', 'e02000', 'e02400', 'p22250', 'p23250', 'e18500', 'e19200', 'e26270', 'e19800', 'e20100' ] def mtr(self, variable_str='e00200p', negative_finite_diff=False, zero_out_calculated_vars=False, wrt_full_compensation=True): """ Calculates the marginal payroll, individual income, and combined tax rates for every tax filing unit. The marginal tax rates are approximated as the change in tax liability caused by a small increase (the finite_diff) in the variable specified by the variable_str divided by that small increase in the variable, when wrt_full_compensation is false. If wrt_full_compensation is true, then the marginal tax rates are computed as the change in tax liability divided by the change in total compensation caused by the small increase in the variable (where the change in total compensation is the sum of the small increase in the variable and any increase in the employer share of payroll taxes caused by the small increase in the variable). If using 'e00200s' as variable_str, the marginal tax rate for all records where MARS != 2 will be missing. If you want to perform a function such as np.mean() on the returned arrays, you will need to account for this. Parameters ---------- variable_str: string specifies type of income or expense that is increased to compute the marginal tax rates. See Notes for list of valid variables. negative_finite_diff: boolean specifies whether or not marginal tax rates are computed by subtracting (rather than adding) a small finite_diff amount to the specified variable. zero_out_calculated_vars: boolean specifies value of zero_out_calc_vars parameter used in calls of Calculator.calc_all() method. wrt_full_compensation: boolean specifies whether or not marginal tax rates on earned income are computed with respect to (wrt) changes in total compensation that includes the employer share of OASDI and HI payroll taxes. Returns ------- mtr_payrolltax: an array of marginal payroll tax rates. mtr_incometax: an array of marginal individual income tax rates. mtr_combined: an array of marginal combined tax rates, which is the sum of mtr_payrolltax and mtr_incometax. Notes ----- Valid variable_str values are: 'e00200p', taxpayer wage/salary earnings (also included in e00200); 'e00200s', spouse wage/salary earnings (also included in e00200); 'e00900p', taxpayer Schedule C self-employment income (also in e00900); 'e00300', taxable interest income; 'e00400', federally-tax-exempt interest income; 'e00600', all dividends included in AGI 'e00650', qualified dividends (also included in e00600) 'e01400', federally-taxable IRA distribution; 'e01700', federally-taxable pension benefits; 'e02000', Schedule E net income/loss 'e02400', all social security (OASDI) benefits; 'p22250', short-term capital gains; 'p23250', long-term capital gains; 'e18500', Schedule A real-estate-tax paid; 'e19200', Schedule A interest paid; 'e26270', S-corporation/partnership income (also included in e02000); 'e19800', Charity cash contributions; 'e20100', Charity non-cash contributions. """ # pylint: disable=too-many-locals,too-many-statements,too-many-branches # check validity of variable_str parameter if variable_str not in Calculator.MTR_VALID_VARIABLES: msg = 'mtr variable_str="{}" is not valid' raise ValueError(msg.format(variable_str)) # specify value for finite_diff parameter finite_diff = 0.01 # a one-cent difference if negative_finite_diff: finite_diff *= -1.0 # save records object in order to restore it after mtr computations recs0 = copy.deepcopy(self.records) # extract variable array(s) from embedded records object variable = getattr(self.records, variable_str) if variable_str == 'e00200p': earnings_var = self.records.e00200 elif variable_str == 'e00200s': earnings_var = self.records.e00200 elif variable_str == 'e00900p': seincome_var = self.records.e00900 elif variable_str == 'e00650': divincome_var = self.records.e00600 elif variable_str == 'e26270': schEincome_var = self.records.e02000 # calculate level of taxes after a marginal increase in income setattr(self.records, variable_str, variable + finite_diff) if variable_str == 'e00200p': self.records.e00200 = earnings_var + finite_diff elif variable_str == 'e00200s': self.records.e00200 = earnings_var + finite_diff elif variable_str == 'e00900p': self.records.e00900 = seincome_var + finite_diff elif variable_str == 'e00650': self.records.e00600 = divincome_var + finite_diff elif variable_str == 'e26270': self.records.e02000 = schEincome_var + finite_diff if self.consumption.has_response(): self.consumption.response(self.records, finite_diff) self.calc_all(zero_out_calc_vars=zero_out_calculated_vars) payrolltax_chng = copy.deepcopy(self.records.payrolltax) incometax_chng = copy.deepcopy(self.records.iitax) combined_taxes_chng = incometax_chng + payrolltax_chng # calculate base level of taxes after restoring records object setattr(self, 'records', recs0) self.calc_all(zero_out_calc_vars=zero_out_calculated_vars) payrolltax_base = copy.deepcopy(self.records.payrolltax) incometax_base = copy.deepcopy(self.records.iitax) combined_taxes_base = incometax_base + payrolltax_base # compute marginal changes in combined tax liability payrolltax_diff = payrolltax_chng - payrolltax_base incometax_diff = incometax_chng - incometax_base combined_diff = combined_taxes_chng - combined_taxes_base # specify optional adjustment for employer (er) OASDI+HI payroll taxes mtr_on_earnings = (variable_str == 'e00200p' or variable_str == 'e00200s') if wrt_full_compensation and mtr_on_earnings: adj = np.where( variable < self.policy.SS_Earnings_c, 0.5 * (self.policy.FICA_ss_trt + self.policy.FICA_mc_trt), 0.5 * self.policy.FICA_mc_trt) else: adj = 0.0 # compute marginal tax rates mtr_payrolltax = payrolltax_diff / (finite_diff * (1.0 + adj)) mtr_incometax = incometax_diff / (finite_diff * (1.0 + adj)) mtr_combined = combined_diff / (finite_diff * (1.0 + adj)) # if variable_str is e00200s, set MTR to NaN for units without a spouse if variable_str == 'e00200s': mtr_payrolltax = np.where(self.records.MARS == 2, mtr_payrolltax, np.nan) mtr_incometax = np.where(self.records.MARS == 2, mtr_incometax, np.nan) mtr_combined = np.where(self.records.MARS == 2, mtr_combined, np.nan) # return the three marginal tax rate arrays return (mtr_payrolltax, mtr_incometax, mtr_combined) def current_law_version(self): """ Return Calculator object same as self except with current-law policy. """ clp = self.policy.current_law_version() recs = copy.deepcopy(self.records) cons = copy.deepcopy(self.consumption) behv = copy.deepcopy(self.behavior) calc = Calculator(policy=clp, records=recs, sync_years=False, consumption=cons, behavior=behv) return calc @staticmethod def read_json_param_files(reform_filename, assump_filename, arrays_not_lists=True): """ Read JSON files and call Calculator.read_json_*_text methods returning a single dictionary containing five key:dict pairs: 'policy':dict, 'consumption':dict, 'behavior':dict, 'growdiff_baseline':dict and 'growdiff_response':dict. """ if reform_filename is None: rpol_dict = dict() elif os.path.isfile(reform_filename): txt = open(reform_filename, 'r').read() rpol_dict = (Calculator._read_json_policy_reform_text( txt, arrays_not_lists)) else: msg = 'policy reform file {} could not be found' raise ValueError(msg.format(reform_filename)) if assump_filename is None: cons_dict = dict() behv_dict = dict() gdiff_base_dict = dict() gdiff_resp_dict = dict() elif os.path.isfile(assump_filename): txt = open(assump_filename, 'r').read() (cons_dict, behv_dict, gdiff_base_dict, gdiff_resp_dict) = (Calculator._read_json_econ_assump_text( txt, arrays_not_lists)) else: msg = 'economic assumption file {} could not be found' raise ValueError(msg.format(assump_filename)) param_dict = dict() param_dict['policy'] = rpol_dict param_dict['consumption'] = cons_dict param_dict['behavior'] = behv_dict param_dict['growdiff_baseline'] = gdiff_base_dict param_dict['growdiff_response'] = gdiff_resp_dict return param_dict REQUIRED_REFORM_KEYS = set(['policy']) REQUIRED_ASSUMP_KEYS = set( ['consumption', 'behavior', 'growdiff_baseline', 'growdiff_response']) # ----- begin private methods of Calculator class ----- def _taxinc_to_amt(self): """ Call TaxInc through AMT functions. """ TaxInc(self.policy, self.records) SchXYZTax(self.policy, self.records) GainsTax(self.policy, self.records) AGIsurtax(self.policy, self.records) NetInvIncTax(self.policy, self.records) AMT(self.policy, self.records) def _calc_one_year(self, zero_out_calc_vars=False): """ Call all the functions except those in the calc_all() method. """ if zero_out_calc_vars: self.records.zero_out_changing_calculated_vars() # pdb.set_trace() EI_PayrollTax(self.policy, self.records) DependentCare(self.policy, self.records) Adj(self.policy, self.records) ALD_InvInc_ec_base(self.policy, self.records) CapGains(self.policy, self.records) SSBenefits(self.policy, self.records) UBI(self.policy, self.records) AGI(self.policy, self.records) ItemDed(self.policy, self.records) AdditionalMedicareTax(self.policy, self.records) StdDed(self.policy, self.records) # Store calculated standard deduction, calculate # taxes with standard deduction, store AMT + Regular Tax std = copy.deepcopy(self.records.standard) item = copy.deepcopy(self.records.c04470) item_no_limit = copy.deepcopy(self.records.c21060) item_phaseout = copy.deepcopy(self.records.c21040) self.records.c04470 = np.zeros(self.records.dim) self.records.c21060 = np.zeros(self.records.dim) self.records.c21040 = np.zeros(self.records.dim) self._taxinc_to_amt() std_taxes = copy.deepcopy(self.records.c05800) # Set standard deduction to zero, calculate taxes w/o # standard deduction, and store AMT + Regular Tax self.records.standard = np.zeros(self.records.dim) self.records.c21060 = item_no_limit self.records.c21040 = item_phaseout self.records.c04470 = item self._taxinc_to_amt() item_taxes = copy.deepcopy(self.records.c05800) # Replace standard deduction with zero where the taxpayer # would be better off itemizing self.records.standard[:] = np.where(item_taxes < std_taxes, 0., std) self.records.c04470[:] = np.where(item_taxes < std_taxes, item, 0.) self.records.c21060[:] = np.where(item_taxes < std_taxes, item_no_limit, 0.) self.records.c21040[:] = np.where(item_taxes < std_taxes, item_phaseout, 0.) # Calculate taxes with optimal itemized deduction self._taxinc_to_amt() F2441(self.policy, self.records) EITC(self.policy, self.records) ChildTaxCredit(self.policy, self.records) AmOppCreditParts(self.policy, self.records) SchR(self.policy, self.records) EducationTaxCredit(self.policy, self.records) NonrefundableCredits(self.policy, self.records) AdditionalCTC(self.policy, self.records) C1040(self.policy, self.records) CTC_new(self.policy, self.records) IITAX(self.policy, self.records) @staticmethod def _read_json_policy_reform_text(text_string, arrays_not_lists): """ Strip //-comments from text_string and return 1 dict based on the JSON. Specified text is JSON with at least 1 high-level string:object pair: a "policy": {...} pair. Other high-level pairs will be ignored by this method, except that a "consumption", "behavior", "growdiff_baseline" or "growdiff_response" key will raise a ValueError. The {...} object may be empty (that is, be {}), or may contain one or more pairs with parameter string primary keys and string years as secondary keys. See tests/test_calculate.py for an extended example of a commented JSON policy reform text that can be read by this method. Returned dictionary rpol_dict has integer years as primary keys and string parameters as secondary keys. This returned dictionary is suitable as the argument to the Policy implement_reform(rpol_dict) method ONLY if the function argument arrays_not_lists is True. """ # strip out //-comments without changing line numbers json_str = re.sub('//.*', ' ', text_string) # convert JSON text into a Python dictionary try: raw_dict = json.loads(json_str) except ValueError as valerr: msg = 'Policy reform text below contains invalid JSON:\n' msg += str(valerr) + '\n' msg += 'Above location of the first error may be approximate.\n' msg += 'The invalid JSON reform text is between the lines:\n' bline = 'XX----.----1----.----2----.----3----.----4' bline += '----.----5----.----6----.----7' msg += bline + '\n' linenum = 0 for line in json_str.split('\n'): linenum += 1 msg += '{:02d}{}'.format(linenum, line) + '\n' msg += bline + '\n' raise ValueError(msg) # check key contents of dictionary actual_keys = raw_dict.keys() for rkey in Calculator.REQUIRED_REFORM_KEYS: if rkey not in actual_keys: msg = 'key "{}" is not in policy reform file' raise ValueError(msg.format(rkey)) for rkey in actual_keys: if rkey in Calculator.REQUIRED_ASSUMP_KEYS: msg = 'key "{}" should be in economic assumption file' raise ValueError(msg.format(rkey)) # convert the policy dictionary in raw_dict rpol_dict = Calculator._convert_parameter_dict(raw_dict['policy'], arrays_not_lists) return rpol_dict @staticmethod def _read_json_econ_assump_text(text_string, arrays_not_lists): """ Strip //-comments from text_string and return 4 dict based on the JSON. Specified text is JSON with at least 4 high-level string:object pairs: a "consumption": {...} pair, a "behavior": {...} pair, a "growdiff_baseline": {...} pair, and a "growdiff_response": {...} pair. Other high-level pairs will be ignored by this method, except that a "policy" key will raise a ValueError. The {...} object may be empty (that is, be {}), or may contain one or more pairs with parameter string primary keys and string years as secondary keys. See tests/test_calculate.py for an extended example of a commented JSON economic assumption text that can be read by this method. Note that an example is shown in the ASSUMP_CONTENTS string in tests/test_calculate.py file. Returned dictionaries (cons_dict, behv_dict, gdiff_baseline_dict, gdiff_respose_dict) have integer years as primary keys and string parameters as secondary keys. These returned dictionaries are suitable as the arguments to the Consumption.update_consumption(cons_dict) method, or the Behavior.update_behavior(behv_dict) method, or the Growdiff.update_growdiff(gdiff_dict) method, but ONLY if the function argument arrays_not_lists is True. """ # pylint: disable=too-many-locals # strip out //-comments without changing line numbers json_str = re.sub('//.*', ' ', text_string) # convert JSON text into a Python dictionary try: raw_dict = json.loads(json_str) except ValueError as valerr: msg = 'Economic assumption text below contains invalid JSON:\n' msg += str(valerr) + '\n' msg += 'Above location of the first error may be approximate.\n' msg += 'The invalid JSON asssump text is between the lines:\n' bline = 'XX----.----1----.----2----.----3----.----4' bline += '----.----5----.----6----.----7' msg += bline + '\n' linenum = 0 for line in json_str.split('\n'): linenum += 1 msg += '{:02d}{}'.format(linenum, line) + '\n' msg += bline + '\n' raise ValueError(msg) # check key contents of dictionary actual_keys = raw_dict.keys() for rkey in Calculator.REQUIRED_ASSUMP_KEYS: if rkey not in actual_keys: msg = 'key "{}" is not in economic assumption file' raise ValueError(msg.format(rkey)) for rkey in actual_keys: if rkey in Calculator.REQUIRED_REFORM_KEYS: msg = 'key "{}" should be in policy reform file' raise ValueError(msg.format(rkey)) # convert the assumption dictionaries in raw_dict key = 'consumption' cons_dict = Calculator._convert_parameter_dict(raw_dict[key], arrays_not_lists) key = 'behavior' behv_dict = Calculator._convert_parameter_dict(raw_dict[key], arrays_not_lists) key = 'growdiff_baseline' gdiff_base_dict = Calculator._convert_parameter_dict( raw_dict[key], arrays_not_lists) key = 'growdiff_response' gdiff_resp_dict = Calculator._convert_parameter_dict( raw_dict[key], arrays_not_lists) return (cons_dict, behv_dict, gdiff_base_dict, gdiff_resp_dict) @staticmethod def _convert_parameter_dict(param_key_dict, arrays_not_lists): """ Converts specified param_key_dict into a dictionary whose primary keys are calendary years, and hence, is suitable as the argument to the Policy.implement_reform() method, or the Consumption.update_consumption() method, or the Behavior.update_behavior() method, or the Growdiff.update_growdiff() method, but only if function argument is arrays_not_lists=True. Specified input dictionary has string parameter primary keys and string years as secondary keys. Returned dictionary has integer years as primary keys and string parameters as secondary keys. """ # convert year skey strings into integers and # optionally convert lists into np.arrays year_param = dict() for pkey, sdict in param_key_dict.items(): if not isinstance(pkey, six.string_types): msg = 'pkey {} in reform is not a string' raise ValueError(msg.format(pkey)) rdict = dict() if not isinstance(sdict, dict): msg = 'pkey {} in reform is not paired with a dict' raise ValueError(msg.format(pkey)) for skey, val in sdict.items(): if not isinstance(skey, six.string_types): msg = 'skey {} in reform is not a string' raise ValueError(msg.format(skey)) else: year = int(skey) if isinstance(val, list) and arrays_not_lists: rdict[year] = np.array(val) else: rdict[year] = val year_param[pkey] = rdict # convert year_param dictionary to year_key_dict dictionary year_key_dict = dict() years = set() for param, sdict in year_param.items(): for year, val in sdict.items(): if year not in years: years.add(year) year_key_dict[year] = dict() year_key_dict[year][param] = val return year_key_dict
def init(self, input_data, tax_year, reform, assump, growdiff_response, aging_input_data, exact_calculations): """ TaxCalcIO class post-constructor method that completes initialization. Parameters ---------- First four parameters are same as for TaxCalcIO constructor: input_data, tax_year, reform, assump. growdiff_response: Growdiff object or None growdiff_response Growdiff object is used only by the TaxCalcIO.growmodel_analysis method; must be None in all other cases. aging_input_data: boolean whether or not to extrapolate Records data from data year to tax_year. exact_calculations: boolean specifies whether or not exact tax calculations are done without any smoothing of "stair-step" provisions in the tax law. """ # pylint: disable=too-many-arguments,too-many-locals # pylint: disable=too-many-statements,too-many-branches self.errmsg = '' # get parameter dictionaries from --reform and --assump files paramdict = Calculator.read_json_param_objects(reform, assump) # create Behavior object beh = Behavior() beh.update_behavior(paramdict['behavior']) self.behavior_has_any_response = beh.has_any_response() # create gdiff_baseline object gdiff_baseline = Growdiff() gdiff_baseline.update_growdiff(paramdict['growdiff_baseline']) # create Growfactors clp object that incorporates gdiff_baseline gfactors_clp = Growfactors() gdiff_baseline.apply_to(gfactors_clp) # specify gdiff_response object if growdiff_response is None: gdiff_response = Growdiff() gdiff_response.update_growdiff(paramdict['growdiff_response']) elif isinstance(growdiff_response, Growdiff): gdiff_response = growdiff_response else: gdiff_response = None msg = 'TaxCalcIO.more_init: growdiff_response is neither None ' msg += 'nor a Growdiff object' self.errmsg += 'ERROR: {}\n'.format(msg) if gdiff_response is not None: some_gdiff_response = gdiff_response.has_any_response() if self.behavior_has_any_response and some_gdiff_response: msg = 'ASSUMP file cannot specify any "behavior" when using ' msg += 'GrowModel or when ASSUMP file has "growdiff_response"' self.errmsg += 'ERROR: {}\n'.format(msg) # create Growfactors ref object that has both gdiff objects applied gfactors_ref = Growfactors() gdiff_baseline.apply_to(gfactors_ref) if gdiff_response is not None: gdiff_response.apply_to(gfactors_ref) # create Policy objects if self.specified_reform: pol = Policy(gfactors=gfactors_ref) try: pol.implement_reform(paramdict['policy']) self.errmsg += pol.reform_errors except ValueError as valerr_msg: self.errmsg += valerr_msg.__str__() else: pol = Policy(gfactors=gfactors_clp) clp = Policy(gfactors=gfactors_clp) # check for valid tax_year value if tax_year < pol.start_year: msg = 'tax_year {} less than policy.start_year {}' msg = msg.format(tax_year, pol.start_year) self.errmsg += 'ERROR: {}\n'.format(msg) if tax_year > pol.end_year: msg = 'tax_year {} greater than policy.end_year {}' msg = msg.format(tax_year, pol.end_year) self.errmsg += 'ERROR: {}\n'.format(msg) # any errors imply cannot proceed with calculations if self.errmsg: return # set policy to tax_year pol.set_year(tax_year) clp.set_year(tax_year) # read input file contents into Records objects if aging_input_data: if self.cps_input_data: recs = Records.cps_constructor( gfactors=gfactors_ref, exact_calculations=exact_calculations) recs_clp = Records.cps_constructor( gfactors=gfactors_clp, exact_calculations=exact_calculations) else: # if not cps_input_data recs = Records(data=input_data, gfactors=gfactors_ref, exact_calculations=exact_calculations) recs_clp = Records(data=input_data, gfactors=gfactors_clp, exact_calculations=exact_calculations) else: # input_data are raw data that are not being aged recs = Records(data=input_data, gfactors=None, exact_calculations=exact_calculations, weights=None, adjust_ratios=None, start_year=tax_year) recs_clp = copy.deepcopy(recs) if tax_year < recs.data_year: msg = 'tax_year {} less than records.data_year {}' msg = msg.format(tax_year, recs.data_year) self.errmsg += 'ERROR: {}\n'.format(msg) # create Calculator objects con = Consumption() con.update_consumption(paramdict['consumption']) self.calc = Calculator(policy=pol, records=recs, verbose=True, consumption=con, behavior=beh, sync_years=aging_input_data) self.calc_clp = Calculator(policy=clp, records=recs_clp, verbose=False, consumption=con, sync_years=aging_input_data) # remember parameter dictionary for reform documentation self.param_dict = paramdict
def analyze(self, writing_output_file=False, output_tables=False, output_graphs=False, output_ceeu=False, dump_varset=None, output_dump=False, output_sqldb=False): """ Conduct tax analysis. Parameters ---------- writing_output_file: boolean whether or not to generate and write output file output_tables: boolean whether or not to generate and write distributional tables to a text file output_graphs: boolean whether or not to generate and write HTML graphs of average and marginal tax rates by income percentile output_ceeu: boolean whether or not to calculate and write to stdout standard certainty-equivalent expected-utility statistics dump_varset: set custom set of variables to include in dump and sqldb output; None implies include all variables in dump and sqldb output output_dump: boolean whether or not to replace standard output with all input and calculated variables using their Tax-Calculator names output_sqldb: boolean whether or not to write SQLite3 database with dump table containing same output as written by output_dump to a csv file Returns ------- Nothing """ # pylint: disable=too-many-arguments,too-many-branches # in order to use print(), pylint: disable=superfluous-parens if self.calc.policy.reform_warnings: warn = 'PARAMETER VALUE WARNING(S): {}\n{}{}' print( warn.format('(read documentation for each parameter)', self.calc.policy.reform_warnings, 'CONTINUING WITH CALCULATIONS...')) calc_clp_calculated = False if output_dump or output_sqldb: # might need marginal tax rates (mtr_paytax, mtr_inctax, _) = self.calc.mtr(wrt_full_compensation=False) else: # definitely do not need marginal tax rates mtr_paytax = None mtr_inctax = None if self.behavior_has_any_response: self.calc = Behavior.response(self.calc_clp, self.calc) calc_clp_calculated = True else: self.calc.calc_all() # optionally conduct normative welfare analysis if output_ceeu: if self.behavior_has_any_response: ceeu_results = 'SKIP --ceeu output because baseline and ' ceeu_results += 'reform cannot be sensibly compared\n ' ceeu_results += ' ' ceeu_results += 'when specifying "behavior" with --assump ' ceeu_results += 'option' elif self.calc.total_weight() <= 0.: ceeu_results = 'SKIP --ceeu output because ' ceeu_results += 'sum of weights is not positive' else: self.calc_clp.calc_all() calc_clp_calculated = True cedict = self.calc_clp.ce_aftertax_income( self.calc, custom_params=None, require_no_agg_tax_change=False) ceeu_results = TaxCalcIO.ceeu_output(cedict) else: ceeu_results = None # extract output if writing_output_file if writing_output_file: self.write_output_file(output_dump, dump_varset, mtr_paytax, mtr_inctax) self.write_doc_file() # optionally write --sqldb output to SQLite3 database if output_sqldb: self.write_sqldb_file(dump_varset, mtr_paytax, mtr_inctax) # optionally write --tables output to text file if output_tables: if not calc_clp_calculated: self.calc_clp.calc_all() calc_clp_calculated = True self.write_tables_file() # optionally write --graphs output to HTML files if output_graphs: if not calc_clp_calculated: self.calc_clp.calc_all() calc_clp_calculated = True self.write_graph_files() # optionally write --ceeu output to stdout if ceeu_results: print(ceeu_results)
def init(self, input_data, tax_year, baseline, reform, assump, growdiff_growmodel, aging_input_data, exact_calculations): """ TaxCalcIO class post-constructor method that completes initialization. Parameters ---------- First five are same as the first five of the TaxCalcIO constructor: input_data, tax_year, baseline, reform, assump. growdiff_growmodel: GrowDiff object or None growdiff_growmodel GrowDiff object is used only in the TaxCalcIO.growmodel_analysis method. aging_input_data: boolean whether or not to extrapolate Records data from data year to tax_year. exact_calculations: boolean specifies whether or not exact tax calculations are done without any smoothing of "stair-step" provisions in the tax law. """ # pylint: disable=too-many-arguments,too-many-locals # pylint: disable=too-many-statements,too-many-branches self.errmsg = '' # get policy parameter dictionary from --baseline file basedict = Calculator.read_json_param_objects(baseline, None) # get assumption sub-dictionaries paramdict = Calculator.read_json_param_objects(None, assump) # get policy parameter dictionaries from --reform file(s) policydicts = list() if self.specified_reform: reforms = reform.split('+') for ref in reforms: pdict = Calculator.read_json_param_objects(ref, None) policydicts.append(pdict['policy']) paramdict['policy'] = policydicts[0] # remember parameters for reform documentation self.param_dict = paramdict self.policy_dicts = policydicts # create Behavior object beh = Behavior() try: beh.update_behavior(paramdict['behavior']) except ValueError as valerr_msg: self.errmsg += valerr_msg.__str__() self.behavior_has_any_response = beh.has_any_response() # create gdiff_baseline object gdiff_baseline = GrowDiff() try: gdiff_baseline.update_growdiff(paramdict['growdiff_baseline']) except ValueError as valerr_msg: self.errmsg += valerr_msg.__str__() # create GrowFactors base object that incorporates gdiff_baseline gfactors_base = GrowFactors() gdiff_baseline.apply_to(gfactors_base) # specify gdiff_response object gdiff_response = GrowDiff() try: gdiff_response.update_growdiff(paramdict['growdiff_response']) except ValueError as valerr_msg: self.errmsg += valerr_msg.__str__() # create GrowFactors ref object that has all gdiff objects applied gfactors_ref = GrowFactors() gdiff_baseline.apply_to(gfactors_ref) gdiff_response.apply_to(gfactors_ref) if growdiff_growmodel: growdiff_growmodel.apply_to(gfactors_ref) # create Policy objects: # ... the baseline Policy object base = Policy(gfactors=gfactors_base) try: base.implement_reform(basedict['policy'], print_warnings=False, raise_errors=False) self.errmsg += base.parameter_errors except ValueError as valerr_msg: self.errmsg += valerr_msg.__str__() # ... the reform Policy object if self.specified_reform: pol = Policy(gfactors=gfactors_ref) for poldict in policydicts: try: pol.implement_reform(poldict, print_warnings=False, raise_errors=False) self.errmsg += pol.parameter_errors except ValueError as valerr_msg: self.errmsg += valerr_msg.__str__() else: pol = Policy(gfactors=gfactors_base) # create Consumption object con = Consumption() try: con.update_consumption(paramdict['consumption']) except ValueError as valerr_msg: self.errmsg += valerr_msg.__str__() # create GrowModel object self.growmodel = GrowModel() try: self.growmodel.update_growmodel(paramdict['growmodel']) except ValueError as valerr_msg: self.errmsg += valerr_msg.__str__() # check for valid tax_year value if tax_year < pol.start_year: msg = 'tax_year {} less than policy.start_year {}' msg = msg.format(tax_year, pol.start_year) self.errmsg += 'ERROR: {}\n'.format(msg) if tax_year > pol.end_year: msg = 'tax_year {} greater than policy.end_year {}' msg = msg.format(tax_year, pol.end_year) self.errmsg += 'ERROR: {}\n'.format(msg) # any errors imply cannot proceed with calculations if self.errmsg: return # set policy to tax_year pol.set_year(tax_year) base.set_year(tax_year) # read input file contents into Records objects if aging_input_data: if self.cps_input_data: recs = Records.cps_constructor( gfactors=gfactors_ref, exact_calculations=exact_calculations) recs_base = Records.cps_constructor( gfactors=gfactors_base, exact_calculations=exact_calculations) else: # if not cps_input_data but aging_input_data recs = Records(data=input_data, gfactors=gfactors_ref, exact_calculations=exact_calculations) recs_base = Records(data=input_data, gfactors=gfactors_base, exact_calculations=exact_calculations) else: # input_data are raw data that are not being aged recs = Records(data=input_data, gfactors=None, exact_calculations=exact_calculations, weights=None, adjust_ratios=None, start_year=tax_year) recs_base = copy.deepcopy(recs) if tax_year < recs.data_year: msg = 'tax_year {} less than records.data_year {}' msg = msg.format(tax_year, recs.data_year) self.errmsg += 'ERROR: {}\n'.format(msg) # create Calculator objects self.calc = Calculator(policy=pol, records=recs, verbose=True, consumption=con, behavior=beh, sync_years=aging_input_data) self.calc_base = Calculator(policy=base, records=recs_base, verbose=False, consumption=con, sync_years=aging_input_data)
def analyze(self, writing_output_file=False, output_tables=False, output_graphs=False, dump_varset=None, output_dump=False, output_sqldb=False): """ Conduct tax analysis. Parameters ---------- writing_output_file: boolean whether or not to generate and write output file output_tables: boolean whether or not to generate and write distributional tables to a text file output_graphs: boolean whether or not to generate and write HTML graphs of average and marginal tax rates by income percentile dump_varset: set custom set of variables to include in dump and sqldb output; None implies include all variables in dump and sqldb output output_dump: boolean whether or not to replace standard output with all input and calculated variables using their Tax-Calculator names output_sqldb: boolean whether or not to write SQLite3 database with dump table containing same output as written by output_dump to a csv file Returns ------- Nothing """ # pylint: disable=too-many-arguments,too-many-branches,too-many-locals if self.puf_input_data and self.calc.reform_warnings: warn = 'PARAMETER VALUE WARNING(S): {}\n{}{}' # pragma: no cover print( # pragma: no cover warn.format('(read documentation for each parameter)', self.calc.reform_warnings, 'CONTINUING WITH CALCULATIONS...') ) calc_base_calculated = False if self.behavior_has_any_response: self.calc = Behavior.response(self.calc_base, self.calc) calc_base_calculated = True else: self.calc.calc_all() if output_dump or output_sqldb: # might need marginal tax rates (mtr_paytax, mtr_inctax, _) = self.calc.mtr(wrt_full_compensation=False, calc_all_already_called=True) else: # definitely do not need marginal tax rates mtr_paytax = None mtr_inctax = None # extract output if writing_output_file if writing_output_file: self.write_output_file(output_dump, dump_varset, mtr_paytax, mtr_inctax) self.write_doc_file() # optionally write --sqldb output to SQLite3 database if output_sqldb: self.write_sqldb_file(dump_varset, mtr_paytax, mtr_inctax) # optionally write --tables output to text file if output_tables: if not calc_base_calculated: self.calc_base.calc_all() calc_base_calculated = True self.write_tables_file() # optionally write --graphs output to HTML files if output_graphs: if not calc_base_calculated: self.calc_base.calc_all() calc_base_calculated = True self.write_graph_files()
class Calculator(object): """ Constructor for the Calculator class. Parameters ---------- policy: Policy class object this argument must be specified and object is copied for internal use records: Records class object this argument must be specified and object is copied for internal use verbose: boolean specifies whether or not to write to stdout data-loaded and data-extrapolated progress reports; default value is true. sync_years: boolean specifies whether or not to synchronize policy year and records year; default value is true. consumption: Consumption class object specifies consumption response assumptions used to calculate "effective" marginal tax rates; default is None, which implies no consumption responses assumed in marginal tax rate calculations; when argument is an object it is copied for internal use behavior: Behavior class object specifies behavioral responses used by Calculator; default is None, which implies no behavioral responses to policy reform; when argument is an object it is copied for internal use Raises ------ ValueError: if parameters are not the appropriate type. Returns ------- class instance: Calculator Notes ----- The most efficient way to specify current-law and reform Calculator objects is as follows: pol = Policy() rec = Records() calc1 = Calculator(policy=pol, records=rec) # current-law pol.implement_reform(...) calc2 = Calculator(policy=pol, records=rec) # reform All calculations are done on the internal copies of the Policy and Records objects passed to each of the two Calculator constructors. """ def __init__(self, policy=None, records=None, verbose=True, sync_years=True, consumption=None, behavior=None): # pylint: disable=too-many-arguments,too-many-branches if isinstance(policy, Policy): self.policy = copy.deepcopy(policy) else: raise ValueError('must specify policy as a Policy object') if isinstance(records, Records): self.records = copy.deepcopy(records) else: raise ValueError('must specify records as a Records object') if self.policy.current_year < self.records.data_year: self.policy.set_year(self.records.data_year) if consumption is None: self.consumption = Consumption(start_year=policy.start_year) elif isinstance(consumption, Consumption): self.consumption = copy.deepcopy(consumption) while self.consumption.current_year < self.policy.current_year: next_year = self.consumption.current_year + 1 self.consumption.set_year(next_year) else: raise ValueError('consumption must be None or Consumption object') if behavior is None: self.behavior = Behavior(start_year=policy.start_year) elif isinstance(behavior, Behavior): self.behavior = copy.deepcopy(behavior) while self.behavior.current_year < self.policy.current_year: next_year = self.behavior.current_year + 1 self.behavior.set_year(next_year) else: raise ValueError('behavior must be None or Behavior object') if sync_years and self.records.current_year == self.records.data_year: if verbose: print('You loaded data for ' + str(self.records.data_year) + '.') if self.records.IGNORED_VARS: print('Your data include the following unused ' + 'variables that will be ignored:') for var in self.records.IGNORED_VARS: print(' ' + var) while self.records.current_year < self.policy.current_year: self.records.increment_year() if verbose: print('Tax-Calculator startup automatically ' + 'extrapolated your data to ' + str(self.records.current_year) + '.') assert self.policy.current_year == self.records.current_year def calc_all(self, zero_out_calc_vars=False): """ Call all tax-calculation functions. """ # conducts static analysis of Calculator object for current_year assert self.records.current_year == self.policy.current_year self._calc_one_year(zero_out_calc_vars) BenefitSurtax(self) BenefitLimitation(self) FairShareTax(self.policy, self.records) LumpSumTax(self.policy, self.records) ExpandIncome(self.policy, self.records) AfterTaxIncome(self.policy, self.records) def increment_year(self): """ Advance all objects to next year. """ next_year = self.policy.current_year + 1 self.records.increment_year() self.policy.set_year(next_year) self.consumption.set_year(next_year) self.behavior.set_year(next_year) def advance_to_year(self, year): """ The advance_to_year function gives an optional way of implementing increment year functionality by immediately specifying the year as input. New year must be at least the current year. """ iteration = year - self.records.current_year if iteration < 0: raise ValueError('New current year must be ' + 'greater than current year!') for _ in range(iteration): self.increment_year() assert self.records.current_year == year @property def current_year(self): """ Calculator class current calendar year property. """ return self.policy.current_year @property def data_year(self): """ Calculator class initial (i.e., first) records data year property. """ return self.records.data_year MTR_VALID_VARIABLES = [ 'e00200p', 'e00200s', 'e00900p', 'e00300', 'e00400', 'e00600', 'e00650', 'e01400', 'e01700', 'e02000', 'e02400', 'p22250', 'p23250', 'e18500', 'e19200', 'e26270', 'e19800', 'e20100' ] def mtr(self, variable_str='e00200p', negative_finite_diff=False, zero_out_calculated_vars=False, wrt_full_compensation=True): """ Calculates the marginal payroll, individual income, and combined tax rates for every tax filing unit. The marginal tax rates are approximated as the change in tax liability caused by a small increase (the finite_diff) in the variable specified by the variable_str divided by that small increase in the variable, when wrt_full_compensation is false. If wrt_full_compensation is true, then the marginal tax rates are computed as the change in tax liability divided by the change in total compensation caused by the small increase in the variable (where the change in total compensation is the sum of the small increase in the variable and any increase in the employer share of payroll taxes caused by the small increase in the variable). If using 'e00200s' as variable_str, the marginal tax rate for all records where MARS != 2 will be missing. If you want to perform a function such as np.mean() on the returned arrays, you will need to account for this. Parameters ---------- variable_str: string specifies type of income or expense that is increased to compute the marginal tax rates. See Notes for list of valid variables. negative_finite_diff: boolean specifies whether or not marginal tax rates are computed by subtracting (rather than adding) a small finite_diff amount to the specified variable. zero_out_calculated_vars: boolean specifies value of zero_out_calc_vars parameter used in calls of Calculator.calc_all() method. wrt_full_compensation: boolean specifies whether or not marginal tax rates on earned income are computed with respect to (wrt) changes in total compensation that includes the employer share of OASDI and HI payroll taxes. Returns ------- mtr_payrolltax: an array of marginal payroll tax rates. mtr_incometax: an array of marginal individual income tax rates. mtr_combined: an array of marginal combined tax rates, which is the sum of mtr_payrolltax and mtr_incometax. Notes ----- Valid variable_str values are: 'e00200p', taxpayer wage/salary earnings (also included in e00200); 'e00200s', spouse wage/salary earnings (also included in e00200); 'e00900p', taxpayer Schedule C self-employment income (also in e00900); 'e00300', taxable interest income; 'e00400', federally-tax-exempt interest income; 'e00600', all dividends included in AGI 'e00650', qualified dividends (also included in e00600) 'e01400', federally-taxable IRA distribution; 'e01700', federally-taxable pension benefits; 'e02000', Schedule E total net income/loss 'e02400', all social security (OASDI) benefits; 'p22250', short-term capital gains; 'p23250', long-term capital gains; 'e18500', Schedule A real-estate-tax paid; 'e19200', Schedule A interest paid; 'e26270', S-corporation/partnership income (also included in e02000); 'e19800', Charity cash contributions; 'e20100', Charity non-cash contributions. """ # pylint: disable=too-many-locals,too-many-statements,too-many-branches # check validity of variable_str parameter if variable_str not in Calculator.MTR_VALID_VARIABLES: msg = 'mtr variable_str="{}" is not valid' raise ValueError(msg.format(variable_str)) # specify value for finite_diff parameter finite_diff = 0.01 # a one-cent difference if negative_finite_diff: finite_diff *= -1.0 # save records object in order to restore it after mtr computations recs0 = copy.deepcopy(self.records) # extract variable array(s) from embedded records object variable = getattr(self.records, variable_str) if variable_str == 'e00200p': earnings_var = self.records.e00200 elif variable_str == 'e00200s': earnings_var = self.records.e00200 elif variable_str == 'e00900p': seincome_var = self.records.e00900 elif variable_str == 'e00650': divincome_var = self.records.e00600 elif variable_str == 'e26270': schEincome_var = self.records.e02000 # calculate level of taxes after a marginal increase in income setattr(self.records, variable_str, variable + finite_diff) if variable_str == 'e00200p': self.records.e00200 = earnings_var + finite_diff elif variable_str == 'e00200s': self.records.e00200 = earnings_var + finite_diff elif variable_str == 'e00900p': self.records.e00900 = seincome_var + finite_diff elif variable_str == 'e00650': self.records.e00600 = divincome_var + finite_diff elif variable_str == 'e26270': self.records.e02000 = schEincome_var + finite_diff if self.consumption.has_response(): self.consumption.response(self.records, finite_diff) self.calc_all(zero_out_calc_vars=zero_out_calculated_vars) payrolltax_chng = copy.deepcopy(self.records.payrolltax) incometax_chng = copy.deepcopy(self.records.iitax) combined_taxes_chng = incometax_chng + payrolltax_chng # calculate base level of taxes after restoring records object setattr(self, 'records', recs0) self.calc_all(zero_out_calc_vars=zero_out_calculated_vars) payrolltax_base = copy.deepcopy(self.records.payrolltax) incometax_base = copy.deepcopy(self.records.iitax) combined_taxes_base = incometax_base + payrolltax_base # compute marginal changes in combined tax liability payrolltax_diff = payrolltax_chng - payrolltax_base incometax_diff = incometax_chng - incometax_base combined_diff = combined_taxes_chng - combined_taxes_base # specify optional adjustment for employer (er) OASDI+HI payroll taxes mtr_on_earnings = (variable_str == 'e00200p' or variable_str == 'e00200s') if wrt_full_compensation and mtr_on_earnings: adj = np.where( variable < self.policy.SS_Earnings_c, 0.5 * (self.policy.FICA_ss_trt + self.policy.FICA_mc_trt), 0.5 * self.policy.FICA_mc_trt) else: adj = 0.0 # compute marginal tax rates mtr_payrolltax = payrolltax_diff / (finite_diff * (1.0 + adj)) mtr_incometax = incometax_diff / (finite_diff * (1.0 + adj)) mtr_combined = combined_diff / (finite_diff * (1.0 + adj)) # if variable_str is e00200s, set MTR to NaN for units without a spouse if variable_str == 'e00200s': mtr_payrolltax = np.where(self.records.MARS == 2, mtr_payrolltax, np.nan) mtr_incometax = np.where(self.records.MARS == 2, mtr_incometax, np.nan) mtr_combined = np.where(self.records.MARS == 2, mtr_combined, np.nan) # return the three marginal tax rate arrays return (mtr_payrolltax, mtr_incometax, mtr_combined) def current_law_version(self): """ Return Calculator object same as self except with current-law policy. """ return Calculator(policy=self.policy.current_law_version(), records=copy.deepcopy(self.records), sync_years=False, consumption=copy.deepcopy(self.consumption), behavior=copy.deepcopy(self.behavior)) @staticmethod def read_json_param_objects(reform, assump): """ Read JSON reform and assump objects and return a single dictionary containing five key:dict pairs: 'policy':dict, 'consumption':dict, 'behavior':dict, 'growdiff_baseline':dict and 'growdiff_response':dict. Note that either of the first two parameters may be None. If reform is None, the dict in the 'policy':dict pair is empty. If assump is None, the dict in the 'consumption':dict pair, in the 'behavior':dict pair, in the 'growdiff_baseline':dict pair, and in the 'growdiff_response':dict pair, are all empty. Also note that either of the first two parameters can be strings containing a valid JSON string (rather than a filename), in which case the file reading is skipped and the appropriate read_json_*_text method is called. The reform file contents or JSON string must be like this: {"policy": {...}} and the assump file contents or JSON string must be like: {"consumption": {...}, "behavior": {...}, "growdiff_baseline": {...}, "growdiff_response": {...} } The returned dictionary contains parameter lists (not arrays). """ # first process second assump parameter if assump is None: cons_dict = dict() behv_dict = dict() gdiff_base_dict = dict() gdiff_resp_dict = dict() elif isinstance(assump, six.string_types): if os.path.isfile(assump): txt = open(assump, 'r').read() else: txt = assump (cons_dict, behv_dict, gdiff_base_dict, gdiff_resp_dict) = Calculator._read_json_econ_assump_text(txt) else: raise ValueError('assump is neither None nor string') # next process first reform parameter if reform is None: rpol_dict = dict() elif isinstance(reform, six.string_types): if os.path.isfile(reform): txt = open(reform, 'r').read() else: txt = reform rpol_dict = (Calculator._read_json_policy_reform_text( txt, gdiff_base_dict, gdiff_resp_dict)) else: raise ValueError('reform is neither None nor string') # finally construct and return single composite dictionary param_dict = dict() param_dict['policy'] = rpol_dict param_dict['consumption'] = cons_dict param_dict['behavior'] = behv_dict param_dict['growdiff_baseline'] = gdiff_base_dict param_dict['growdiff_response'] = gdiff_resp_dict return param_dict REQUIRED_REFORM_KEYS = set(['policy']) REQUIRED_ASSUMP_KEYS = set( ['consumption', 'behavior', 'growdiff_baseline', 'growdiff_response']) @staticmethod def reform_documentation(params): """ Generate reform documentation. Parameters ---------- params: dict compound dictionary structured as dict returned from the static Calculator method read_json_param_objects() Returns ------- doc: String the documentation for the policy reform specified in params """ # pylint: disable=too-many-statements,too-many-branches # nested function used only in reform_documentation def param_doc(years, change, base): """ Parameters ---------- years: list of change years change: dictionary of parameter changes base: Policy or Growdiff object with baseline values syear: parameter start calendar year Returns ------- doc: String """ # nested function used only in param_doc def lines(text, num_indent_spaces, max_line_length=77): """ Return list of text lines, each one of which is no longer than max_line_length, with the second and subsequent lines being indented by the number of specified num_indent_spaces; each line in the list ends with the '\n' character """ if len(text) < max_line_length: # all text fits on one line line = text + '\n' return [line] # all text does not fix on one line first_line = True line_list = list() words = text.split() while words: if first_line: line = '' first_line = False else: line = ' ' * num_indent_spaces while (words and (len(words[0]) + len(line)) < max_line_length): line += words.pop(0) + ' ' line = line[:-1] + '\n' line_list.append(line) return line_list # begin main logic of param_doc # pylint: disable=too-many-nested-blocks assert len(years) == len(change.keys()) basevals = getattr(base, '_vals', None) assert isinstance(basevals, dict) doc = '' for year in years: # write year base.set_year(year) doc += '{}:\n'.format(year) # write info for each param in year for param in sorted(change[year].keys()): # ... write param:value line pval = change[year][param] if isinstance(pval, list): pval = pval[0] if basevals[param]['boolean_value']: if isinstance(pval, list): pval = [ True if item else False for item in pval ] else: pval = bool(pval) doc += ' {} : {}\n'.format(param, pval) # ... write optional param-index line if isinstance(pval, list): pval = basevals[param]['col_label'] pval = [str(item) for item in pval] doc += ' ' * (4 + len(param)) + '{}\n'.format(pval) # ... write name line if param.endswith('_cpi'): rootparam = param[:-4] name = '{} inflation indexing status'.format(rootparam) else: name = basevals[param]['long_name'] for line in lines('name: ' + name, 6): doc += ' ' + line # ... write optional desc line if not param.endswith('_cpi'): desc = basevals[param]['description'] for line in lines('desc: ' + desc, 6): doc += ' ' + line # ... write baseline_value line if isinstance(base, Policy): if param.endswith('_cpi'): rootparam = param[:-4] bval = basevals[rootparam].get( 'cpi_inflated', False) else: bval = getattr(base, param[1:], None) if isinstance(bval, np.ndarray): # pylint: disable=no-member bval = bval.tolist() if basevals[param]['boolean_value']: bval = [ True if item else False for item in bval ] elif basevals[param]['boolean_value']: bval = bool(bval) doc += ' baseline_value: {}\n'.format(bval) else: # if base is Growdiff object # all Growdiff parameters have zero as default value doc += ' baseline_value: 0.0\n' return doc # begin main logic of reform_documentation # create Policy object with pre-reform (i.e., baseline) values # ... create gdiff_baseline object gdb = Growdiff() gdb.update_growdiff(params['growdiff_baseline']) # ... create Growfactors clp object that incorporates gdiff_baseline gfactors_clp = Growfactors() gdb.apply_to(gfactors_clp) # ... create Policy object containing pre-reform parameter values clp = Policy(gfactors=gfactors_clp) # generate documentation text doc = 'REFORM DOCUMENTATION\n' doc += 'Baseline Growth-Difference Assumption Values by Year:\n' years = sorted(params['growdiff_baseline'].keys()) if years: doc += param_doc(years, params['growdiff_baseline'], gdb) else: doc += 'none: using default baseline growth assumptions\n' doc += 'Policy Reform Parameter Values by Year:\n' years = sorted(params['policy'].keys()) if years: doc += param_doc(years, params['policy'], clp) else: doc += 'none: using current-law policy parameters\n' return doc # ----- begin private methods of Calculator class ----- def _taxinc_to_amt(self): """ Call TaxInc through AMT functions. """ TaxInc(self.policy, self.records) SchXYZTax(self.policy, self.records) GainsTax(self.policy, self.records) AGIsurtax(self.policy, self.records) NetInvIncTax(self.policy, self.records) AMT(self.policy, self.records) def _calc_one_year(self, zero_out_calc_vars=False): """ Call all the functions except those in the calc_all() method. """ if zero_out_calc_vars: self.records.zero_out_changing_calculated_vars() # pdb.set_trace() EI_PayrollTax(self.policy, self.records) DependentCare(self.policy, self.records) Adj(self.policy, self.records) ALD_InvInc_ec_base(self.policy, self.records) CapGains(self.policy, self.records) SSBenefits(self.policy, self.records) UBI(self.policy, self.records) AGI(self.policy, self.records) ItemDedCap(self.policy, self.records) ItemDed(self.policy, self.records) AdditionalMedicareTax(self.policy, self.records) StdDed(self.policy, self.records) # Store calculated standard deduction, calculate # taxes with standard deduction, store AMT + Regular Tax std = copy.deepcopy(self.records.standard) item = copy.deepcopy(self.records.c04470) item_no_limit = copy.deepcopy(self.records.c21060) item_phaseout = copy.deepcopy(self.records.c21040) self.records.c04470 = np.zeros(self.records.dim) self.records.c21060 = np.zeros(self.records.dim) self.records.c21040 = np.zeros(self.records.dim) self._taxinc_to_amt() std_taxes = copy.deepcopy(self.records.c05800) # Set standard deduction to zero, calculate taxes w/o # standard deduction, and store AMT + Regular Tax self.records.standard = np.zeros(self.records.dim) self.records.c21060 = item_no_limit self.records.c21040 = item_phaseout self.records.c04470 = item self._taxinc_to_amt() item_taxes = copy.deepcopy(self.records.c05800) # Replace standard deduction with zero where the taxpayer # would be better off itemizing self.records.standard[:] = np.where(item_taxes < std_taxes, 0., std) self.records.c04470[:] = np.where(item_taxes < std_taxes, item, 0.) self.records.c21060[:] = np.where(item_taxes < std_taxes, item_no_limit, 0.) self.records.c21040[:] = np.where(item_taxes < std_taxes, item_phaseout, 0.) # Calculate taxes with optimal itemized deduction self._taxinc_to_amt() F2441(self.policy, self.records) EITC(self.policy, self.records) ChildTaxCredit(self.policy, self.records) PersonalTaxCredit(self.policy, self.records) AmOppCreditParts(self.policy, self.records) SchR(self.policy, self.records) EducationTaxCredit(self.policy, self.records) NonrefundableCredits(self.policy, self.records) AdditionalCTC(self.policy, self.records) C1040(self.policy, self.records) CTC_new(self.policy, self.records) IITAX(self.policy, self.records) @staticmethod def _read_json_policy_reform_text(text_string, growdiff_baseline_dict, growdiff_response_dict): """ Strip //-comments from text_string and return 1 dict based on the JSON. Specified text is JSON with at least 1 high-level string:object pair: a "policy": {...} pair. Other high-level pairs will be ignored by this method, except that a "consumption", "behavior", "growdiff_baseline" or "growdiff_response" key will raise a ValueError. The {...} object may be empty (that is, be {}), or may contain one or more pairs with parameter string primary keys and string years as secondary keys. See tests/test_calculate.py for an extended example of a commented JSON policy reform text that can be read by this method. Returned dictionary prdict has integer years as primary keys and string parameters as secondary keys. This returned dictionary is suitable as the argument to the Policy implement_reform(prdict) method. """ # strip out //-comments without changing line numbers json_str = re.sub('//.*', ' ', text_string) # convert JSON text into a Python dictionary try: raw_dict = json.loads(json_str) except ValueError as valerr: msg = 'Policy reform text below contains invalid JSON:\n' msg += str(valerr) + '\n' msg += 'Above location of the first error may be approximate.\n' msg += 'The invalid JSON reform text is between the lines:\n' bline = 'XX----.----1----.----2----.----3----.----4' bline += '----.----5----.----6----.----7' msg += bline + '\n' linenum = 0 for line in json_str.split('\n'): linenum += 1 msg += '{:02d}{}'.format(linenum, line) + '\n' msg += bline + '\n' raise ValueError(msg) # check key contents of dictionary actual_keys = raw_dict.keys() for rkey in Calculator.REQUIRED_REFORM_KEYS: if rkey not in actual_keys: msg = 'key "{}" is not in policy reform file' raise ValueError(msg.format(rkey)) for rkey in actual_keys: if rkey in Calculator.REQUIRED_ASSUMP_KEYS: msg = 'key "{}" should be in economic assumption file' raise ValueError(msg.format(rkey)) # convert raw_dict['policy'] dictionary into prdict tdict = Policy.translate_json_reform_suffixes(raw_dict['policy'], growdiff_baseline_dict, growdiff_response_dict) prdict = Calculator._convert_parameter_dict(tdict) return prdict @staticmethod def _read_json_econ_assump_text(text_string): """ Strip //-comments from text_string and return 4 dict based on the JSON. Specified text is JSON with at least 4 high-level string:object pairs: a "consumption": {...} pair, a "behavior": {...} pair, a "growdiff_baseline": {...} pair, and a "growdiff_response": {...} pair. Other high-level pairs will be ignored by this method, except that a "policy" key will raise a ValueError. The {...} object may be empty (that is, be {}), or may contain one or more pairs with parameter string primary keys and string years as secondary keys. See tests/test_calculate.py for an extended example of a commented JSON economic assumption text that can be read by this method. Note that an example is shown in the ASSUMP_CONTENTS string in tests/test_calculate.py file. Returned dictionaries (cons_dict, behv_dict, gdiff_baseline_dict, gdiff_respose_dict) have integer years as primary keys and string parameters as secondary keys. These returned dictionaries are suitable as the arguments to the Consumption.update_consumption(cons_dict) method, or the Behavior.update_behavior(behv_dict) method, or the Growdiff.update_growdiff(gdiff_dict) method. """ # pylint: disable=too-many-locals # strip out //-comments without changing line numbers json_str = re.sub('//.*', ' ', text_string) # convert JSON text into a Python dictionary try: raw_dict = json.loads(json_str) except ValueError as valerr: msg = 'Economic assumption text below contains invalid JSON:\n' msg += str(valerr) + '\n' msg += 'Above location of the first error may be approximate.\n' msg += 'The invalid JSON asssump text is between the lines:\n' bline = 'XX----.----1----.----2----.----3----.----4' bline += '----.----5----.----6----.----7' msg += bline + '\n' linenum = 0 for line in json_str.split('\n'): linenum += 1 msg += '{:02d}{}'.format(linenum, line) + '\n' msg += bline + '\n' raise ValueError(msg) # check key contents of dictionary actual_keys = raw_dict.keys() for rkey in Calculator.REQUIRED_ASSUMP_KEYS: if rkey not in actual_keys: msg = 'key "{}" is not in economic assumption file' raise ValueError(msg.format(rkey)) for rkey in actual_keys: if rkey in Calculator.REQUIRED_REFORM_KEYS: msg = 'key "{}" should be in policy reform file' raise ValueError(msg.format(rkey)) # convert the assumption dictionaries in raw_dict key = 'consumption' cons_dict = Calculator._convert_parameter_dict(raw_dict[key]) key = 'behavior' behv_dict = Calculator._convert_parameter_dict(raw_dict[key]) key = 'growdiff_baseline' gdiff_base_dict = Calculator._convert_parameter_dict(raw_dict[key]) key = 'growdiff_response' gdiff_resp_dict = Calculator._convert_parameter_dict(raw_dict[key]) return (cons_dict, behv_dict, gdiff_base_dict, gdiff_resp_dict) @staticmethod def _convert_parameter_dict(param_key_dict): """ Converts specified param_key_dict into a dictionary whose primary keys are calendar years, and hence, is suitable as the argument to the Policy.implement_reform() method, or the Consumption.update_consumption() method, or the Behavior.update_behavior() method, or the Growdiff.update_growdiff() method. Specified input dictionary has string parameter primary keys and string years as secondary keys. Returned dictionary has integer years as primary keys and string parameters as secondary keys. """ # convert year skey strings into integers and # optionally convert lists into np.arrays year_param = dict() for pkey, sdict in param_key_dict.items(): if not isinstance(pkey, six.string_types): msg = 'pkey {} in reform is not a string' raise ValueError(msg.format(pkey)) rdict = dict() if not isinstance(sdict, dict): msg = 'pkey {} in reform is not paired with a dict' raise ValueError(msg.format(pkey)) for skey, val in sdict.items(): if not isinstance(skey, six.string_types): msg = 'skey {} in reform is not a string' raise ValueError(msg.format(skey)) else: year = int(skey) rdict[year] = val year_param[pkey] = rdict # convert year_param dictionary to year_key_dict dictionary year_key_dict = dict() years = set() for param, sdict in year_param.items(): for year, val in sdict.items(): if year not in years: years.add(year) year_key_dict[year] = dict() year_key_dict[year][param] = val return year_key_dict
def __init__( self, input_data, tax_year, reform, assump, growdiff_response, # =None in static analysis aging_input_data, exact_calculations): """ TaxCalcIO class constructor. """ # pylint: disable=too-many-arguments # pylint: disable=too-many-locals # pylint: disable=too-many-branches # pylint: disable=too-many-statements # check for existence of INPUT file if isinstance(input_data, six.string_types): # remove any leading directory path from INPUT filename fname = os.path.basename(input_data) # check if fname ends with ".csv" if fname.endswith('.csv'): inp = '{}-{}'.format(fname[:-4], str(tax_year)[2:]) else: msg = 'INPUT file named {} does not end in .csv' raise ValueError(msg.format(fname)) # check existence of INPUT file if not os.path.isfile(input_data): msg = 'INPUT file named {} could not be found' raise ValueError(msg.format(input_data)) elif isinstance(input_data, pd.DataFrame): inp = 'df-{}'.format(str(tax_year)[2:]) else: msg = 'INPUT is neither string nor Pandas DataFrame' raise ValueError(msg) # construct output_filename and delete old output file if it exists if reform is None: self._reform = False ref = '' elif isinstance(reform, six.string_types): self._reform = True # remove any leading directory path from REFORM filename fname = os.path.basename(reform) # check if fname ends with ".json" if fname.endswith('.json'): ref = '-{}'.format(fname[:-5]) else: msg = 'REFORM file named {} does not end in .json' raise ValueError(msg.format(fname)) else: msg = 'TaxCalcIO.ctor reform is neither None nor str' raise ValueError(msg) if assump is None: asm = '' elif isinstance(assump, six.string_types): # remove any leading directory path from ASSUMP filename fname = os.path.basename(assump) # check if fname ends with ".json" if fname.endswith('.json'): asm = '-{}'.format(fname[:-5]) else: msg = 'ASSUMP file named {} does not end in .json' raise ValueError(msg.format(fname)) else: msg = 'TaxCalcIO.ctor assump is neither None nor str' raise ValueError(msg) self._output_filename = '{}{}{}.csv'.format(inp, ref, asm) delete_file(self._output_filename) # get parameter dictionaries from --reform and --assump files param_dict = Calculator.read_json_param_files(reform, assump) # make sure no behavioral response is specified in --assump beh = Behavior() beh.update_behavior(param_dict['behavior']) if beh.has_any_response(): msg = '--assump ASSUMP cannot assume any "behavior"' raise ValueError(msg) # make sure no growdiff_response is specified in --assump gdiff_response = Growdiff() gdiff_response.update_growdiff(param_dict['growdiff_response']) if gdiff_response.has_any_response(): msg = '--assump ASSUMP cannot assume any "growdiff_response"' raise ValueError(msg) # create gdiff_baseline object gdiff_baseline = Growdiff() gdiff_baseline.update_growdiff(param_dict['growdiff_baseline']) # create Growfactors clp object that incorporates gdiff_baseline gfactors_clp = Growfactors() gdiff_baseline.apply_to(gfactors_clp) # specify gdiff_response object if growdiff_response is None: gdiff_response = Growdiff() elif isinstance(growdiff_response, Growdiff): gdiff_response = growdiff_response else: msg = 'TaxCalcIO.ctor growdiff_response is neither None nor {}' raise ValueError(msg.format('a Growdiff object')) # create Growfactors ref object that has both gdiff objects applied gfactors_ref = Growfactors() gdiff_baseline.apply_to(gfactors_ref) gdiff_response.apply_to(gfactors_ref) # create Policy object and implement reform if specified if self._reform: pol = Policy(gfactors=gfactors_ref) pol.implement_reform(param_dict['policy']) clp = Policy(gfactors=gfactors_clp) else: pol = Policy(gfactors=gfactors_clp) # check for valid tax_year value if tax_year < pol.start_year: msg = 'tax_year {} less than policy.start_year {}' raise ValueError(msg.format(tax_year, pol.start_year)) if tax_year > pol.end_year: msg = 'tax_year {} greater than policy.end_year {}' raise ValueError(msg.format(tax_year, pol.end_year)) # set policy to tax_year pol.set_year(tax_year) if self._reform: clp.set_year(tax_year) # read input file contents into Records object(s) if aging_input_data: if self._reform: recs = Records(data=input_data, gfactors=gfactors_ref, exact_calculations=exact_calculations) recs_clp = Records(data=input_data, gfactors=gfactors_clp, exact_calculations=exact_calculations) else: recs = Records(data=input_data, gfactors=gfactors_clp, exact_calculations=exact_calculations) else: # input_data are raw data that are not being aged recs = Records(data=input_data, exact_calculations=exact_calculations, gfactors=None, adjust_ratios=None, weights=None, start_year=tax_year) if self._reform: recs_clp = copy.deepcopy(recs) # create Calculator object(s) con = Consumption() con.update_consumption(param_dict['consumption']) self._calc = Calculator(policy=pol, records=recs, verbose=True, consumption=con, sync_years=aging_input_data) if self._reform: self._calc_clp = Calculator(policy=clp, records=recs_clp, verbose=False, consumption=con, sync_years=aging_input_data)
def analyze(self, writing_output_file=False, output_tables=False, output_graphs=False, output_ceeu=False, output_dump=False, output_sqldb=False): """ Conduct tax analysis. Parameters ---------- writing_output_file: boolean whether or not to generate and write output file output_tables: boolean whether or not to generate and write distributional tables to a text file output_graphs: boolean whether or not to generate and write HTML graphs of average and marginal tax rates by income percentile output_ceeu: boolean whether or not to calculate and write to stdout standard certainty-equivalent expected-utility statistics output_dump: boolean whether or not to replace standard output with all input and calculated variables using their Tax-Calculator names output_sqldb: boolean whether or not to write SQLite3 database with dump table containing same output as written by output_dump to a csv file Returns ------- Nothing """ # pylint: disable=too-many-arguments,too-many-branches calc_clp_calculated = False if output_dump or output_sqldb: (mtr_paytax, mtr_inctax, _) = self.calc.mtr(wrt_full_compensation=False) else: # do not need marginal tax rates mtr_paytax = None mtr_inctax = None if self.behavior_has_any_response: self.calc = Behavior.response(self.calc_clp, self.calc) calc_clp_calculated = True else: self.calc.calc_all() # optionally conduct normative welfare analysis if output_ceeu: if self.behavior_has_any_response: ceeu_results = 'SKIP --ceeu output because baseline and ' ceeu_results += 'reform cannot be sensibly compared\n ' ceeu_results += ' ' ceeu_results += 'when specifying "behavior" with --assump ' ceeu_results += 'option' elif self.calc.records.s006.sum() <= 0.: ceeu_results = 'SKIP --ceeu output because ' ceeu_results += 'sum of weights is not positive' else: self.calc_clp.calc_all() calc_clp_calculated = True cedict = ce_aftertax_income(self.calc_clp, self.calc, require_no_agg_tax_change=False) ceeu_results = TaxCalcIO.ceeu_output(cedict) else: ceeu_results = None # extract output if writing_output_file if writing_output_file: self.write_output_file(output_dump, mtr_paytax, mtr_inctax) # optionally write --sqldb output to SQLite3 database if output_sqldb: self.write_sqldb_file(mtr_paytax, mtr_inctax) # optionally write --tables output to text file if output_tables: if not calc_clp_calculated: self.calc_clp.calc_all() calc_clp_calculated = True self.write_tables_file() # optionally write --graphs output to HTML files if output_graphs: if not calc_clp_calculated: self.calc_clp.calc_all() calc_clp_calculated = True self.write_graph_files() # optionally write --ceeu output to stdout if ceeu_results: print(ceeu_results) # pylint: disable=superfluous-parens