from ogusa import estimate_beta_j, bequest_transmission
from ogusa import macro_params, transfer_distribution, income
from ogusa import get_micro_data
from ogusa import calibrate_lifecycle
import copy
import json
import os
import warnings
import numpy as np
from taxcalc import Records
from ogcore import txfunc, demographics
from ogcore.utils import safe_read_pickle, mkdirs
[docs]
class Calibration:
"""OG-USA calibration class"""
def __init__(
self,
p,
estimate_tax_functions=False,
estimate_beta=False,
estimate_chi_n=False,
estimate_lifecycle_prefs=False,
estimate_pop=False,
get_macro_params=False,
tax_func_path=None,
lifecycle_params_path=None,
lifecycle_config=None,
lifecycle_options=None,
lifecycle_initial_ss=None,
lifecycle_kwargs=None,
iit_baseline=None,
iit_reform={},
guid="",
data="cps",
gfactors=None,
weights=None,
records_start_year=Records.CPSCSV_YEAR,
client=None,
num_workers=1,
demographic_data_path=None,
output_path=None,
):
"""
Constructor for the Calibration class. This class is used to find
parameter values for the OG-USA model.
Args:
p (OG-USA Parameters object): parameters object
estimate_tax_functions (bool): whether to estimate tax functions
estimate_beta (bool): whether to estimate beta with the
legacy wealth-moment SMM (`estimate_beta_j`)
estimate_chi_n (bool): deprecated alias for
`estimate_lifecycle_prefs`; `chi_n` is only calibrated
jointly with `beta_annual` and `chi_b`
estimate_lifecycle_prefs (bool): whether to calibrate
`beta_annual` by type, `chi_b`, and the `chi_n` age
profile with the nested general-equilibrium routine in
`calibrate_lifecycle.calibrate_lifecycle_preferences`
(20 to 40 minutes serially; see
`LIFECYCLE_CALIBRATION_PLAN.md`)
estimate_pop (bool): whether to estimate population
get_macro_params (bool): whether to get macro parameters
tax_func_path (str): path to tax function parameters
lifecycle_params_path (str): JSON file with calibrated
lifecycle preference parameters. If it exists and is
consistent with `p`, the parameters are read instead of
re-calibrated; otherwise the calibration runs and the
result is written there. When None the calibration
always runs and is saved to `p.output_base`.
lifecycle_config (LifecycleCalibrationConfig): moment
configuration for the lifecycle calibration
lifecycle_options (PreferenceCalibrationOptions): options
for the beta / chi_b step
lifecycle_initial_ss (dict): OG-Core steady-state output to
start the outer loop from, instead of a cold solve
lifecycle_kwargs (dict): further keyword arguments for
`calibrate_lifecycle_preferences` (for example
`max_outer`, `param_tol`, `ge_client`)
iit_baseline (dict): baseline policy to use
iit_reform (dict): reform tax parameters
guid (str): id for tax function parameters
data (str or Pandas DataFrame): path or DataFrame with
data for Tax-Calculator model
gfactors (str or Pandas DataFrame ): path or DataFrame with
growth factors for Tax-Calculator model
weights (str or Pandas DataFrame): path or DataFrame with
weights for Tax-Calculator model
records_start_year (int): year micro data begins
client (Dask client object): client
num_workers (int): number of workers for Dask client
demographic_data_path (str): path to save or find
downloaded UN demographic data
output_path (str): path to save output to
Returns:
Calibration class object instance
"""
# Create output_path if it doesn't exist
if output_path is not None:
if not os.path.exists(output_path):
os.makedirs(output_path)
self.estimate_tax_functions = estimate_tax_functions
self.estimate_beta = estimate_beta
if estimate_chi_n and not estimate_lifecycle_prefs:
warnings.warn(
"estimate_chi_n is deprecated; chi_n is calibrated jointly "
"with beta_annual and chi_b. Use estimate_lifecycle_prefs.",
DeprecationWarning,
stacklevel=2,
)
estimate_lifecycle_prefs = True
self.estimate_chi_n = estimate_chi_n
self.estimate_lifecycle_prefs = estimate_lifecycle_prefs
self.estimate_pop = estimate_pop
self.get_macro_params = get_macro_params
if estimate_tax_functions:
if tax_func_path is not None:
run_micro = False
else:
run_micro = True
self.tax_function_params = self.get_tax_function_parameters(
p,
iit_baseline,
iit_reform,
guid,
data,
gfactors,
weights,
records_start_year,
client,
num_workers,
run_micro=run_micro,
tax_func_path=tax_func_path,
)
if self.estimate_beta:
self.beta_j, self.beta_j_se = estimate_beta_j.beta_estimate(
np.asarray(p.beta_annual, dtype=float), client=client
)
# Macro estimation
if self.get_macro_params:
self.macro_params = macro_params.get_macro_params()
# eta estimation
self.eta = transfer_distribution.get_transfer_matrix(
p.J, p.lambdas, output_path=output_path
)
# zeta estimation
self.zeta = bequest_transmission.get_bequest_matrix(
p.J, p.lambdas, output_path=output_path
)
# demographics
if estimate_pop:
self.demographic_params = demographics.get_pop_objs(
p.E,
p.S,
p.T,
0,
99,
initial_data_year=p.start_year - 1,
final_data_year=p.start_year,
income_percentiles=p.lambdas.flatten(),
GraphDiag=False,
download_path=demographic_data_path,
)
# demographics for 80 period lives (needed for getting e below)
demog80 = demographics.get_pop_objs(
20,
80,
p.T,
0,
99,
initial_data_year=p.start_year - 1,
final_data_year=p.start_year,
income_percentiles=p.lambdas.flatten(),
GraphDiag=False,
)
# earnings profiles
self.e = income.get_e_interp(
p.S,
self.demographic_params["omega_SS"],
demog80["omega_SS"],
p.lambdas,
plot_path=output_path,
)
else:
self.e = income.get_e_interp(
p.S,
p.omega_SS,
p.omega_SS,
p.lambdas,
plot_path=output_path,
)
# Lifecycle preference parameters: beta by type, chi_b, chi_n
self.lifecycle_outcome = None
if self.estimate_lifecycle_prefs:
self.lifecycle_params = self.get_lifecycle_parameters(
p,
lifecycle_params_path=lifecycle_params_path,
config=lifecycle_config,
options=lifecycle_options,
initial_ss=lifecycle_initial_ss,
client=client,
**(lifecycle_kwargs or {}),
)
# Lifecycle preference parameters
def get_lifecycle_parameters(
self,
p,
lifecycle_params_path=None,
config=None,
options=None,
initial_ss=None,
client=None,
**kwargs,
):
"""
Reads calibrated lifecycle preference parameters from a JSON file
or calibrates them with the nested general-equilibrium routine.
The calibration runs on a copy of ``p`` that already carries every
other parameter this class has produced so far (tax functions,
transfer and bequest matrices, earnings profiles, demographics,
macro parameters), so the calibrated preferences are consistent
with the rest of ``get_dict()``. ``p`` itself is not modified.
Args:
p (OG-Core Specifications object): parameters object
lifecycle_params_path (str): JSON file to read from or write
to; None writes to ``p.output_base``
config (LifecycleCalibrationConfig): moment configuration
options (PreferenceCalibrationOptions): beta / chi_b options
initial_ss (dict): steady state to start the outer loop from
client (Dask client object): client for household solves
kwargs: passed to ``calibrate_lifecycle_preferences``
Returns:
dict: ``beta_annual``, ``chi_b``, ``chi_n`` as lists
"""
run_calibration = True
if lifecycle_params_path is None:
lifecycle_params_path = os.path.join(
p.output_base, "LifecyclePrefEst.json"
)
else:
params, run_calibration = self.read_lifecycle_parameters(
p, lifecycle_params_path
)
mkdirs(os.path.split(lifecycle_params_path)[0])
if run_calibration:
p_calib = copy.deepcopy(p)
updates = {
k: v
for k, v in self._parameter_updates().items()
if k in p_calib._data
}
if updates:
p_calib.update_specifications(updates)
outcome = calibrate_lifecycle.calibrate_lifecycle_preferences(
p_calib,
config=config,
options=options,
initial_ss=initial_ss,
client=client,
**kwargs,
)
self.lifecycle_outcome = outcome
params = outcome.parameter_dict
record = dict(params)
record["_meta"] = {
"S": int(p.S),
"J": int(p.J),
"start_year": int(p.start_year),
"converged": bool(outcome.converged),
"iterations": int(outcome.iterations),
}
with open(lifecycle_params_path, "w", encoding="utf-8") as file:
json.dump(record, file, indent=1)
print(
"Saved lifecycle preference parameters to ",
lifecycle_params_path,
)
return params
def read_lifecycle_parameters(self, p, lifecycle_params_path):
"""
Reads calibrated lifecycle preference parameters from a JSON
file and checks that they fit the model dimensions.
Args:
p (OG-Core Specifications object): parameters object
lifecycle_params_path (str): path to the JSON file
Returns:
params (dict or None): ``beta_annual``, ``chi_b``, ``chi_n``
run_calibration (bool): whether the calibration must run
"""
keys = ("beta_annual", "chi_b", "chi_n")
if not os.path.exists(lifecycle_params_path):
print(
"Lifecycle preference parameters do not exist at given "
"path. Running new calibration."
)
return None, True
with open(lifecycle_params_path, "r", encoding="utf-8") as file:
record = json.load(file)
if not all(k in record for k in keys):
raise RuntimeError(
"Lifecycle preference parameter file at given path is "
"missing one of " + ", ".join(keys)
)
params = {k: list(record[k]) for k in keys}
consistent = (
len(params["beta_annual"]) == p.J
and len(params["chi_b"]) == p.J
and len(params["chi_n"]) == p.S
)
if not consistent:
print(
"Lifecycle preference parameters at given path do not "
"match the model's S and J. Running new calibration."
)
return None, True
print(
"Using lifecycle preference parameters from ",
lifecycle_params_path,
)
return params, False
# Tax Functions
[docs]
def get_tax_function_parameters(
self,
p,
iit_baseline=None,
iit_reform={},
guid="",
data="",
gfactors=None,
weights=None,
records_start_year=Records.CPSCSV_YEAR,
client=None,
num_workers=1,
run_micro=False,
tax_func_path=None,
):
"""
Reads pickle file of tax function parameters or estimates the
parameters from microsimulation model output.
Args:
p (OG-Core Parameters object): parameters object
iit_baseline (dict): baseline policy to use
iit_reform (dict): reform tax parameters
guid (string): id for tax function parameters
data (str or Pandas DataFrame): path or DataFrame with
data for Tax-Calculator model
gfactors (str or Pandas DataFrame ): path or DataFrame with
growth factors for Tax-Calculator model
weights (str or Pandas DataFrame): path or DataFrame with
weights for Tax-Calculator model
records_start_year (int): year micro data begins
client (Dask client object): client
num_workers (int): number of workers for Dask client
run_micro (bool): whether to estimate parameters from
microsimulation model
tax_func_path (string): path where find or save tax
function parameter estimates
Returns:
None
"""
# set paths if none given
if tax_func_path is None:
if p.baseline:
pckl = "TxFuncEst_baseline{}.pkl".format(guid)
tax_func_path = os.path.join(p.output_base, pckl)
print("Using baseline tax parameters from ", tax_func_path)
else:
pckl = "TxFuncEst_policy{}.pkl".format(guid)
tax_func_path = os.path.join(p.output_base, pckl)
print(
"Using reform policy tax parameters from ", tax_func_path
)
# create directory for tax function pickles to be saved to
mkdirs(os.path.split(tax_func_path)[0])
# If run_micro is false, check to see if parameters file exists
# and if it is consistent with Specifications instance
if not run_micro:
dict_params, run_micro = self.read_tax_func_estimate(
p, tax_func_path
)
taxcalc_version = "Cached tax parameters, no taxcalc version"
if run_micro:
micro_data, taxcalc_version = get_micro_data.get_data(
baseline=p.baseline,
start_year=p.start_year,
iit_baseline=iit_baseline,
iit_reform=iit_reform,
data=data,
gfactors=gfactors,
weights=weights,
path=p.output_base,
client=client,
num_workers=num_workers,
)
p.BW = len(micro_data)
dict_params = txfunc.tax_func_estimate( # pragma: no cover
micro_data,
p.BW,
p.S,
p.starting_age,
p.ending_age,
start_year=p.start_year,
analytical_mtrs=p.analytical_mtrs,
tax_func_type=p.tax_func_type,
age_specific=p.age_specific,
client=client,
num_workers=num_workers,
tax_func_path=tax_func_path,
)
mean_income_data = dict_params["tfunc_avginc"][0]
frac_tax_payroll = np.append(
dict_params["tfunc_frac_tax_payroll"],
np.ones(p.T + p.S - p.BW)
* dict_params["tfunc_frac_tax_payroll"][-1],
)
# Conduct checks to be sure tax function params are consistent
# with the model run
params_list = ["etr", "mtrx", "mtry"]
BW_in_tax_params = dict_params["BW"]
start_year_in_tax_params = dict_params["start_year"]
S_in_tax_params = len(dict_params["tfunc_etr_params_S"][0])
# Check that start years are consistent in model and cached tax functions
if p.start_year != start_year_in_tax_params:
print(
"Input Error: There is a discrepancy between the start"
+ " year of the model and that of the tax functions!!"
)
assert False
# Check that S is consistent in model and cached tax functions
# Note: even if p.age_specific = False, the arrays coming from
# ogcore.txfunc_est should be of length S
if p.S != S_in_tax_params:
print(
"Input Error: There is a discrepancy between the ages"
+ " used in the model and those in the tax functions!!"
)
assert False
# Extrapolate tax function parameters for years after budget window
# list of list: BW x S - either an array of function at that element...
etr_params = [[None] * p.S] * p.T
mtrx_params = [[None] * p.S] * p.T
mtry_params = [[None] * p.S] * p.T
for s in range(p.S):
for t in range(p.T):
if t < p.BW:
etr_params[t][s] = dict_params["tfunc_etr_params_S"][t][s]
mtrx_params[t][s] = dict_params["tfunc_mtrx_params_S"][t][
s
]
mtry_params[t][s] = dict_params["tfunc_mtry_params_S"][t][
s
]
else:
etr_params[t][s] = dict_params["tfunc_etr_params_S"][-1][s]
mtrx_params[t][s] = dict_params["tfunc_mtrx_params_S"][-1][
s
]
mtry_params[t][s] = dict_params["tfunc_mtry_params_S"][-1][
s
]
if p.constant_rates:
print("Using constant rates!")
# Make all tax rates equal the average
p.tax_func_type = "linear"
etr_params = [[None] * p.S] * p.T
mtrx_params = [[None] * p.S] * p.T
mtry_params = [[None] * p.S] * p.T
for s in range(p.S):
for t in range(p.T):
if t < p.BW:
etr_params[t][s] = dict_params["tfunc_avg_etr"][t]
mtrx_params[t][s] = dict_params["tfunc_avg_mtrx"][t]
mtry_params[t][s] = dict_params["tfunc_avg_mtry"][t]
else:
etr_params[t][s] = dict_params["tfunc_avg_etr"][-1]
mtrx_params[t][s] = dict_params["tfunc_avg_mtrx"][-1]
mtry_params[t][s] = dict_params["tfunc_avg_mtry"][-1]
if p.zero_taxes:
print("Zero taxes!")
etr_params = [[0] * p.S] * p.T
mtrx_params = [[0] * p.S] * p.T
mtry_params = [[0] * p.S] * p.T
tax_param_dict = {
"etr_params": etr_params,
"mtrx_params": mtrx_params,
"mtry_params": mtry_params,
"taxcalc_version": taxcalc_version,
"mean_income_data": mean_income_data,
"frac_tax_payroll": frac_tax_payroll,
}
return tax_param_dict
[docs]
def read_tax_func_estimate(self, p, tax_func_path):
"""
This function reads in tax function parameters from pickle
files.
Args:
tax_func_path (str): path to pickle with tax function
parameter estimates
Returns:
(tuple): tax function parameters and estimation flag:
* dict_params (dict): dictionary containing arrays of tax
function parameters
* run_micro (bool): whether to estimate tax function
parameters
"""
flag = 0
if os.path.exists(tax_func_path):
print("Tax Function Path Exists")
dict_params = safe_read_pickle(tax_func_path)
# check to see if tax_functions compatible
try:
if p.start_year != dict_params["start_year"]:
print(
"Model start year not consistent with tax "
+ "function parameter estimates"
)
flag = 1
except KeyError:
pass
try:
p.BW = dict_params["BW"] # QUICK FIX
if p.BW != dict_params["BW"]:
print(
"Model budget window length is "
+ str(p.BW)
+ " but the tax function parameter "
+ "estimates have a budget window length of "
+ str(dict_params["BW"])
)
flag = 1
except KeyError:
pass
try:
if p.tax_func_type != dict_params["tax_func_type"]:
print(
"Model tax function type is not "
+ "consistent with tax function parameter "
+ "estimates"
)
flag = 1
except KeyError:
pass
if flag >= 1:
raise RuntimeError(
"Tax function parameter estimates at given path"
+ " are not consistent with model parameters"
+ " specified."
)
else:
flag = 1
print(
"Tax function parameter estimates do not exist at"
+ " given path. Running new estimation."
)
if flag >= 1:
dict_params = None
run_micro = True
else:
run_micro = False
return dict_params, run_micro
def _parameter_updates(self):
"""
Collects the calibrated parameters other than the lifecycle
preferences (tax functions, legacy beta, eta, zeta, macro
parameters, e, demographics).
Returns:
dict (dict): parameter updates in `update_specifications`
format
"""
dict = {}
if self.estimate_tax_functions:
dict.update(self.tax_function_params)
if self.estimate_beta:
dict["beta_annual"] = self.beta_j
dict["eta"] = self.eta
dict["zeta"] = self.zeta
if self.get_macro_params:
dict.update(self.macro_params)
dict["e"] = self.e
if self.estimate_pop:
dict.update(self.demographic_params)
return dict
# method to return all newly calibrated parameters in a dictionary
[docs]
def get_dict(self):
"""
Returns all newly calibrated parameters in a dictionary.
Returns:
dict (dict): parameter updates in `update_specifications`
format, including `beta_annual`, `chi_b`, and `chi_n`
when the lifecycle preferences were calibrated
"""
dict = self._parameter_updates()
if self.estimate_lifecycle_prefs:
dict.update(self.lifecycle_params)
return dict