From 1dce953bfdb4681ce86a65348a5881f8ddd5314f Mon Sep 17 00:00:00 2001 From: jdebacker Date: Fri, 28 Jun 2024 17:14:17 -0400 Subject: [PATCH 01/50] move pensions to new module --- ogcore/pensions.py | 137 ++++++++++++++++++++++++++++++++++++++++++++ ogcore/tax.py | 138 +-------------------------------------------- 2 files changed, 139 insertions(+), 136 deletions(-) create mode 100644 ogcore/pensions.py diff --git a/ogcore/pensions.py b/ogcore/pensions.py new file mode 100644 index 000000000..0273e31ab --- /dev/null +++ b/ogcore/pensions.py @@ -0,0 +1,137 @@ +# Packages +import numpy as np +from ogcore import utils + + +def replacement_rate_vals(nssmat, wss, factor_ss, j, p): + """ + Calculates replacement rate values for the social security system. + + Args: + nssmat (Numpy array): initial guess at labor supply, size = SxJ + new_w (scalar): steady state real wage rate + factor_ss (scalar): scaling factor converting model units to + dollars + j (int): index of lifetime income group + p (OG-Core Specifications object): model parameters + + Returns: + theta (Numpy array): social security replacement rate value for + lifetime income group j + + """ + if j is not None: + e = np.squeeze(p.e[-1, :, j]) # Only computes using SS earnings + else: + e = np.squeeze(p.e[-1, :, :]) # Only computes using SS earnings + # adjust number of calendar years AIME computed from int model periods + equiv_periods = int(round((p.S / 80.0) * p.AIME_num_years)) - 1 + if e.ndim == 2: + dim2 = e.shape[1] + else: + dim2 = 1 + earnings = (e * (wss * nssmat * factor_ss)).reshape(p.S, dim2) + # get highest earning years for number of years AIME computed from + highest_earn = ( + -1.0 * np.sort(-1.0 * earnings[: p.retire[-1], :], axis=0) + )[:equiv_periods] + AIME = highest_earn.sum(0) / ((12.0 * (p.S / 80.0)) * equiv_periods) + PIA = np.zeros(dim2) + # Compute level of replacement using AIME brackets and PIA rates + for j in range(dim2): + if AIME[j] < p.AIME_bkt_1: + PIA[j] = p.PIA_rate_bkt_1 * AIME[j] + elif AIME[j] < p.AIME_bkt_2: + PIA[j] = p.PIA_rate_bkt_1 * p.AIME_bkt_1 + p.PIA_rate_bkt_2 * ( + AIME[j] - p.AIME_bkt_1 + ) + else: + PIA[j] = ( + p.PIA_rate_bkt_1 * p.AIME_bkt_1 + + p.PIA_rate_bkt_2 * (p.AIME_bkt_2 - p.AIME_bkt_1) + + p.PIA_rate_bkt_3 * (AIME[j] - p.AIME_bkt_2) + ) + # Set the maximum monthly replacment rate from SS benefits tables + PIA[PIA > p.PIA_maxpayment] = p.PIA_maxpayment + if p.PIA_minpayment != 0.0: + PIA[PIA < p.PIA_minpayment] = p.PIA_minpayment + theta = (PIA * (12.0 * p.S / 80.0)) / (factor_ss * wss) + return theta + + +def pension_amount(w, n, theta, t, j, shift, method, e, p): + """ + Calculate public pension benefit amounts for each household. + + Args: + w (array_like): real wage rate + n (Numpy array): labor supply + theta (Numpy array): social security replacement rate value for + lifetime income group j + t (int): time period + j (int): index of lifetime income group + shift (bool): whether computing for periods 0--s or 1--(s+1), + =True for 1--(s+1) + method (str): adjusts calculation dimensions based on 'SS' or + 'TPI' + e (Numpy array): effective labor units + p (OG-Core Specifications object): model parameters + + Returns: + pension (Numpy array): pension amount for each household + + """ + if j is not None: + if method == "TPI": + if n.ndim == 2: + w = w.reshape(w.shape[0], 1) + else: + if method == "TPI": + w = utils.to_timepath_shape(w) + + pension = np.zeros_like(n) + if method == "SS": + # Depending on if we are looking at b_s or b_s+1, the + # entry for retirement will change (it shifts back one). + # The shift boolean makes sure we start replacement rates + # at the correct age. + if shift is False: + pension[p.retire[-1] :] = theta * w + else: + pension[p.retire[-1] - 1 :] = theta * w + elif method == "TPI": + length = w.shape[0] + if not shift: + # retireTPI is different from retire, because in TP income + # we are counting backwards with different length lists. + # This will always be the correct location of retirement, + # depending on the shape of the lists. + retireTPI = p.retire[t : t + length] - p.S + else: + retireTPI = p.retire[t : t + length] - 1 - p.S + if len(n.shape) == 1: + if not shift: + retireTPI = p.retire[t] - p.S + else: + retireTPI = p.retire[t] - 1 - p.S + pension[retireTPI:] = ( + theta[j] * p.replacement_rate_adjust[t] * w[retireTPI:] + ) + elif len(n.shape) == 2: + for tt in range(pension.shape[0]): + pension[tt, retireTPI[tt] :] = ( + theta * p.replacement_rate_adjust[t + tt] * w[tt] + ) + else: + for tt in range(pension.shape[0]): + pension[tt, retireTPI[tt] :, :] = ( + theta.reshape(1, p.J) + * p.replacement_rate_adjust[t + tt] + * w[tt] + ) + elif method == "TPI_scalar": + # The above methods won't work if scalars are used. This option + # is only called by the SS_TPI_firstdoughnutring function in TPI. + pension = theta * p.replacement_rate_adjust[0] * w + + return pension \ No newline at end of file diff --git a/ogcore/tax.py b/ogcore/tax.py index eb988ed60..f7f360228 100644 --- a/ogcore/tax.py +++ b/ogcore/tax.py @@ -6,7 +6,7 @@ # Packages import numpy as np -from ogcore import utils +from ogcore import utils, pensions from ogcore.txfunc import get_tax_rates """ @@ -16,62 +16,6 @@ """ -def replacement_rate_vals(nssmat, wss, factor_ss, j, p): - """ - Calculates replacement rate values for the social security system. - - Args: - nssmat (Numpy array): initial guess at labor supply, size = SxJ - new_w (scalar): steady state real wage rate - factor_ss (scalar): scaling factor converting model units to - dollars - j (int): index of lifetime income group - p (OG-Core Specifications object): model parameters - - Returns: - theta (Numpy array): social security replacement rate value for - lifetime income group j - - """ - if j is not None: - e = np.squeeze(p.e[-1, :, j]) # Only computes using SS earnings - else: - e = np.squeeze(p.e[-1, :, :]) # Only computes using SS earnings - # adjust number of calendar years AIME computed from int model periods - equiv_periods = int(round((p.S / 80.0) * p.AIME_num_years)) - 1 - if e.ndim == 2: - dim2 = e.shape[1] - else: - dim2 = 1 - earnings = (e * (wss * nssmat * factor_ss)).reshape(p.S, dim2) - # get highest earning years for number of years AIME computed from - highest_earn = ( - -1.0 * np.sort(-1.0 * earnings[: p.retire[-1], :], axis=0) - )[:equiv_periods] - AIME = highest_earn.sum(0) / ((12.0 * (p.S / 80.0)) * equiv_periods) - PIA = np.zeros(dim2) - # Compute level of replacement using AIME brackets and PIA rates - for j in range(dim2): - if AIME[j] < p.AIME_bkt_1: - PIA[j] = p.PIA_rate_bkt_1 * AIME[j] - elif AIME[j] < p.AIME_bkt_2: - PIA[j] = p.PIA_rate_bkt_1 * p.AIME_bkt_1 + p.PIA_rate_bkt_2 * ( - AIME[j] - p.AIME_bkt_1 - ) - else: - PIA[j] = ( - p.PIA_rate_bkt_1 * p.AIME_bkt_1 - + p.PIA_rate_bkt_2 * (p.AIME_bkt_2 - p.AIME_bkt_1) - + p.PIA_rate_bkt_3 * (AIME[j] - p.AIME_bkt_2) - ) - # Set the maximum monthly replacment rate from SS benefits tables - PIA[PIA > p.PIA_maxpayment] = p.PIA_maxpayment - if p.PIA_minpayment != 0.0: - PIA[PIA < p.PIA_minpayment] = p.PIA_minpayment - theta = (PIA * (12.0 * p.S / 80.0)) / (factor_ss * wss) - return theta - - def ETR_wealth(b, h_wealth, m_wealth, p_wealth): r""" Calculates the effective tax rate on wealth. @@ -337,7 +281,7 @@ def net_taxes( """ T_I = income_tax_liab(r, w, b, n, factor, t, j, method, e, etr_params, p) - pension = pension_amount(w, n, theta, t, j, shift, method, e, p) + pension = pensions.pension_amount(w, n, theta, t, j, shift, method, e, p) T_BQ = bequest_tax_liab(r, b, bq, t, j, method, p) T_W = wealth_tax_liab(r, b, t, j, method, p) @@ -445,84 +389,6 @@ def income_tax_liab(r, w, b, n, factor, t, j, method, e, etr_params, p): return income_payroll_tax_liab -def pension_amount(w, n, theta, t, j, shift, method, e, p): - """ - Calculate public pension benefit amounts for each household. - - Args: - w (array_like): real wage rate - n (Numpy array): labor supply - theta (Numpy array): social security replacement rate value for - lifetime income group j - t (int): time period - j (int): index of lifetime income group - shift (bool): whether computing for periods 0--s or 1--(s+1), - =True for 1--(s+1) - method (str): adjusts calculation dimensions based on 'SS' or - 'TPI' - e (Numpy array): effective labor units - p (OG-Core Specifications object): model parameters - - Returns: - pension (Numpy array): pension amount for each household - - """ - if j is not None: - if method == "TPI": - if n.ndim == 2: - w = w.reshape(w.shape[0], 1) - else: - if method == "TPI": - w = utils.to_timepath_shape(w) - - pension = np.zeros_like(n) - if method == "SS": - # Depending on if we are looking at b_s or b_s+1, the - # entry for retirement will change (it shifts back one). - # The shift boolean makes sure we start replacement rates - # at the correct age. - if shift is False: - pension[p.retire[-1] :] = theta * w - else: - pension[p.retire[-1] - 1 :] = theta * w - elif method == "TPI": - length = w.shape[0] - if not shift: - # retireTPI is different from retire, because in TP income - # we are counting backwards with different length lists. - # This will always be the correct location of retirement, - # depending on the shape of the lists. - retireTPI = p.retire[t : t + length] - p.S - else: - retireTPI = p.retire[t : t + length] - 1 - p.S - if len(n.shape) == 1: - if not shift: - retireTPI = p.retire[t] - p.S - else: - retireTPI = p.retire[t] - 1 - p.S - pension[retireTPI:] = ( - theta[j] * p.replacement_rate_adjust[t] * w[retireTPI:] - ) - elif len(n.shape) == 2: - for tt in range(pension.shape[0]): - pension[tt, retireTPI[tt] :] = ( - theta * p.replacement_rate_adjust[t + tt] * w[tt] - ) - else: - for tt in range(pension.shape[0]): - pension[tt, retireTPI[tt] :, :] = ( - theta.reshape(1, p.J) - * p.replacement_rate_adjust[t + tt] - * w[tt] - ) - elif method == "TPI_scalar": - # The above methods won't work if scalars are used. This option - # is only called by the SS_TPI_firstdoughnutring function in TPI. - pension = theta * p.replacement_rate_adjust[0] * w - - return pension - - def wealth_tax_liab(r, b, t, j, method, p): """ Calculate wealth tax liability for each household. From 090c95301cbd381caa54899ab706a80545744cf7 Mon Sep 17 00:00:00 2001 From: jdebacker Date: Fri, 28 Jun 2024 17:16:06 -0400 Subject: [PATCH 02/50] update testing for new pension module --- tests/test_pensions.py | 72 ++++++++++++++++++++++++++++++++++++++++++ tests/test_tax.py | 67 --------------------------------------- 2 files changed, 72 insertions(+), 67 deletions(-) create mode 100644 tests/test_pensions.py diff --git a/tests/test_pensions.py b/tests/test_pensions.py new file mode 100644 index 000000000..98adbcefb --- /dev/null +++ b/tests/test_pensions.py @@ -0,0 +1,72 @@ +import numpy as np +import copy +import pytest +from ogcore import tax +from ogcore.parameters import Specifications + + +p = Specifications() +rho_vec = np.zeros((1, 4)) +rho_vec[0, -1] = 1.0 +new_param_values = { + "S": 4, + "rho": rho_vec.tolist(), + "lambdas": [1.0], + "labor_income_tax_noncompliance_rate": [[0.0]], + "capital_income_tax_noncompliance_rate": [[0.0]], + "J": 1, + "T": 4, + "chi_n": np.ones(4), + "eta": (np.ones((4, 1)) / (4 * 1)), + "e": np.ones((4, 1)), +} +p.update_specifications(new_param_values) +p.retire = [3, 3, 3, 3, 3, 3, 3, 3] +p1 = copy.deepcopy(p) +p2 = copy.deepcopy(p) +p3 = copy.deepcopy(p) +# Use just a column of e +p1.e = np.transpose(np.array([[0.1, 0.3, 0.5, 0.2], [0.1, 0.3, 0.5, 0.2]])) +# e has two dimensions +p2.e = np.array([[0.4, 0.3], [0.5, 0.4], [0.6, 0.4], [0.4, 0.3]]) +p3.e = np.array([[0.35, 0.3], [0.55, 0.4], [0.65, 0.4], [0.45, 0.3]]) +p5 = copy.deepcopy(p3) +p5.PIA_minpayment = 125.0 +wss = 0.5 +n1 = np.array([0.5, 0.5, 0.5, 0.5]) +n2 = nssmat = np.array([[0.4, 0.4], [0.4, 0.4], [0.4, 0.4], [0.4, 0.4]]) +n3 = nssmat = np.array([[0.3, 0.35], [0.3, 0.35], [0.3, 0.35], [0.3, 0.35]]) +factor1 = 100000 +factor3 = 10000 +factor4 = 1000 +expected1 = np.array([0.042012]) +expected2 = np.array([0.042012, 0.03842772]) +expected3 = np.array([0.1145304, 0.0969304]) +expected4 = np.array([0.1755, 0.126]) +expected5 = np.array([0.1755, 0.126 * 1.1904761904761905]) + +test_data = [ + (n1, wss, factor1, 0, p1, expected1), + (n2, wss, factor1, None, p2, expected2), + (n3, wss, factor3, None, p3, expected3), + (n3, wss, factor4, None, p3, expected4), + (n3, wss, factor4, None, p5, expected5), +] + + +@pytest.mark.parametrize( + "n,w,factor,j,p_in,expected", + test_data, + ids=["1D e", "2D e", "AIME case 2", "AIME case 3", "Min PIA case"], +) +def test_replacement_rate_vals(n, w, factor, j, p_in, expected): + # Test replacement rate function, making sure to trigger all three + # cases of AIME + # make e 3D + p = copy.deepcopy(p_in) + # p.e = np.tile(np.reshape(p.e, (1, p.S, p.J)), (p.T, 1, 1)) + p.e = np.tile( + np.reshape(p.e, (1, p.e.shape[0], p.e.shape[1])), (p.T, 1, 1) + ) + theta = tax.replacement_rate_vals(n, w, factor, j, p) + assert np.allclose(theta, expected) diff --git a/tests/test_tax.py b/tests/test_tax.py index c07e47e6f..12b9f0be0 100644 --- a/tests/test_tax.py +++ b/tests/test_tax.py @@ -5,73 +5,6 @@ from ogcore.parameters import Specifications -p = Specifications() -rho_vec = np.zeros((1, 4)) -rho_vec[0, -1] = 1.0 -new_param_values = { - "S": 4, - "rho": rho_vec.tolist(), - "lambdas": [1.0], - "labor_income_tax_noncompliance_rate": [[0.0]], - "capital_income_tax_noncompliance_rate": [[0.0]], - "J": 1, - "T": 4, - "chi_n": np.ones(4), - "eta": (np.ones((4, 1)) / (4 * 1)), - "e": np.ones((4, 1)), -} -p.update_specifications(new_param_values) -p.retire = [3, 3, 3, 3, 3, 3, 3, 3] -p1 = copy.deepcopy(p) -p2 = copy.deepcopy(p) -p3 = copy.deepcopy(p) -# Use just a column of e -p1.e = np.transpose(np.array([[0.1, 0.3, 0.5, 0.2], [0.1, 0.3, 0.5, 0.2]])) -# e has two dimensions -p2.e = np.array([[0.4, 0.3], [0.5, 0.4], [0.6, 0.4], [0.4, 0.3]]) -p3.e = np.array([[0.35, 0.3], [0.55, 0.4], [0.65, 0.4], [0.45, 0.3]]) -p5 = copy.deepcopy(p3) -p5.PIA_minpayment = 125.0 -wss = 0.5 -n1 = np.array([0.5, 0.5, 0.5, 0.5]) -n2 = nssmat = np.array([[0.4, 0.4], [0.4, 0.4], [0.4, 0.4], [0.4, 0.4]]) -n3 = nssmat = np.array([[0.3, 0.35], [0.3, 0.35], [0.3, 0.35], [0.3, 0.35]]) -factor1 = 100000 -factor3 = 10000 -factor4 = 1000 -expected1 = np.array([0.042012]) -expected2 = np.array([0.042012, 0.03842772]) -expected3 = np.array([0.1145304, 0.0969304]) -expected4 = np.array([0.1755, 0.126]) -expected5 = np.array([0.1755, 0.126 * 1.1904761904761905]) - -test_data = [ - (n1, wss, factor1, 0, p1, expected1), - (n2, wss, factor1, None, p2, expected2), - (n3, wss, factor3, None, p3, expected3), - (n3, wss, factor4, None, p3, expected4), - (n3, wss, factor4, None, p5, expected5), -] - - -@pytest.mark.parametrize( - "n,w,factor,j,p_in,expected", - test_data, - ids=["1D e", "2D e", "AIME case 2", "AIME case 3", "Min PIA case"], -) -def test_replacement_rate_vals(n, w, factor, j, p_in, expected): - # Test replacement rate function, making sure to trigger all three - # cases of AIME - # make e 3D - p = copy.deepcopy(p_in) - # p.e = np.tile(np.reshape(p.e, (1, p.S, p.J)), (p.T, 1, 1)) - p.e = np.tile( - np.reshape(p.e, (1, p.e.shape[0], p.e.shape[1])), (p.T, 1, 1) - ) - theta = tax.replacement_rate_vals(n, w, factor, j, p) - assert np.allclose(theta, expected) - - b1 = np.array([0.1, 0.5, 0.9]) p1 = Specifications() rho_vec = np.zeros((1, 3)) From 18d8523d3fa37e9f7f38685fb704e20bc9ec47b7 Mon Sep 17 00:00:00 2001 From: jdebacker Date: Fri, 28 Jun 2024 17:20:01 -0400 Subject: [PATCH 03/50] update module used --- tests/test_pensions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_pensions.py b/tests/test_pensions.py index 98adbcefb..b982cd7d5 100644 --- a/tests/test_pensions.py +++ b/tests/test_pensions.py @@ -1,7 +1,7 @@ import numpy as np import copy import pytest -from ogcore import tax +from ogcore import pensions from ogcore.parameters import Specifications @@ -68,5 +68,5 @@ def test_replacement_rate_vals(n, w, factor, j, p_in, expected): p.e = np.tile( np.reshape(p.e, (1, p.e.shape[0], p.e.shape[1])), (p.T, 1, 1) ) - theta = tax.replacement_rate_vals(n, w, factor, j, p) + theta = pensions.replacement_rate_vals(n, w, factor, j, p) assert np.allclose(theta, expected) From 7568cb0d272d9f8dacf3ac49b29743a654a6db3c Mon Sep 17 00:00:00 2001 From: jdebacker Date: Fri, 28 Jun 2024 23:10:21 -0400 Subject: [PATCH 04/50] add parameter for type of pension system --- ogcore/default_parameters.json | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/ogcore/default_parameters.json b/ogcore/default_parameters.json index c0e772eca..eea852d63 100644 --- a/ogcore/default_parameters.json +++ b/ogcore/default_parameters.json @@ -2897,6 +2897,29 @@ } } }, + "pension_system": { + "title": "Pension system", + "description": "Pension system.", + "section_1": "Fiscal Policy Parameters", + "section_2": "Government Pension Parameters", + "notes": "", + "type": "str", + "value": [ + { + "value": "US-Style Social Securit" + } + ], + "validators": { + "choice": { + "choices": [ + "US-Style Social Security", + "Defined Benefits", + "Notional Defined Contribution", + "Points System" + ] + } + } + }, "AIME_num_years": { "title": "Number of years used to compute average index monthly earnings (AIME)", "description": "Number of years used to compute average index monthly earnings (AIME).", From 8c041e4a83acb11a908ba5cb6f238210e3534571 Mon Sep 17 00:00:00 2001 From: jdebacker Date: Fri, 28 Jun 2024 23:11:57 -0400 Subject: [PATCH 05/50] add numba to env --- environment.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/environment.yml b/environment.yml index c770c358e..a33d4f754 100644 --- a/environment.yml +++ b/environment.yml @@ -8,6 +8,7 @@ dependencies: - setuptools - scipy>=1.7.1 - pandas>=1.2.5 +- numba - matplotlib - dask>=2.30.0 - dask-core>=2.30.0 From adaffceec2bbfcef5521508a6928d27f38fbe8f4 Mon Sep 17 00:00:00 2001 From: jdebacker Date: Sat, 29 Jun 2024 08:14:09 -0400 Subject: [PATCH 06/50] add parameters for other pension systems --- ogcore/default_parameters.json | 114 +++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/ogcore/default_parameters.json b/ogcore/default_parameters.json index eea852d63..0b130901c 100644 --- a/ogcore/default_parameters.json +++ b/ogcore/default_parameters.json @@ -2920,6 +2920,120 @@ } } }, + "tau_p": { + "title": "TODO: fill in description", + "description": "TODO: fill in description", + "section_1": "Fiscal Policy Parameters", + "section_2": "Government Pension Parameters", + "notes": "", + "type": "float", + "value": [ + { + "value": 0.0 + } + ], + "validators": { + "range": { + "min": 0.0, + "max": 1.0 + } + } + }, + "k_ret": { + "title": "TODO: fill in description", + "description": "TODO: fill in description", + "section_1": "Fiscal Policy Parameters", + "section_2": "Government Pension Parameters", + "notes": "", + "type": "float", + "value": [ + { + "value": 0.0 + } + ], + "validators": { + "range": { + "min": 0.0, + "max": 1.0 + } + } + }, + "rep_rate_py": { + "title": "TODO: fill in description", + "description": "TODO: fill in description", + "section_1": "Fiscal Policy Parameters", + "section_2": "Government Pension Parameters", + "notes": "", + "type": "float", + "value": [ + { + "value": 0.0 + } + ], + "validators": { + "range": { + "min": 0.0, + "max": 1.0 + } + } + }, + "vpoint": { + "title": "TODO: fill in description", + "description": "TODO: fill in description", + "section_1": "Fiscal Policy Parameters", + "section_2": "Government Pension Parameters", + "notes": "", + "type": "float", + "value": [ + { + "value": 0.0 + } + ], + "validators": { + "range": { + "min": 0.0, + "max": 1.0 + } + } + }, + "last_career_years": { + "title": "Number of years used to compute TODO: complete", + "description": "Number of years used to compute TODO: complete.", + "section_1": "Fiscal Policy Parameters", + "section_2": "Government Pension Parameters", + "notes": "TODO: add note about how compute works", + "type": "int", + "value": [ + { + "value": 10 + } + ], + "validators": { + "range": { + "min": 1, + "max": "S" + } + } + }, + "yr_contr": { + "title": "Number of years used to compute TODO: complete", + "description": "Number of years used to compute TODO: complete.", + "section_1": "Fiscal Policy Parameters", + "section_2": "Government Pension Parameters", + "notes": "TODO: add note about how compute works", + "type": "int", + "value": [ + { + "value": 40 + } + ], + "validators": { + "range": { + "min": 1, + "max": "retirement_age" + } + } + }, "AIME_num_years": { "title": "Number of years used to compute average index monthly earnings (AIME)", "description": "Number of years used to compute average index monthly earnings (AIME).", From d73d85a5c778357dff36b7a4f4ad06bfdccd0b28 Mon Sep 17 00:00:00 2001 From: jdebacker Date: Sat, 29 Jun 2024 08:14:27 -0400 Subject: [PATCH 07/50] copy over initial functions for other pension systems --- ogcore/pensions.py | 489 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 487 insertions(+), 2 deletions(-) diff --git a/ogcore/pensions.py b/ogcore/pensions.py index 0273e31ab..9436c8ba3 100644 --- a/ogcore/pensions.py +++ b/ogcore/pensions.py @@ -1,5 +1,6 @@ # Packages import numpy as np +import numba from ogcore import utils @@ -59,7 +60,7 @@ def replacement_rate_vals(nssmat, wss, factor_ss, j, p): return theta -def pension_amount(w, n, theta, t, j, shift, method, e, p): +def pension_amount(w, n, t, j, shift, method, e, p): """ Calculate public pension benefit amounts for each household. @@ -134,4 +135,488 @@ def pension_amount(w, n, theta, t, j, shift, method, e, p): # is only called by the SS_TPI_firstdoughnutring function in TPI. pension = theta * p.replacement_rate_adjust[0] * w - return pension \ No newline at end of file + return pension + + +def SS_amount(w, n, theta, t, j, shift, method, e, p): + """ + Calculate public pension benefit amounts for each household under + a US-style social security system. + + Args: + w (array_like): real wage rate + n (Numpy array): labor supply + theta (Numpy array): social security replacement rate value for + lifetime income group j + t (int): time period + j (int): index of lifetime income group + shift (bool): whether computing for periods 0--s or 1--(s+1), + =True for 1--(s+1) + method (str): adjusts calculation dimensions based on 'SS' or + 'TPI' + e (Numpy array): effective labor units + p (OG-Core Specifications object): model parameters + + Returns: + pension (Numpy array): pension amount for each household + + """ + if j is not None: + if method == "TPI": + if n.ndim == 2: + w = w.reshape(w.shape[0], 1) + else: + if method == "TPI": + w = utils.to_timepath_shape(w) + + pension = np.zeros_like(n) + if method == "SS": + # Depending on if we are looking at b_s or b_s+1, the + # entry for retirement will change (it shifts back one). + # The shift boolean makes sure we start replacement rates + # at the correct age. + if shift is False: + pension[p.retire[-1] :] = theta * w + else: + pension[p.retire[-1] - 1 :] = theta * w + elif method == "TPI": + length = w.shape[0] + if not shift: + # retireTPI is different from retire, because in TP income + # we are counting backwards with different length lists. + # This will always be the correct location of retirement, + # depending on the shape of the lists. + retireTPI = p.retire[t : t + length] - p.S + else: + retireTPI = p.retire[t : t + length] - 1 - p.S + if len(n.shape) == 1: + if not shift: + retireTPI = p.retire[t] - p.S + else: + retireTPI = p.retire[t] - 1 - p.S + pension[retireTPI:] = ( + theta[j] * p.replacement_rate_adjust[t] * w[retireTPI:] + ) + elif len(n.shape) == 2: + for tt in range(pension.shape[0]): + pension[tt, retireTPI[tt] :] = ( + theta * p.replacement_rate_adjust[t + tt] * w[tt] + ) + else: + for tt in range(pension.shape[0]): + pension[tt, retireTPI[tt] :, :] = ( + theta.reshape(1, p.J) + * p.replacement_rate_adjust[t + tt] + * w[tt] + ) + elif method == "TPI_scalar": + # The above methods won't work if scalars are used. This option + # is only called by the SS_TPI_firstdoughnutring function in TPI. + pension = theta * p.replacement_rate_adjust[0] * w + + return pension + + +def DB_amount(self, households, firms, w, e, n, j_ind): + """ + Calculate public pension from a defined benefits system. + """ + L_inc_avg = np.zeros(0) + L_inc_avg_s = np.zeros(self.last_career_yrs) + + if n.shape[0] < households.S: + per_rmn = n.shape[0] + w_S = np.append((households.w_preTP * + np.ones(households.S))[:(-per_rmn)], w) + n_S = np.append(households.n_preTP[:(-per_rmn), j_ind], n) + + DB_s = np.zeros(households.S_ret) + DB = np.zeros(households.S) + + DB = DB_1dim_loop(w_S, households.emat[:, j_ind], n_S, + households.S_ret, households.S, firms.g_y, L_inc_avg_s, + L_inc_avg, DB_s, DB, self.last_career_yrs, self.rep_rate, + self.rep_rate_py, self.yr_contr) + DB = DB[-per_rmn:] + + else: + if np.ndim(n) == 1: + DB_s = np.zeros(households.S_ret) + DB = np.zeros(households.S) + DB = DB_1dim_loop( + w, e, n, households.S_ret, households.S, firms.g_y, + L_inc_avg_s, L_inc_avg, DB_s, DB, self.last_career_yrs, + self.rep_rate, self.rep_rate_py, self.yr_contr) + + elif np.ndim(n) == 2: + DB_sj = np.zeros((households.S_ret, households.J)) + DB = np.zeros((households.S, households.J)) + L_inc_avg_sj = np.zeros((self.last_career_yrs, households.J)) + DB = DB_2dim_loop( + w, e, n, households.S_ret, households.S, firms.g_y, + L_inc_avg_sj, L_inc_avg, DB_sj, DB, self.last_career_yrs, + self.rep_rate, self.rep_rate_py, self.yr_contr) + + return DB + + +def NDC_amount(self, demographics, households, firms, w, e, + n, r, Y, j_ind): + """ + Calculate public pension from a notional defined contribution + system. + """ + self.get_g_ndc(r, Y, demographics.g_n_SS, firms.g_y, ) + self.get_delta_ret(r, Y, demographics, households, firms) + + if n.shape[0] < households.S: + per_rmn = n.shape[0] + + w_S = np.append((households.w_preTP * + np.ones(households.S))[:(-per_rmn)], w) + n_S = np.append(households.n_preTP[:(-per_rmn), j_ind], n) + + NDC_s = np.zeros(households.S_ret) + NDC = np.zeros(households.S) + NDC = NDC_1dim_loop( + w_S, households.emat[:, j_ind], n_S, households.S_ret, + households.S, firms.g_y, self.tau_p, self.g_ndc, + self.delta_ret, NDC_s, NDC) + NDC = NDC[-per_rmn:] + + else: + if np.ndim(n) == 1: + NDC_s = np.zeros(households.S_ret) + NDC = np.zeros(households.S) + NDC = NDC_1dim_loop( + w, e, n, households.S_ret, households.S, firms.g_y, + self.tau_p, self.g_ndc, self.delta_ret, NDC_s, NDC) + elif np.ndim(n) == 2: + NDC_sj = np.zeros((households.S_ret, households.J)) + NDC = np.zeros((households.S, households.J)) + NDC = NDC_2dim_loop( + w, e, n, households.S_ret, households.S, firms.g_y, + self.tau_p, self.g_ndc, self.delta_ret, NDC_sj, NDC) + + return NDC + + +def PS_amount(self, demographics, households, firms, w, e, n, r, Y, lambdas, + j_ind, factor): + """ + Calculate public pension from a points system. + """ + + if n.shape[0] < households.S: + per_rmn = n.shape[0] + w_S = np.append((households.w_preTP * + np.ones(households.S))[:(-per_rmn)], w) + n_S = np.append(households.n_preTP[:(-per_rmn), j_ind], n) + L_inc_avg_s = np.zeros(households.S_ret) + PPB = np.zeros(households.S) + PPB = PPB_1dim_loop(w_S, households.emat[:, j_ind], n_S, + households.S_ret, households.S, + firms.g_y, self.vpoint, + factor, L_inc_avg_s, PPB) + PPB = PPB[-per_rmn:] + + else: + if np.ndim(n) == 1: + L_inc_avg_s = np.zeros(households.S_ret) + PPB = np.zeros(households.S) + PPB = PPB_1dim_loop(w, e, n, households.S_ret, households.S, + firms.g_y, self.vpoint, + factor, L_inc_avg_s, PPB) + + elif np.ndim(n) == 2: + L_inc_avg_sj = np.zeros((households.S_ret, households.J)) + PPB = np.zeros((households.S, households.J)) + PPB = PPB_2dim_loop(w, e, n, households.S_ret, households.S, + households.J, firms.g_y, self.vpoint, factor, + L_inc_avg_sj, PPB) + + return PPB + + +def deriv_theta(self, demographics, households, firms, r, w, e, + Y, per_rmn, factor): + ''' + Change in pension benefits for another unit of labor supply for + pension system selected + ''' + if self.pension_system == "DB": + d_theta = self.deriv_DB(households, firms, w, e, per_rmn) + d_theta = d_theta[-per_rmn:] + elif self.pension_system == "NDC": + d_theta = self.deriv_NDC(demographics, households, firms, + r, w, e, Y, per_rmn) + elif self.pension_system == "PS": + d_theta = self.deriv_PPB(demographics, households, firms, + w, e, per_rmn, factor) + + return d_theta + + +def deriv_NDC(self, demographics, households, firms, r, w, e, Y, + per_rmn): + ''' + Change in NDC pension benefits for another unit of labor supply + ''' + if per_rmn == 1: + d_theta = 0 + elif per_rmn < (households.S - households.S_ret + 1): + d_theta = np.zeros(per_rmn) + else: + d_theta_empty = np.zeros(per_rmn) + self.get_delta_ret(r, Y, demographics, households, firms) + self.get_g_ndc(r, Y, demographics.g_n_SS, firms.g_y) + d_theta = deriv_NDC_loop( + w, e, per_rmn, households.S, + households.S_ret, firms.g_y, self.tau_p, + self.g_ndc, self.delta_ret, d_theta_empty) + + return d_theta + + +def deriv_DB(self, households, firms, w, e, per_rmn): + ''' + Change in DB pension benefits for another unit of labor supply + ''' + + if per_rmn < (households.S - households.S_ret + 1): + d_theta = np.zeros(households.S) + else: + d_theta_empty = np.zeros(households.S) + d_theta = deriv_DB_loop( + w, e, households.S,households.S_ret, per_rmn, firms.g_y, + d_theta_empty, self.last_career_yrs, self.rep_rate_py, + self.yr_contr) + return d_theta + + +def deriv_PS(self, demographics, households, firms, w, e, + per_rmn, factor): + ''' + Change in points system pension benefits for another unit of labor supply + ''' + + if per_rmn < (households.S - households.S_ret + 1): + d_theta = np.zeros(households.S) + else: + d_theta_empty = np.zeros(households.S) + d_theta = deriv_PS_loop(w, e, households.S, households.S_ret, per_rmn, + firms.g_y, d_theta_empty, + self.vpoint, factor) + d_theta = d_theta[-per_rmn:] + + return d_theta + + +def delta_point(self, r, Y, g_n, g_y): + ''' + Compute growth rate used for contributions to points system pension + ''' + # TODO: Add option to allow use to enter growth rate amount + # Also to allow rate to vary by year + # Do this for all these growth rates for each system + # Might also allow for option to grow at per capital GDP growth rate + if self.points_growth_rate == 'r': + self.delta_point = r + elif self.points_growth_rate == 'Curr GDP': + self.delta_point = (Y[1:] - Y[:-1]) / Y[:-1] + elif self.points_growth_rate == 'LR GDP': + self.delta_point = g_y + g_n + else: + self.delta_point = g_y + g_n + + +def g_ndc(self, r, Y, g_n, g_y,): + ''' + Compute growth rate used for contributions to NDC pension + ''' + if self.ndc_growth_rate == 'r': + self.g_ndc = r + elif self.ndc_growth_rate == 'Curr GDP': + self.g_ndc = (Y[1:] - Y[:-1]) / Y[:-1] + elif self.ndc_growth_rate == 'LR GDP': + self.g_ndc = g_y + g_n + else: + self.g_ndc = g_y + g_n + + +def g_dir(self, r, Y, g_n, g_y): + ''' + Compute growth rate used for contributions to NDC pension + ''' + if self.dir_growth_rate == 'r': + self.g_dir = r + elif self.dir_growth_rate == 'Curr GDP': + self.g_dir = (Y[1:] - Y[:-1]) / Y[:-1] + elif self.dir_growth_rate == 'LR GDP': + self.g_dir = g_y + g_n +# self.g_dir = 0.015 + else: + self.g_dir = g_y + g_n + + +def delta_ret(self, r, Y, demographics, households, firms): + ''' + Compute conversion coefficient for the NDC pension amount + ''' + surv_rates = 1 - demographics.mort_rates_SS + dir_delta_s_empty = np.zeros(households.S - households.S_ret + 1) + self.get_g_dir(r, Y, demographics.g_n_SS, firms.g_y) + dir_delta = delta_ret_loop( + households.S, households.S_ret, surv_rates, self.g_dir, + dir_delta_s_empty) + #####TODO: formula below needs to be changed if we had separate calculations for both sexes + self.delta_ret = 1 / (dir_delta - self.k_ret) +# print("self.delta_ret", self.delta_ret) + + +def pension_benefit(self, demographics, households, firms, w, e, n, + r, Y, lambdas, j_ind, factor): + if self.pension_system == "DB": + theta = self.get_DB(households, firms, w, e, n, j_ind) + elif self.pension_system == "NDC": + theta = self.get_NDC( + demographics, households, firms, w, e, n, r, Y, j_ind) + elif self.pension_system == "PS": + theta = self.get_PPB(demographics, households, firms, + w, e, n, r, Y, lambdas, j_ind, factor) + + return theta + + +@numba.jit +def deriv_DB_loop(w, e, S, S_ret, per_rmn, g_y, d_theta, + last_career_yrs, rep_rate_py, yr_contr): + d_theta = np.zeros(per_rmn) + num_per_retire = S - S_ret + for s in range(per_rmn): + d_theta[s] = w[s] * e[s] * rep_rate_py * (yr_contr / last_career_yrs) + d_theta[-num_per_retire:] = 0.0 + + return d_theta + + +@numba.jit +def deriv_PS_loop(w, e, S, S_ret, per_rmn, g_y, d_theta, vpoint, factor): + + for s in range((S - per_rmn), S_ret): + d_theta[s] = ((w[s] * e[s] * vpoint * constants.MONTHS_IN_A_YEAR) / + (factor * constants.THOUSAND)) + + return d_theta + + +@numba.jit +def deriv_NDC_loop(w, e, per_rmn, S, S_ret, g_y, tau_p, g_ndc, + delta_ret, d_theta): + + for s in range((S - per_rmn), S_ret): + d_theta[s - (S - per_rmn)] = (tau_p * w[s - (S - per_rmn)] * + e[s - (S - per_rmn)] * delta_ret * + (1 + g_ndc) ** (S_ret - s - 1)) + + return d_theta + + +@numba.jit +def delta_ret_loop(S, S_ret, surv_rates, g_dir, dir_delta_s): + + cumul_surv_rates = np.ones(S - S_ret + 1) + for s in range(S - S_ret + 1): + surv_rates_vec = surv_rates[S_ret: S_ret + s + 1] + surv_rates_vec[0] = 1.0 + cumul_surv_rates[s] = np.prod(surv_rates_vec) + cumul_g_y = np.ones(S - S_ret + 1) + cumul_g_y[s] = (1/(1 + g_dir)) ** s + dir_delta_s[s] = cumul_surv_rates[s] * cumul_g_y[s] + dir_delta = dir_delta_s.sum() + return dir_delta + +@numba.jit +def PPB_1dim_loop(w, e, n, S_ret, S, g_y, vpoint, factor, L_inc_avg_s, + PPB): + + for u in range(S_ret, S): + for s in range(S_ret): + L_inc_avg_s[s] = \ + w[s] / np.exp(g_y * (u - s)) * e[s] * n[s] + PPB[u] = ((constants.MONTHS_IN_A_YEAR * vpoint * L_inc_avg_s.sum()) + / (factor * constants.THOUSAND)) + + return PPB + +@numba.jit +def PPB_2dim_loop(w, e, n, S_ret, S, J, g_y, vpoint, factor, + L_inc_avg_sj, PPB): + + for u in range(S_ret, S): + for s in range(S_ret): + L_inc_avg_sj[s, :] = \ + w[s] / np.exp(g_y * (u - s)) * e[s, :] * n[s, :] + PPB[u, :] = ((constants.MONTHS_IN_A_YEAR * vpoint * L_inc_avg_sj.sum(axis=0)) + / (factor * constants.THOUSAND)) + + return PPB + +@numba.jit +def DB_1dim_loop(w, e, n, S_ret, S, g_y, L_inc_avg_s, + L_inc_avg, DB_s, DB, last_career_yrs, rep_rate, + rep_rate_py, yr_contr): + + for u in range(S_ret, S): + for s in range(S_ret - last_career_yrs, + S_ret): + L_inc_avg_s[s - (S_ret - last_career_yrs)] = \ + w[s] / np.exp(g_y * (u - s)) * e[s] * n[s] + L_inc_avg = L_inc_avg_s.sum() / last_career_yrs + rep_rate = yr_contr * rep_rate_py + DB[u] = rep_rate * L_inc_avg + + return DB + +@numba.jit +def DB_2dim_loop(w, e, n, S_ret, S, g_y, L_inc_avg_sj, + L_inc_avg, DB_sj, DB, last_career_yrs, rep_rate, + rep_rate_py, yr_contr): + + for u in range(S_ret, S): + for s in range(S_ret - last_career_yrs, + S_ret): + L_inc_avg_sj[s - (S_ret - last_career_yrs), :] = \ + w[s] / np.exp(g_y * (u - s)) * e[s, :] * n[s, :] + L_inc_avg = L_inc_avg_sj.sum(axis=0) / last_career_yrs + rep_rate = yr_contr * rep_rate_py + DB[u, :] = rep_rate * L_inc_avg + + return DB + +@numba.jit +def NDC_1dim_loop(w, e, n, S_ret, S, g_y, tau_p, g_ndc, delta_ret, + NDC_s, NDC): + + for u in range(S_ret, S): + for s in range(0, S_ret): + NDC_s[s] = ( + tau_p * + (w[s] / np.exp(g_y * (u - s))) * e[s] * + n[s] * ((1 + g_ndc) ** + (S_ret - s - 1))) + NDC[u] = delta_ret * NDC_s.sum() + return NDC + +@numba.jit +def NDC_2dim_loop(w, e, n, S_ret, S, g_y, tau_p, g_ndc, delta_ret, + NDC_sj, NDC): + for u in range(S_ret, S): + for s in range(0, S_ret): + NDC_sj[s, :] = ( + tau_p * + (w[s] / np.exp(g_y * (u - s))) * + e[s, :] * n[s, :] * ((1 + g_ndc) ** + (S_ret - s - 1))) + NDC[u, :] = delta_ret * NDC_sj.sum(axis=0) + return NDC \ No newline at end of file From bd862ea7a2b17e5503620d666eb5741b7e255b56 Mon Sep 17 00:00:00 2001 From: jdebacker Date: Sun, 30 Jun 2024 21:58:37 -0400 Subject: [PATCH 08/50] update refs to param obj --- ogcore/pensions.py | 652 ++++++++++++++++++++++++++------------------- 1 file changed, 377 insertions(+), 275 deletions(-) diff --git a/ogcore/pensions.py b/ogcore/pensions.py index 9436c8ba3..db249e723 100644 --- a/ogcore/pensions.py +++ b/ogcore/pensions.py @@ -60,7 +60,7 @@ def replacement_rate_vals(nssmat, wss, factor_ss, j, p): return theta -def pension_amount(w, n, t, j, shift, method, e, p): +def pension_amount(w, n, theta, t, j, shift, method, e, p): """ Calculate public pension benefit amounts for each household. @@ -82,59 +82,22 @@ def pension_amount(w, n, t, j, shift, method, e, p): pension (Numpy array): pension amount for each household """ - if j is not None: - if method == "TPI": - if n.ndim == 2: - w = w.reshape(w.shape[0], 1) + # TODO: think about how can allow for transition from one + # pension system to another along the time path + if p.pension_system == "US-Style Social Security": + pension = SS_amount(w, n, theta, t, j, shift, method, e, p) + elif p.pension_system == "Defined Benefits": + pension = DB_amount(w, n, t, j, shift, method, e, p) + elif p.pension_system == "Notional Defined Contribution": + sdf + elif p.pension_system == "Points System": + sdf else: - if method == "TPI": - w = utils.to_timepath_shape(w) - - pension = np.zeros_like(n) - if method == "SS": - # Depending on if we are looking at b_s or b_s+1, the - # entry for retirement will change (it shifts back one). - # The shift boolean makes sure we start replacement rates - # at the correct age. - if shift is False: - pension[p.retire[-1] :] = theta * w - else: - pension[p.retire[-1] - 1 :] = theta * w - elif method == "TPI": - length = w.shape[0] - if not shift: - # retireTPI is different from retire, because in TP income - # we are counting backwards with different length lists. - # This will always be the correct location of retirement, - # depending on the shape of the lists. - retireTPI = p.retire[t : t + length] - p.S - else: - retireTPI = p.retire[t : t + length] - 1 - p.S - if len(n.shape) == 1: - if not shift: - retireTPI = p.retire[t] - p.S - else: - retireTPI = p.retire[t] - 1 - p.S - pension[retireTPI:] = ( - theta[j] * p.replacement_rate_adjust[t] * w[retireTPI:] - ) - elif len(n.shape) == 2: - for tt in range(pension.shape[0]): - pension[tt, retireTPI[tt] :] = ( - theta * p.replacement_rate_adjust[t + tt] * w[tt] - ) - else: - for tt in range(pension.shape[0]): - pension[tt, retireTPI[tt] :, :] = ( - theta.reshape(1, p.J) - * p.replacement_rate_adjust[t + tt] - * w[tt] - ) - elif method == "TPI_scalar": - # The above methods won't work if scalars are used. This option - # is only called by the SS_TPI_firstdoughnutring function in TPI. - pension = theta * p.replacement_rate_adjust[0] * w - + raise ValueError( + "pension_system must be one of the following: " + "'US-Style Social Security', 'Defined Benefits', " + "'Notional Defined Contribution', 'Points System'" + ) return pension @@ -217,280 +180,381 @@ def SS_amount(w, n, theta, t, j, shift, method, e, p): return pension -def DB_amount(self, households, firms, w, e, n, j_ind): +def DB_amount(w, e, n, j, p): """ Calculate public pension from a defined benefits system. """ L_inc_avg = np.zeros(0) - L_inc_avg_s = np.zeros(self.last_career_yrs) + L_inc_avg_s = np.zeros(p.last_career_yrs) - if n.shape[0] < households.S: + if n.shape[0] < p.S: per_rmn = n.shape[0] - w_S = np.append((households.w_preTP * - np.ones(households.S))[:(-per_rmn)], w) - n_S = np.append(households.n_preTP[:(-per_rmn), j_ind], n) - - DB_s = np.zeros(households.S_ret) - DB = np.zeros(households.S) - - DB = DB_1dim_loop(w_S, households.emat[:, j_ind], n_S, - households.S_ret, households.S, firms.g_y, L_inc_avg_s, - L_inc_avg, DB_s, DB, self.last_career_yrs, self.rep_rate, - self.rep_rate_py, self.yr_contr) + # TODO: think about how to handle setting w_preTP and n_preTP + w_S = np.append((p.w_preTP * np.ones(p.S))[:(-per_rmn)], w) + n_S = np.append(p.n_preTP[:(-per_rmn), j], n) + + DB_s = np.zeros(p.retirement_age) + DB = np.zeros(p.S) + # TODO: we set a rep_rate_py in params, but not rep_rate. What is it??? + DB = DB_1dim_loop( + w_S, + p.e[:, j], + n_S, + p.retirement_age, + p.S, + p.g_y, + L_inc_avg_s, + L_inc_avg, + DB_s, + DB, + p.last_career_yrs, + p.rep_rate, + p.rep_rate_py, + p.yr_contr, + ) DB = DB[-per_rmn:] else: if np.ndim(n) == 1: - DB_s = np.zeros(households.S_ret) - DB = np.zeros(households.S) + DB_s = np.zeros(p.retirement_age) + DB = np.zeros(p.S) DB = DB_1dim_loop( - w, e, n, households.S_ret, households.S, firms.g_y, - L_inc_avg_s, L_inc_avg, DB_s, DB, self.last_career_yrs, - self.rep_rate, self.rep_rate_py, self.yr_contr) + w, + e, + n, + p.retiremet_age, + p.S, + p.g_y, + L_inc_avg_s, + L_inc_avg, + DB_s, + DB, + p.last_career_yrs, + p.rep_rate, + p.rep_rate_py, + p.yr_contr, + ) elif np.ndim(n) == 2: - DB_sj = np.zeros((households.S_ret, households.J)) - DB = np.zeros((households.S, households.J)) - L_inc_avg_sj = np.zeros((self.last_career_yrs, households.J)) + DB_sj = np.zeros((p.retirement_age, p.J)) + DB = np.zeros((p.S, p.J)) + L_inc_avg_sj = np.zeros((p.last_career_yrs, p.J)) DB = DB_2dim_loop( - w, e, n, households.S_ret, households.S, firms.g_y, - L_inc_avg_sj, L_inc_avg, DB_sj, DB, self.last_career_yrs, - self.rep_rate, self.rep_rate_py, self.yr_contr) + w, + e, + n, + p.retirement_age, + p.S, + p.g_y, + L_inc_avg_sj, + L_inc_avg, + DB_sj, + DB, + p.last_career_yrs, + p.rep_rate, + p.rep_rate_py, + p.yr_contr, + ) return DB -def NDC_amount(self, demographics, households, firms, w, e, - n, r, Y, j_ind): +def NDC_amount(w, e, n, r, Y, j, p): """ - Calculate public pension from a notional defined contribution - system. + Calculate public pension from a notional defined contribution + system. """ - self.get_g_ndc(r, Y, demographics.g_n_SS, firms.g_y, ) - self.get_delta_ret(r, Y, demographics, households, firms) - - if n.shape[0] < households.S: + self.get_g_ndc( + r, + Y, + p.g_n_SS, + p.g_y, + ) + self.get_delta_ret(r, Y, p) + + if n.shape[0] < p.S: per_rmn = n.shape[0] - w_S = np.append((households.w_preTP * - np.ones(households.S))[:(-per_rmn)], w) - n_S = np.append(households.n_preTP[:(-per_rmn), j_ind], n) + w_S = np.append((p.w_preTP * np.ones(p.S))[:(-per_rmn)], w) + n_S = np.append(p.n_preTP[:(-per_rmn), j], n) - NDC_s = np.zeros(households.S_ret) - NDC = np.zeros(households.S) + NDC_s = np.zeros(p.S_ret) + NDC = np.zeros(p.S) NDC = NDC_1dim_loop( - w_S, households.emat[:, j_ind], n_S, households.S_ret, - households.S, firms.g_y, self.tau_p, self.g_ndc, - self.delta_ret, NDC_s, NDC) + w_S, + p.emat[:, j], + n_S, + p.S_ret, + p.S, + p.g_y, + self.tau_p, + self.g_ndc, + self.delta_ret, + NDC_s, + NDC, + ) NDC = NDC[-per_rmn:] else: if np.ndim(n) == 1: - NDC_s = np.zeros(households.S_ret) - NDC = np.zeros(households.S) + NDC_s = np.zeros(p.S_ret) + NDC = np.zeros(p.S) NDC = NDC_1dim_loop( - w, e, n, households.S_ret, households.S, firms.g_y, - self.tau_p, self.g_ndc, self.delta_ret, NDC_s, NDC) + w, + e, + n, + p.S_ret, + p.S, + p.g_y, + self.tau_p, + self.g_ndc, + self.delta_ret, + NDC_s, + NDC, + ) elif np.ndim(n) == 2: - NDC_sj = np.zeros((households.S_ret, households.J)) - NDC = np.zeros((households.S, households.J)) + NDC_sj = np.zeros((p.S_ret, p.J)) + NDC = np.zeros((p.S, p.J)) NDC = NDC_2dim_loop( - w, e, n, households.S_ret, households.S, firms.g_y, - self.tau_p, self.g_ndc, self.delta_ret, NDC_sj, NDC) + w, + e, + n, + p.S_ret, + p.S, + p.g_y, + self.tau_p, + self.g_ndc, + self.delta_ret, + NDC_sj, + NDC, + ) return NDC -def PS_amount(self, demographics, households, firms, w, e, n, r, Y, lambdas, - j_ind, factor): +def PS_amount(w, e, n, j, factor, p): """ - Calculate public pension from a points system. + Calculate public pension from a points system. """ - if n.shape[0] < households.S: + if n.shape[0] < p.S: per_rmn = n.shape[0] - w_S = np.append((households.w_preTP * - np.ones(households.S))[:(-per_rmn)], w) - n_S = np.append(households.n_preTP[:(-per_rmn), j_ind], n) - L_inc_avg_s = np.zeros(households.S_ret) - PPB = np.zeros(households.S) - PPB = PPB_1dim_loop(w_S, households.emat[:, j_ind], n_S, - households.S_ret, households.S, - firms.g_y, self.vpoint, - factor, L_inc_avg_s, PPB) + w_S = np.append((p.w_preTP * np.ones(p.S))[:(-per_rmn)], w) + n_S = np.append(p.n_preTP[:(-per_rmn), j], n) + L_inc_avg_s = np.zeros(p.S_ret) + PPB = np.zeros(p.S) + PPB = PPB_1dim_loop( + w_S, + p.emat[:, j], + n_S, + p.S_ret, + p.S, + p.g_y, + self.vpoint, + factor, + L_inc_avg_s, + PPB, + ) PPB = PPB[-per_rmn:] else: if np.ndim(n) == 1: - L_inc_avg_s = np.zeros(households.S_ret) - PPB = np.zeros(households.S) - PPB = PPB_1dim_loop(w, e, n, households.S_ret, households.S, - firms.g_y, self.vpoint, - factor, L_inc_avg_s, PPB) + L_inc_avg_s = np.zeros(p.S_ret) + PPB = np.zeros(p.S) + PPB = PPB_1dim_loop( + w, + e, + n, + p.S_ret, + p.S, + p.g_y, + self.vpoint, + factor, + L_inc_avg_s, + PPB, + ) elif np.ndim(n) == 2: - L_inc_avg_sj = np.zeros((households.S_ret, households.J)) - PPB = np.zeros((households.S, households.J)) - PPB = PPB_2dim_loop(w, e, n, households.S_ret, households.S, - households.J, firms.g_y, self.vpoint, factor, - L_inc_avg_sj, PPB) + L_inc_avg_sj = np.zeros((p.S_ret, p.J)) + PPB = np.zeros((p.S, p.J)) + PPB = PPB_2dim_loop( + w, + e, + n, + p.S_ret, + p.S, + p.J, + p.g_y, + self.vpoint, + factor, + L_inc_avg_sj, + PPB, + ) return PPB -def deriv_theta(self, demographics, households, firms, r, w, e, - Y, per_rmn, factor): - ''' +def deriv_theta(r, w, e, Y, per_rmn, factor): + """ Change in pension benefits for another unit of labor supply for pension system selected - ''' - if self.pension_system == "DB": - d_theta = self.deriv_DB(households, firms, w, e, per_rmn) + """ + # TODO: Add SS here... + if p.pension_system == "Defined Benefits": + d_theta = deriv_DB(w, e, per_rmn, p) d_theta = d_theta[-per_rmn:] - elif self.pension_system == "NDC": - d_theta = self.deriv_NDC(demographics, households, firms, - r, w, e, Y, per_rmn) - elif self.pension_system == "PS": - d_theta = self.deriv_PPB(demographics, households, firms, - w, e, per_rmn, factor) + elif p.pension_system == "Notional Defined Contribution": + d_theta = deriv_NDC(r, w, e, Y, per_rmn, p) + elif p.pension_system == "Points System": + d_theta = deriv_PS(w, e, per_rmn, factor, p) return d_theta -def deriv_NDC(self, demographics, households, firms, r, w, e, Y, - per_rmn): - ''' +def deriv_NDC(r, w, e, Y, per_rmn, p): + """ Change in NDC pension benefits for another unit of labor supply - ''' + """ if per_rmn == 1: d_theta = 0 - elif per_rmn < (households.S - households.S_ret + 1): + elif per_rmn < (p.S - p.S_ret + 1): d_theta = np.zeros(per_rmn) else: d_theta_empty = np.zeros(per_rmn) - self.get_delta_ret(r, Y, demographics, households, firms) - self.get_g_ndc(r, Y, demographics.g_n_SS, firms.g_y) + delta_ret = get_delta_ret(r, Y) + g_ndc = get_g_ndc(r, Y, p.g_n_SS, p.g_y) d_theta = deriv_NDC_loop( - w, e, per_rmn, households.S, - households.S_ret, firms.g_y, self.tau_p, - self.g_ndc, self.delta_ret, d_theta_empty) + w, + e, + per_rmn, + p.S, + p.S_ret, + p.g_y, + self.tau_p, + self.g_ndc, + self.delta_ret, + d_theta_empty, + ) return d_theta -def deriv_DB(self, households, firms, w, e, per_rmn): - ''' +def deriv_DB(w, e, per_rmn, p): + """ Change in DB pension benefits for another unit of labor supply - ''' + """ - if per_rmn < (households.S - households.S_ret + 1): - d_theta = np.zeros(households.S) + if per_rmn < (p.S - p.S_ret + 1): + d_theta = np.zeros(p.S) else: - d_theta_empty = np.zeros(households.S) + d_theta_empty = np.zeros(p.S) d_theta = deriv_DB_loop( - w, e, households.S,households.S_ret, per_rmn, firms.g_y, - d_theta_empty, self.last_career_yrs, self.rep_rate_py, - self.yr_contr) + w, + e, + p.S, + p.S_ret, + per_rmn, + p.g_y, + d_theta_empty, + p.last_career_yrs, + p.rep_rate_py, + p.yr_contr, + ) return d_theta -def deriv_PS(self, demographics, households, firms, w, e, - per_rmn, factor): - ''' +def deriv_PS(w, e, per_rmn, factor, p): + """ Change in points system pension benefits for another unit of labor supply - ''' + """ - if per_rmn < (households.S - households.S_ret + 1): - d_theta = np.zeros(households.S) + if per_rmn < (p.S - p.S_ret + 1): + d_theta = np.zeros(p.S) else: - d_theta_empty = np.zeros(households.S) - d_theta = deriv_PS_loop(w, e, households.S, households.S_ret, per_rmn, - firms.g_y, d_theta_empty, - self.vpoint, factor) + d_theta_empty = np.zeros(p.S) + d_theta = deriv_PS_loop( + w, e, p.S, p.S_ret, per_rmn, p.g_y, d_theta_empty, p.vpoint, factor + ) d_theta = d_theta[-per_rmn:] return d_theta -def delta_point(self, r, Y, g_n, g_y): - ''' +# TODO: can probably assign these growth rates in the if statements in +# the pension_amount function +# TODO: create a parameter for pension growth rates -- a single param should do +def delta_point(r, Y, g_n, g_y, p): + """ Compute growth rate used for contributions to points system pension - ''' + """ # TODO: Add option to allow use to enter growth rate amount # Also to allow rate to vary by year # Do this for all these growth rates for each system # Might also allow for option to grow at per capital GDP growth rate - if self.points_growth_rate == 'r': - self.delta_point = r - elif self.points_growth_rate == 'Curr GDP': - self.delta_point = (Y[1:] - Y[:-1]) / Y[:-1] - elif self.points_growth_rate == 'LR GDP': - self.delta_point = g_y + g_n + if p.points_growth_rate == "r": + delta_point = r + elif p.points_growth_rate == "Curr GDP": + delta_point = (Y[1:] - Y[:-1]) / Y[:-1] + elif p.points_growth_rate == "LR GDP": + delta_point = g_y + g_n else: - self.delta_point = g_y + g_n + delta_point = g_y + g_n + + return delta_point -def g_ndc(self, r, Y, g_n, g_y,): - ''' +def g_ndc(r, Y, g_n, g_y, p): + """ Compute growth rate used for contributions to NDC pension - ''' - if self.ndc_growth_rate == 'r': - self.g_ndc = r - elif self.ndc_growth_rate == 'Curr GDP': - self.g_ndc = (Y[1:] - Y[:-1]) / Y[:-1] - elif self.ndc_growth_rate == 'LR GDP': - self.g_ndc = g_y + g_n + """ + if p.ndc_growth_rate == "r": + g_ndc = r + elif p.ndc_growth_rate == "Curr GDP": + g_ndc = (Y[1:] - Y[:-1]) / Y[:-1] + elif p.ndc_growth_rate == "LR GDP": + g_ndc = g_y + g_n else: - self.g_ndc = g_y + g_n + g_ndc = g_y + g_n + + return g_ndc -def g_dir(self, r, Y, g_n, g_y): - ''' +def g_dir(r, Y, g_n, g_y, p): + """ Compute growth rate used for contributions to NDC pension - ''' - if self.dir_growth_rate == 'r': - self.g_dir = r - elif self.dir_growth_rate == 'Curr GDP': - self.g_dir = (Y[1:] - Y[:-1]) / Y[:-1] - elif self.dir_growth_rate == 'LR GDP': - self.g_dir = g_y + g_n -# self.g_dir = 0.015 + """ + if p.dir_growth_rate == "r": + g_dir = r + elif self.dir_growth_rate == "Curr GDP": + g_dir = (Y[1:] - Y[:-1]) / Y[:-1] + elif self.dir_growth_rate == "LR GDP": + g_dir = g_y + g_n + # self.g_dir = 0.015 else: - self.g_dir = g_y + g_n + g_dir = g_y + g_n + + return g_dir -def delta_ret(self, r, Y, demographics, households, firms): - ''' +def delta_ret(self, r, Y, p): + """ Compute conversion coefficient for the NDC pension amount - ''' - surv_rates = 1 - demographics.mort_rates_SS - dir_delta_s_empty = np.zeros(households.S - households.S_ret + 1) - self.get_g_dir(r, Y, demographics.g_n_SS, firms.g_y) + """ + surv_rates = 1 - p.mort_rates_SS + dir_delta_s_empty = np.zeros(p.S - p.S_ret + 1) + g_dir_value = g_dir(r, Y, p.g_n_SS, p.g_y) dir_delta = delta_ret_loop( - households.S, households.S_ret, surv_rates, self.g_dir, - dir_delta_s_empty) - #####TODO: formula below needs to be changed if we had separate calculations for both sexes - self.delta_ret = 1 / (dir_delta - self.k_ret) -# print("self.delta_ret", self.delta_ret) - - -def pension_benefit(self, demographics, households, firms, w, e, n, - r, Y, lambdas, j_ind, factor): - if self.pension_system == "DB": - theta = self.get_DB(households, firms, w, e, n, j_ind) - elif self.pension_system == "NDC": - theta = self.get_NDC( - demographics, households, firms, w, e, n, r, Y, j_ind) - elif self.pension_system == "PS": - theta = self.get_PPB(demographics, households, firms, - w, e, n, r, Y, lambdas, j_ind, factor) + p.S, p.S_ret, surv_rates, g_dir_value, dir_delta_s_empty + ) + delta_ret = 1 / (dir_delta - self.k_ret) - return theta + return delta_ret @numba.jit -def deriv_DB_loop(w, e, S, S_ret, per_rmn, g_y, d_theta, - last_career_yrs, rep_rate_py, yr_contr): +def deriv_DB_loop( + w, e, S, S_ret, per_rmn, d_theta, last_career_yrs, rep_rate_py, yr_contr +): d_theta = np.zeros(per_rmn) num_per_retire = S - S_ret for s in range(per_rmn): @@ -501,23 +565,27 @@ def deriv_DB_loop(w, e, S, S_ret, per_rmn, g_y, d_theta, @numba.jit -def deriv_PS_loop(w, e, S, S_ret, per_rmn, g_y, d_theta, vpoint, factor): - +def deriv_PS_loop(w, e, S, S_ret, per_rmn, d_theta, vpoint, factor): + # TODO: do we need these constants or can we scale vpoint to annual?? for s in range((S - per_rmn), S_ret): - d_theta[s] = ((w[s] * e[s] * vpoint * constants.MONTHS_IN_A_YEAR) / - (factor * constants.THOUSAND)) + d_theta[s] = (w[s] * e[s] * vpoint * constants.MONTHS_IN_A_YEAR) / ( + factor * constants.THOUSAND + ) return d_theta @numba.jit -def deriv_NDC_loop(w, e, per_rmn, S, S_ret, g_y, tau_p, g_ndc, - delta_ret, d_theta): +def deriv_NDC_loop(w, e, per_rmn, S, S_ret, tau_p, g_ndc, delta_ret, d_theta): for s in range((S - per_rmn), S_ret): - d_theta[s - (S - per_rmn)] = (tau_p * w[s - (S - per_rmn)] * - e[s - (S - per_rmn)] * delta_ret * - (1 + g_ndc) ** (S_ret - s - 1)) + d_theta[s - (S - per_rmn)] = ( + tau_p + * w[s - (S - per_rmn)] + * e[s - (S - per_rmn)] + * delta_ret + * (1 + g_ndc) ** (S_ret - s - 1) + ) return d_theta @@ -527,96 +595,130 @@ def delta_ret_loop(S, S_ret, surv_rates, g_dir, dir_delta_s): cumul_surv_rates = np.ones(S - S_ret + 1) for s in range(S - S_ret + 1): - surv_rates_vec = surv_rates[S_ret: S_ret + s + 1] + surv_rates_vec = surv_rates[S_ret : S_ret + s + 1] surv_rates_vec[0] = 1.0 cumul_surv_rates[s] = np.prod(surv_rates_vec) cumul_g_y = np.ones(S - S_ret + 1) - cumul_g_y[s] = (1/(1 + g_dir)) ** s + cumul_g_y[s] = (1 / (1 + g_dir)) ** s dir_delta_s[s] = cumul_surv_rates[s] * cumul_g_y[s] dir_delta = dir_delta_s.sum() return dir_delta -@numba.jit -def PPB_1dim_loop(w, e, n, S_ret, S, g_y, vpoint, factor, L_inc_avg_s, - PPB): +@numba.jit +def PS_1dim_loop(w, e, n, S_ret, S, g_y, vpoint, factor, L_inc_avg_s, PPB): + # TODO: do we need these constants or can we scale vpoint to annual?? for u in range(S_ret, S): for s in range(S_ret): - L_inc_avg_s[s] = \ - w[s] / np.exp(g_y * (u - s)) * e[s] * n[s] - PPB[u] = ((constants.MONTHS_IN_A_YEAR * vpoint * L_inc_avg_s.sum()) - / (factor * constants.THOUSAND)) + L_inc_avg_s[s] = w[s] / np.exp(g_y * (u - s)) * e[s] * n[s] + PPB[u] = (constants.MONTHS_IN_A_YEAR * vpoint * L_inc_avg_s.sum()) / ( + factor * constants.THOUSAND + ) return PPB -@numba.jit -def PPB_2dim_loop(w, e, n, S_ret, S, J, g_y, vpoint, factor, - L_inc_avg_sj, PPB): +@numba.jit +def PS_2dim_loop(w, e, n, S_ret, S, J, g_y, vpoint, factor, L_inc_avg_sj, PPB): + # TODO: do we need these constants or can we scale vpoint to annual?? for u in range(S_ret, S): for s in range(S_ret): - L_inc_avg_sj[s, :] = \ - w[s] / np.exp(g_y * (u - s)) * e[s, :] * n[s, :] - PPB[u, :] = ((constants.MONTHS_IN_A_YEAR * vpoint * L_inc_avg_sj.sum(axis=0)) - / (factor * constants.THOUSAND)) + L_inc_avg_sj[s, :] = ( + w[s] / np.exp(g_y * (u - s)) * e[s, :] * n[s, :] + ) + PPB[u, :] = ( + constants.MONTHS_IN_A_YEAR * vpoint * L_inc_avg_sj.sum(axis=0) + ) / (factor * constants.THOUSAND) return PPB + @numba.jit -def DB_1dim_loop(w, e, n, S_ret, S, g_y, L_inc_avg_s, - L_inc_avg, DB_s, DB, last_career_yrs, rep_rate, - rep_rate_py, yr_contr): +def DB_1dim_loop( + w, + e, + n, + S_ret, + S, + g_y, + L_inc_avg_s, + L_inc_avg, + DB, + last_career_yrs, + rep_rate, + rep_rate_py, + yr_contr, +): for u in range(S_ret, S): - for s in range(S_ret - last_career_yrs, - S_ret): - L_inc_avg_s[s - (S_ret - last_career_yrs)] = \ - w[s] / np.exp(g_y * (u - s)) * e[s] * n[s] + for s in range(S_ret - last_career_yrs, S_ret): + L_inc_avg_s[s - (S_ret - last_career_yrs)] = ( + w[s] / np.exp(g_y * (u - s)) * e[s] * n[s] + ) L_inc_avg = L_inc_avg_s.sum() / last_career_yrs rep_rate = yr_contr * rep_rate_py DB[u] = rep_rate * L_inc_avg return DB + @numba.jit -def DB_2dim_loop(w, e, n, S_ret, S, g_y, L_inc_avg_sj, - L_inc_avg, DB_sj, DB, last_career_yrs, rep_rate, - rep_rate_py, yr_contr): +def DB_2dim_loop( + w, + e, + n, + S_ret, + S, + g_y, + L_inc_avg_sj, + L_inc_avg, + DB, + last_career_yrs, + rep_rate, + rep_rate_py, + yr_contr, +): for u in range(S_ret, S): - for s in range(S_ret - last_career_yrs, - S_ret): - L_inc_avg_sj[s - (S_ret - last_career_yrs), :] = \ - w[s] / np.exp(g_y * (u - s)) * e[s, :] * n[s, :] + for s in range(S_ret - last_career_yrs, S_ret): + L_inc_avg_sj[s - (S_ret - last_career_yrs), :] = ( + w[s] / np.exp(g_y * (u - s)) * e[s, :] * n[s, :] + ) L_inc_avg = L_inc_avg_sj.sum(axis=0) / last_career_yrs rep_rate = yr_contr * rep_rate_py DB[u, :] = rep_rate * L_inc_avg return DB + @numba.jit -def NDC_1dim_loop(w, e, n, S_ret, S, g_y, tau_p, g_ndc, delta_ret, - NDC_s, NDC): +def NDC_1dim_loop(w, e, n, S_ret, S, g_y, tau_p, g_ndc, delta_ret, NDC_s, NDC): for u in range(S_ret, S): for s in range(0, S_ret): NDC_s[s] = ( - tau_p * - (w[s] / np.exp(g_y * (u - s))) * e[s] * - n[s] * ((1 + g_ndc) ** - (S_ret - s - 1))) + tau_p + * (w[s] / np.exp(g_y * (u - s))) + * e[s] + * n[s] + * ((1 + g_ndc) ** (S_ret - s - 1)) + ) NDC[u] = delta_ret * NDC_s.sum() return NDC + @numba.jit -def NDC_2dim_loop(w, e, n, S_ret, S, g_y, tau_p, g_ndc, delta_ret, - NDC_sj, NDC): +def NDC_2dim_loop( + w, e, n, S_ret, S, g_y, tau_p, g_ndc, delta_ret, NDC_sj, NDC +): for u in range(S_ret, S): for s in range(0, S_ret): NDC_sj[s, :] = ( - tau_p * - (w[s] / np.exp(g_y * (u - s))) * - e[s, :] * n[s, :] * ((1 + g_ndc) ** - (S_ret - s - 1))) + tau_p + * (w[s] / np.exp(g_y * (u - s))) + * e[s, :] + * n[s, :] + * ((1 + g_ndc) ** (S_ret - s - 1)) + ) NDC[u, :] = delta_ret * NDC_sj.sum(axis=0) - return NDC \ No newline at end of file + return NDC From 0cf250b23d9bf4523c0a30126cf77bce0a576c40 Mon Sep 17 00:00:00 2001 From: jdebacker Date: Sun, 30 Jun 2024 21:58:48 -0400 Subject: [PATCH 09/50] fix typo --- ogcore/default_parameters.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ogcore/default_parameters.json b/ogcore/default_parameters.json index 0b130901c..233556fdf 100644 --- a/ogcore/default_parameters.json +++ b/ogcore/default_parameters.json @@ -2906,7 +2906,7 @@ "type": "str", "value": [ { - "value": "US-Style Social Securit" + "value": "US-Style Social Security" } ], "validators": { From 40496e5690223d98a2c9b5800ca32674adf182f6 Mon Sep 17 00:00:00 2001 From: jdebacker Date: Mon, 1 Jul 2024 23:51:48 -0400 Subject: [PATCH 10/50] add test --- tests/test_pensions.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/tests/test_pensions.py b/tests/test_pensions.py index b982cd7d5..7cf6f7305 100644 --- a/tests/test_pensions.py +++ b/tests/test_pensions.py @@ -70,3 +70,41 @@ def test_replacement_rate_vals(n, w, factor, j, p_in, expected): ) theta = pensions.replacement_rate_vals(n, w, factor, j, p) assert np.allclose(theta, expected) + + +p = Specifications() +p.update_specifications({ + "S": 7, + "retirement_age": 4, + "last_career_yrs": 3, + "yr_contr": 4, + "rep_rate_py": 0.2 +}) +j = 1 +w = np.array([1.2, 1.1, 1.21, 1.0, 1.01, 0.99, 0.8]) +e = np.array([1.1, 1.11, 0.9, 0.87, 0.87, 0.7, 0.6]) +n = np.array([0.4, 0.45, 0.4, 0.42, 0.3, 0.2, 0.2]) +g_y = 0.03 +L_inc_avg = np.zeros(0) +L_inc_avg_s = np.zeros(p.last_career_yrs) +DB_s = np.zeros(p.retirement_age) +DB = np.zeros(p.S) +DB_loop_expected1 = np.array([0, 0, 0, 0, 0.337864778, 0.327879365, 0.318189065]) +args1 = w, e, n, p.retirement_age, p.S, p.g_y, L_inc_avg_s, L_inc_avg, DB_s, DB + +test_data = [(args1, DB_loop_expected1)] +# (classes2, args2, NDC_expected2)] + +@pytest.mark.parametrize('args,DB_loop_expected', test_data, + ids=['SS/Complete']) +def test_DB_1dim_loop(args, DB_loop_expected): + """ + Test of the pensions.DB_1dim_loop() function. + """ + + w, e, n, S_ret, S, g_y, L_inc_avg_s, L_inc_avg, DB_s, DB = args + DB_loop = pensions.DB_1dim_loop( + w, e, n, S_ret, S, g_y, L_inc_avg_s, L_inc_avg, DB_s, DB, + p.last_career_yrs, p.rep_rate, + p.rep_rate_py, p.yr_contr) + assert (np.allclose(DB_loop, DB_loop_expected)) From 9bdd3c4e299eb03f80063b8598c2c6e2d731460d Mon Sep 17 00:00:00 2001 From: jdebacker Date: Wed, 3 Jul 2024 16:24:46 -0400 Subject: [PATCH 11/50] test passing --- tests/test_pensions.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/tests/test_pensions.py b/tests/test_pensions.py index 7cf6f7305..fb51cff84 100644 --- a/tests/test_pensions.py +++ b/tests/test_pensions.py @@ -73,18 +73,20 @@ def test_replacement_rate_vals(n, w, factor, j, p_in, expected): p = Specifications() -p.update_specifications({ - "S": 7, - "retirement_age": 4, - "last_career_yrs": 3, - "yr_contr": 4, - "rep_rate_py": 0.2 -}) +# p.update_specifications({ +# "S": 7, +# "rep_rate_py": 0.2 +# }) +p.S = 7 +p.rep_rate_py = 0.2 +p.retirement_age = 4 +p.last_career_yrs = 3 +p.yr_contr = 4 +p.g_y = 0.03 j = 1 w = np.array([1.2, 1.1, 1.21, 1.0, 1.01, 0.99, 0.8]) e = np.array([1.1, 1.11, 0.9, 0.87, 0.87, 0.7, 0.6]) n = np.array([0.4, 0.45, 0.4, 0.42, 0.3, 0.2, 0.2]) -g_y = 0.03 L_inc_avg = np.zeros(0) L_inc_avg_s = np.zeros(p.last_career_yrs) DB_s = np.zeros(p.retirement_age) @@ -104,7 +106,7 @@ def test_DB_1dim_loop(args, DB_loop_expected): w, e, n, S_ret, S, g_y, L_inc_avg_s, L_inc_avg, DB_s, DB = args DB_loop = pensions.DB_1dim_loop( - w, e, n, S_ret, S, g_y, L_inc_avg_s, L_inc_avg, DB_s, DB, - p.last_career_yrs, p.rep_rate, + w, e, n, S_ret, S, g_y, L_inc_avg_s, L_inc_avg, DB, + p.last_career_yrs, p.rep_rate_py, p.yr_contr) assert (np.allclose(DB_loop, DB_loop_expected)) From a5f3b5bff840197b203830c7abb5ddef381c50d2 Mon Sep 17 00:00:00 2001 From: jdebacker Date: Wed, 3 Jul 2024 16:24:57 -0400 Subject: [PATCH 12/50] fix param references --- ogcore/pensions.py | 100 ++++++++++++++++++++++----------------------- 1 file changed, 49 insertions(+), 51 deletions(-) diff --git a/ogcore/pensions.py b/ogcore/pensions.py index db249e723..36ee866b9 100644 --- a/ogcore/pensions.py +++ b/ogcore/pensions.py @@ -3,6 +3,10 @@ import numba from ogcore import utils +# set constants +MONTHS_IN_A_YEAR = 12 +THOUSAND = 1000 + def replacement_rate_vals(nssmat, wss, factor_ss, j, p): """ @@ -208,7 +212,6 @@ def DB_amount(w, e, n, j, p): DB_s, DB, p.last_career_yrs, - p.rep_rate, p.rep_rate_py, p.yr_contr, ) @@ -230,7 +233,6 @@ def DB_amount(w, e, n, j, p): DB_s, DB, p.last_career_yrs, - p.rep_rate, p.rep_rate_py, p.yr_contr, ) @@ -251,7 +253,6 @@ def DB_amount(w, e, n, j, p): DB_sj, DB, p.last_career_yrs, - p.rep_rate, p.rep_rate_py, p.yr_contr, ) @@ -264,13 +265,13 @@ def NDC_amount(w, e, n, r, Y, j, p): Calculate public pension from a notional defined contribution system. """ - self.get_g_ndc( + g_ndc_amount = g_ndc( r, Y, p.g_n_SS, p.g_y, ) - self.get_delta_ret(r, Y, p) + delta_ret_amount = delta_ret(r, Y, p) if n.shape[0] < p.S: per_rmn = n.shape[0] @@ -287,9 +288,9 @@ def NDC_amount(w, e, n, r, Y, j, p): p.S_ret, p.S, p.g_y, - self.tau_p, - self.g_ndc, - self.delta_ret, + p.tau_p, + g_ndc_amount, + delta_ret_amount, NDC_s, NDC, ) @@ -306,9 +307,9 @@ def NDC_amount(w, e, n, r, Y, j, p): p.S_ret, p.S, p.g_y, - self.tau_p, - self.g_ndc, - self.delta_ret, + p.tau_p, + g_ndc_amount, + delta_ret_amount, NDC_s, NDC, ) @@ -322,9 +323,9 @@ def NDC_amount(w, e, n, r, Y, j, p): p.S_ret, p.S, p.g_y, - self.tau_p, - self.g_ndc, - self.delta_ret, + p.tau_p, + g_ndc_amount, + delta_ret_amount, NDC_sj, NDC, ) @@ -342,42 +343,42 @@ def PS_amount(w, e, n, j, factor, p): w_S = np.append((p.w_preTP * np.ones(p.S))[:(-per_rmn)], w) n_S = np.append(p.n_preTP[:(-per_rmn), j], n) L_inc_avg_s = np.zeros(p.S_ret) - PPB = np.zeros(p.S) - PPB = PPB_1dim_loop( + PS = np.zeros(p.S) + PS = PS_1dim_loop( w_S, p.emat[:, j], n_S, p.S_ret, p.S, p.g_y, - self.vpoint, + p.vpoint, factor, L_inc_avg_s, - PPB, + PS, ) - PPB = PPB[-per_rmn:] + PS = PS[-per_rmn:] else: if np.ndim(n) == 1: L_inc_avg_s = np.zeros(p.S_ret) - PPB = np.zeros(p.S) - PPB = PPB_1dim_loop( + PS = np.zeros(p.S) + PS = PS_1dim_loop( w, e, n, p.S_ret, p.S, p.g_y, - self.vpoint, + p.vpoint, factor, L_inc_avg_s, - PPB, + PS, ) elif np.ndim(n) == 2: L_inc_avg_sj = np.zeros((p.S_ret, p.J)) - PPB = np.zeros((p.S, p.J)) - PPB = PPB_2dim_loop( + PS = np.zeros((p.S, p.J)) + PS = PS_2dim_loop( w, e, n, @@ -385,16 +386,16 @@ def PS_amount(w, e, n, j, factor, p): p.S, p.J, p.g_y, - self.vpoint, + p.vpoint, factor, L_inc_avg_sj, - PPB, + PS, ) - return PPB + return PS -def deriv_theta(r, w, e, Y, per_rmn, factor): +def deriv_theta(r, w, e, Y, per_rmn, factor, p): """ Change in pension benefits for another unit of labor supply for pension system selected @@ -421,8 +422,8 @@ def deriv_NDC(r, w, e, Y, per_rmn, p): d_theta = np.zeros(per_rmn) else: d_theta_empty = np.zeros(per_rmn) - delta_ret = get_delta_ret(r, Y) - g_ndc = get_g_ndc(r, Y, p.g_n_SS, p.g_y) + delta_ret_amount = delta_ret(r, Y) + g_ndc_amount = g_ndc(r, Y, p.g_n_SS, p.g_y) d_theta = deriv_NDC_loop( w, e, @@ -430,9 +431,9 @@ def deriv_NDC(r, w, e, Y, per_rmn, p): p.S, p.S_ret, p.g_y, - self.tau_p, - self.g_ndc, - self.delta_ret, + p.tau_p, + g_ndc_amount, + delta_ret_amount, d_theta_empty, ) @@ -525,11 +526,10 @@ def g_dir(r, Y, g_n, g_y, p): """ if p.dir_growth_rate == "r": g_dir = r - elif self.dir_growth_rate == "Curr GDP": + elif p.dir_growth_rate == "Curr GDP": g_dir = (Y[1:] - Y[:-1]) / Y[:-1] - elif self.dir_growth_rate == "LR GDP": + elif p.dir_growth_rate == "LR GDP": g_dir = g_y + g_n - # self.g_dir = 0.015 else: g_dir = g_y + g_n @@ -546,7 +546,7 @@ def delta_ret(self, r, Y, p): dir_delta = delta_ret_loop( p.S, p.S_ret, surv_rates, g_dir_value, dir_delta_s_empty ) - delta_ret = 1 / (dir_delta - self.k_ret) + delta_ret = 1 / (dir_delta - p.k_ret) return delta_ret @@ -568,8 +568,8 @@ def deriv_DB_loop( def deriv_PS_loop(w, e, S, S_ret, per_rmn, d_theta, vpoint, factor): # TODO: do we need these constants or can we scale vpoint to annual?? for s in range((S - per_rmn), S_ret): - d_theta[s] = (w[s] * e[s] * vpoint * constants.MONTHS_IN_A_YEAR) / ( - factor * constants.THOUSAND + d_theta[s] = (w[s] * e[s] * vpoint * MONTHS_IN_A_YEAR) / ( + factor * THOUSAND ) return d_theta @@ -606,31 +606,31 @@ def delta_ret_loop(S, S_ret, surv_rates, g_dir, dir_delta_s): @numba.jit -def PS_1dim_loop(w, e, n, S_ret, S, g_y, vpoint, factor, L_inc_avg_s, PPB): +def PS_1dim_loop(w, e, n, S_ret, S, g_y, vpoint, factor, L_inc_avg_s, PS): # TODO: do we need these constants or can we scale vpoint to annual?? for u in range(S_ret, S): for s in range(S_ret): L_inc_avg_s[s] = w[s] / np.exp(g_y * (u - s)) * e[s] * n[s] - PPB[u] = (constants.MONTHS_IN_A_YEAR * vpoint * L_inc_avg_s.sum()) / ( - factor * constants.THOUSAND + PS[u] = (MONTHS_IN_A_YEAR * vpoint * L_inc_avg_s.sum()) / ( + factor * THOUSAND ) - return PPB + return PS @numba.jit -def PS_2dim_loop(w, e, n, S_ret, S, J, g_y, vpoint, factor, L_inc_avg_sj, PPB): +def PS_2dim_loop(w, e, n, S_ret, S, J, g_y, vpoint, factor, L_inc_avg_sj, PS): # TODO: do we need these constants or can we scale vpoint to annual?? for u in range(S_ret, S): for s in range(S_ret): L_inc_avg_sj[s, :] = ( w[s] / np.exp(g_y * (u - s)) * e[s, :] * n[s, :] ) - PPB[u, :] = ( - constants.MONTHS_IN_A_YEAR * vpoint * L_inc_avg_sj.sum(axis=0) - ) / (factor * constants.THOUSAND) + PS[u, :] = ( + MONTHS_IN_A_YEAR * vpoint * L_inc_avg_sj.sum(axis=0) + ) / (factor * THOUSAND) - return PPB + return PS @numba.jit @@ -645,7 +645,6 @@ def DB_1dim_loop( L_inc_avg, DB, last_career_yrs, - rep_rate, rep_rate_py, yr_contr, ): @@ -674,7 +673,6 @@ def DB_2dim_loop( L_inc_avg, DB, last_career_yrs, - rep_rate, rep_rate_py, yr_contr, ): From 4615306c24e90399bdc3669214f7133726f22398 Mon Sep 17 00:00:00 2001 From: jdebacker Date: Wed, 3 Jul 2024 16:42:30 -0400 Subject: [PATCH 13/50] more tests --- ogcore/pensions.py | 18 +++--- tests/test_pensions.py | 136 +++++++++++++++++++++++++++++++++++++---- 2 files changed, 133 insertions(+), 21 deletions(-) diff --git a/ogcore/pensions.py b/ogcore/pensions.py index 36ee866b9..ce1f80815 100644 --- a/ogcore/pensions.py +++ b/ogcore/pensions.py @@ -197,14 +197,14 @@ def DB_amount(w, e, n, j, p): w_S = np.append((p.w_preTP * np.ones(p.S))[:(-per_rmn)], w) n_S = np.append(p.n_preTP[:(-per_rmn), j], n) - DB_s = np.zeros(p.retirement_age) + DB_s = np.zeros(p.retire) DB = np.zeros(p.S) # TODO: we set a rep_rate_py in params, but not rep_rate. What is it??? DB = DB_1dim_loop( w_S, p.e[:, j], n_S, - p.retirement_age, + p.retire, p.S, p.g_y, L_inc_avg_s, @@ -219,7 +219,7 @@ def DB_amount(w, e, n, j, p): else: if np.ndim(n) == 1: - DB_s = np.zeros(p.retirement_age) + DB_s = np.zeros(p.retire) DB = np.zeros(p.S) DB = DB_1dim_loop( w, @@ -238,14 +238,14 @@ def DB_amount(w, e, n, j, p): ) elif np.ndim(n) == 2: - DB_sj = np.zeros((p.retirement_age, p.J)) + DB_sj = np.zeros((p.retire, p.J)) DB = np.zeros((p.S, p.J)) L_inc_avg_sj = np.zeros((p.last_career_yrs, p.J)) DB = DB_2dim_loop( w, e, n, - p.retirement_age, + p.retire, p.S, p.g_y, L_inc_avg_sj, @@ -553,7 +553,7 @@ def delta_ret(self, r, Y, p): @numba.jit def deriv_DB_loop( - w, e, S, S_ret, per_rmn, d_theta, last_career_yrs, rep_rate_py, yr_contr + w, e, S, S_ret, per_rmn, last_career_yrs, rep_rate_py, yr_contr ): d_theta = np.zeros(per_rmn) num_per_retire = S - S_ret @@ -626,9 +626,9 @@ def PS_2dim_loop(w, e, n, S_ret, S, J, g_y, vpoint, factor, L_inc_avg_sj, PS): L_inc_avg_sj[s, :] = ( w[s] / np.exp(g_y * (u - s)) * e[s, :] * n[s, :] ) - PS[u, :] = ( - MONTHS_IN_A_YEAR * vpoint * L_inc_avg_sj.sum(axis=0) - ) / (factor * THOUSAND) + PS[u, :] = (MONTHS_IN_A_YEAR * vpoint * L_inc_avg_sj.sum(axis=0)) / ( + factor * THOUSAND + ) return PS diff --git a/tests/test_pensions.py b/tests/test_pensions.py index fb51cff84..0a88d1018 100644 --- a/tests/test_pensions.py +++ b/tests/test_pensions.py @@ -79,7 +79,7 @@ def test_replacement_rate_vals(n, w, factor, j, p_in, expected): # }) p.S = 7 p.rep_rate_py = 0.2 -p.retirement_age = 4 +p.retire = 4 p.last_career_yrs = 3 p.yr_contr = 4 p.g_y = 0.03 @@ -89,24 +89,136 @@ def test_replacement_rate_vals(n, w, factor, j, p_in, expected): n = np.array([0.4, 0.45, 0.4, 0.42, 0.3, 0.2, 0.2]) L_inc_avg = np.zeros(0) L_inc_avg_s = np.zeros(p.last_career_yrs) -DB_s = np.zeros(p.retirement_age) DB = np.zeros(p.S) -DB_loop_expected1 = np.array([0, 0, 0, 0, 0.337864778, 0.327879365, 0.318189065]) -args1 = w, e, n, p.retirement_age, p.S, p.g_y, L_inc_avg_s, L_inc_avg, DB_s, DB +DB_loop_expected1 = np.array( + [0, 0, 0, 0, 0.337864778, 0.327879365, 0.318189065] +) +args1 = ( + w, + e, + n, + p.retire, + p.S, + p.g_y, + L_inc_avg_s, + L_inc_avg, + DB, + p.last_career_yrs, + p.rep_rate_py, + p.yr_contr, +) test_data = [(args1, DB_loop_expected1)] -# (classes2, args2, NDC_expected2)] -@pytest.mark.parametrize('args,DB_loop_expected', test_data, - ids=['SS/Complete']) + +@pytest.mark.parametrize( + "args,DB_loop_expected", test_data, ids=["SS/Complete"] +) def test_DB_1dim_loop(args, DB_loop_expected): """ Test of the pensions.DB_1dim_loop() function. """ - w, e, n, S_ret, S, g_y, L_inc_avg_s, L_inc_avg, DB_s, DB = args + ( + w, + e, + n, + S_ret, + S, + g_y, + L_inc_avg_s, + L_inc_avg, + DB, + last_career_yrs, + rep_rate_py, + yr_contr, + ) = args DB_loop = pensions.DB_1dim_loop( - w, e, n, S_ret, S, g_y, L_inc_avg_s, L_inc_avg, DB, - p.last_career_yrs, - p.rep_rate_py, p.yr_contr) - assert (np.allclose(DB_loop, DB_loop_expected)) + w, + e, + n, + S_ret, + S, + g_y, + L_inc_avg_s, + L_inc_avg, + DB, + last_career_yrs, + rep_rate_py, + yr_contr, + ) + assert np.allclose(DB_loop, DB_loop_expected) + + +p = Specifications() +p.S = 7 +p.retire = 4 +per_rmn = p.S +p.last_career_yrs = 3 +p.yr_contr = p.retire +p.rep_rate_py = 0.2 +p.g_y = 0.03 +w = np.array([1.2, 1.1, 1.21, 1, 1.01, 0.99, 0.8]) +e = np.array([1.1, 1.11, 0.9, 0.87, 0.87, 0.7, 0.6]) +deriv_DB_loop_expected = np.array( + [0.352, 0.3256, 0.2904, 0.232, 0.0, 0.0, 0.0] +) +d_theta_empty = np.zeros_like(w) +args3 = ( + w, + e, + p.S, + p.retire, + per_rmn, + p.last_career_yrs, + p.rep_rate_py, + p.yr_contr, +) + +test_data = [(args3, deriv_DB_loop_expected)] + + +@pytest.mark.parametrize("args,deriv_DB_loop_expected", test_data) +def test_deriv_DB_loop(args, deriv_DB_loop_expected): + """ + Test of the pensions.deriv_DB_loop() function. + """ + (w, e, S, retire, per_rmn, last_career_yrs, rep_rate_py, yr_contr) = args + deriv_DB_loop = pensions.deriv_DB_loop( + w, e, S, retire, per_rmn, last_career_yrs, rep_rate_py, yr_contr + ) + + assert np.allclose(deriv_DB_loop, deriv_DB_loop_expected) + + +p = Specifications() +p.S = 7 +p.retire = 4 +p.vpoint = 0.4 +w = np.array([1.2, 1.1, 1.21, 1, 1.01, 0.99, 0.8]) +e = np.array([1.1, 1.11, 0.9, 0.87, 0.87, 0.7, 0.6]) +p.g_y = 0.03 +factor = 2 +d_theta_empty = np.zeros_like(w) +deriv_PS_loop_expected1 = np.array( + [0.003168, 0.0029304, 0.0026136, 0.002088, 0, 0, 0] +) +args3 = (w, e, p.S, p.retire, per_rmn, d_theta_empty, p.vpoint, factor) + +test_data = [(args3, deriv_PS_loop_expected1)] + + +@pytest.mark.parametrize( + "args,deriv_PS_loop_expected", test_data, ids=["SS/Complete"] +) +def test_deriv_PS_loop(args, deriv_PS_loop_expected): + """ + Test of the pensions.deriv_PS_loop() function. + """ + (w, e, S, retire, per_rmn, d_theta_empty, vpoint, factor) = args + + deriv_PS_loop = pensions.deriv_PS_loop( + w, e, S, retire, per_rmn, d_theta_empty, vpoint, factor + ) + + assert np.allclose(deriv_PS_loop, deriv_PS_loop_expected) From 7916656312a2c5eb029513bc12cfc1fb0dcf44f9 Mon Sep 17 00:00:00 2001 From: jdebacker Date: Wed, 3 Jul 2024 23:36:47 -0400 Subject: [PATCH 14/50] more tests --- ogcore/pensions.py | 7 ++---- tests/test_pensions.py | 51 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/ogcore/pensions.py b/ogcore/pensions.py index ce1f80815..47c65139f 100644 --- a/ogcore/pensions.py +++ b/ogcore/pensions.py @@ -445,18 +445,15 @@ def deriv_DB(w, e, per_rmn, p): Change in DB pension benefits for another unit of labor supply """ - if per_rmn < (p.S - p.S_ret + 1): + if per_rmn < (p.S - p.retire + 1): d_theta = np.zeros(p.S) else: - d_theta_empty = np.zeros(p.S) d_theta = deriv_DB_loop( w, e, p.S, - p.S_ret, + p.retire, per_rmn, - p.g_y, - d_theta_empty, p.last_career_yrs, p.rep_rate_py, p.yr_contr, diff --git a/tests/test_pensions.py b/tests/test_pensions.py index 0a88d1018..fcfe5b5e8 100644 --- a/tests/test_pensions.py +++ b/tests/test_pensions.py @@ -222,3 +222,54 @@ def test_deriv_PS_loop(args, deriv_PS_loop_expected): ) assert np.allclose(deriv_PS_loop, deriv_PS_loop_expected) + + +#############non-zero d_theta: case 1############ +p = Specifications() +p.S = 7 +p.retire = 4 +p.last_career_yrs = 3 +p.yr_contr = p.retire +p.rep_rate_py = 0.2 +p.g_y = 0.03 +n_ddb1 = np.array([0.4, 0.45, 0.4, 0.42, 0.3, 0.2, 0.2]) +w_ddb1 = np.array([1.2, 1.1, 1.21, 1, 1.01, 0.99, 0.8]) +e_ddb1 = np.array([1.1, 1.11, 0.9, 0.87, 0.87, 0.7, 0.6]) +per_rmn = n_ddb1.shape[0] +d_theta_empty = np.zeros_like(per_rmn) +deriv_DB_expected1 = np.array( + [0.352, 0.3256, 0.2904, 0.232, 0.0, 0.0, 0.0]) +args_ddb1 = (w_ddb1, e_ddb1, per_rmn, p) + +#############non-zero d_theta: case 2############ +p2 = Specifications() +p2.S = 7 +p2.retire = 5 +p2.last_career_yrs = 2 +p2.yr_contr = p2.retire +p2.rep_rate_py = 0.2 +p2.g_y = 0.03 +n_ddb2 = np.array([0.45, 0.4, 0.42, 0.3, 0.2, 0.2]) +w_ddb1 = np.array([1.1, 1.21, 1, 1.01, 0.99, 0.8]) +e_ddb1 = np.array([1.11, 0.9, 0.87, 0.87, 0.7, 0.6]) +per_rmn = n_ddb2.shape[0] +d_theta_empty = np.zeros_like(per_rmn) +deriv_DB_expected2 = np.array( + [0.6105, 0.5445, 0.435, 0.43935, 0.0, 0.0]) +args_ddb2 = (w_ddb1, e_ddb1, per_rmn, p2) + +test_data = [(args_ddb1, deriv_DB_expected1), + (args_ddb2, deriv_DB_expected2)] + + +@pytest.mark.parametrize('args,deriv_DB_expected', test_data, + ids=['non-zero d_theta: case 1', + 'non-zero d_theta: case 2']) +def test_deriv_DB(args, deriv_DB_expected): + """ + Test of the pensions.deriv_DB() function. + """ + (w, e, per_rmn, p) = args + deriv_DB = pensions.deriv_DB(w, e, per_rmn, p) + + assert (np.allclose(deriv_DB, deriv_DB_expected)) \ No newline at end of file From 28d4afdc5230af4f21be3c8b982f97c2aedd5c77 Mon Sep 17 00:00:00 2001 From: jdebacker Date: Thu, 4 Jul 2024 11:22:42 -0400 Subject: [PATCH 15/50] more tests --- ogcore/pensions.py | 36 +++++++++++------------ tests/test_pensions.py | 67 +++++++++++++++++++++++++++++++++++------- 2 files changed, 75 insertions(+), 28 deletions(-) diff --git a/ogcore/pensions.py b/ogcore/pensions.py index 47c65139f..2344930fa 100644 --- a/ogcore/pensions.py +++ b/ogcore/pensions.py @@ -279,13 +279,13 @@ def NDC_amount(w, e, n, r, Y, j, p): w_S = np.append((p.w_preTP * np.ones(p.S))[:(-per_rmn)], w) n_S = np.append(p.n_preTP[:(-per_rmn), j], n) - NDC_s = np.zeros(p.S_ret) + NDC_s = np.zeros(p.retire) NDC = np.zeros(p.S) NDC = NDC_1dim_loop( w_S, p.emat[:, j], n_S, - p.S_ret, + p.retire, p.S, p.g_y, p.tau_p, @@ -298,13 +298,13 @@ def NDC_amount(w, e, n, r, Y, j, p): else: if np.ndim(n) == 1: - NDC_s = np.zeros(p.S_ret) + NDC_s = np.zeros(p.retire) NDC = np.zeros(p.S) NDC = NDC_1dim_loop( w, e, n, - p.S_ret, + p.retire, p.S, p.g_y, p.tau_p, @@ -314,13 +314,13 @@ def NDC_amount(w, e, n, r, Y, j, p): NDC, ) elif np.ndim(n) == 2: - NDC_sj = np.zeros((p.S_ret, p.J)) + NDC_sj = np.zeros((p.retire, p.J)) NDC = np.zeros((p.S, p.J)) NDC = NDC_2dim_loop( w, e, n, - p.S_ret, + p.retire, p.S, p.g_y, p.tau_p, @@ -342,13 +342,13 @@ def PS_amount(w, e, n, j, factor, p): per_rmn = n.shape[0] w_S = np.append((p.w_preTP * np.ones(p.S))[:(-per_rmn)], w) n_S = np.append(p.n_preTP[:(-per_rmn), j], n) - L_inc_avg_s = np.zeros(p.S_ret) + L_inc_avg_s = np.zeros(p.retire) PS = np.zeros(p.S) PS = PS_1dim_loop( w_S, p.emat[:, j], n_S, - p.S_ret, + p.retire, p.S, p.g_y, p.vpoint, @@ -360,13 +360,13 @@ def PS_amount(w, e, n, j, factor, p): else: if np.ndim(n) == 1: - L_inc_avg_s = np.zeros(p.S_ret) + L_inc_avg_s = np.zeros(p.retire) PS = np.zeros(p.S) PS = PS_1dim_loop( w, e, n, - p.S_ret, + p.retire, p.S, p.g_y, p.vpoint, @@ -376,13 +376,13 @@ def PS_amount(w, e, n, j, factor, p): ) elif np.ndim(n) == 2: - L_inc_avg_sj = np.zeros((p.S_ret, p.J)) + L_inc_avg_sj = np.zeros((p.retire, p.J)) PS = np.zeros((p.S, p.J)) PS = PS_2dim_loop( w, e, n, - p.S_ret, + p.retire, p.S, p.J, p.g_y, @@ -418,7 +418,7 @@ def deriv_NDC(r, w, e, Y, per_rmn, p): """ if per_rmn == 1: d_theta = 0 - elif per_rmn < (p.S - p.S_ret + 1): + elif per_rmn < (p.S - p.retire + 1): d_theta = np.zeros(per_rmn) else: d_theta_empty = np.zeros(per_rmn) @@ -429,7 +429,7 @@ def deriv_NDC(r, w, e, Y, per_rmn, p): e, per_rmn, p.S, - p.S_ret, + p.retire, p.g_y, p.tau_p, g_ndc_amount, @@ -466,12 +466,12 @@ def deriv_PS(w, e, per_rmn, factor, p): Change in points system pension benefits for another unit of labor supply """ - if per_rmn < (p.S - p.S_ret + 1): + if per_rmn < (p.S - p.retire + 1): d_theta = np.zeros(p.S) else: d_theta_empty = np.zeros(p.S) d_theta = deriv_PS_loop( - w, e, p.S, p.S_ret, per_rmn, p.g_y, d_theta_empty, p.vpoint, factor + w, e, p.S, p.retire, per_rmn, d_theta_empty, p.vpoint, factor ) d_theta = d_theta[-per_rmn:] @@ -538,10 +538,10 @@ def delta_ret(self, r, Y, p): Compute conversion coefficient for the NDC pension amount """ surv_rates = 1 - p.mort_rates_SS - dir_delta_s_empty = np.zeros(p.S - p.S_ret + 1) + dir_delta_s_empty = np.zeros(p.S - p.retire + 1) g_dir_value = g_dir(r, Y, p.g_n_SS, p.g_y) dir_delta = delta_ret_loop( - p.S, p.S_ret, surv_rates, g_dir_value, dir_delta_s_empty + p.S, p.retire, surv_rates, g_dir_value, dir_delta_s_empty ) delta_ret = 1 / (dir_delta - p.k_ret) diff --git a/tests/test_pensions.py b/tests/test_pensions.py index fcfe5b5e8..92c57410f 100644 --- a/tests/test_pensions.py +++ b/tests/test_pensions.py @@ -237,8 +237,7 @@ def test_deriv_PS_loop(args, deriv_PS_loop_expected): e_ddb1 = np.array([1.1, 1.11, 0.9, 0.87, 0.87, 0.7, 0.6]) per_rmn = n_ddb1.shape[0] d_theta_empty = np.zeros_like(per_rmn) -deriv_DB_expected1 = np.array( - [0.352, 0.3256, 0.2904, 0.232, 0.0, 0.0, 0.0]) +deriv_DB_expected1 = np.array([0.352, 0.3256, 0.2904, 0.232, 0.0, 0.0, 0.0]) args_ddb1 = (w_ddb1, e_ddb1, per_rmn, p) #############non-zero d_theta: case 2############ @@ -254,17 +253,17 @@ def test_deriv_PS_loop(args, deriv_PS_loop_expected): e_ddb1 = np.array([1.11, 0.9, 0.87, 0.87, 0.7, 0.6]) per_rmn = n_ddb2.shape[0] d_theta_empty = np.zeros_like(per_rmn) -deriv_DB_expected2 = np.array( - [0.6105, 0.5445, 0.435, 0.43935, 0.0, 0.0]) +deriv_DB_expected2 = np.array([0.6105, 0.5445, 0.435, 0.43935, 0.0, 0.0]) args_ddb2 = (w_ddb1, e_ddb1, per_rmn, p2) -test_data = [(args_ddb1, deriv_DB_expected1), - (args_ddb2, deriv_DB_expected2)] +test_data = [(args_ddb1, deriv_DB_expected1), (args_ddb2, deriv_DB_expected2)] -@pytest.mark.parametrize('args,deriv_DB_expected', test_data, - ids=['non-zero d_theta: case 1', - 'non-zero d_theta: case 2']) +@pytest.mark.parametrize( + "args,deriv_DB_expected", + test_data, + ids=["non-zero d_theta: case 1", "non-zero d_theta: case 2"], +) def test_deriv_DB(args, deriv_DB_expected): """ Test of the pensions.deriv_DB() function. @@ -272,4 +271,52 @@ def test_deriv_DB(args, deriv_DB_expected): (w, e, per_rmn, p) = args deriv_DB = pensions.deriv_DB(w, e, per_rmn, p) - assert (np.allclose(deriv_DB, deriv_DB_expected)) \ No newline at end of file + assert np.allclose(deriv_DB, deriv_DB_expected) + + +#############PS deriv SS or complete lifetimes############ +p = Specifications() +p.S = 7 +p.retire = 4 +p.vpoint = 0.4 +omegas = 1 / (p.S) * np.ones(p.S) +p.omega_SS = omegas +p.g_y = 0.03 +per_rmn_dps1 = p.S +factor = 2 +w = np.array([1.2, 1.1, 1.21, 1, 1.01, 0.99, 0.8]) +e = np.array([1.1, 1.11, 0.9, 0.87, 0.87, 0.7, 0.6]) +deriv_PS_expected1 = np.array( + [0.003168, 0.0029304, 0.0026136, 0.002088, 0, 0, 0] +) +args_dps1 = (w, e, per_rmn_dps1, factor, p) + +##############PS deriv incomplete lifetimes############ +p2 = Specifications() +p2.S = 7 +p2.retire = 4 +p2.vpoint = 0.4 +omegas = 1 / (p2.S) * np.ones(p2.S) +p2.omega_SS = omegas +p2.g_y = 0.03 +per_rmn_dps2 = 5 +factor = 2 +w = np.array([1.2, 1.1, 1.21, 1, 1.01, 0.99, 0.8]) +e = np.array([1.1, 1.11, 0.9, 0.87, 0.87, 0.7, 0.6]) +deriv_PS_expected2 = np.array([0.0026136, 0.002088, 0, 0, 0]) +args_dps2 = (w, e, per_rmn_dps2, factor, p2) +test_data = [(args_dps1, deriv_PS_expected1), (args_dps2, deriv_PS_expected2)] + + +@pytest.mark.parametrize( + "args,deriv_PS_expected", test_data, ids=["SS/Complete", "Incomplete"] +) +def test_deriv_S(args, deriv_PS_expected): + """ + Test of the pensions.deriv_PS() function. + """ + (w, e, per_rmn, factor, p) = args + + deriv_PS = pensions.deriv_PS(w, e, per_rmn, factor, p) + + assert np.allclose(deriv_PS, deriv_PS_expected) From ee8916e428b2add06c737620d59b743922d876fc Mon Sep 17 00:00:00 2001 From: jdebacker Date: Fri, 5 Jul 2024 23:39:25 -0400 Subject: [PATCH 16/50] more tests --- ogcore/pensions.py | 30 +++++++++++++----------- tests/test_pensions.py | 52 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 13 deletions(-) diff --git a/ogcore/pensions.py b/ogcore/pensions.py index 2344930fa..bd31260a5 100644 --- a/ogcore/pensions.py +++ b/ogcore/pensions.py @@ -422,15 +422,14 @@ def deriv_NDC(r, w, e, Y, per_rmn, p): d_theta = np.zeros(per_rmn) else: d_theta_empty = np.zeros(per_rmn) - delta_ret_amount = delta_ret(r, Y) - g_ndc_amount = g_ndc(r, Y, p.g_n_SS, p.g_y) + delta_ret_amount = delta_ret(r, Y, p) + g_ndc_amount = g_ndc(r, Y, p) d_theta = deriv_NDC_loop( w, e, per_rmn, p.S, p.retire, - p.g_y, p.tau_p, g_ndc_amount, delta_ret_amount, @@ -501,7 +500,7 @@ def delta_point(r, Y, g_n, g_y, p): return delta_point -def g_ndc(r, Y, g_n, g_y, p): +def g_ndc(r, Y, p): """ Compute growth rate used for contributions to NDC pension """ @@ -510,14 +509,14 @@ def g_ndc(r, Y, g_n, g_y, p): elif p.ndc_growth_rate == "Curr GDP": g_ndc = (Y[1:] - Y[:-1]) / Y[:-1] elif p.ndc_growth_rate == "LR GDP": - g_ndc = g_y + g_n + g_ndc = p.g_y + p.g_n else: - g_ndc = g_y + g_n + g_ndc = p.g_y + p.g_n return g_ndc -def g_dir(r, Y, g_n, g_y, p): +def g_dir(r, Y, p): """ Compute growth rate used for contributions to NDC pension """ @@ -526,20 +525,25 @@ def g_dir(r, Y, g_n, g_y, p): elif p.dir_growth_rate == "Curr GDP": g_dir = (Y[1:] - Y[:-1]) / Y[:-1] elif p.dir_growth_rate == "LR GDP": - g_dir = g_y + g_n + g_dir = p.g_y + p.g_n else: - g_dir = g_y + g_n + g_dir = p.g_y + p.g_n return g_dir -def delta_ret(self, r, Y, p): +def delta_ret(r, Y, p): """ Compute conversion coefficient for the NDC pension amount """ surv_rates = 1 - p.mort_rates_SS dir_delta_s_empty = np.zeros(p.S - p.retire + 1) - g_dir_value = g_dir(r, Y, p.g_n_SS, p.g_y) + g_dir_value = g_dir(r, Y, p) + print("G dir value type = ", type(p.S)) + print("G dir value type = ", type(p.retire)) + print("G dir value type = ", type(surv_rates)) + print("G dir value type = ", type(g_dir_value)) + print("G dir value type = ", type(dir_delta_s_empty)) dir_delta = delta_ret_loop( p.S, p.retire, surv_rates, g_dir_value, dir_delta_s_empty ) @@ -588,7 +592,7 @@ def deriv_NDC_loop(w, e, per_rmn, S, S_ret, tau_p, g_ndc, delta_ret, d_theta): @numba.jit -def delta_ret_loop(S, S_ret, surv_rates, g_dir, dir_delta_s): +def delta_ret_loop(S, S_ret, surv_rates, g_dir_value, dir_delta_s): cumul_surv_rates = np.ones(S - S_ret + 1) for s in range(S - S_ret + 1): @@ -596,7 +600,7 @@ def delta_ret_loop(S, S_ret, surv_rates, g_dir, dir_delta_s): surv_rates_vec[0] = 1.0 cumul_surv_rates[s] = np.prod(surv_rates_vec) cumul_g_y = np.ones(S - S_ret + 1) - cumul_g_y[s] = (1 / (1 + g_dir)) ** s + cumul_g_y[s] = (1 / (1 + g_dir_value)) ** s dir_delta_s[s] = cumul_surv_rates[s] * cumul_g_y[s] dir_delta = dir_delta_s.sum() return dir_delta diff --git a/tests/test_pensions.py b/tests/test_pensions.py index 92c57410f..2e63374de 100644 --- a/tests/test_pensions.py +++ b/tests/test_pensions.py @@ -320,3 +320,55 @@ def test_deriv_S(args, deriv_PS_expected): deriv_PS = pensions.deriv_PS(w, e, per_rmn, factor, p) assert np.allclose(deriv_PS, deriv_PS_expected) + + +#############complete lifetimes, S = 4################### +p = Specifications() +p.S = 4 +p.retire = 2 +per_rmn = p.S +p.g_y = 0.03 +p.g_n_SS = 0.0 +p.ndc_growth_rate = "LR GDP" +p.dir_growth_rate = "r" +p.tau_p = 0.3 +p.k_ret = 0.4615 +p.mort_rates_SS = np.array([0.01, 0.05, 0.3, 1]) +w = np.array([1.2, 1.1, 1.21, 1]) +e = np.array([1.1, 1.11, 0.9, 0.87]) +r = 0.02 +d_NDC_expected1 = np.array([0.757437326, 0.680222841, 0.0, 0.0]) +args1 = (r, w, e, None, per_rmn, p) + +#############Incomplete lifetimes################### +p2 = Specifications() +p2.S = 4 +p2.retire = 2 +per_rmn2 = 3 +p2.g_y = 0.04 +p2.g_n_SS = 0.0 +p2.ndc_growth_rate = "LR GDP" +p2.dir_growth_rate = "LR GDP" +p2.tau_p = 0.3 +p2.k_ret = 0.4615 +p2.mort_rates_SS = np.array([0.1, 0.2, 0.4, 0.6, 1.0]) +w2 = np.array([1.1, 1.21, 1.25]) +e2 = np.array([1.11, 0.9, 1.0]) +r2 = 0.04 +d_NDC_expected2 = np.array([0.396808466, 0.0, 0.0]) +args2 = (r2, w2, e2, None, per_rmn2, p2) + +test_data = [(args1, d_NDC_expected1), (args2, d_NDC_expected2)] + + +@pytest.mark.parametrize( + "args,d_NDC_expected", test_data, ids=["SS/Complete", "Incomplete"] +) +def test_deriv_NDC(args, d_NDC_expected): + """ + Test of the pensions.deriv_NDC() function. + """ + r, w, e, Y, per_rmn, p = args + d_NDC = pensions.deriv_NDC(r, w, e, Y, per_rmn, p) + + assert np.allclose(d_NDC, d_NDC_expected) From 40af2ed385f78344aaa6f948e2c98abeaaf4a250 Mon Sep 17 00:00:00 2001 From: jdebacker Date: Sat, 6 Jul 2024 13:08:00 -0400 Subject: [PATCH 17/50] more tetss --- ogcore/pensions.py | 33 ++++++++++++++++++--------------- tests/test_pensions.py | 10 ++++++---- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/ogcore/pensions.py b/ogcore/pensions.py index bd31260a5..17a4da179 100644 --- a/ogcore/pensions.py +++ b/ogcore/pensions.py @@ -424,6 +424,8 @@ def deriv_NDC(r, w, e, Y, per_rmn, p): d_theta_empty = np.zeros(per_rmn) delta_ret_amount = delta_ret(r, Y, p) g_ndc_amount = g_ndc(r, Y, p) + print("g_ndc = ", g_ndc_amount) + print("delta_ret = ", delta_ret_amount) d_theta = deriv_NDC_loop( w, e, @@ -505,29 +507,29 @@ def g_ndc(r, Y, p): Compute growth rate used for contributions to NDC pension """ if p.ndc_growth_rate == "r": - g_ndc = r + g_ndc = r[-1] elif p.ndc_growth_rate == "Curr GDP": g_ndc = (Y[1:] - Y[:-1]) / Y[:-1] elif p.ndc_growth_rate == "LR GDP": - g_ndc = p.g_y + p.g_n + g_ndc = p.g_y[-1] + p.g_n[-1] else: - g_ndc = p.g_y + p.g_n + g_ndc = p.g_y[-1] + p.g_n[-1] return g_ndc -def g_dir(r, Y, p): +def g_dir(r, Y, g_y, g_n, dir_growth_rate): """ Compute growth rate used for contributions to NDC pension """ - if p.dir_growth_rate == "r": - g_dir = r - elif p.dir_growth_rate == "Curr GDP": + if dir_growth_rate == "r": + g_dir = r[-1] + elif dir_growth_rate == "Curr GDP": g_dir = (Y[1:] - Y[:-1]) / Y[:-1] - elif p.dir_growth_rate == "LR GDP": - g_dir = p.g_y + p.g_n + elif dir_growth_rate == "LR GDP": + g_dir = g_y[-1] + g_n[-1] else: - g_dir = p.g_y + p.g_n + g_dir = g_y[-1] + g_n[-1] return g_dir @@ -538,7 +540,7 @@ def delta_ret(r, Y, p): """ surv_rates = 1 - p.mort_rates_SS dir_delta_s_empty = np.zeros(p.S - p.retire + 1) - g_dir_value = g_dir(r, Y, p) + g_dir_value = g_dir(r, Y, p.g_y, p.g_n, p.dir_growth_rate) print("G dir value type = ", type(p.S)) print("G dir value type = ", type(p.retire)) print("G dir value type = ", type(surv_rates)) @@ -577,15 +579,16 @@ def deriv_PS_loop(w, e, S, S_ret, per_rmn, d_theta, vpoint, factor): @numba.jit -def deriv_NDC_loop(w, e, per_rmn, S, S_ret, tau_p, g_ndc, delta_ret, d_theta): - +def deriv_NDC_loop( + w, e, per_rmn, S, S_ret, tau_p, g_ndc_value, delta_ret_value, d_theta +): for s in range((S - per_rmn), S_ret): d_theta[s - (S - per_rmn)] = ( tau_p * w[s - (S - per_rmn)] * e[s - (S - per_rmn)] - * delta_ret - * (1 + g_ndc) ** (S_ret - s - 1) + * delta_ret_value + * (1 + g_ndc_value) ** (S_ret - s - 1) ) return d_theta diff --git a/tests/test_pensions.py b/tests/test_pensions.py index 2e63374de..0d7d678db 100644 --- a/tests/test_pensions.py +++ b/tests/test_pensions.py @@ -327,7 +327,8 @@ def test_deriv_S(args, deriv_PS_expected): p.S = 4 p.retire = 2 per_rmn = p.S -p.g_y = 0.03 +p.g_y = np.ones(p.T) * 0.03 +p.g_n = np.ones(p.T) * 0.0 p.g_n_SS = 0.0 p.ndc_growth_rate = "LR GDP" p.dir_growth_rate = "r" @@ -336,7 +337,7 @@ def test_deriv_S(args, deriv_PS_expected): p.mort_rates_SS = np.array([0.01, 0.05, 0.3, 1]) w = np.array([1.2, 1.1, 1.21, 1]) e = np.array([1.1, 1.11, 0.9, 0.87]) -r = 0.02 +r = np.ones(p.T) * 0.02 d_NDC_expected1 = np.array([0.757437326, 0.680222841, 0.0, 0.0]) args1 = (r, w, e, None, per_rmn, p) @@ -345,7 +346,8 @@ def test_deriv_S(args, deriv_PS_expected): p2.S = 4 p2.retire = 2 per_rmn2 = 3 -p2.g_y = 0.04 +p2.g_y = np.ones(p2.T) * 0.04 +p2.g_n = np.ones(p2.T) * 0.0 p2.g_n_SS = 0.0 p2.ndc_growth_rate = "LR GDP" p2.dir_growth_rate = "LR GDP" @@ -354,7 +356,7 @@ def test_deriv_S(args, deriv_PS_expected): p2.mort_rates_SS = np.array([0.1, 0.2, 0.4, 0.6, 1.0]) w2 = np.array([1.1, 1.21, 1.25]) e2 = np.array([1.11, 0.9, 1.0]) -r2 = 0.04 +r2 = np.ones(p2.T) * 0.04 d_NDC_expected2 = np.array([0.396808466, 0.0, 0.0]) args2 = (r2, w2, e2, None, per_rmn2, p2) From eda9ef2147596ddad3ccb5f227577033897aac6d Mon Sep 17 00:00:00 2001 From: jdebacker Date: Sun, 7 Jul 2024 22:56:21 -0400 Subject: [PATCH 18/50] add another test --- ogcore/pensions.py | 6 +- tests/test_pensions.py | 165 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 166 insertions(+), 5 deletions(-) diff --git a/ogcore/pensions.py b/ogcore/pensions.py index 17a4da179..6cb0f055a 100644 --- a/ogcore/pensions.py +++ b/ogcore/pensions.py @@ -424,8 +424,6 @@ def deriv_NDC(r, w, e, Y, per_rmn, p): d_theta_empty = np.zeros(per_rmn) delta_ret_amount = delta_ret(r, Y, p) g_ndc_amount = g_ndc(r, Y, p) - print("g_ndc = ", g_ndc_amount) - print("delta_ret = ", delta_ret_amount) d_theta = deriv_NDC_loop( w, e, @@ -579,9 +577,7 @@ def deriv_PS_loop(w, e, S, S_ret, per_rmn, d_theta, vpoint, factor): @numba.jit -def deriv_NDC_loop( - w, e, per_rmn, S, S_ret, tau_p, g_ndc_value, delta_ret_value, d_theta -): +def deriv_NDC_loop(w, e, per_rmn, S, S_ret, tau_p, g_ndc_value, delta_ret_value, d_theta): for s in range((S - per_rmn), S_ret): d_theta[s - (S - per_rmn)] = ( tau_p diff --git a/tests/test_pensions.py b/tests/test_pensions.py index 0d7d678db..130b3e274 100644 --- a/tests/test_pensions.py +++ b/tests/test_pensions.py @@ -374,3 +374,168 @@ def test_deriv_NDC(args, d_NDC_expected): d_NDC = pensions.deriv_NDC(r, w, e, Y, per_rmn, p) assert np.allclose(d_NDC, d_NDC_expected) + + + + + +################pension benefit: DB############ +demographics_ndc = deepcopy(demographics) +households_pb = deepcopy(households) +pensions_db = deepcopy(pensions_class) +pensions_db.pension_system = 'DB' +households_pb.S = 7 +households_pb.S_ret = 4 +w_db = np.array([1.2, 1.1, 1.21, 1.0, 1.01, 0.99, 0.8]) +e_db = np.array([1.1, 1.11, 0.9, 0.87, 0.87, 0.7, 0.6]) +n_db = np.array([0.4, 0.45, 0.4, 0.42, 0.3, 0.2, 0.2]) +pensions_db.last_career_yrs = 3 +pensions_db.yr_contr = S_ret +pensions_db.rep_rate_py = 0.2 +Y = None +j_ind = 1 +firms.g_y = 0.03 +factor = 2 +omegas = None +lambdas = 1 +pension_expected_db = [0, 0, 0, 0, 0.337864778, 0.327879365, 0.318189065] +classes_pb = (demographics_ndc, households_pb, firms, pensions_db) +args_pb = (w_db, e_db, n_db, r, Y, lambdas, j_ind, factor) + +################pension benefit: NDC############ +households_ndc = deepcopy(households) +pensions_ndc = deepcopy(pensions_class) +pensions_ndc.pension_system = 'NDC' +pensions_ndc.ndc_growth_rate = 'LR GDP' +pensions_ndc.dir_growth_rate = 'r' +households_ndc.S = 4 +households_ndc.S_ret = 2 +w_ndc = np.array([1.2, 1.1, 1.21, 1]) +e_ndc = np.array([1.1, 1.11, 0.9, 0.87]) +n_ndc = np.array([0.4, 0.45, 0.4, 0.3]) +Y = None +j_ind = 1 +firms.g_y = 0.03 +demographics_ndc.g_n_SS = 0.0 +r = 0.03 +factor = 2 +pensions_ndc.tau_p = 0.3 +pensions_ndc.k_ret = 0.4615 +demographics_ndc.mort_rates_SS = np.array([0.01, 0.05, 0.3, 0.4, 1]) +omegas = None +lambdas = 1 +pension_expected_ndc = [0, 0, 0.279756794, 0.271488732] +classes_ndc = (demographics_ndc, households_ndc, firms, pensions_ndc) +args_ndc = (w_ndc, e_ndc, n_ndc, r, Y, lambdas, j_ind, factor) + +################pension benefit: PS############ +demographics_ppb = deepcopy(demographics) +households_ppb = deepcopy(households) +pensions_ppb = deepcopy(pensions_class) +pensions_ppb.pension_system = 'PS' +households_ppb.S = 7 +households_ppb.S_ret = 4 +w_ppb = np.array([1.2, 1.1, 1.21, 1.0, 1.01, 0.99, 0.8]) +e_ppb = np.array([1.1, 1.11, 0.9, 0.87, 0.87, 0.7, 0.6]) +n_ppb = np.array([0.4, 0.45, 0.4, 0.42, 0.3, 0.2, 0.2]) +omegas = (1/households_ppb.S) * np.ones(households_ppb.S) +demographics_ppb.omega_SS = omegas +pensions_ppb.vpoint = 0.4 +factor = 2 +Y = None +lambdas = 1 +j_ind = 1 +firms.g_y = 0.03 +pension_expected_ppb = [0, 0, 0, 0, 0.004164689, 0.004041603, 0.003922156] +classes_ppb = (demographics_ppb, households_ppb, firms, pensions_ppb) +args_ppb = (w_db, e_db, n_db, r, Y, lambdas, j_ind, factor) + +test_data = [(classes_pb, args_pb, pension_expected_db), + (classes_ndc, args_ndc, pension_expected_ndc), + (classes_ppb, args_ppb, pension_expected_ppb)] + +@pytest.mark.parametrize('classes,args,pension_expected', test_data, + ids=['DB', 'NDC', 'PS']) +def test_pension_benefit(classes, args, pension_expected): + ''' + Test of pensions.get_pension_benefit + ''' + + demographics_ndc, households, firms, pensions = classes + w, e, n, r, Y, lambdas, j_ind, factor = args + + pension = pensions.pension_benefit( + demographics_ndc, households, firms, w, e, n, r, + Y, lambdas, j_ind, factor) + assert (np.allclose(pension, pension_expected)) +############SS or complete lifetimes############ +households_pl1 = deepcopy(households) +pensions_pl1 = deepcopy(pensions_class) +households_pl1.S = 3 +households_pl1.S_ret = 2 +per_rmn = households_pl1.S +w = np.array([1.2, 1.1, 1.21]) +e = np.array([1.1, 1.11, 0.9]) +firms.g_y = 0.03 +demographics.g_n_SS = 0.0 +pensions_pl1.ndc_growth_rate = 'LR GDP' +pensions_pl1.dir_growth_rate = 'r' +r = 0.02 +pensions_pl1.tau_p = 0.3 +pensions_pl1.k_ret = 0.4615 +pensions_pl1.delta_ret = 1.857010214 +pensions_pl1.g_ndc = firms.g_y + demographics.g_n_SS +demographics.mort_rates_SS = np.array([0.1, 0.2, 0.4, 1.0]) +deriv_NDC_loop_expected1 = np.array([0.757437326, 0.680222841, 0.0]) +d_theta_empty = np.zeros_like(w) + +#args3 = (pensions_pl1, households_pl1, w, e, per_rmn) +args3 = (w, e, per_rmn, households_pl1.S, households_pl1.S_ret, firms.g_y, + pensions_pl1.tau_p, pensions_pl1.g_ndc, pensions_pl1.delta_ret, + d_theta_empty) + +################Incomplete lifetimes################# +households_pl2 = deepcopy(households) +pensions_pl2 = deepcopy(pensions_class) +households_pl2.S = 4 +households_pl2.S_ret = 2 +per_rmn = 3 +#w = np.array([1.2, 1.1, 1.21, 1.25]) +#e = np.array([1.1, 1.11, 0.9, 1.0]) +w = np.array([1.1, 1.21, 1.25]) +e = np.array([1.11, 0.9, 1.0]) +firms.g_y = 0.04 +demographics.g_n_SS = 0.0 +pensions_pl2.ndc_growth_rate = 'LR GDP' +pensions_pl2.dir_growth_rate = 'r' +#r = np.array([0.05, 0.03, 0.04, 0.03]) +r = np.array([0.03, 0.04, 0.03]) +pensions_pl2.tau_p = 0.3 +pensions_pl2.k_ret = 0.4615 +pensions_pl2.delta_ret = 1.083288196 +pensions_pl2.g_ndc = firms.g_y + demographics.g_n_SS +#demographics.mort_rates_SS = np.array([0.1, 0.2, 0.4, 1.0]) +demographics.mort_rates_SS = np.array([0.2, 0.4, 1.0]) +deriv_NDC_loop_expected2 = np.array([0.396808466, 0.0, 0.0]) +#d_theta_empty = np.zeros_like(w) +d_theta_empty = np.zeros(per_rmn) +args4 = (w, e, per_rmn, households_pl2.S, households_pl2.S_ret, firms.g_y, + pensions_pl2.tau_p, pensions_pl2.g_ndc, pensions_pl2.delta_ret, + d_theta_empty) + +test_data = [(args3, deriv_NDC_loop_expected1), + (args4, deriv_NDC_loop_expected2)] + +@pytest.mark.parametrize('args,deriv_NDC_loop_expected', test_data, + ids=['SS', 'incomplete']) +def test_deriv_NDC_loop(args, deriv_NDC_loop_expected): + """ + Test of the pensions.deriv_NDC_loop() function. + """ + (w, e, per_rmn, households.S, households.S_ret, firms.g_y, pensions_pl2.tau_p, + pensions_pl2.g_ndc, pensions_pl2.delta_ret, d_theta_empty) = args + + deriv_NDC_loop = pensions.deriv_NDC_loop( + w, e, per_rmn, households.S, households.S_ret, firms.g_y, + pensions_pl2.tau_p, pensions_pl2.g_ndc, pensions_pl2.delta_ret, + d_theta_empty) \ No newline at end of file From 33e5206367dd88e60ad9aba6096651cc723b6ffc Mon Sep 17 00:00:00 2001 From: jdebacker Date: Mon, 8 Jul 2024 23:25:34 -0400 Subject: [PATCH 19/50] more tests --- ogcore/pensions.py | 10 +- tests/test_pensions.py | 304 ++++++++++++++++++++++------------------- 2 files changed, 166 insertions(+), 148 deletions(-) diff --git a/ogcore/pensions.py b/ogcore/pensions.py index 6cb0f055a..cde2fb4da 100644 --- a/ogcore/pensions.py +++ b/ogcore/pensions.py @@ -539,11 +539,6 @@ def delta_ret(r, Y, p): surv_rates = 1 - p.mort_rates_SS dir_delta_s_empty = np.zeros(p.S - p.retire + 1) g_dir_value = g_dir(r, Y, p.g_y, p.g_n, p.dir_growth_rate) - print("G dir value type = ", type(p.S)) - print("G dir value type = ", type(p.retire)) - print("G dir value type = ", type(surv_rates)) - print("G dir value type = ", type(g_dir_value)) - print("G dir value type = ", type(dir_delta_s_empty)) dir_delta = delta_ret_loop( p.S, p.retire, surv_rates, g_dir_value, dir_delta_s_empty ) @@ -577,8 +572,11 @@ def deriv_PS_loop(w, e, S, S_ret, per_rmn, d_theta, vpoint, factor): @numba.jit -def deriv_NDC_loop(w, e, per_rmn, S, S_ret, tau_p, g_ndc_value, delta_ret_value, d_theta): +def deriv_NDC_loop( + w, e, per_rmn, S, S_ret, tau_p, g_ndc_value, delta_ret_value, d_theta +): for s in range((S - per_rmn), S_ret): + print("TESTING", tau_p, delta_ret_value, g_ndc_value) d_theta[s - (S - per_rmn)] = ( tau_p * w[s - (S - per_rmn)] diff --git a/tests/test_pensions.py b/tests/test_pensions.py index 130b3e274..5ac824441 100644 --- a/tests/test_pensions.py +++ b/tests/test_pensions.py @@ -376,166 +376,186 @@ def test_deriv_NDC(args, d_NDC_expected): assert np.allclose(d_NDC, d_NDC_expected) +# ################pension benefit: DB############ +# p = Specifications() +# p.pension_system = 'DB' +# p.S = 7 +# p.retire = 4 +# p.last_career_yrs = 3 +# p.yr_contr = p.retire +# p.rep_rate_py = 0.2 +# p.g_y = np.ones(p.T) * 0.03 +# w_db = np.array([1.2, 1.1, 1.21, 1.0, 1.01, 0.99, 0.8]) +# e_db = np.array([1.1, 1.11, 0.9, 0.87, 0.87, 0.7, 0.6]) +# n_db = np.array([0.4, 0.45, 0.4, 0.42, 0.3, 0.2, 0.2]) +# Y = None +# j = 1 +# factor = 2 +# omegas = None +# lambdas = 1 +# pension_expected_db = [0, 0, 0, 0, 0.337864778, 0.327879365, 0.318189065] +# args_pb = (w_db, e_db, n_db, r, Y, lambdas, j, factor) + +# ################pension benefit: NDC############ +# p2 = Specifications() +# p2.pension_system = 'NDC' +# p2.ndc_growth_rate = 'LR GDP' +# p2.dir_growth_rate = 'r' +# p2.S = 4 +# p2.retire = 2 +# w_ndc = np.array([1.2, 1.1, 1.21, 1]) +# e_ndc = np.array([1.1, 1.11, 0.9, 0.87]) +# n_ndc = np.array([0.4, 0.45, 0.4, 0.3]) +# Y = None +# j = 1 +# p2.g_y = 0.03 +# p2.g_n_SS = 0.0 +# r = 0.03 +# factor = 2 +# p2.tau_p = 0.3 +# p2.k_ret = 0.4615 +# p2.mort_rates_SS = np.array([0.01, 0.05, 0.3, 0.4, 1]) +# omegas = None +# lambdas = 1 +# pension_expected_ndc = [0, 0, 0.279756794, 0.271488732] +# args_ndc = (w_ndc, e_ndc, n_ndc, r, Y, lambdas, j, factor) + +# ################pension benefit: PS############ +# p3 = Specifications() +# p3.pension_system = 'PS' +# p3.S = 7 +# p3.S_ret = 4 +# w_ppb = np.array([1.2, 1.1, 1.21, 1.0, 1.01, 0.99, 0.8]) +# e_ppb = np.array([1.1, 1.11, 0.9, 0.87, 0.87, 0.7, 0.6]) +# n_ppb = np.array([0.4, 0.45, 0.4, 0.42, 0.3, 0.2, 0.2]) +# omegas = (1/p3.S) * np.ones(p3.S) +# p3.omega_SS = omegas +# p3.vpoint = 0.4 +# factor = 2 +# Y = None +# lambdas = 1 +# j_ind = 1 +# p3.g_y = np.ones(p3.T) * 0.03 +# pension_expected_ppb = [0, 0, 0, 0, 0.004164689, 0.004041603, 0.003922156] +# args_ps = (w_db, e_db, n_db, r, Y, lambdas, j_ind, factor) + +# test_data = [(args_pb, pension_expected_db), +# (args_ndc, pension_expected_ndc), +# (args_ps, pension_expected_ppb)] + +# @pytest.mark.parametrize('classes,args,pension_expected', test_data, +# ids=['DB', 'NDC', 'PS']) +# def test_pension_benefit(classes, args, pension_expected): +# ''' +# Test of pensions.get_pension_benefit +# ''' +# w, e, n, r, Y, lambdas, j_ind, factor = args + +# pension = pensions.pension_amount( +# w, n, theta, t, j, shift, method, e, p) +# assert (np.allclose(pension, pension_expected)) - -################pension benefit: DB############ -demographics_ndc = deepcopy(demographics) -households_pb = deepcopy(households) -pensions_db = deepcopy(pensions_class) -pensions_db.pension_system = 'DB' -households_pb.S = 7 -households_pb.S_ret = 4 -w_db = np.array([1.2, 1.1, 1.21, 1.0, 1.01, 0.99, 0.8]) -e_db = np.array([1.1, 1.11, 0.9, 0.87, 0.87, 0.7, 0.6]) -n_db = np.array([0.4, 0.45, 0.4, 0.42, 0.3, 0.2, 0.2]) -pensions_db.last_career_yrs = 3 -pensions_db.yr_contr = S_ret -pensions_db.rep_rate_py = 0.2 -Y = None -j_ind = 1 -firms.g_y = 0.03 -factor = 2 -omegas = None -lambdas = 1 -pension_expected_db = [0, 0, 0, 0, 0.337864778, 0.327879365, 0.318189065] -classes_pb = (demographics_ndc, households_pb, firms, pensions_db) -args_pb = (w_db, e_db, n_db, r, Y, lambdas, j_ind, factor) - -################pension benefit: NDC############ -households_ndc = deepcopy(households) -pensions_ndc = deepcopy(pensions_class) -pensions_ndc.pension_system = 'NDC' -pensions_ndc.ndc_growth_rate = 'LR GDP' -pensions_ndc.dir_growth_rate = 'r' -households_ndc.S = 4 -households_ndc.S_ret = 2 -w_ndc = np.array([1.2, 1.1, 1.21, 1]) -e_ndc = np.array([1.1, 1.11, 0.9, 0.87]) -n_ndc = np.array([0.4, 0.45, 0.4, 0.3]) -Y = None -j_ind = 1 -firms.g_y = 0.03 -demographics_ndc.g_n_SS = 0.0 -r = 0.03 -factor = 2 -pensions_ndc.tau_p = 0.3 -pensions_ndc.k_ret = 0.4615 -demographics_ndc.mort_rates_SS = np.array([0.01, 0.05, 0.3, 0.4, 1]) -omegas = None -lambdas = 1 -pension_expected_ndc = [0, 0, 0.279756794, 0.271488732] -classes_ndc = (demographics_ndc, households_ndc, firms, pensions_ndc) -args_ndc = (w_ndc, e_ndc, n_ndc, r, Y, lambdas, j_ind, factor) - -################pension benefit: PS############ -demographics_ppb = deepcopy(demographics) -households_ppb = deepcopy(households) -pensions_ppb = deepcopy(pensions_class) -pensions_ppb.pension_system = 'PS' -households_ppb.S = 7 -households_ppb.S_ret = 4 -w_ppb = np.array([1.2, 1.1, 1.21, 1.0, 1.01, 0.99, 0.8]) -e_ppb = np.array([1.1, 1.11, 0.9, 0.87, 0.87, 0.7, 0.6]) -n_ppb = np.array([0.4, 0.45, 0.4, 0.42, 0.3, 0.2, 0.2]) -omegas = (1/households_ppb.S) * np.ones(households_ppb.S) -demographics_ppb.omega_SS = omegas -pensions_ppb.vpoint = 0.4 -factor = 2 -Y = None -lambdas = 1 -j_ind = 1 -firms.g_y = 0.03 -pension_expected_ppb = [0, 0, 0, 0, 0.004164689, 0.004041603, 0.003922156] -classes_ppb = (demographics_ppb, households_ppb, firms, pensions_ppb) -args_ppb = (w_db, e_db, n_db, r, Y, lambdas, j_ind, factor) - -test_data = [(classes_pb, args_pb, pension_expected_db), - (classes_ndc, args_ndc, pension_expected_ndc), - (classes_ppb, args_ppb, pension_expected_ppb)] - -@pytest.mark.parametrize('classes,args,pension_expected', test_data, - ids=['DB', 'NDC', 'PS']) -def test_pension_benefit(classes, args, pension_expected): - ''' - Test of pensions.get_pension_benefit - ''' - - demographics_ndc, households, firms, pensions = classes - w, e, n, r, Y, lambdas, j_ind, factor = args - - pension = pensions.pension_benefit( - demographics_ndc, households, firms, w, e, n, r, - Y, lambdas, j_ind, factor) - assert (np.allclose(pension, pension_expected)) ############SS or complete lifetimes############ -households_pl1 = deepcopy(households) -pensions_pl1 = deepcopy(pensions_class) -households_pl1.S = 3 -households_pl1.S_ret = 2 -per_rmn = households_pl1.S +p = Specifications() +p.S = 3 +p.retire = 2 +per_rmn = p.S +p.g_n_SS = 0.0 +p.g_y = np.ones(p.T) * 0.03 +p.ndc_growth_rate = "LR GDP" +p.dir_growth_rate = "r" w = np.array([1.2, 1.1, 1.21]) e = np.array([1.1, 1.11, 0.9]) -firms.g_y = 0.03 -demographics.g_n_SS = 0.0 -pensions_pl1.ndc_growth_rate = 'LR GDP' -pensions_pl1.dir_growth_rate = 'r' -r = 0.02 -pensions_pl1.tau_p = 0.3 -pensions_pl1.k_ret = 0.4615 -pensions_pl1.delta_ret = 1.857010214 -pensions_pl1.g_ndc = firms.g_y + demographics.g_n_SS -demographics.mort_rates_SS = np.array([0.1, 0.2, 0.4, 1.0]) +r = np.ones(p.T) * 0.02 +p.tau_p = 0.3 +p.k_ret = 0.4615 +delta_ret = 1.857010214 +g_ndc = p.g_y[-1] + p.g_n_SS +p.mort_rates_SS = np.array([0.1, 0.2, 0.4, 1.0]) deriv_NDC_loop_expected1 = np.array([0.757437326, 0.680222841, 0.0]) d_theta_empty = np.zeros_like(w) - -#args3 = (pensions_pl1, households_pl1, w, e, per_rmn) -args3 = (w, e, per_rmn, households_pl1.S, households_pl1.S_ret, firms.g_y, - pensions_pl1.tau_p, pensions_pl1.g_ndc, pensions_pl1.delta_ret, - d_theta_empty) +args3 = (w, e, per_rmn, g_ndc, delta_ret, d_theta_empty, p) ################Incomplete lifetimes################# -households_pl2 = deepcopy(households) -pensions_pl2 = deepcopy(pensions_class) -households_pl2.S = 4 -households_pl2.S_ret = 2 +p2 = Specifications() +p2.S = 4 +p2.retire = 2 per_rmn = 3 -#w = np.array([1.2, 1.1, 1.21, 1.25]) -#e = np.array([1.1, 1.11, 0.9, 1.0]) -w = np.array([1.1, 1.21, 1.25]) -e = np.array([1.11, 0.9, 1.0]) -firms.g_y = 0.04 -demographics.g_n_SS = 0.0 -pensions_pl2.ndc_growth_rate = 'LR GDP' -pensions_pl2.dir_growth_rate = 'r' -#r = np.array([0.05, 0.03, 0.04, 0.03]) +w2 = np.array([1.1, 1.21, 1.25]) +e2 = np.array([1.11, 0.9, 1.0]) +p2.g_y = np.ones(p.T) * 0.04 +p2.g_n_SS = 0.0 +p2.ndc_growth_rate = "LR GDP" +p2.dir_growth_rate = "r" r = np.array([0.03, 0.04, 0.03]) -pensions_pl2.tau_p = 0.3 -pensions_pl2.k_ret = 0.4615 -pensions_pl2.delta_ret = 1.083288196 -pensions_pl2.g_ndc = firms.g_y + demographics.g_n_SS -#demographics.mort_rates_SS = np.array([0.1, 0.2, 0.4, 1.0]) -demographics.mort_rates_SS = np.array([0.2, 0.4, 1.0]) +p2.tau_p = 0.3 +p2.k_ret = 0.4615 +delta_ret2 = 1.083288196 +g_ndc2 = p2.g_y[-1] + p2.g_n_SS +p2.mort_rates_SS = np.array([0.2, 0.4, 1.0]) deriv_NDC_loop_expected2 = np.array([0.396808466, 0.0, 0.0]) -#d_theta_empty = np.zeros_like(w) d_theta_empty = np.zeros(per_rmn) -args4 = (w, e, per_rmn, households_pl2.S, households_pl2.S_ret, firms.g_y, - pensions_pl2.tau_p, pensions_pl2.g_ndc, pensions_pl2.delta_ret, - d_theta_empty) +args4 = (w2, e2, per_rmn2, g_ndc2, delta_ret2, d_theta_empty, p2) + +test_data = [ + (args3, deriv_NDC_loop_expected1), + (args4, deriv_NDC_loop_expected2), +] -test_data = [(args3, deriv_NDC_loop_expected1), - (args4, deriv_NDC_loop_expected2)] -@pytest.mark.parametrize('args,deriv_NDC_loop_expected', test_data, - ids=['SS', 'incomplete']) +@pytest.mark.parametrize( + "args,deriv_NDC_loop_expected", test_data, ids=["SS", "incomplete"] +) def test_deriv_NDC_loop(args, deriv_NDC_loop_expected): """ Test of the pensions.deriv_NDC_loop() function. """ - (w, e, per_rmn, households.S, households.S_ret, firms.g_y, pensions_pl2.tau_p, - pensions_pl2.g_ndc, pensions_pl2.delta_ret, d_theta_empty) = args + (w, e, per_rmn, g_ndc_value, delta_ret_value, d_theta, p) = args + print("TESTING", p.tau_p, delta_ret_value, g_ndc_value) deriv_NDC_loop = pensions.deriv_NDC_loop( - w, e, per_rmn, households.S, households.S_ret, firms.g_y, - pensions_pl2.tau_p, pensions_pl2.g_ndc, pensions_pl2.delta_ret, - d_theta_empty) \ No newline at end of file + w, + e, + per_rmn, + p.S, + p.retire, + p.tau_p, + g_ndc_value, + delta_ret_value, + d_theta, + ) + + assert np.allclose(deriv_NDC_loop, deriv_NDC_loop_expected) + + +############SS or complete lifietimes############ +p = Specifications() +p.S = 4 +p.retire = 2 +p.g_y = np.ones(p.T) * 0.04 +p.g_n_SS = 0.0 +p.ndc_growth_rate = "LR GDP" +p.dir_growth_rate = "r" +r = 0.02 +g_ndc_amount = p.g_y[-1] + p.g_n_SS +p.mort_rates_SS = np.array([0.1, 0.2, 0.4, 0.6, 1.0]) +dir_delta_s_empty = np.zeros(p.S - p.retire + 1) +dir_delta_ret_expected1 = 1.384615385 +surv_rates = np.zeros(p.S - p.retire + 1) +surv_rates = 1 - p.mort_rates_SS +args5 = (surv_rates, g_ndc_amount, dir_delta_s_empty, p) +test_data = [(args5, dir_delta_ret_expected1)] + + +@pytest.mark.parametrize("args,dir_delta_ret_expected", test_data, ids=["SS"]) +def test_delta_ret_loop(args, dir_delta_ret_expected): + """ + Test of the pensions.delta_ret_loop() function. + """ + (surv_rates, g_ndc_value, dir_delta_s_empty, p) = args + dir_delta = pensions.delta_ret_loop( + p.S, p.retire, surv_rates, g_ndc_value, dir_delta_s_empty + ) + + assert np.allclose(dir_delta, dir_delta_ret_expected) From d031c2b29c53252b184764556f596e28b3eacfda Mon Sep 17 00:00:00 2001 From: jdebacker Date: Tue, 9 Jul 2024 16:29:18 -0400 Subject: [PATCH 20/50] all old tests passing --- ogcore/pensions.py | 38 +++-- tests/test_pensions.py | 361 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 364 insertions(+), 35 deletions(-) diff --git a/ogcore/pensions.py b/ogcore/pensions.py index cde2fb4da..346a1472f 100644 --- a/ogcore/pensions.py +++ b/ogcore/pensions.py @@ -194,12 +194,14 @@ def DB_amount(w, e, n, j, p): if n.shape[0] < p.S: per_rmn = n.shape[0] # TODO: think about how to handle setting w_preTP and n_preTP + # TODO: will need to update how the e matrix is handled here + # and else where to allow for it to be time varying w_S = np.append((p.w_preTP * np.ones(p.S))[:(-per_rmn)], w) n_S = np.append(p.n_preTP[:(-per_rmn), j], n) DB_s = np.zeros(p.retire) DB = np.zeros(p.S) - # TODO: we set a rep_rate_py in params, but not rep_rate. What is it??? + print("DB_1dim_loop", w_S, p.e[:, j], n_S) DB = DB_1dim_loop( w_S, p.e[:, j], @@ -209,7 +211,6 @@ def DB_amount(w, e, n, j, p): p.g_y, L_inc_avg_s, L_inc_avg, - DB_s, DB, p.last_career_yrs, p.rep_rate_py, @@ -225,12 +226,11 @@ def DB_amount(w, e, n, j, p): w, e, n, - p.retiremet_age, + p.retire, p.S, p.g_y, L_inc_avg_s, L_inc_avg, - DB_s, DB, p.last_career_yrs, p.rep_rate_py, @@ -250,7 +250,6 @@ def DB_amount(w, e, n, j, p): p.g_y, L_inc_avg_sj, L_inc_avg, - DB_sj, DB, p.last_career_yrs, p.rep_rate_py, @@ -265,12 +264,7 @@ def NDC_amount(w, e, n, r, Y, j, p): Calculate public pension from a notional defined contribution system. """ - g_ndc_amount = g_ndc( - r, - Y, - p.g_n_SS, - p.g_y, - ) + g_ndc_amount = g_ndc(r, Y, p) delta_ret_amount = delta_ret(r, Y, p) if n.shape[0] < p.S: @@ -283,7 +277,7 @@ def NDC_amount(w, e, n, r, Y, j, p): NDC = np.zeros(p.S) NDC = NDC_1dim_loop( w_S, - p.emat[:, j], + p.e[:, j], n_S, p.retire, p.S, @@ -346,7 +340,7 @@ def PS_amount(w, e, n, j, factor, p): PS = np.zeros(p.S) PS = PS_1dim_loop( w_S, - p.emat[:, j], + p.e[:, j], n_S, p.retire, p.S, @@ -408,6 +402,12 @@ def deriv_theta(r, w, e, Y, per_rmn, factor, p): d_theta = deriv_NDC(r, w, e, Y, per_rmn, p) elif p.pension_system == "Points System": d_theta = deriv_PS(w, e, per_rmn, factor, p) + else: + raise ValueError( + "pension_system must be one of the following: " + "'US-style Social Security', 'Defined Benefits', " + "'Notional Defined Contribution', 'Points System'" + ) return d_theta @@ -576,7 +576,6 @@ def deriv_NDC_loop( w, e, per_rmn, S, S_ret, tau_p, g_ndc_value, delta_ret_value, d_theta ): for s in range((S - per_rmn), S_ret): - print("TESTING", tau_p, delta_ret_value, g_ndc_value) d_theta[s - (S - per_rmn)] = ( tau_p * w[s - (S - per_rmn)] @@ -607,8 +606,9 @@ def delta_ret_loop(S, S_ret, surv_rates, g_dir_value, dir_delta_s): def PS_1dim_loop(w, e, n, S_ret, S, g_y, vpoint, factor, L_inc_avg_s, PS): # TODO: do we need these constants or can we scale vpoint to annual?? for u in range(S_ret, S): + # TODO: allow for g_y to be time varying for s in range(S_ret): - L_inc_avg_s[s] = w[s] / np.exp(g_y * (u - s)) * e[s] * n[s] + L_inc_avg_s[s] = w[s] / np.exp(g_y[-1] * (u - s)) * e[s] * n[s] PS[u] = (MONTHS_IN_A_YEAR * vpoint * L_inc_avg_s.sum()) / ( factor * THOUSAND ) @@ -649,8 +649,11 @@ def DB_1dim_loop( for u in range(S_ret, S): for s in range(S_ret - last_career_yrs, S_ret): + # TODO: pass t so that can pull correct g_y value + # Just need to make if doing over time path makes sense + # or if should just do SS L_inc_avg_s[s - (S_ret - last_career_yrs)] = ( - w[s] / np.exp(g_y * (u - s)) * e[s] * n[s] + w[s] / np.exp(g_y[-1] * (u - s)) * e[s] * n[s] ) L_inc_avg = L_inc_avg_s.sum() / last_career_yrs rep_rate = yr_contr * rep_rate_py @@ -692,9 +695,10 @@ def NDC_1dim_loop(w, e, n, S_ret, S, g_y, tau_p, g_ndc, delta_ret, NDC_s, NDC): for u in range(S_ret, S): for s in range(0, S_ret): + # TODO: update so can take g_y from period t NDC_s[s] = ( tau_p - * (w[s] / np.exp(g_y * (u - s))) + * (w[s] / np.exp(g_y[-1] * (u - s))) * e[s] * n[s] * ((1 + g_ndc) ** (S_ret - s - 1)) diff --git a/tests/test_pensions.py b/tests/test_pensions.py index 5ac824441..a89a0a50f 100644 --- a/tests/test_pensions.py +++ b/tests/test_pensions.py @@ -82,7 +82,7 @@ def test_replacement_rate_vals(n, w, factor, j, p_in, expected): p.retire = 4 p.last_career_yrs = 3 p.yr_contr = 4 -p.g_y = 0.03 +p.g_y = np.ones(p.T) * 0.03 j = 1 w = np.array([1.2, 1.1, 1.21, 1.0, 1.01, 0.99, 0.8]) e = np.array([1.1, 1.11, 0.9, 0.87, 0.87, 0.7, 0.6]) @@ -157,7 +157,7 @@ def test_DB_1dim_loop(args, DB_loop_expected): p.last_career_yrs = 3 p.yr_contr = p.retire p.rep_rate_py = 0.2 -p.g_y = 0.03 +p.g_y = np.ones(p.T) * 0.03 w = np.array([1.2, 1.1, 1.21, 1, 1.01, 0.99, 0.8]) e = np.array([1.1, 1.11, 0.9, 0.87, 0.87, 0.7, 0.6]) deriv_DB_loop_expected = np.array( @@ -197,7 +197,7 @@ def test_deriv_DB_loop(args, deriv_DB_loop_expected): p.vpoint = 0.4 w = np.array([1.2, 1.1, 1.21, 1, 1.01, 0.99, 0.8]) e = np.array([1.1, 1.11, 0.9, 0.87, 0.87, 0.7, 0.6]) -p.g_y = 0.03 +p.g_y = np.ones(p.T) * 0.03 factor = 2 d_theta_empty = np.zeros_like(w) deriv_PS_loop_expected1 = np.array( @@ -231,7 +231,7 @@ def test_deriv_PS_loop(args, deriv_PS_loop_expected): p.last_career_yrs = 3 p.yr_contr = p.retire p.rep_rate_py = 0.2 -p.g_y = 0.03 +p.g_y = np.ones(p.T) * 0.03 n_ddb1 = np.array([0.4, 0.45, 0.4, 0.42, 0.3, 0.2, 0.2]) w_ddb1 = np.array([1.2, 1.1, 1.21, 1, 1.01, 0.99, 0.8]) e_ddb1 = np.array([1.1, 1.11, 0.9, 0.87, 0.87, 0.7, 0.6]) @@ -281,7 +281,7 @@ def test_deriv_DB(args, deriv_DB_expected): p.vpoint = 0.4 omegas = 1 / (p.S) * np.ones(p.S) p.omega_SS = omegas -p.g_y = 0.03 +p.g_y = np.ones(p.T) * 0.03 per_rmn_dps1 = p.S factor = 2 w = np.array([1.2, 1.1, 1.21, 1, 1.01, 0.99, 0.8]) @@ -327,7 +327,7 @@ def test_deriv_S(args, deriv_PS_expected): p.S = 4 p.retire = 2 per_rmn = p.S -p.g_y = np.ones(p.T) * 0.03 +p.g_y = np.ones(p.T) * np.ones(p.T) * 0.03 p.g_n = np.ones(p.T) * 0.0 p.g_n_SS = 0.0 p.ndc_growth_rate = "LR GDP" @@ -384,7 +384,7 @@ def test_deriv_NDC(args, d_NDC_expected): # p.last_career_yrs = 3 # p.yr_contr = p.retire # p.rep_rate_py = 0.2 -# p.g_y = np.ones(p.T) * 0.03 +# p.g_y = np.ones(p.T) * np.ones(p.T) * 0.03 # w_db = np.array([1.2, 1.1, 1.21, 1.0, 1.01, 0.99, 0.8]) # e_db = np.array([1.1, 1.11, 0.9, 0.87, 0.87, 0.7, 0.6]) # n_db = np.array([0.4, 0.45, 0.4, 0.42, 0.3, 0.2, 0.2]) @@ -424,24 +424,24 @@ def test_deriv_NDC(args, d_NDC_expected): # p3 = Specifications() # p3.pension_system = 'PS' # p3.S = 7 -# p3.S_ret = 4 -# w_ppb = np.array([1.2, 1.1, 1.21, 1.0, 1.01, 0.99, 0.8]) -# e_ppb = np.array([1.1, 1.11, 0.9, 0.87, 0.87, 0.7, 0.6]) -# n_ppb = np.array([0.4, 0.45, 0.4, 0.42, 0.3, 0.2, 0.2]) +# p3.retire = 4 +# w_ps = np.array([1.2, 1.1, 1.21, 1.0, 1.01, 0.99, 0.8]) +# e_ps = np.array([1.1, 1.11, 0.9, 0.87, 0.87, 0.7, 0.6]) +# n_ps = np.array([0.4, 0.45, 0.4, 0.42, 0.3, 0.2, 0.2]) # omegas = (1/p3.S) * np.ones(p3.S) # p3.omega_SS = omegas # p3.vpoint = 0.4 # factor = 2 # Y = None # lambdas = 1 -# j_ind = 1 +# j = 1 # p3.g_y = np.ones(p3.T) * 0.03 -# pension_expected_ppb = [0, 0, 0, 0, 0.004164689, 0.004041603, 0.003922156] -# args_ps = (w_db, e_db, n_db, r, Y, lambdas, j_ind, factor) +# pension_expected_ps = [0, 0, 0, 0, 0.004164689, 0.004041603, 0.003922156] +# args_ps = (w_db, e_db, n_db, r, Y, lambdas, j, factor) # test_data = [(args_pb, pension_expected_db), # (args_ndc, pension_expected_ndc), -# (args_ps, pension_expected_ppb)] +# (args_ps, pension_expected_ps)] # @pytest.mark.parametrize('classes,args,pension_expected', test_data, # ids=['DB', 'NDC', 'PS']) @@ -449,7 +449,7 @@ def test_deriv_NDC(args, d_NDC_expected): # ''' # Test of pensions.get_pension_benefit # ''' -# w, e, n, r, Y, lambdas, j_ind, factor = args +# w, e, n, r, Y, lambdas, j, factor = args # pension = pensions.pension_amount( # w, n, theta, t, j, shift, method, e, p) @@ -462,7 +462,7 @@ def test_deriv_NDC(args, d_NDC_expected): p.retire = 2 per_rmn = p.S p.g_n_SS = 0.0 -p.g_y = np.ones(p.T) * 0.03 +p.g_y = np.ones(p.T) * np.ones(p.T) * 0.03 p.ndc_growth_rate = "LR GDP" p.dir_growth_rate = "r" w = np.array([1.2, 1.1, 1.21]) @@ -533,7 +533,7 @@ def test_deriv_NDC_loop(args, deriv_NDC_loop_expected): p = Specifications() p.S = 4 p.retire = 2 -p.g_y = np.ones(p.T) * 0.04 +p.g_y = np.ones(p.T) * np.ones(p.T) * 0.04 p.g_n_SS = 0.0 p.ndc_growth_rate = "LR GDP" p.dir_growth_rate = "r" @@ -559,3 +559,328 @@ def test_delta_ret_loop(args, dir_delta_ret_expected): ) assert np.allclose(dir_delta, dir_delta_ret_expected) + + +#####################SS / Complete lifetimes############ +p = Specifications() +p.S = 7 +p.retire = 4 +p.last_career_yrs = 3 +p.yr_contr = p.retire +p.rep_rate_py = 0.2 +p.g_y = np.ones(p.T) * 0.03 +j = 1 +w = np.array([1.2, 1.1, 1.21, 1.0, 1.01, 0.99, 0.8]) +e = np.array([1.1, 1.11, 0.9, 0.87, 0.87, 0.7, 0.6]) +n = np.array([0.4, 0.45, 0.4, 0.42, 0.3, 0.2, 0.2]) +DB_expected1 = np.array([0, 0, 0, 0, 0.337864778, 0.327879365, 0.318189065]) +args1 = (w, e, n, j, p) + +#####################Incomplete############ +p2 = Specifications() +p2.S = 7 +p2.retire = 4 +p2.last_career_yrs = 3 +p2.yr_contr = p2.retire +p2.rep_rate_py = 0.2 +p2.g_y = np.ones(p2.T) * 0.03 +j = 1 +w2 = np.array([1.21, 1.0, 1.01, 0.99, 0.8]) +e2 = np.array( + [ + [1.1, 1.1], + [1.11, 1.11], + [0.9, 0.9], + [0.87, 0.87], + [0.87, 0.87], + [0.7, 0.7], + [0.6, 0.6], + ] +) +n2 = np.array([0.4, 0.42, 0.3, 0.2, 0.2]) +p2.w_preTP = np.array([1.05]) +p2.n_preTP = np.array( + [ + [0.4, 0.4], + [0.3, 0.3], + [0.2, 0.2], + [0.3, 0.3], + [0.4, 0.4], + [0.45, 0.45], + [0.5, 0.5], + ] +) +p2.e = e2 +DB_expected2 = np.array([0, 0, 0.289170525, 0.280624244, 0.272330544]) +args2 = (w2, e2, n2, j, p2) + +test_data = [(args1, DB_expected1), (args2, DB_expected2)] + + +@pytest.mark.parametrize( + "args,DB_expected", test_data, ids=["SS/Complete", "Incomplete"] +) +def test_DB(args, DB_expected): + """ + Test of the pensions.get_DB() function. + """ + w, e, n, j, p = args + DB = pensions.DB_amount(w, e, n, j, p) + + assert np.allclose(DB, DB_expected) + + +################pension benefit derivative: DB############ +p = Specifications() +p.pension_system = "Defined Benefits" +p.S = 7 +p.retire = 4 +per_rmn = p.S +p.last_career_yrs = 3 +p.yr_contr = p.retire +p.rep_rate_py = 0.2 +w_ddb = np.array([1.2, 1.1, 1.21, 1, 1.01, 0.99, 0.8]) +e_ddb = np.array([1.1, 1.11, 0.9, 0.87, 0.87, 0.7, 0.6]) +p.g_y = np.ones(p.T) * 0.03 +Y = None +r_ddb = np.ones(p.T) * 0.03 +factor = 2 +d_theta_expected_ddb = np.array([0.352, 0.3256, 0.2904, 0.232, 0.0, 0.0, 0.0]) +args_ddb = (r_ddb, w_ddb, e_ddb, Y, per_rmn, factor, p) + +################pension benefit derivative: NDC############ +p2 = Specifications() +p2.pension_system = "Notional Defined Contribution" +p2.S = 4 +p2.retire = 2 +per_rmn = p2.S +w_dndc = np.array([1.2, 1.1, 1.21, 1]) +e_dndc = np.array([1.1, 1.11, 0.9, 0.87]) +p2.g_y = np.ones(p2.T) * 0.03 +p2.g_n_SS = 0.0 +p2.ndc_growth_rate = "LR GDP" +p2.dir_growth_rate = "r" +r_dndc = np.ones(p2.T) * 0.02 +p2.tau_p = 0.3 +p2.k_ret = 0.4615 +p2.mort_rates_SS = np.array([0.01, 0.05, 0.3, 1]) +d_theta_expected_dndc = np.array([0.75838653, 0.680222841, 0, 0]) +# TODO: has to change first element from the below to above. Why? +# check by hand calculation spreadsheet +# d_theta_expected_dndc = np.array([0.757437326, 0.680222841, 0, 0]) +Y = None +factor = 2 +args_dndc = (r_dndc, w_dndc, e_dndc, Y, per_rmn, factor, p2) + + +################pension benefit derivative: PS############ +p3 = Specifications() +p3.pension_system = "Points System" +p3.S = 7 +p3.retire = 4 +w_dps = np.array([1.2, 1.1, 1.21, 1, 1.01, 0.99, 0.8]) +e_dps = np.array([1.1, 1.11, 0.9, 0.87, 0.87, 0.7, 0.6]) +p3.g_y = np.ones(p3.T) * 0.03 +Y = None +factor = 2 +r_dps = np.ones(p3.T) * 0.03 +p3.vpoint = 0.4 +omegas = 1 / (p3.S) * np.ones(p3.S) +p3.omega_SS = omegas +per_rmn_ps = 5 +d_theta_expected_dps = np.array([0.0026136, 0.002088, 0, 0, 0]) +args_dps = (r_dps, w_dps, e_dps, Y, per_rmn_ps, factor, p3) + +test_data = [ + (args_ddb, d_theta_expected_ddb), + (args_dndc, d_theta_expected_dndc), + (args_dps, d_theta_expected_dps), +] + + +@pytest.mark.parametrize( + "args,d_theta_expected", test_data, ids=["DB", "NDC", "PS"] +) +def test_deriv_theta(args, d_theta_expected): + """ + Test of pensions.deriv_theta + """ + r, w, e, Y, per_rmn, factor, p = args + d_theta = pensions.deriv_theta(r, w, e, Y, per_rmn, factor, p) + assert np.allclose(d_theta, d_theta_expected) + + +#############complete lifetimes, S = 4################### +p = Specifications() +p.S = 4 +p.retire = 2 +j = 1 +w = np.array([1.2, 1.1, 1.21, 1]) +e = np.array([1.1, 1.11, 0.9, 0.87]) +n = np.array([0.4, 0.45, 0.4, 0.3]) +p.g_y = np.ones(p.T) * 0.03 +p.g_n_SS = 0.0 +p.ndc_growth_rate = "LR GDP" +p.dir_growth_rate = "r" +r = np.ones(p.T) * 0.03 +p.tau_p = 0.3 +p.k_ret = 0.4615 +p.mort_rates_SS = np.array([0.01, 0.05, 0.3, 0.4, 1]) +NDC_expected1 = np.array([0, 0, 0.27992856, 0.27165542]) +# NDC_expected1 = np.array([0, 0, 0.279756794, 0.271488732]) +args1 = (w, e, n, r, None, j, p) + +#############incomplete lifetimes################### +p2 = Specifications() +p2.S = 4 +p2.retire = 2 +j = 1 +w = np.array([1.1, 1.21, 1]) +e = np.array([[1.0, 1.0], [1.11, 1.11], [0.9, 0.9], [0.87, 0.87]]) +n = np.array([0.45, 0.4, 0.3]) +p2.w_preTP = np.array([1.05]) +p2.n_preTP = np.array([[0.4, 0.4], [0.2, 0.2], [0.3, 0.3], [0.5, 0.5]]) +p2.g_y = np.ones(p2.T) * 0.03 +p2.g_n_SS = 0.0 +p2.ndc_growth_rate = "LR GDP" +p2.dir_growth_rate = "r" +r = np.ones(p2.T) * 0.03 +p2.tau_p = 0.3 +p2.k_ret = 0.4615 +p2.mort_rates_SS = np.array([0.01, 0.05, 0.3, 0.4, 1]) +p2.e = e +NDC_expected2 = np.array([0, 0.25185784, 0.24441432]) +# TODO: why move from numbers below to those above ? Diff in numpy rounding?? +# NDC_expected2 = np.array([0, 0.251721214, 0.244281728]) +args2 = (w, e, n, r, None, j, p2) + +test_data = [(args1, NDC_expected1), (args2, NDC_expected2)] + + +@pytest.mark.parametrize( + "args,NDC_expected", test_data, ids=["SS/Complete", "Incomplete"] +) +def test_NDC(args, NDC_expected): + """ + Test of the pensions.NDC() function. + """ + w, e, n, r, Y, j, p = args + NDC = pensions.NDC_amount(w, e, n, r, Y, j, p) + assert np.allclose(NDC, NDC_expected) + + +#############complete lifetimes, S = 7################### +p = Specifications() +p.S = 7 +p.retire = 4 +p.vpoint = 0.4 +j = 1 +w = np.array([1.2, 1.1, 1.21, 1.0, 1.01, 0.99, 0.8]) +e = np.array([1.1, 1.11, 0.9, 0.87, 0.87, 0.7, 0.6]) +n = np.array([0.4, 0.45, 0.4, 0.42, 0.3, 0.2, 0.2]) +p.g_y = np.ones(p.T) * 0.03 +factor = 2 +points_py_s = np.zeros(p.retire) +L_inc_avg_s = np.zeros(p.retire) +L_inc_avg = np.zeros(1) +PS = np.zeros(p.S) +PS_loop_expected = np.array( + [0, 0, 0, 0, 0.004164689, 0.004041603, 0.003922156] +) +args1 = (w, e, n, p.retire, p.S, p.g_y, p.vpoint, factor, L_inc_avg_s, PS) + +test_data = [(args1, PS_loop_expected)] + + +@pytest.mark.parametrize( + "args,PS_loop_expected", test_data, ids=["SS/Complete"] +) +def test_PS_1dim_loop(args, PS_loop_expected): + """ + Test of the pensions.PS_1dim_loop() function. + """ + (w, e, n, S_ret, S, g_y, vpoint, factor, L_inc_avg_s, PS) = args + PS_loop = pensions.PS_1dim_loop( + w, e, n, S_ret, S, g_y, vpoint, factor, L_inc_avg_s, PS + ) + assert np.allclose(PS_loop, PS_loop_expected) + + +#####################SS / Complete lifetimes############ +p = Specifications() +p.S = 7 +p.retire = 4 +p.g_y = np.ones(p.T) * 0.03 +j = 1 +lambdas = 1 +w = np.array([1.2, 1.1, 1.21, 1.0, 1.01, 0.99, 0.8]) +e = np.array([1.1, 1.11, 0.9, 0.87, 0.87, 0.7, 0.6]) +n = np.array([0.4, 0.45, 0.4, 0.42, 0.3, 0.2, 0.2]) +omegas = (1 / p.S) * np.ones(p.S) +p.omega_SS = omegas +factor = 2 +points_py_s = np.zeros(p.retire) +L_inc_avg_s = np.zeros(p.retire) +L_inc_avg = np.zeros(1) +PS = np.zeros(p.S) +p.vpoint = 0.4 +PS_expected1 = np.array([0, 0, 0, 0, 0.004164689, 0.004041603, 0.003922156]) +args1 = (w, e, n, j, factor, p) + +####################Incomplete############ +p2 = Specifications() +p2.S = 7 +p2.retire = 4 +p2.omega_SS = omegas +factor = 2 +p2.g_y = np.ones(p2.T) * 0.03 +p2.vpoint = 0.4 +j = 1 +lambdas = 1 +points_py_s = np.zeros(p.retire) +L_inc_avg_s = np.zeros(p.retire) +L_inc_avg = np.zeros(1) +PS = np.zeros(p.S) +w2 = np.array([1.21, 1.0, 1.01, 0.99, 0.8]) +e2 = np.array( + [ + [1.1, 1.1], + [1.11, 1.11], + [0.9, 0.9], + [0.87, 0.87], + [0.87, 0.87], + [0.7, 0.7], + [0.6, 0.6], + ] +) +n2 = np.array([0.4, 0.42, 0.3, 0.2, 0.2]) +p2.w_preTP = np.array([1.05]) +p2.n_preTP = np.array( + [ + [0.4, 0.4], + [0.3, 0.3], + [0.2, 0.2], + [0.3, 0.3], + [0.4, 0.4], + [0.45, 0.45], + [0.5, 0.5], + ] +) +p2.e = e2 +PS_expected2 = np.array([0, 0, 0.003585952, 0.003479971, 0.003377123]) +args2 = (w2, e2, n2, j, factor, p2) + +test_data = [(args1, PS_expected1), (args2, PS_expected2)] + + +@pytest.mark.parametrize( + "args,PS_expected", test_data, ids=["SS/Complete", "Incomplete"] +) +def test_get_PS(args, PS_expected): + """ + Test of the pensions.get_PS() function. + """ + w, e, n, j, factor, p = args + PS = pensions.PS_amount(w, e, n, j, factor, p) + print("PS inside of the test", PS) + assert np.allclose(PS, PS_expected) From 11ea38e0c0a11b15a490bd65c10b7d6e3c0da2e6 Mon Sep 17 00:00:00 2001 From: jdebacker Date: Tue, 9 Jul 2024 22:24:34 -0400 Subject: [PATCH 21/50] test main benefit func --- ogcore/pensions.py | 8 +- tests/test_pensions.py | 167 ++++++++++++++++++++++------------------- 2 files changed, 93 insertions(+), 82 deletions(-) diff --git a/ogcore/pensions.py b/ogcore/pensions.py index 346a1472f..337ba4ccb 100644 --- a/ogcore/pensions.py +++ b/ogcore/pensions.py @@ -64,7 +64,7 @@ def replacement_rate_vals(nssmat, wss, factor_ss, j, p): return theta -def pension_amount(w, n, theta, t, j, shift, method, e, p): +def pension_amount(r, w, n, Y, theta, t, j, shift, method, e, factor, p): """ Calculate public pension benefit amounts for each household. @@ -91,11 +91,11 @@ def pension_amount(w, n, theta, t, j, shift, method, e, p): if p.pension_system == "US-Style Social Security": pension = SS_amount(w, n, theta, t, j, shift, method, e, p) elif p.pension_system == "Defined Benefits": - pension = DB_amount(w, n, t, j, shift, method, e, p) + pension = DB_amount(w, e, n, j, p) elif p.pension_system == "Notional Defined Contribution": - sdf + pension = NDC_amount(w, e, n, r, Y, j, p) elif p.pension_system == "Points System": - sdf + pension = PS_amount(w, e, n, j, factor, p) else: raise ValueError( "pension_system must be one of the following: " diff --git a/tests/test_pensions.py b/tests/test_pensions.py index a89a0a50f..c1387ec03 100644 --- a/tests/test_pensions.py +++ b/tests/test_pensions.py @@ -376,84 +376,95 @@ def test_deriv_NDC(args, d_NDC_expected): assert np.allclose(d_NDC, d_NDC_expected) -# ################pension benefit: DB############ -# p = Specifications() -# p.pension_system = 'DB' -# p.S = 7 -# p.retire = 4 -# p.last_career_yrs = 3 -# p.yr_contr = p.retire -# p.rep_rate_py = 0.2 -# p.g_y = np.ones(p.T) * np.ones(p.T) * 0.03 -# w_db = np.array([1.2, 1.1, 1.21, 1.0, 1.01, 0.99, 0.8]) -# e_db = np.array([1.1, 1.11, 0.9, 0.87, 0.87, 0.7, 0.6]) -# n_db = np.array([0.4, 0.45, 0.4, 0.42, 0.3, 0.2, 0.2]) -# Y = None -# j = 1 -# factor = 2 -# omegas = None -# lambdas = 1 -# pension_expected_db = [0, 0, 0, 0, 0.337864778, 0.327879365, 0.318189065] -# args_pb = (w_db, e_db, n_db, r, Y, lambdas, j, factor) - -# ################pension benefit: NDC############ -# p2 = Specifications() -# p2.pension_system = 'NDC' -# p2.ndc_growth_rate = 'LR GDP' -# p2.dir_growth_rate = 'r' -# p2.S = 4 -# p2.retire = 2 -# w_ndc = np.array([1.2, 1.1, 1.21, 1]) -# e_ndc = np.array([1.1, 1.11, 0.9, 0.87]) -# n_ndc = np.array([0.4, 0.45, 0.4, 0.3]) -# Y = None -# j = 1 -# p2.g_y = 0.03 -# p2.g_n_SS = 0.0 -# r = 0.03 -# factor = 2 -# p2.tau_p = 0.3 -# p2.k_ret = 0.4615 -# p2.mort_rates_SS = np.array([0.01, 0.05, 0.3, 0.4, 1]) -# omegas = None -# lambdas = 1 -# pension_expected_ndc = [0, 0, 0.279756794, 0.271488732] -# args_ndc = (w_ndc, e_ndc, n_ndc, r, Y, lambdas, j, factor) - -# ################pension benefit: PS############ -# p3 = Specifications() -# p3.pension_system = 'PS' -# p3.S = 7 -# p3.retire = 4 -# w_ps = np.array([1.2, 1.1, 1.21, 1.0, 1.01, 0.99, 0.8]) -# e_ps = np.array([1.1, 1.11, 0.9, 0.87, 0.87, 0.7, 0.6]) -# n_ps = np.array([0.4, 0.45, 0.4, 0.42, 0.3, 0.2, 0.2]) -# omegas = (1/p3.S) * np.ones(p3.S) -# p3.omega_SS = omegas -# p3.vpoint = 0.4 -# factor = 2 -# Y = None -# lambdas = 1 -# j = 1 -# p3.g_y = np.ones(p3.T) * 0.03 -# pension_expected_ps = [0, 0, 0, 0, 0.004164689, 0.004041603, 0.003922156] -# args_ps = (w_db, e_db, n_db, r, Y, lambdas, j, factor) - -# test_data = [(args_pb, pension_expected_db), -# (args_ndc, pension_expected_ndc), -# (args_ps, pension_expected_ps)] - -# @pytest.mark.parametrize('classes,args,pension_expected', test_data, -# ids=['DB', 'NDC', 'PS']) -# def test_pension_benefit(classes, args, pension_expected): -# ''' -# Test of pensions.get_pension_benefit -# ''' -# w, e, n, r, Y, lambdas, j, factor = args - -# pension = pensions.pension_amount( -# w, n, theta, t, j, shift, method, e, p) -# assert (np.allclose(pension, pension_expected)) +## For all parameterizations below ## +t = 1 +shift = False +method = "SS" +theta = None + +################pension benefit: DB############ +p = Specifications() +p.pension_system = "Defined Benefits" +p.S = 7 +p.retire = 4 +p.last_career_yrs = 3 +p.yr_contr = p.retire +p.rep_rate_py = 0.2 +p.g_y = np.ones(p.T) * 0.03 +w_db = np.array([1.2, 1.1, 1.21, 1.0, 1.01, 0.99, 0.8]) +e_db = np.array([1.1, 1.11, 0.9, 0.87, 0.87, 0.7, 0.6]) +n_db = np.array([0.4, 0.45, 0.4, 0.42, 0.3, 0.2, 0.2]) +Y = None +j = 1 +factor = 2 +omegas = None +lambdas = 1 +pension_expected_db = [0, 0, 0, 0, 0.337864778, 0.327879365, 0.318189065] +args_pb = (r, w_db, n_db, Y, theta, t, j, shift, method, e_db, factor, p) + +################pension benefit: NDC############ +p2 = Specifications() +p2.pension_system = "Notional Defined Contribution" +p2.ndc_growth_rate = "LR GDP" +p2.dir_growth_rate = "r" +p2.S = 4 +p2.retire = 2 +w_ndc = np.array([1.2, 1.1, 1.21, 1]) +e_ndc = np.array([1.1, 1.11, 0.9, 0.87]) +n_ndc = np.array([0.4, 0.45, 0.4, 0.3]) +Y = None +j = 1 +p2.g_y = np.ones(p2.T) * 0.03 +p2.g_n_SS = 0.0 +r = np.ones(p2.T) * 0.03 +factor = 2 +p2.tau_p = 0.3 +p2.k_ret = 0.4615 +p2.mort_rates_SS = np.array([0.01, 0.05, 0.3, 0.4, 1]) +omegas = None +lambdas = 1 +pension_expected_ndc = [0, 0, 0.27992856, 0.27165542] +args_ndc = (r, w_ndc, n_ndc, Y, theta, t, j, shift, method, e_ndc, factor, p2) + +################pension benefit: PS############ +p3 = Specifications() +p3.pension_system = "Points System" +p3.S = 7 +p3.retire = 4 +w_ps = np.array([1.2, 1.1, 1.21, 1.0, 1.01, 0.99, 0.8]) +e_ps = np.array([1.1, 1.11, 0.9, 0.87, 0.87, 0.7, 0.6]) +n_ps = np.array([0.4, 0.45, 0.4, 0.42, 0.3, 0.2, 0.2]) +omegas = (1 / p3.S) * np.ones(p3.S) +p3.omega_SS = omegas +p3.vpoint = 0.4 +factor = 2 +Y = None +lambdas = 1 +j = 1 +p3.g_y = np.ones(p3.T) * 0.03 +pension_expected_ps = [0, 0, 0, 0, 0.004164689, 0.004041603, 0.003922156] +args_ps = (r, w_db, n_db, Y, theta, t, j, shift, method, e_db, factor, p3) + +test_data = [ + (args_pb, pension_expected_db), + (args_ndc, pension_expected_ndc), + (args_ps, pension_expected_ps), +] + + +@pytest.mark.parametrize( + "args,pension_expected", test_data, ids=["DB", "NDC", "PS"] +) +def test_pension_amount(args, pension_expected): + """ + Test of pensions.pension_amount + """ + r, w, n, Y, theta, t, j, shift, method, e, factor, p = args + + pension = pensions.pension_amount( + r, w, n, Y, theta, t, j, shift, method, e, factor, p + ) + assert np.allclose(pension, pension_expected) ############SS or complete lifetimes############ From 8fa4fe293548940d3f3f6d549f64084fcd79705a Mon Sep 17 00:00:00 2001 From: jdebacker Date: Wed, 10 Jul 2024 05:39:34 -0400 Subject: [PATCH 22/50] start test case for SS --- tests/test_pensions.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/tests/test_pensions.py b/tests/test_pensions.py index c1387ec03..5b4172341 100644 --- a/tests/test_pensions.py +++ b/tests/test_pensions.py @@ -445,15 +445,31 @@ def test_deriv_NDC(args, d_NDC_expected): pension_expected_ps = [0, 0, 0, 0, 0.004164689, 0.004041603, 0.003922156] args_ps = (r, w_db, n_db, Y, theta, t, j, shift, method, e_db, factor, p3) +## SS ## +p4 = Specifications() +p4.pension_system = "US-Style Social Security" +p4.S = 7 +p4.retire = 4 +w_ss = np.array([1.2, 1.1, 1.21, 1.0, 1.01, 0.99, 0.8]) +e_ss = np.array([1.1, 1.11, 0.9, 0.87, 0.87, 0.7, 0.6]) +n_ss = np.array([0.4, 0.45, 0.4, 0.42, 0.3, 0.2, 0.2]) +omegas = (1 / p4.S) * np.ones(p4.S) +theta = 0.4 +p.replacement_rate_adjust = np.ones(p4.T) +pension_expected_ss = [0, 0, 0, 0, 0.004164689, 0.004041603, 0.003922156] +args_ss = (r, w_ss, n_ss, Y, theta, t, j, shift, method, e_ss, factor, p4) + + test_data = [ (args_pb, pension_expected_db), (args_ndc, pension_expected_ndc), (args_ps, pension_expected_ps), + (args_ss, pension_expected_ss), ] @pytest.mark.parametrize( - "args,pension_expected", test_data, ids=["DB", "NDC", "PS"] + "args,pension_expected", test_data, ids=["DB", "NDC", "PS", "SS"] ) def test_pension_amount(args, pension_expected): """ From 54bcb91ea6365460346a392b4d2f9c1836501f4e Mon Sep 17 00:00:00 2001 From: jdebacker Date: Sat, 13 Jul 2024 10:02:18 -0400 Subject: [PATCH 23/50] start to update docs --- docs/book/content/theory/government.md | 109 ++++++++++++- docs/book/content/theory/stationarization.md | 151 +++++++++++++++++++ 2 files changed, 259 insertions(+), 1 deletion(-) diff --git a/docs/book/content/theory/government.md b/docs/book/content/theory/government.md index ee584cc42..48b6ed162 100644 --- a/docs/book/content/theory/government.md +++ b/docs/book/content/theory/government.md @@ -317,7 +317,114 @@ Businesses face a linear tax rate $\tau^{b}_{m,t}$, which can vary by industry a #### Pensions -[TODO: Add description of government pensions and the relevant parameters] +The `OG-Core` model allows for four different systems for public pensions: + +1. U.S.-style social security system +2. Defined benefit system +3. Notional defined contribution system +4. Points system + +These can be selected with the `pension_system` parameter. Accepted values are `US-Style Social Security`, `Defined Benefits`, `Notional Defined Contribution`, `Points System`. We discuss each of these in turn below. + +##### U.S.-style social security system + +##### Defined benefit system + +\subsection{Defined Benefits System:} + +\begin{equation}\label{eqn:db_amount} + P = \biggl[\frac{\sum_{s=R-ny}^{R-1}w_{t}e_{j,s}n_{j,s,t}}{ny}\biggr]\times Cy \times \alpha_{DB} +\end{equation} + +\noindent\noindent where: + \begin{itemize} + \item $ny$ are the number of years over which average earnings are calculated. Note that this could be modified to be based on a certain number of highest earning years rather than a number of the last earnings years before retirement as specified above. Our initial specification will be as above. + \item $Cy$ are the number of years of contributions. In our model, there is no exit from the labor force, so workers will contribute for $R$ years, but $Cy$ could be some number less than $R$ if there is a maximum number of years of contributions one can accrue under the DB system. + \item $\alpha_{DB}$ is the replacement rate per year of contribution. + \end{itemize} + + Given this pension system and the fact that there is only variation in labor supply along the intensive margin (so we don't need to consider changes in $Cy$), the partial derivatives from the household section are given by: + + \begin{equation}\label{eqn:db_deriv} + \frac{\partial \theta_{j,u,t+u-s}}{\partial n_{j,s,t}} = + \begin{cases} + 0 , & \text{if}\ s < R - Cy \\ + w_{t}e_{j,s}\alpha_{DB}\times \frac{Cy}{ny}, & \text{if}\ R - Cy <= s < R \\ + 0, & \text{if}\ s \geq R \\ + \end{cases} + \end{equation} + + +##### Notional defined contribution system + +\subsection{Notional Defined Contribution System:} + +\begin{equation}\label{eqn:ndc_amount} + P = \biggl[\sum_{s=E}^{R-1}\tau^{p}_{t}w_{t}e_{j,s}n_{j,s,t}(1 + g_{NDC,t})^{R-s-1}\biggr]\delta_{R, t} +\end{equation} + +\noindent\noindent where: + \begin{itemize} + \item $\bar{g}_j$ the rate of growth applied to contributions. + \begin{itemize} + \item In the Italian system, $g_{NDC,t}$ is the mean nominal GDP growth rate in the 5 years before seniority + \item i.e., $g_{NDC,t}=\prod_{j=i}^{R-1}\bar{g}_{j}$ + \item This is not $g_y$ - in the SS, it's $(\bar{g}_{y} + \bar{g}_{n})$, in the transition, it's not a function of exogenous variables). + \end{itemize} + \item $\delta_{R, t}$ is the conversion coefficient at time $t$ and its calculation is detailed below. + \end{itemize} + +\begin{equation} + \delta_{R} = (dir_{R} + ind_{R} - k)^{-1} +\end{equation} + +\noindent\noindent where $k$ is an adjustment that takes into account the number of payments per year. $k=0.5 - (6/13n)$, where $n$ is the number of payments per year. Given the monthly payment system, $n=12$ and thus $k=0.4615$. I do not know where the other numbers in $k$ come from - maybe those should be parameters too? + +\begin{equation} + dir_{R, t} = \sum_{u=0}^{E+S-R}\left[\prod_{s=R}^{u}(1-\hat{\rho}_{s, t})\right](1+\hat{g}_{y, t})^{-u} +\end{equation} + +\noindent\noindent where $\hat{\rho}_{s,t}$ are the mortality tables used in the pension system at time $t$ and $\hat{g}_{y, t}$ is the long run expected nominal GDP growth rate used in the pension system at time $t$. + +\begin{equation} + ind_{R} = 0 +\end{equation} + +Given that we model households we set $ind_{R} = 0$. We might want to think about some scaling to account for the fact that households lose members over time, but for now, I think we can ignore the gender/martial/survivor components of the pension formula and just say both members contribute and payouts are related to those contributions as long as the household survives. + + +Given this pension system, the partial derivatives from the household section are given by: + +\begin{equation}\label{eqn:ndc_deriv} + \frac{\partial \theta_{j,u,t+u-s}}{\partial n_{j,s,t}} = + \begin{cases} + \tau^{p}_{t}w_{t}e_{j,s}(1+g_{NDC,t})^{u - s}\delta_{R,t}, & \text{if}\ s Date: Sun, 14 Jul 2024 16:48:23 -0400 Subject: [PATCH 24/50] update docs --- docs/book/content/theory/government.md | 86 +++++++++------ docs/book/content/theory/stationarization.md | 110 +++++-------------- 2 files changed, 79 insertions(+), 117 deletions(-) diff --git a/docs/book/content/theory/government.md b/docs/book/content/theory/government.md index 48b6ed162..db9673a61 100644 --- a/docs/book/content/theory/government.md +++ b/docs/book/content/theory/government.md @@ -328,15 +328,29 @@ These can be selected with the `pension_system` parameter. Accepted values are ##### U.S.-style social security system -##### Defined benefit system + Because individual lifetime income type (and thus their lifecycle earnings profile) are deterministic from birth, the Social Security replacement rate $\theta_j$ in the payroll tax \eqref{EqPayTax} can be thought of as simply an percent of the age $R-1$ labor earnings. This replacement rate, $\theta_j$, is indexed to current average wage $w_t$, and then the ability $j$-specific $\theta_j$ captures the percent consistent with the average replacement amount of each type. In this way, $e_{j,s}$ is included $\theta_j$. $R$ is the age at which the individual becomes eligible to receive the retirement benefit from the payroll tax. + + As mentioned in Section \ref{SecIndProb} and in Table \ref{TabExogVars}, we calibrate the retirement age to be $R = E+s = 65$ and the payroll tax rate to $\tau^P=0.15$. To calibrate the payroll tax replacement rates $\{\theta_j\}_{j=1}^J$, first we solve for the steady state equilibrium without the retirement benefits. Then, we calculate the monthly level of income for each ability type in dollars in our simulated model. We use the 2014 statutory formula to calculate the monthly retirement benefits or ``primary insurance amount'' (PIA) using the worker's earnings from the year prior to retirement in place of the average index of monthly earnings (AIME) for each ability type. By multiplying the PIA by the average effective labor participation rate and dividing by the monthly level of income, we generate the replacement rates for each ability type. We cap the replacement rates so that the maximum monthly retirement rate is thirty thousand dollars. In reality the cap is much lower than this, but in our model all wage income is subject to the payroll tax and this cap binds. + + With this set of replacement rates in hand, we resolve the model including retirement benefits and repeat the calibration. We do this until the replacement rates assumed when the simulation is performed match those calculated from the statutory formula. -\subsection{Defined Benefits System:} + The statutory formula we use for PIA is as follows: + \begin{itemize} + \item 90\% of AIME for AIME less than \$749. + \item 32\% of addition AIME up to \$4519. + \item 15\% of addition AIME up to a maximum payment of \$30,000 + \end{itemize} -\begin{equation}\label{eqn:db_amount} + Our seven calibrated replacement rate values are $\theta_1=0.1332$, $\theta_2=0.1368$, $\theta_3=0.1368$, $\theta_4=0.1368$, $\theta_5=0.1368$, $\theta_6=0.1368$, and $\theta_7=0.1368$. + +##### Defined benefit system + + ```{math} + :label: eqn:db_amount P = \biggl[\frac{\sum_{s=R-ny}^{R-1}w_{t}e_{j,s}n_{j,s,t}}{ny}\biggr]\times Cy \times \alpha_{DB} -\end{equation} + ``` -\noindent\noindent where: +where: \begin{itemize} \item $ny$ are the number of years over which average earnings are calculated. Note that this could be modified to be based on a certain number of highest earning years rather than a number of the last earnings years before retirement as specified above. Our initial specification will be as above. \item $Cy$ are the number of years of contributions. In our model, there is no exit from the labor force, so workers will contribute for $R$ years, but $Cy$ could be some number less than $R$ if there is a maximum number of years of contributions one can accrue under the DB system. @@ -345,25 +359,24 @@ These can be selected with the `pension_system` parameter. Accepted values are Given this pension system and the fact that there is only variation in labor supply along the intensive margin (so we don't need to consider changes in $Cy$), the partial derivatives from the household section are given by: - \begin{equation}\label{eqn:db_deriv} + ```{math} + :label: eqn:db_deriv \frac{\partial \theta_{j,u,t+u-s}}{\partial n_{j,s,t}} = \begin{cases} 0 , & \text{if}\ s < R - Cy \\ w_{t}e_{j,s}\alpha_{DB}\times \frac{Cy}{ny}, & \text{if}\ R - Cy <= s < R \\ 0, & \text{if}\ s \geq R \\ \end{cases} - \end{equation} - + ``` ##### Notional defined contribution system -\subsection{Notional Defined Contribution System:} - -\begin{equation}\label{eqn:ndc_amount} - P = \biggl[\sum_{s=E}^{R-1}\tau^{p}_{t}w_{t}e_{j,s}n_{j,s,t}(1 + g_{NDC,t})^{R-s-1}\biggr]\delta_{R, t} -\end{equation} + ```{math} + :label: eqn:ndc_amount + P = \biggl[\sum_{s=E}^{R-1}\tau^{p}_{t}w_{t}e_{j,s}n_{j,s,t}(1 + g_{NDC,t})^{R-s-1}\biggr]\delta_{R, t} + ``` -\noindent\noindent where: +where: \begin{itemize} \item $\bar{g}_j$ the rate of growth applied to contributions. \begin{itemize} @@ -374,42 +387,41 @@ These can be selected with the `pension_system` parameter. Accepted values are \item $\delta_{R, t}$ is the conversion coefficient at time $t$ and its calculation is detailed below. \end{itemize} -\begin{equation} + ```{math} \delta_{R} = (dir_{R} + ind_{R} - k)^{-1} -\end{equation} + ``` -\noindent\noindent where $k$ is an adjustment that takes into account the number of payments per year. $k=0.5 - (6/13n)$, where $n$ is the number of payments per year. Given the monthly payment system, $n=12$ and thus $k=0.4615$. I do not know where the other numbers in $k$ come from - maybe those should be parameters too? +where $k$ is an adjustment that takes into account the number of payments per year. $k=0.5 - (6/13n)$, where $n$ is the number of payments per year. Given the monthly payment system, $n=12$ and thus $k=0.4615$. I do not know where the other numbers in $k$ come from - maybe those should be parameters too? -\begin{equation} + ```{math} dir_{R, t} = \sum_{u=0}^{E+S-R}\left[\prod_{s=R}^{u}(1-\hat{\rho}_{s, t})\right](1+\hat{g}_{y, t})^{-u} -\end{equation} + ``` -\noindent\noindent where $\hat{\rho}_{s,t}$ are the mortality tables used in the pension system at time $t$ and $\hat{g}_{y, t}$ is the long run expected nominal GDP growth rate used in the pension system at time $t$. +where $\hat{\rho}_{s,t}$ are the mortality tables used in the pension system at time $t$ and $\hat{g}_{y, t}$ is the long run expected nominal GDP growth rate used in the pension system at time $t$. -\begin{equation} + ```{math} ind_{R} = 0 -\end{equation} + ``` Given that we model households we set $ind_{R} = 0$. We might want to think about some scaling to account for the fact that households lose members over time, but for now, I think we can ignore the gender/martial/survivor components of the pension formula and just say both members contribute and payouts are related to those contributions as long as the household survives. - Given this pension system, the partial derivatives from the household section are given by: -\begin{equation}\label{eqn:ndc_deriv} + ```{math} + :label: eqn:ndc_deriv \frac{\partial \theta_{j,u,t+u-s}}{\partial n_{j,s,t}} = \begin{cases} \tau^{p}_{t}w_{t}e_{j,s}(1+g_{NDC,t})^{u - s}\delta_{R,t}, & \text{if}\ s Date: Tue, 16 Jul 2024 12:51:32 -0400 Subject: [PATCH 25/50] add other taxes to revenue eq'n --- docs/book/content/theory/government.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/book/content/theory/government.md b/docs/book/content/theory/government.md index db9673a61..2e1e646aa 100644 --- a/docs/book/content/theory/government.md +++ b/docs/book/content/theory/government.md @@ -538,8 +538,10 @@ Given this pension system, the partial derivatives from the household section ar ```{math} :label: EqUnbalGBCgovRev - Rev_t &= \underbrace{\sum_{m=1}^M\Bigl[\tau^{corp}_{m,t}\bigl(p_{m,t}Y_{m,t} - w_t L_t\bigr) - \tau^{corp}_{m,t}\delta^\tau_{m,t}K_{m,t} - \tau^{inv}_{m,t}\delta_{M,t}K_{m,t}\Bigr]}_{\text{corporate tax revenue}} \\ - &\quad + \underbrace{\sum_{s=E+1}^{E+S}\sum_{j=1}^J\lambda_j\omega_{s,t}\tau^{etr}_{s,t}\left(x_{j,s,t},y_{j,s,t}\right)\bigl(x_{j,s,t} + y_{j,s,t}\bigr)}_{\text{household tax revenue}} \quad\forall t + Rev_t &= \underbrace{\sum_{m=1}^M\Bigl[\tau^{corp}_{m,t}\bigl(p_{m,t}Y_{m,t} - w_t L_t\bigr) - \tau^{corp}_{m,t}\delta^\tau_{m,t}K_{m,t} - \tau^{inv}_{m,t}\delta_{M,t}K_{m,t}\Bigr]}_{\text{corporate income tax revenue}} \\ + &\quad + \underbrace{\sum_{s=E+1}^{E+S}\sum_{j=1}^J\lambda_j\omega_{s,t}\tau^{etr}_{s,t}\left(x_{j,s,t},y_{j,s,t}\right)\bigl(x_{j,s,t} + y_{j,s,t}\bigr)}_{\text{household income tax revenue}} \\ + &\quad + \underbrace{\sum_{s=E+1}^{E+S}\sum_{j=1}^J\sum_{i=1}^I\lambda_j\omega_{s,t}\tau^{c}_{i,t}p{i,t}c_{i,j,s,t}}_{\text{consumption tax revenue}} \\ + &\quad + \underbrace{\sum_{s=E+1}^{E+S}\sum_{j=1}^J\lambda_j\omega_{s,t}\tau^{w}_{t}b_{j,s,t}}_{\text{wealth tax revenue}} \quad\forall t ``` where household labor income is defined as $x_{j,s,t}\equiv w_t e_{j,s}n_{j,s,t}$ and capital income $y_{j,s,t}\equiv r_{p,t} b_{j,s,t}$. From cca3b7e7ce63f5ac4558428f2124026ce9058278 Mon Sep 17 00:00:00 2001 From: jdebacker Date: Tue, 16 Jul 2024 12:53:47 -0400 Subject: [PATCH 26/50] do same for statinoarized eq'n --- docs/book/content/theory/stationarization.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/book/content/theory/stationarization.md b/docs/book/content/theory/stationarization.md index 2303e56cc..cdb8a64b7 100644 --- a/docs/book/content/theory/stationarization.md +++ b/docs/book/content/theory/stationarization.md @@ -209,7 +209,9 @@ Where $\hat{MTR}^w_{j,s,t} = \left( \frac{h^{w}p_{w}\hat{b}_{j,s,t}}{(\hat{b}_{j ```{math} :label: EqStnrzGovRev \hat{Rev}_t &= \underbrace{\sum_{m=1}^M\Bigl[\tau^{corp}_{m,t}\bigl(p_{m,t}\hat{Y}_{m,t} - \hat{w}_t\hat{L}_t\bigr) - \tau^{corp}_{m,t}\delta^\tau_{m,t}\hat{K}_{m,t} - \tau^{inv}_{m,t}\hat{I}_{m,t}\Bigr]}_{\text{corporate tax revenue}} \\ - &\qquad + \underbrace{\sum_{s=E+1}^{E+S}\sum_{j=1}^J\lambda_j\hat{\omega}_{s,t}\tau^{etr}_{s,t}\left(\hat{x}_{j,s,t},\hat{y}_{j,s,t}\right)\bigl(\hat{x}_{j,s,t} + \hat{y}_{j,s,t}\bigr)}_{\text{household tax revenue}} \quad\forall t + &\qquad + \underbrace{\sum_{s=E+1}^{E+S}\sum_{j=1}^J\lambda_j\hat{\omega}_{s,t}\tau^{etr}_{s,t}\left(\hat{x}_{j,s,t},\hat{y}_{j,s,t}\right)\bigl(\hat{x}_{j,s,t} + \hat{y}_{j,s,t}\bigr)}_{\text{household tax revenue}} \\ + &\quad + \underbrace{\sum_{s=E+1}^{E+S}\sum_{j=1}^J\sum_{i=1}^I\lambda_j\omega_{s,t}\tau^{c}_{i,t}p{i,t}\hat{c}_{i,j,s,t}}_{\text{consumption tax revenue}} \\ + &\quad + \underbrace{\sum_{s=E+1}^{E+S}\sum_{j=1}^J\lambda_j\omega_{s,t}\tau^{w}_{t}\hat{b}_{j,s,t}}_{\text{wealth tax revenue}} \quad\forall t \quad\forall t ``` Every term in the government budget constraint {eq}`EqUnbalGBCbudgConstr` is growing at both the productivity growth rate and the population growth rate, so we stationarize it by dividing both sides by $e^{g_y t}\tilde{N}_t$. We also have to multiply and divide the next period debt term $D_{t+1}$ by $e^{g_y(t+1)}\tilde{N}_{t+1}$, leaving the term $e^{g_y}(1 + \tilde{g}_{n,t+1})$. From 39bf423d00b6ca693101c319ac578778d68cc7ff Mon Sep 17 00:00:00 2001 From: jdebacker Date: Tue, 16 Jul 2024 12:58:14 -0400 Subject: [PATCH 27/50] break out pensions in GBC --- docs/book/content/theory/government.md | 2 +- docs/book/content/theory/stationarization.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/book/content/theory/government.md b/docs/book/content/theory/government.md index 2e1e646aa..d689c508d 100644 --- a/docs/book/content/theory/government.md +++ b/docs/book/content/theory/government.md @@ -553,7 +553,7 @@ Given this pension system, the partial derivatives from the household section ar ```{math} :label: EqUnbalGBCbudgConstr - D_{t+1} + Rev_t = (1 + r_{gov,t})D_t + G_t + I_{g,t} + TR_t + UBI_t \quad\forall t + D_{t+1} + Rev_t = (1 + r_{gov,t})D_t + G_t + I_{g,t} + Pensions_t + TR_t + UBI_t \quad\forall t ``` where $r_{gov,t}$ is the interest rate paid by the government defined in equation {eq}`EqUnbalGBC_rate_wedge` below, $G_{t}$ is government spending on public goods, $I_{g,t}$ is total government spending on infrastructure investment, $TR_{t}$ are non-pension government transfers, and $UBI_t$ is the total UBI transfer outlays across households in time $t$. diff --git a/docs/book/content/theory/stationarization.md b/docs/book/content/theory/stationarization.md index cdb8a64b7..cfde149b6 100644 --- a/docs/book/content/theory/stationarization.md +++ b/docs/book/content/theory/stationarization.md @@ -211,14 +211,14 @@ Where $\hat{MTR}^w_{j,s,t} = \left( \frac{h^{w}p_{w}\hat{b}_{j,s,t}}{(\hat{b}_{j \hat{Rev}_t &= \underbrace{\sum_{m=1}^M\Bigl[\tau^{corp}_{m,t}\bigl(p_{m,t}\hat{Y}_{m,t} - \hat{w}_t\hat{L}_t\bigr) - \tau^{corp}_{m,t}\delta^\tau_{m,t}\hat{K}_{m,t} - \tau^{inv}_{m,t}\hat{I}_{m,t}\Bigr]}_{\text{corporate tax revenue}} \\ &\qquad + \underbrace{\sum_{s=E+1}^{E+S}\sum_{j=1}^J\lambda_j\hat{\omega}_{s,t}\tau^{etr}_{s,t}\left(\hat{x}_{j,s,t},\hat{y}_{j,s,t}\right)\bigl(\hat{x}_{j,s,t} + \hat{y}_{j,s,t}\bigr)}_{\text{household tax revenue}} \\ &\quad + \underbrace{\sum_{s=E+1}^{E+S}\sum_{j=1}^J\sum_{i=1}^I\lambda_j\omega_{s,t}\tau^{c}_{i,t}p{i,t}\hat{c}_{i,j,s,t}}_{\text{consumption tax revenue}} \\ - &\quad + \underbrace{\sum_{s=E+1}^{E+S}\sum_{j=1}^J\lambda_j\omega_{s,t}\tau^{w}_{t}\hat{b}_{j,s,t}}_{\text{wealth tax revenue}} \quad\forall t \quad\forall t + &\quad + \underbrace{\sum_{s=E+1}^{E+S}\sum_{j=1}^J\lambda_j\omega_{s,t}\tau^{w}_{t}\hat{b}_{j,s,t}}_{\text{wealth tax revenue}} \quad\forall t ``` Every term in the government budget constraint {eq}`EqUnbalGBCbudgConstr` is growing at both the productivity growth rate and the population growth rate, so we stationarize it by dividing both sides by $e^{g_y t}\tilde{N}_t$. We also have to multiply and divide the next period debt term $D_{t+1}$ by $e^{g_y(t+1)}\tilde{N}_{t+1}$, leaving the term $e^{g_y}(1 + \tilde{g}_{n,t+1})$. ```{math} :label: EqStnrzGovBC - e^{g_y}\left(1 + \tilde{g}_{n,t+1}\right)\hat{D}_{t+1} + \hat{Rev}_t = (1 + r_{gov,t})\hat{D}_t + \hat{G}_t + \hat{I}_{g,t} + \hat{TR}_t + \hat{UBI}_t \quad\forall t + e^{g_y}\left(1 + \tilde{g}_{n,t+1}\right)\hat{D}_{t+1} + \hat{Rev}_t = (1 + r_{gov,t})\hat{D}_t + \hat{G}_t + \hat{I}_{g,t} + \hat{Pensions}_t + \hat{TR}_t + \hat{UBI}_t \quad\forall t ``` The stationarized versions of the rule for total government infrastructure investment spending $I_{g,t}$ in {eq}`EqUnbalGBC_Igt` and the rule for government investment spending in each industry in {eq}`EqUnbalGBC_Igt` are found by dividing both sides of the respective equations by $e^{g_y t}\tilde{N}_t$. From e58df575836dcd745958c5127ce888d6fb0a03c5 Mon Sep 17 00:00:00 2001 From: jdebacker Date: Tue, 16 Jul 2024 13:49:45 -0400 Subject: [PATCH 28/50] describe SS system in detail --- docs/book/content/theory/government.md | 43 ++++++++++++++++++++------ ogcore/pensions.py | 2 +- 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/docs/book/content/theory/government.md b/docs/book/content/theory/government.md index d689c508d..a420bf172 100644 --- a/docs/book/content/theory/government.md +++ b/docs/book/content/theory/government.md @@ -326,22 +326,45 @@ The `OG-Core` model allows for four different systems for public pensions: These can be selected with the `pension_system` parameter. Accepted values are `US-Style Social Security`, `Defined Benefits`, `Notional Defined Contribution`, `Points System`. We discuss each of these in turn below. +For all systems, $R$ represents the age at which the individual becomes eligible to receive the government provided retirement benefit. + ##### U.S.-style social security system - Because individual lifetime income type (and thus their lifecycle earnings profile) are deterministic from birth, the Social Security replacement rate $\theta_j$ in the payroll tax \eqref{EqPayTax} can be thought of as simply an percent of the age $R-1$ labor earnings. This replacement rate, $\theta_j$, is indexed to current average wage $w_t$, and then the ability $j$-specific $\theta_j$ captures the percent consistent with the average replacement amount of each type. In this way, $e_{j,s}$ is included $\theta_j$. $R$ is the age at which the individual becomes eligible to receive the retirement benefit from the payroll tax. +Under the U.S.-style social security system, households over age $R$ received a pension amount that is a function of their earnings history. The earings history includes the highest earning `AIME_num_periods` prior to retirement (which OG-Core assumes happens at age $R$). This history determines the Average Indexed Monthly Earnings (AIME): - As mentioned in Section \ref{SecIndProb} and in Table \ref{TabExogVars}, we calibrate the retirement age to be $R = E+s = 65$ and the payroll tax rate to $\tau^P=0.15$. To calibrate the payroll tax replacement rates $\{\theta_j\}_{j=1}^J$, first we solve for the steady state equilibrium without the retirement benefits. Then, we calculate the monthly level of income for each ability type in dollars in our simulated model. We use the 2014 statutory formula to calculate the monthly retirement benefits or ``primary insurance amount'' (PIA) using the worker's earnings from the year prior to retirement in place of the average index of monthly earnings (AIME) for each ability type. By multiplying the PIA by the average effective labor participation rate and dividing by the monthly level of income, we generate the replacement rates for each ability type. We cap the replacement rates so that the maximum monthly retirement rate is thirty thousand dollars. In reality the cap is much lower than this, but in our model all wage income is subject to the payroll tax and this cap binds. + ```{math} + :label: eqn:AIME + AIME_{j,R,t+R} = {\sum_{u=0}^{AIMEper} w_{t+u}e_{j,u,t+u}n{j,u,t+u}}{ 12 * AIMEper} + ``` - With this set of replacement rates in hand, we resolve the model including retirement benefits and repeat the calibration. We do this until the replacement rates assumed when the simulation is performed match those calculated from the statutory formula. + The AIME in turn, determines a household's (PIA) based on three rates and brackets: - The statutory formula we use for PIA is as follows: - \begin{itemize} - \item 90\% of AIME for AIME less than \$749. - \item 32\% of addition AIME up to \$4519. - \item 15\% of addition AIME up to a maximum payment of \$30,000 - \end{itemize} + ```{math} + :label: eqn:PIA + PIAbase_{j,R,t+R} = + \begin{cases} + PIArate_1 \times AIME_{j,R,t+R}, \text{for} AIME_{j,R,t+R} \leq AIMEbkt_1 \\ + PIArate_2 \times AIME_{j,R,t+R}, \text{for} AIMEbkt_1 < AIME_{j,R,t+R} \leq AIMEbkt_2 \\ + PIArate_3 \times AIME_{j,R,t+R}, \text{for} AIMEbkt_2 < AIME_{j,R,t+R} \\ + \end{cases} + ``` + The PIA is then capped at a maximum, set by the parameter `PIA_maxpayment`, $PIA_{j,R,t+R} = max{PIAbase_{j,R,t+R}, \text{PIA max payment amount}}$. + + The replacement rate, $\theta_j$ is then calculated as annual earnings, with an adjustment for the wage rate.: + + ```{math} + :label: eqn:theta + \theta_{j,R,t+R} = \frac{PIA_{j,R,t+R}} \times 12}{factor \times w_{t+R}} + ``` + Note that $aIME_{j,R,t+R}$ is a function of each households' earning history, but their choice of earning may depend on their retirement benefit. Solving this exactly would introduce and additional fixed point problem in both the steady state solution and in the time path solution. The latter would be extremely computationally taxing. Therefore, we make the simplification of determining the AIME for each type $j$ using the steady state solution and earnings for type $j$ in the steady state. This leads to some approximation error, but because $\theta_j$ is adjusted by the current wage rate, this approximation error is minized. + + The pension amount for households under the US-style social security system is then: + + ```{math} + :label: eqn:ss_pension + pension_{j,s,t} = \theta_j \times w_t \forall s > R + ``` - Our seven calibrated replacement rate values are $\theta_1=0.1332$, $\theta_2=0.1368$, $\theta_3=0.1368$, $\theta_4=0.1368$, $\theta_5=0.1368$, $\theta_6=0.1368$, and $\theta_7=0.1368$. ##### Defined benefit system diff --git a/ogcore/pensions.py b/ogcore/pensions.py index 337ba4ccb..fc354c0ad 100644 --- a/ogcore/pensions.py +++ b/ogcore/pensions.py @@ -56,7 +56,7 @@ def replacement_rate_vals(nssmat, wss, factor_ss, j, p): + p.PIA_rate_bkt_2 * (p.AIME_bkt_2 - p.AIME_bkt_1) + p.PIA_rate_bkt_3 * (AIME[j] - p.AIME_bkt_2) ) - # Set the maximum monthly replacment rate from SS benefits tables + # Set the maximum monthly replacement rate from SS benefits tables PIA[PIA > p.PIA_maxpayment] = p.PIA_maxpayment if p.PIA_minpayment != 0.0: PIA[PIA < p.PIA_minpayment] = p.PIA_minpayment From 1b4f0428755c311858c7c6e024a0e618e5437614 Mon Sep 17 00:00:00 2001 From: jdebacker Date: Tue, 16 Jul 2024 14:18:54 -0400 Subject: [PATCH 29/50] update docs --- docs/book/content/theory/government.md | 44 ++++++++++++++------------ ogcore/default_parameters.json | 22 ++++++------- 2 files changed, 34 insertions(+), 32 deletions(-) diff --git a/docs/book/content/theory/government.md b/docs/book/content/theory/government.md index a420bf172..32ba13d3a 100644 --- a/docs/book/content/theory/government.md +++ b/docs/book/content/theory/government.md @@ -368,14 +368,16 @@ Under the U.S.-style social security system, households over age $R$ received a ##### Defined benefit system +The defined benefit system pension amount is given as: + ```{math} - :label: eqn:db_amount - P = \biggl[\frac{\sum_{s=R-ny}^{R-1}w_{t}e_{j,s}n_{j,s,t}}{ny}\biggr]\times Cy \times \alpha_{DB} + :label: eqn:db_pension + pension{j,s,t} = \biggl[\frac{\sum_{s=R-ny}^{R-1}w_{t}e_{j,s,t}n_{j,s,t}}{ny}\biggr]\times Cy \times \alpha_{DB} \forall s > R ``` where: \begin{itemize} - \item $ny$ are the number of years over which average earnings are calculated. Note that this could be modified to be based on a certain number of highest earning years rather than a number of the last earnings years before retirement as specified above. Our initial specification will be as above. + \item $ny$ are the number of years over which average earnings are calculated \item $Cy$ are the number of years of contributions. In our model, there is no exit from the labor force, so workers will contribute for $R$ years, but $Cy$ could be some number less than $R$ if there is a maximum number of years of contributions one can accrue under the DB system. \item $\alpha_{DB}$ is the replacement rate per year of contribution. \end{itemize} @@ -394,18 +396,21 @@ where: ##### Notional defined contribution system +The pension amount under a notional defined contribution system is given as: + ```{math} - :label: eqn:ndc_amount - P = \biggl[\sum_{s=E}^{R-1}\tau^{p}_{t}w_{t}e_{j,s}n_{j,s,t}(1 + g_{NDC,t})^{R-s-1}\biggr]\delta_{R, t} + :label: eqn:ndc_pension + pension{j,s,t} = \biggl[\sum_{s=E}^{R-1}\tau^{p}_{t}w_{t}e_{j,s,t}n_{j,s,t}(1 + g_{NDC,t})^{R-s-1}\biggr]\delta_{R, t} \forall s > R ``` where: \begin{itemize} - \item $\bar{g}_j$ the rate of growth applied to contributions. + \item $\tau^p$ is the pension contribution tax rate + \item $g_{NDC,t}$ the rate of growth applied to contributions. \begin{itemize} - \item In the Italian system, $g_{NDC,t}$ is the mean nominal GDP growth rate in the 5 years before seniority + \item For example, In the Italian system, $g_{NDC,t}$ is the mean nominal GDP growth rate in the 5 years before seniority \item i.e., $g_{NDC,t}=\prod_{j=i}^{R-1}\bar{g}_{j}$ - \item This is not $g_y$ - in the SS, it's $(\bar{g}_{y} + \bar{g}_{n})$, in the transition, it's not a function of exogenous variables). + \item Note, this is not $g_y$. In the SS, it's $(\bar{g}_{y} + \bar{g}_{n})$, and in the transition path equilibrium, it's not a function of exogenous variables since the growth rate of nominal GDP is endogenous. \end{itemize} \item $\delta_{R, t}$ is the conversion coefficient at time $t$ and its calculation is detailed below. \end{itemize} @@ -414,7 +419,9 @@ where: \delta_{R} = (dir_{R} + ind_{R} - k)^{-1} ``` -where $k$ is an adjustment that takes into account the number of payments per year. $k=0.5 - (6/13n)$, where $n$ is the number of payments per year. Given the monthly payment system, $n=12$ and thus $k=0.4615$. I do not know where the other numbers in $k$ come from - maybe those should be parameters too? +where $k$ is an adjustment that takes into account the number of payments per year. In particular, $k=0.5 - (6/13n)$, where $n$ is the number of payments per year. So if the payments are made monthly, $n=12$ and thus $k=0.4615$. + +The $dir_{R, t}$ term is an adjustment to make the payments actuarially fair given mortality risk: ```{math} dir_{R, t} = \sum_{u=0}^{E+S-R}\left[\prod_{s=R}^{u}(1-\hat{\rho}_{s, t})\right](1+\hat{g}_{y, t})^{-u} @@ -422,11 +429,7 @@ where $k$ is an adjustment that takes into account the number of payments per ye where $\hat{\rho}_{s,t}$ are the mortality tables used in the pension system at time $t$ and $\hat{g}_{y, t}$ is the long run expected nominal GDP growth rate used in the pension system at time $t$. - ```{math} - ind_{R} = 0 - ``` - -Given that we model households we set $ind_{R} = 0$. We might want to think about some scaling to account for the fact that households lose members over time, but for now, I think we can ignore the gender/martial/survivor components of the pension formula and just say both members contribute and payouts are related to those contributions as long as the household survives. +Finally, $ind_{R}$ is an adjustment for survivor benefits. Since we model households (and not individuals), we set $ind_{R} = 0$ by default. This can be changed with the parameter `indR` if one would like to account for the fact that households lose members over time. Given this pension system, the partial derivatives from the household section are given by: @@ -441,17 +444,16 @@ Given this pension system, the partial derivatives from the household section ar ##### Points system +Under a points system, the pension amount is given as: + ```{math} - :label: eqn:ps_amount - P = \sum_{s=E}^{R-1}w_{t}e_{j,s}n_{j,s,t}\times v_{t} + :label: eqn:ps_pension + pension{j,s,t} = \sum_{s=E}^{R-1}w_{t}e_{j,s,t}n_{j,s,t}\times v_{t} \forall s > R ``` -\noindent\noindent where: - \begin{itemize} - \item $v_{t}$ is the value of a point at time $t$ - \end{itemize} +where $v_{t}$ is the value of a point at time $t$ - Given this pension system, the partial derivatives from the household section are given by: +Given this pension system, the partial derivatives from the household section are given by: ```{math} :label: eqn:ps_deriv diff --git a/ogcore/default_parameters.json b/ogcore/default_parameters.json index 233556fdf..9187c21fc 100644 --- a/ogcore/default_parameters.json +++ b/ogcore/default_parameters.json @@ -2921,8 +2921,8 @@ } }, "tau_p": { - "title": "TODO: fill in description", - "description": "TODO: fill in description", + "title": "Pension system contribution tax rate under a notional defined contribution system.", + "description": "Pension system contribution tax rate under a notional defined contribution system.", "section_1": "Fiscal Policy Parameters", "section_2": "Government Pension Parameters", "notes": "", @@ -2940,11 +2940,11 @@ } }, "k_ret": { - "title": "TODO: fill in description", - "description": "TODO: fill in description", + "title": "Adjustment for frequency of pension payments under a notional defined contribution system.", + "description": "Adjustment for frequency of pension payments under a notional defined contribution system.", "section_1": "Fiscal Policy Parameters", "section_2": "Government Pension Parameters", - "notes": "", + "notes": "k = 0.5 - (6/13n), where n is the number of payments per year", "type": "float", "value": [ { @@ -2978,8 +2978,8 @@ } }, "vpoint": { - "title": "TODO: fill in description", - "description": "TODO: fill in description", + "title": "The value of a point under a points system pension.", + "description": "The value of a point under a points system pension.", "section_1": "Fiscal Policy Parameters", "section_2": "Government Pension Parameters", "notes": "", @@ -3016,11 +3016,11 @@ } }, "yr_contr": { - "title": "Number of years used to compute TODO: complete", - "description": "Number of years used to compute TODO: complete.", + "title": "Number of years of contributions made to defined benefits pension system.", + "description": "Number of years of contributions made to defined benefits pension system.", "section_1": "Fiscal Policy Parameters", "section_2": "Government Pension Parameters", - "notes": "TODO: add note about how compute works", + "notes": "Since there is not exit from the labor force in the model, the number of years of contributions is set exogenously.", "type": "int", "value": [ { @@ -3029,7 +3029,7 @@ ], "validators": { "range": { - "min": 1, + "min": 0, "max": "retirement_age" } } From 262b21441b6ae915d9dd0764344dc33c96f58bcb Mon Sep 17 00:00:00 2001 From: jdebacker Date: Wed, 17 Jul 2024 08:25:00 -0400 Subject: [PATCH 30/50] consolidate parameters for average earnings --- ogcore/default_parameters.json | 27 ++++----------------------- ogcore/pensions.py | 34 +++++++++++++++++----------------- 2 files changed, 21 insertions(+), 40 deletions(-) diff --git a/ogcore/default_parameters.json b/ogcore/default_parameters.json index 9187c21fc..951aaa78d 100644 --- a/ogcore/default_parameters.json +++ b/ogcore/default_parameters.json @@ -2996,25 +2996,6 @@ } } }, - "last_career_years": { - "title": "Number of years used to compute TODO: complete", - "description": "Number of years used to compute TODO: complete.", - "section_1": "Fiscal Policy Parameters", - "section_2": "Government Pension Parameters", - "notes": "TODO: add note about how compute works", - "type": "int", - "value": [ - { - "value": 10 - } - ], - "validators": { - "range": { - "min": 1, - "max": "S" - } - } - }, "yr_contr": { "title": "Number of years of contributions made to defined benefits pension system.", "description": "Number of years of contributions made to defined benefits pension system.", @@ -3034,12 +3015,12 @@ } } }, - "AIME_num_years": { - "title": "Number of years used to compute average index monthly earnings (AIME)", - "description": "Number of years used to compute average index monthly earnings (AIME).", + "avg_earn_num_years": { + "title": "Number of years used to compute average earnings for pension benefits.", + "description": "Number of years used to compute average earnings for pension benefits.", "section_1": "Fiscal Policy Parameters", "section_2": "Government Pension Parameters", - "notes": "AIME is computed using average earnings from the highest earnings years for the number of years specified here.", + "notes": "US-styel Social Security AIME is computed using average earnings from the highest earnings years for the number of years specified here.", "type": "int", "value": [ { diff --git a/ogcore/pensions.py b/ogcore/pensions.py index fc354c0ad..5040cebd3 100644 --- a/ogcore/pensions.py +++ b/ogcore/pensions.py @@ -30,7 +30,7 @@ def replacement_rate_vals(nssmat, wss, factor_ss, j, p): else: e = np.squeeze(p.e[-1, :, :]) # Only computes using SS earnings # adjust number of calendar years AIME computed from int model periods - equiv_periods = int(round((p.S / 80.0) * p.AIME_num_years)) - 1 + equiv_periods = int(round((p.S / 80.0) * p.avg_earn_num_years)) - 1 if e.ndim == 2: dim2 = e.shape[1] else: @@ -189,7 +189,7 @@ def DB_amount(w, e, n, j, p): Calculate public pension from a defined benefits system. """ L_inc_avg = np.zeros(0) - L_inc_avg_s = np.zeros(p.last_career_yrs) + L_inc_avg_s = np.zeros(p.avg_earn_num_years) if n.shape[0] < p.S: per_rmn = n.shape[0] @@ -212,7 +212,7 @@ def DB_amount(w, e, n, j, p): L_inc_avg_s, L_inc_avg, DB, - p.last_career_yrs, + p.avg_earn_num_years, p.rep_rate_py, p.yr_contr, ) @@ -232,7 +232,7 @@ def DB_amount(w, e, n, j, p): L_inc_avg_s, L_inc_avg, DB, - p.last_career_yrs, + p.avg_earn_num_years, p.rep_rate_py, p.yr_contr, ) @@ -240,7 +240,7 @@ def DB_amount(w, e, n, j, p): elif np.ndim(n) == 2: DB_sj = np.zeros((p.retire, p.J)) DB = np.zeros((p.S, p.J)) - L_inc_avg_sj = np.zeros((p.last_career_yrs, p.J)) + L_inc_avg_sj = np.zeros((p.avg_earn_num_years, p.J)) DB = DB_2dim_loop( w, e, @@ -251,7 +251,7 @@ def DB_amount(w, e, n, j, p): L_inc_avg_sj, L_inc_avg, DB, - p.last_career_yrs, + p.avg_earn_num_years, p.rep_rate_py, p.yr_contr, ) @@ -453,7 +453,7 @@ def deriv_DB(w, e, per_rmn, p): p.S, p.retire, per_rmn, - p.last_career_yrs, + p.avg_earn_num_years, p.rep_rate_py, p.yr_contr, ) @@ -549,12 +549,12 @@ def delta_ret(r, Y, p): @numba.jit def deriv_DB_loop( - w, e, S, S_ret, per_rmn, last_career_yrs, rep_rate_py, yr_contr + w, e, S, S_ret, per_rmn, avg_earn_num_years, rep_rate_py, yr_contr ): d_theta = np.zeros(per_rmn) num_per_retire = S - S_ret for s in range(per_rmn): - d_theta[s] = w[s] * e[s] * rep_rate_py * (yr_contr / last_career_yrs) + d_theta[s] = w[s] * e[s] * rep_rate_py * (yr_contr / avg_earn_num_years) d_theta[-num_per_retire:] = 0.0 return d_theta @@ -642,20 +642,20 @@ def DB_1dim_loop( L_inc_avg_s, L_inc_avg, DB, - last_career_yrs, + avg_earn_num_years, rep_rate_py, yr_contr, ): for u in range(S_ret, S): - for s in range(S_ret - last_career_yrs, S_ret): + for s in range(S_ret - avg_earn_num_years, S_ret): # TODO: pass t so that can pull correct g_y value # Just need to make if doing over time path makes sense # or if should just do SS - L_inc_avg_s[s - (S_ret - last_career_yrs)] = ( + L_inc_avg_s[s - (S_ret - avg_earn_num_years)] = ( w[s] / np.exp(g_y[-1] * (u - s)) * e[s] * n[s] ) - L_inc_avg = L_inc_avg_s.sum() / last_career_yrs + L_inc_avg = L_inc_avg_s.sum() / avg_earn_num_years rep_rate = yr_contr * rep_rate_py DB[u] = rep_rate * L_inc_avg @@ -673,17 +673,17 @@ def DB_2dim_loop( L_inc_avg_sj, L_inc_avg, DB, - last_career_yrs, + avg_earn_num_years, rep_rate_py, yr_contr, ): for u in range(S_ret, S): - for s in range(S_ret - last_career_yrs, S_ret): - L_inc_avg_sj[s - (S_ret - last_career_yrs), :] = ( + for s in range(S_ret - avg_earn_num_years, S_ret): + L_inc_avg_sj[s - (S_ret - avg_earn_num_years), :] = ( w[s] / np.exp(g_y * (u - s)) * e[s, :] * n[s, :] ) - L_inc_avg = L_inc_avg_sj.sum(axis=0) / last_career_yrs + L_inc_avg = L_inc_avg_sj.sum(axis=0) / avg_earn_num_years rep_rate = yr_contr * rep_rate_py DB[u, :] = rep_rate * L_inc_avg From 699d4b5965c9c3759e82d3d298fe8fb71bee6350 Mon Sep 17 00:00:00 2001 From: jdebacker Date: Wed, 17 Jul 2024 08:29:46 -0400 Subject: [PATCH 31/50] update db replacement rate parameter --- ogcore/default_parameters.json | 6 +++--- ogcore/pensions.py | 20 ++++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/ogcore/default_parameters.json b/ogcore/default_parameters.json index 951aaa78d..f0afa1357 100644 --- a/ogcore/default_parameters.json +++ b/ogcore/default_parameters.json @@ -2958,9 +2958,9 @@ } } }, - "rep_rate_py": { - "title": "TODO: fill in description", - "description": "TODO: fill in description", + "alpha_db": { + "title": "Replacement rate under a defined contribution system.", + "description": "Replacement rate under a defined contribution system.", "section_1": "Fiscal Policy Parameters", "section_2": "Government Pension Parameters", "notes": "", diff --git a/ogcore/pensions.py b/ogcore/pensions.py index 5040cebd3..2eb773945 100644 --- a/ogcore/pensions.py +++ b/ogcore/pensions.py @@ -213,7 +213,7 @@ def DB_amount(w, e, n, j, p): L_inc_avg, DB, p.avg_earn_num_years, - p.rep_rate_py, + p.alpha_db, p.yr_contr, ) DB = DB[-per_rmn:] @@ -233,7 +233,7 @@ def DB_amount(w, e, n, j, p): L_inc_avg, DB, p.avg_earn_num_years, - p.rep_rate_py, + p.alpha_db, p.yr_contr, ) @@ -252,7 +252,7 @@ def DB_amount(w, e, n, j, p): L_inc_avg, DB, p.avg_earn_num_years, - p.rep_rate_py, + p.alpha_db, p.yr_contr, ) @@ -454,7 +454,7 @@ def deriv_DB(w, e, per_rmn, p): p.retire, per_rmn, p.avg_earn_num_years, - p.rep_rate_py, + p.alpha_db, p.yr_contr, ) return d_theta @@ -549,12 +549,12 @@ def delta_ret(r, Y, p): @numba.jit def deriv_DB_loop( - w, e, S, S_ret, per_rmn, avg_earn_num_years, rep_rate_py, yr_contr + w, e, S, S_ret, per_rmn, avg_earn_num_years, alpha_db, yr_contr ): d_theta = np.zeros(per_rmn) num_per_retire = S - S_ret for s in range(per_rmn): - d_theta[s] = w[s] * e[s] * rep_rate_py * (yr_contr / avg_earn_num_years) + d_theta[s] = w[s] * e[s] * alpha_db * (yr_contr / avg_earn_num_years) d_theta[-num_per_retire:] = 0.0 return d_theta @@ -643,7 +643,7 @@ def DB_1dim_loop( L_inc_avg, DB, avg_earn_num_years, - rep_rate_py, + alpha_db, yr_contr, ): @@ -656,7 +656,7 @@ def DB_1dim_loop( w[s] / np.exp(g_y[-1] * (u - s)) * e[s] * n[s] ) L_inc_avg = L_inc_avg_s.sum() / avg_earn_num_years - rep_rate = yr_contr * rep_rate_py + rep_rate = yr_contr * alpha_db DB[u] = rep_rate * L_inc_avg return DB @@ -674,7 +674,7 @@ def DB_2dim_loop( L_inc_avg, DB, avg_earn_num_years, - rep_rate_py, + alpha_db, yr_contr, ): @@ -684,7 +684,7 @@ def DB_2dim_loop( w[s] / np.exp(g_y * (u - s)) * e[s, :] * n[s, :] ) L_inc_avg = L_inc_avg_sj.sum(axis=0) / avg_earn_num_years - rep_rate = yr_contr * rep_rate_py + rep_rate = yr_contr * alpha_db DB[u, :] = rep_rate * L_inc_avg return DB From 52e7480edebe539550fcbc7379f4b692e66808a2 Mon Sep 17 00:00:00 2001 From: jdebacker Date: Wed, 17 Jul 2024 09:17:30 -0400 Subject: [PATCH 32/50] tests with new parameter definitions --- ogcore/pensions.py | 15 +++++------ tests/test_pensions.py | 57 +++++++++++++++++++++--------------------- 2 files changed, 37 insertions(+), 35 deletions(-) diff --git a/ogcore/pensions.py b/ogcore/pensions.py index 2eb773945..d23c59d95 100644 --- a/ogcore/pensions.py +++ b/ogcore/pensions.py @@ -189,7 +189,8 @@ def DB_amount(w, e, n, j, p): Calculate public pension from a defined benefits system. """ L_inc_avg = np.zeros(0) - L_inc_avg_s = np.zeros(p.avg_earn_num_years) + equiv_periods = int(round((p.S / 80.0) * p.avg_earn_num_years)) - 1 + L_inc_avg_s = np.zeros(equiv_periods) if n.shape[0] < p.S: per_rmn = n.shape[0] @@ -212,7 +213,7 @@ def DB_amount(w, e, n, j, p): L_inc_avg_s, L_inc_avg, DB, - p.avg_earn_num_years, + equiv_periods, p.alpha_db, p.yr_contr, ) @@ -232,7 +233,7 @@ def DB_amount(w, e, n, j, p): L_inc_avg_s, L_inc_avg, DB, - p.avg_earn_num_years, + equiv_periods, p.alpha_db, p.yr_contr, ) @@ -240,7 +241,7 @@ def DB_amount(w, e, n, j, p): elif np.ndim(n) == 2: DB_sj = np.zeros((p.retire, p.J)) DB = np.zeros((p.S, p.J)) - L_inc_avg_sj = np.zeros((p.avg_earn_num_years, p.J)) + L_inc_avg_sj = np.zeros((equiv_periods, p.J)) DB = DB_2dim_loop( w, e, @@ -251,7 +252,7 @@ def DB_amount(w, e, n, j, p): L_inc_avg_sj, L_inc_avg, DB, - p.avg_earn_num_years, + equiv_periods, p.alpha_db, p.yr_contr, ) @@ -443,7 +444,7 @@ def deriv_DB(w, e, per_rmn, p): """ Change in DB pension benefits for another unit of labor supply """ - + equiv_periods = int(round((p.S / 80.0) * p.avg_earn_num_years)) - 1 if per_rmn < (p.S - p.retire + 1): d_theta = np.zeros(p.S) else: @@ -453,7 +454,7 @@ def deriv_DB(w, e, per_rmn, p): p.S, p.retire, per_rmn, - p.avg_earn_num_years, + equiv_periods, p.alpha_db, p.yr_contr, ) diff --git a/tests/test_pensions.py b/tests/test_pensions.py index 5b4172341..9833e2eca 100644 --- a/tests/test_pensions.py +++ b/tests/test_pensions.py @@ -73,26 +73,24 @@ def test_replacement_rate_vals(n, w, factor, j, p_in, expected): p = Specifications() -# p.update_specifications({ -# "S": 7, -# "rep_rate_py": 0.2 -# }) p.S = 7 -p.rep_rate_py = 0.2 +p.alpha_db = 0.2 p.retire = 4 -p.last_career_yrs = 3 +p.avg_earn_num_years = 50 p.yr_contr = 4 p.g_y = np.ones(p.T) * 0.03 j = 1 w = np.array([1.2, 1.1, 1.21, 1.0, 1.01, 0.99, 0.8]) e = np.array([1.1, 1.11, 0.9, 0.87, 0.87, 0.7, 0.6]) n = np.array([0.4, 0.45, 0.4, 0.42, 0.3, 0.2, 0.2]) +equiv_periods = int(round((p.S / 80.0) * p.avg_earn_num_years)) - 1 L_inc_avg = np.zeros(0) -L_inc_avg_s = np.zeros(p.last_career_yrs) +L_inc_avg_s = np.zeros(equiv_periods) DB = np.zeros(p.S) DB_loop_expected1 = np.array( [0, 0, 0, 0, 0.337864778, 0.327879365, 0.318189065] ) + args1 = ( w, e, @@ -103,8 +101,8 @@ def test_replacement_rate_vals(n, w, factor, j, p_in, expected): L_inc_avg_s, L_inc_avg, DB, - p.last_career_yrs, - p.rep_rate_py, + equiv_periods, + p.alpha_db, p.yr_contr, ) @@ -154,9 +152,9 @@ def test_DB_1dim_loop(args, DB_loop_expected): p.S = 7 p.retire = 4 per_rmn = p.S -p.last_career_yrs = 3 +p.avg_earn_num_years = 50 p.yr_contr = p.retire -p.rep_rate_py = 0.2 +p.alpha_db = 0.2 p.g_y = np.ones(p.T) * 0.03 w = np.array([1.2, 1.1, 1.21, 1, 1.01, 0.99, 0.8]) e = np.array([1.1, 1.11, 0.9, 0.87, 0.87, 0.7, 0.6]) @@ -164,14 +162,15 @@ def test_DB_1dim_loop(args, DB_loop_expected): [0.352, 0.3256, 0.2904, 0.232, 0.0, 0.0, 0.0] ) d_theta_empty = np.zeros_like(w) +equiv_periods = int(round((p.S / 80.0) * p.avg_earn_num_years)) - 1 args3 = ( w, e, p.S, p.retire, per_rmn, - p.last_career_yrs, - p.rep_rate_py, + equiv_periods, + p.alpha_db, p.yr_contr, ) @@ -228,9 +227,9 @@ def test_deriv_PS_loop(args, deriv_PS_loop_expected): p = Specifications() p.S = 7 p.retire = 4 -p.last_career_yrs = 3 +p.avg_earn_num_years = 50 p.yr_contr = p.retire -p.rep_rate_py = 0.2 +p.alpha_db = 0.2 p.g_y = np.ones(p.T) * 0.03 n_ddb1 = np.array([0.4, 0.45, 0.4, 0.42, 0.3, 0.2, 0.2]) w_ddb1 = np.array([1.2, 1.1, 1.21, 1, 1.01, 0.99, 0.8]) @@ -246,7 +245,7 @@ def test_deriv_PS_loop(args, deriv_PS_loop_expected): p2.retire = 5 p2.last_career_yrs = 2 p2.yr_contr = p2.retire -p2.rep_rate_py = 0.2 +p2.alpha_db = 0.2 p2.g_y = 0.03 n_ddb2 = np.array([0.45, 0.4, 0.42, 0.3, 0.2, 0.2]) w_ddb1 = np.array([1.1, 1.21, 1, 1.01, 0.99, 0.8]) @@ -387,9 +386,9 @@ def test_deriv_NDC(args, d_NDC_expected): p.pension_system = "Defined Benefits" p.S = 7 p.retire = 4 -p.last_career_yrs = 3 +p.avg_earn_num_years = 50 p.yr_contr = p.retire -p.rep_rate_py = 0.2 +p.alpha_db = 0.2 p.g_y = np.ones(p.T) * 0.03 w_db = np.array([1.2, 1.1, 1.21, 1.0, 1.01, 0.99, 0.8]) e_db = np.array([1.1, 1.11, 0.9, 0.87, 0.87, 0.7, 0.6]) @@ -449,14 +448,16 @@ def test_deriv_NDC(args, d_NDC_expected): p4 = Specifications() p4.pension_system = "US-Style Social Security" p4.S = 7 -p4.retire = 4 +p4.retire = (np.ones(p4.T) * 4).astype(int) w_ss = np.array([1.2, 1.1, 1.21, 1.0, 1.01, 0.99, 0.8]) e_ss = np.array([1.1, 1.11, 0.9, 0.87, 0.87, 0.7, 0.6]) n_ss = np.array([0.4, 0.45, 0.4, 0.42, 0.3, 0.2, 0.2]) omegas = (1 / p4.S) * np.ones(p4.S) -theta = 0.4 -p.replacement_rate_adjust = np.ones(p4.T) -pension_expected_ss = [0, 0, 0, 0, 0.004164689, 0.004041603, 0.003922156] +theta = np.array([0.4, 0.4]) +p4.replacement_rate_adjust = np.ones(p4.T) +pension_expected_ss = [0, 0, 0, 0, 0.404, 0.396, 0.32] +method = "TPI" +shift = False args_ss = (r, w_ss, n_ss, Y, theta, t, j, shift, method, e_ss, factor, p4) @@ -592,9 +593,9 @@ def test_delta_ret_loop(args, dir_delta_ret_expected): p = Specifications() p.S = 7 p.retire = 4 -p.last_career_yrs = 3 +p.avg_earn_num_years = 50 p.yr_contr = p.retire -p.rep_rate_py = 0.2 +p.alpha_db = 0.2 p.g_y = np.ones(p.T) * 0.03 j = 1 w = np.array([1.2, 1.1, 1.21, 1.0, 1.01, 0.99, 0.8]) @@ -607,9 +608,9 @@ def test_delta_ret_loop(args, dir_delta_ret_expected): p2 = Specifications() p2.S = 7 p2.retire = 4 -p2.last_career_yrs = 3 +p.avg_earn_num_years = 50 p2.yr_contr = p2.retire -p2.rep_rate_py = 0.2 +p2.alpha_db = 0.2 p2.g_y = np.ones(p2.T) * 0.03 j = 1 w2 = np.array([1.21, 1.0, 1.01, 0.99, 0.8]) @@ -663,9 +664,9 @@ def test_DB(args, DB_expected): p.S = 7 p.retire = 4 per_rmn = p.S -p.last_career_yrs = 3 +p.avg_earn_num_years = 50 p.yr_contr = p.retire -p.rep_rate_py = 0.2 +p.alpha_db = 0.2 w_ddb = np.array([1.2, 1.1, 1.21, 1, 1.01, 0.99, 0.8]) e_ddb = np.array([1.1, 1.11, 0.9, 0.87, 0.87, 0.7, 0.6]) p.g_y = np.ones(p.T) * 0.03 From 955b97ef687ab3886222b10dce16799e6d2a07d0 Mon Sep 17 00:00:00 2001 From: jdebacker Date: Fri, 19 Jul 2024 21:44:58 -0400 Subject: [PATCH 33/50] start with doc strings --- ogcore/pensions.py | 78 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 73 insertions(+), 5 deletions(-) diff --git a/ogcore/pensions.py b/ogcore/pensions.py index d23c59d95..fc5f60b0f 100644 --- a/ogcore/pensions.py +++ b/ogcore/pensions.py @@ -187,6 +187,16 @@ def SS_amount(w, n, theta, t, j, shift, method, e, p): def DB_amount(w, e, n, j, p): """ Calculate public pension from a defined benefits system. + + Args: + w (array_like): real wage rate + e (Numpy array): effective labor units + n (Numpy array): labor supply + j (int): index of lifetime income group + p (OG-Core Specifications object): model parameters + + Returns: + DB (Numpy array): pension amount for each household """ L_inc_avg = np.zeros(0) equiv_periods = int(round((p.S / 80.0) * p.avg_earn_num_years)) - 1 @@ -200,9 +210,7 @@ def DB_amount(w, e, n, j, p): w_S = np.append((p.w_preTP * np.ones(p.S))[:(-per_rmn)], w) n_S = np.append(p.n_preTP[:(-per_rmn), j], n) - DB_s = np.zeros(p.retire) DB = np.zeros(p.S) - print("DB_1dim_loop", w_S, p.e[:, j], n_S) DB = DB_1dim_loop( w_S, p.e[:, j], @@ -221,7 +229,6 @@ def DB_amount(w, e, n, j, p): else: if np.ndim(n) == 1: - DB_s = np.zeros(p.retire) DB = np.zeros(p.S) DB = DB_1dim_loop( w, @@ -239,7 +246,6 @@ def DB_amount(w, e, n, j, p): ) elif np.ndim(n) == 2: - DB_sj = np.zeros((p.retire, p.J)) DB = np.zeros((p.S, p.J)) L_inc_avg_sj = np.zeros((equiv_periods, p.J)) DB = DB_2dim_loop( @@ -264,6 +270,18 @@ def NDC_amount(w, e, n, r, Y, j, p): """ Calculate public pension from a notional defined contribution system. + + Args: + w (array_like): real wage rate + e (Numpy array): effective labor units + n (Numpy array): labor supply + r (array_like): interest rate + Y (array_like): GDP + j (int): index of lifetime income group + p (OG-Core Specifications object): model parameters + + Returns: + NDC (Numpy array): pension amount for each household """ g_ndc_amount = g_ndc(r, Y, p) delta_ret_amount = delta_ret(r, Y, p) @@ -331,6 +349,18 @@ def NDC_amount(w, e, n, r, Y, j, p): def PS_amount(w, e, n, j, factor, p): """ Calculate public pension from a points system. + + Args: + w (array_like): real wage rate + e (Numpy array): effective labor units + n (Numpy array): labor supply + j (int): index of lifetime income group + factor (scalar): scaling factor converting model units to + dollars + p (OG-Core Specifications object): model parameters + + Returns: + PS (Numpy array): pension amount for each household """ if n.shape[0] < p.S: @@ -394,6 +424,18 @@ def deriv_theta(r, w, e, Y, per_rmn, factor, p): """ Change in pension benefits for another unit of labor supply for pension system selected + + Args: + r (array_like): interest rate + w (array_like): real wage rate + e (Numpy array): effective labor units + Y (array_like): GDP + per_rmn (int): number of periods remaining in the model + factor (scalar): scaling factor converting model units to + + Returns: + d_theta (Numpy array): change in pension benefits for another + unit of labor supply """ # TODO: Add SS here... if p.pension_system == "Defined Benefits": @@ -416,6 +458,18 @@ def deriv_theta(r, w, e, Y, per_rmn, factor, p): def deriv_NDC(r, w, e, Y, per_rmn, p): """ Change in NDC pension benefits for another unit of labor supply + + Args: + r (array_like): interest rate + w (array_like): real wage rate + e (Numpy array): effective labor units + Y (array_like): GDP + per_rmn (int): number of periods remaining in the model + p (OG-Core Specifications object): model parameters + + Returns: + d_theta (Numpy array): change in NDC pension benefits for + another unit of labor supply """ if per_rmn == 1: d_theta = 0 @@ -443,6 +497,16 @@ def deriv_NDC(r, w, e, Y, per_rmn, p): def deriv_DB(w, e, per_rmn, p): """ Change in DB pension benefits for another unit of labor supply + + Args: + w (array_like): real wage rate + e (Numpy array): effective labor units + per_rmn (int): number of periods remaining in the model + p (OG-Core Specifications object): model parameters + + Returns: + d_theta: change in DB pension benefits for another unit of labor + supply """ equiv_periods = int(round((p.S / 80.0) * p.avg_earn_num_years)) - 1 if per_rmn < (p.S - p.retire + 1): @@ -463,7 +527,11 @@ def deriv_DB(w, e, per_rmn, p): def deriv_PS(w, e, per_rmn, factor, p): """ - Change in points system pension benefits for another unit of labor supply + Change in points system pension benefits for another unit of + labor supply + + Args: + w """ if per_rmn < (p.S - p.retire + 1): From f6d7b826626ff4c12aed1156a1651d1d0b8a2756 Mon Sep 17 00:00:00 2001 From: jdebacker Date: Sat, 20 Jul 2024 21:57:25 -0400 Subject: [PATCH 34/50] complete doc strings --- ogcore/pensions.py | 244 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 243 insertions(+), 1 deletion(-) diff --git a/ogcore/pensions.py b/ogcore/pensions.py index fc5f60b0f..689079c53 100644 --- a/ogcore/pensions.py +++ b/ogcore/pensions.py @@ -531,7 +531,16 @@ def deriv_PS(w, e, per_rmn, factor, p): labor supply Args: - w + w (array_like): real wage rate + e (Numpy array): effective labor units + per_rmn (int): number of periods remaining in the model + factor (scalar): scaling factor converting model units to + p (OG-Core Specifications object): model parameters + + Returns: + d_theta (Numpy array): change in points system pension benefits + for another unit of labor supply + """ if per_rmn < (p.S - p.retire + 1): @@ -552,6 +561,17 @@ def deriv_PS(w, e, per_rmn, factor, p): def delta_point(r, Y, g_n, g_y, p): """ Compute growth rate used for contributions to points system pension + + Args: + r (array_like): interest rate + Y (array_like): GDP + g_n (array_like): population growth rate + g_y (array_like): GDP growth rate + p (OG-Core Specifications object): model parameters + + Returns: + delta_point (Numpy array): growth rate used for contributions to + points """ # TODO: Add option to allow use to enter growth rate amount # Also to allow rate to vary by year @@ -572,6 +592,15 @@ def delta_point(r, Y, g_n, g_y, p): def g_ndc(r, Y, p): """ Compute growth rate used for contributions to NDC pension + + Args: + r (array_like): interest rate + Y (array_like): GDP + p (OG-Core Specifications object): model parameters + + Returns: + g_ndc (Numpy array): growth rate used for contributions to NDC + """ if p.ndc_growth_rate == "r": g_ndc = r[-1] @@ -588,6 +617,17 @@ def g_ndc(r, Y, p): def g_dir(r, Y, g_y, g_n, dir_growth_rate): """ Compute growth rate used for contributions to NDC pension + + Args: + r (array_like): interest rate + Y (array_like): GDP + g_y (array_like): GDP growth rate + g_n (array_like): population growth rate + dir_growth_rate (str): growth rate used for contributions to NDC + + Returns: + g_dir (Numpy array): growth rate used for contributions to NDC + """ if dir_growth_rate == "r": g_dir = r[-1] @@ -604,6 +644,16 @@ def g_dir(r, Y, g_y, g_n, dir_growth_rate): def delta_ret(r, Y, p): """ Compute conversion coefficient for the NDC pension amount + + Args: + r (array_like): interest rate + Y (array_like): GDP + p (OG-Core Specifications object): model parameters + + Returns: + delta_ret (Numpy array): conversion coefficient for the NDC + pension amount + """ surv_rates = 1 - p.mort_rates_SS dir_delta_s_empty = np.zeros(p.S - p.retire + 1) @@ -620,6 +670,23 @@ def delta_ret(r, Y, p): def deriv_DB_loop( w, e, S, S_ret, per_rmn, avg_earn_num_years, alpha_db, yr_contr ): + """ + Change in DB pension benefits for another unit of labor supply + + Args: + w (array_like): real wage rate + e (Numpy array): effective labor units + S (int): number of periods in the model + S_ret (int): retirement age + per_rmn (int): number of periods remaining in the model + avg_earn_num_years (int): number of years AIME is computed from + alpha_db (scalar): replacement rate + yr_contr (scalar): years of contribution + + Returns: + d_theta (Numpy array): change in DB pension benefits for + another unit of labor supply + """ d_theta = np.zeros(per_rmn) num_per_retire = S - S_ret for s in range(per_rmn): @@ -631,6 +698,27 @@ def deriv_DB_loop( @numba.jit def deriv_PS_loop(w, e, S, S_ret, per_rmn, d_theta, vpoint, factor): + """ + Change in points system pension benefits for another unit of + labor supply + + Args: + w (array_like): real wage rate + e (Numpy array): effective labor units + S (int): number of periods in the model + S_ret (int): retirement age + per_rmn (int): number of periods remaining in the model + d_theta (Numpy array): change in points system pension benefits + for another unit of labor supply + vpoint (scalar): value of points + factor (scalar): scaling factor converting model units to + local currency + + Returns: + d_theta (Numpy array): change in points system pension benefits + for another unit of labor supply + + """ # TODO: do we need these constants or can we scale vpoint to annual?? for s in range((S - per_rmn), S_ret): d_theta[s] = (w[s] * e[s] * vpoint * MONTHS_IN_A_YEAR) / ( @@ -644,6 +732,27 @@ def deriv_PS_loop(w, e, S, S_ret, per_rmn, d_theta, vpoint, factor): def deriv_NDC_loop( w, e, per_rmn, S, S_ret, tau_p, g_ndc_value, delta_ret_value, d_theta ): + """ + Change in NDC pension benefits for another unit of labor supply + + Args: + w (array_like): real wage rate + e (Numpy array): effective labor units + per_rmn (int): number of periods remaining in the model + S (int): number of periods in the model + S_ret (int): retirement age + tau_p (scalar): tax rate + g_ndc_value (scalar): growth rate of NDC pension + delta_ret_value (scalar): conversion coefficient for the NDC + pension amount + d_theta (Numpy array): change in NDC pension benefits for + another unit of labor supply + + Returns: + d_theta (Numpy array): change in NDC pension benefits for + another unit of labor supply + + """ for s in range((S - per_rmn), S_ret): d_theta[s - (S - per_rmn)] = ( tau_p @@ -658,7 +767,21 @@ def deriv_NDC_loop( @numba.jit def delta_ret_loop(S, S_ret, surv_rates, g_dir_value, dir_delta_s): + """ + Compute conversion coefficient for the NDC pension amount + Args: + S (int): number of periods in the model + S_ret (int): retirement age + surv_rates (Numpy array): survival rates + g_dir_value (scalar): growth rate of NDC pension + dir_delta_s (Numpy array): conversion coefficient for the NDC + pension amount + + Returns: + dir_delta (scalar): conversion coefficient for the NDC pension + amount + """ cumul_surv_rates = np.ones(S - S_ret + 1) for s in range(S - S_ret + 1): surv_rates_vec = surv_rates[S_ret : S_ret + s + 1] @@ -673,6 +796,26 @@ def delta_ret_loop(S, S_ret, surv_rates, g_dir_value, dir_delta_s): @numba.jit def PS_1dim_loop(w, e, n, S_ret, S, g_y, vpoint, factor, L_inc_avg_s, PS): + """ + Calculate public pension from a points system. + + Args: + w (array_like): real wage rate + e (Numpy array): effective labor units + n (Numpy array): labor supply + S_ret (int): retirement age + S (int): number of periods in the model + g_y (array_like): GDP growth rate + vpoint (scalar): value of points + factor (scalar): scaling factor converting model units to + local currency + L_inc_avg_s (Numpy array): average labor income + PS (Numpy array): pension amount for each household + + Returns: + PS (Numpy array): pension amount for each household + + """ # TODO: do we need these constants or can we scale vpoint to annual?? for u in range(S_ret, S): # TODO: allow for g_y to be time varying @@ -687,6 +830,27 @@ def PS_1dim_loop(w, e, n, S_ret, S, g_y, vpoint, factor, L_inc_avg_s, PS): @numba.jit def PS_2dim_loop(w, e, n, S_ret, S, J, g_y, vpoint, factor, L_inc_avg_sj, PS): + """ + Calculate public pension from a points system. + + Args: + w (array_like): real wage rate + e (Numpy array): effective labor units + n (Numpy array): labor supply + S_ret (int): retirement age + S (int): number of periods in the model + J (int): number of lifetime income groups + g_y (array_like): GDP growth rate + vpoint (scalar): value of points + factor (scalar): scaling factor converting model units to + local currency + L_inc_avg_sj (Numpy array): average labor income + PS (Numpy array): pension amount for each household + + Returns: + PS (Numpy array): pension amount for each household + + """ # TODO: do we need these constants or can we scale vpoint to annual?? for u in range(S_ret, S): for s in range(S_ret): @@ -715,7 +879,26 @@ def DB_1dim_loop( alpha_db, yr_contr, ): + """ + Calculate public pension from a defined benefits system. + + Args: + w (array_like): real wage rate + e (Numpy array): effective labor units + n (Numpy array): labor supply + S_ret (int): retirement age + S (int): number of periods in the model + g_y (array_like): GDP growth rate + L_inc_avg_s (Numpy array): average labor income + L_inc_avg (scalar): average labor income + DB (Numpy array): pension amount for each household + avg_earn_num_years (int): number of years AIME is computed from + alpha_db (scalar): replacement rate + yr_contr (scalar): years of contribution + Returns: + DB (Numpy array): pension amount for each household + """ for u in range(S_ret, S): for s in range(S_ret - avg_earn_num_years, S_ret): # TODO: pass t so that can pull correct g_y value @@ -746,7 +929,27 @@ def DB_2dim_loop( alpha_db, yr_contr, ): + """ + Calculate public pension from a defined benefits system. + + Args: + w (array_like): real wage rate + e (Numpy array): effective labor units + n (Numpy array): labor supply + S_ret (int): retirement age + S (int): number of periods in the model + g_y (array_like): GDP growth rate + L_inc_avg_sj (Numpy array): average labor income + L_inc_avg (scalar): average labor income + DB (Numpy array): pension amount for each household + avg_earn_num_years (int): number of years AIME is computed from + alpha_db (scalar): replacement rate + yr_contr (scalar): years of contribution + Returns: + DB (Numpy array): pension amount for each household + + """ for u in range(S_ret, S): for s in range(S_ret - avg_earn_num_years, S_ret): L_inc_avg_sj[s - (S_ret - avg_earn_num_years), :] = ( @@ -761,7 +964,26 @@ def DB_2dim_loop( @numba.jit def NDC_1dim_loop(w, e, n, S_ret, S, g_y, tau_p, g_ndc, delta_ret, NDC_s, NDC): + """ + Calculate public pension from a notional defined contribution + Args: + w (array_like): real wage rate + e (Numpy array): effective labor units + n (Numpy array): labor supply + S_ret (int): retirement age + S (int): number of periods in the model + g_y (array_like): GDP growth rate + tau_p (scalar): tax rate + g_ndc (scalar): growth rate of NDC pension + delta_ret (scalar): conversion coefficient for the NDC pension amount + NDC_s (Numpy array): average labor income + NDC (Numpy array): pension amount for each household + + Returns: + NDC (Numpy array): pension amount for each household + + """ for u in range(S_ret, S): for s in range(0, S_ret): # TODO: update so can take g_y from period t @@ -780,6 +1002,26 @@ def NDC_1dim_loop(w, e, n, S_ret, S, g_y, tau_p, g_ndc, delta_ret, NDC_s, NDC): def NDC_2dim_loop( w, e, n, S_ret, S, g_y, tau_p, g_ndc, delta_ret, NDC_sj, NDC ): + """ + Calculate public pension from a notional defined contribution + + Args: + w (array_like): real wage rate + e (Numpy array): effective labor units + n (Numpy array): labor supply + S_ret (int): retirement age + S (int): number of periods in the model + g_y (array_like): GDP growth rate + tau_p (scalar): tax rate + g_ndc (scalar): growth rate of NDC pension + delta_ret (scalar): conversion coefficient for the NDC pension amount + NDC_sj (Numpy array): average labor income + NDC (Numpy array): pension amount for each household + + Returns: + NDC (Numpy array): pension amount for each household + + """ for u in range(S_ret, S): for s in range(0, S_ret): NDC_sj[s, :] = ( From 4fe13f84a7fc5538d5b00873cd932e5902794872 Mon Sep 17 00:00:00 2001 From: jdebacker Date: Fri, 26 Jul 2024 19:20:10 -0400 Subject: [PATCH 35/50] add indR as parameter --- ogcore/default_parameters.json | 19 +++++++++++++++++++ ogcore/pensions.py | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/ogcore/default_parameters.json b/ogcore/default_parameters.json index f0afa1357..bf24960ff 100644 --- a/ogcore/default_parameters.json +++ b/ogcore/default_parameters.json @@ -2939,6 +2939,25 @@ } } }, + "indR": { + "title": "Adjustment for survivor benefits under a notional defined contribution system.", + "description": "Adjustment for survivor benefits under a notional defined contribution system.", + "section_1": "Fiscal Policy Parameters", + "section_2": "Government Pension Parameters", + "notes": "indR = 0.0 for no survivor benefits, indR = 0.5 for survivor benefits", + "type": "float", + "value": [ + { + "value": 0.0 + } + ], + "validators": { + "range": { + "min": 0.0, + "max": 1.0 + } + } + }, "k_ret": { "title": "Adjustment for frequency of pension payments under a notional defined contribution system.", "description": "Adjustment for frequency of pension payments under a notional defined contribution system.", diff --git a/ogcore/pensions.py b/ogcore/pensions.py index 689079c53..eb054a95a 100644 --- a/ogcore/pensions.py +++ b/ogcore/pensions.py @@ -661,7 +661,7 @@ def delta_ret(r, Y, p): dir_delta = delta_ret_loop( p.S, p.retire, surv_rates, g_dir_value, dir_delta_s_empty ) - delta_ret = 1 / (dir_delta - p.k_ret) + delta_ret = 1 / (dir_delta + p.indR - p.k_ret) return delta_ret From 2614498c595b67d6651e045fddfe62c2914c5166 Mon Sep 17 00:00:00 2001 From: jdebacker Date: Fri, 26 Jul 2024 19:20:20 -0400 Subject: [PATCH 36/50] fix book formatting --- docs/book/content/theory/government.md | 37 +++++++++++++------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/docs/book/content/theory/government.md b/docs/book/content/theory/government.md index 32ba13d3a..086317787 100644 --- a/docs/book/content/theory/government.md +++ b/docs/book/content/theory/government.md @@ -343,26 +343,28 @@ Under the U.S.-style social security system, households over age $R$ received a :label: eqn:PIA PIAbase_{j,R,t+R} = \begin{cases} - PIArate_1 \times AIME_{j,R,t+R}, \text{for} AIME_{j,R,t+R} \leq AIMEbkt_1 \\ - PIArate_2 \times AIME_{j,R,t+R}, \text{for} AIMEbkt_1 < AIME_{j,R,t+R} \leq AIMEbkt_2 \\ - PIArate_3 \times AIME_{j,R,t+R}, \text{for} AIMEbkt_2 < AIME_{j,R,t+R} \\ + PIArate_1 \times AIME_{j,R,t+R}, \text{for } AIME_{j,R,t+R} \leq AIMEbkt_1 \\ + PIArate_2 \times AIME_{j,R,t+R}, \text{for } AIMEbkt_1 < AIME_{j,R,t+R} \leq AIMEbkt_2 \\ + PIArate_3 \times AIME_{j,R,t+R}, \text{for } AIMEbkt_2 < AIME_{j,R,t+R} \\ \end{cases} ``` - The PIA is then capped at a maximum, set by the parameter `PIA_maxpayment`, $PIA_{j,R,t+R} = max{PIAbase_{j,R,t+R}, \text{PIA max payment amount}}$. + + The PIA is then capped at a maximum, set by the parameter `PIA_maxpayment`, $PIA_{j,R,t+R} = \max\{PIAbase_{j,R,t+R}, \text{ PIA max payment amount}\}$. The replacement rate, $\theta_j$ is then calculated as annual earnings, with an adjustment for the wage rate.: - ```{math} + ```{math} :label: eqn:theta - \theta_{j,R,t+R} = \frac{PIA_{j,R,t+R}} \times 12}{factor \times w_{t+R}} + \theta_{j,R,t+R} = \frac{PIA_{j,R,t+R} \times 12}{factor \times w_{t+R}} ``` + Note that $aIME_{j,R,t+R}$ is a function of each households' earning history, but their choice of earning may depend on their retirement benefit. Solving this exactly would introduce and additional fixed point problem in both the steady state solution and in the time path solution. The latter would be extremely computationally taxing. Therefore, we make the simplification of determining the AIME for each type $j$ using the steady state solution and earnings for type $j$ in the steady state. This leads to some approximation error, but because $\theta_j$ is adjusted by the current wage rate, this approximation error is minized. The pension amount for households under the US-style social security system is then: ```{math} :label: eqn:ss_pension - pension_{j,s,t} = \theta_j \times w_t \forall s > R + pension_{j,s,t} = \theta_j \times w_t \quad \forall s > R ``` @@ -372,7 +374,7 @@ The defined benefit system pension amount is given as: ```{math} :label: eqn:db_pension - pension{j,s,t} = \biggl[\frac{\sum_{s=R-ny}^{R-1}w_{t}e_{j,s,t}n_{j,s,t}}{ny}\biggr]\times Cy \times \alpha_{DB} \forall s > R + pension{j,s,t} = \biggl[\frac{\sum_{s=R-ny}^{R-1}w_{t}e_{j,s,t}n_{j,s,t}}{ny}\biggr]\times Cy \times \alpha_{DB} \quad \forall s > R ``` where: @@ -400,20 +402,17 @@ The pension amount under a notional defined contribution system is given as: ```{math} :label: eqn:ndc_pension - pension{j,s,t} = \biggl[\sum_{s=E}^{R-1}\tau^{p}_{t}w_{t}e_{j,s,t}n_{j,s,t}(1 + g_{NDC,t})^{R-s-1}\biggr]\delta_{R, t} \forall s > R + pension{j,s,t} = \biggl[\sum_{s=E}^{R-1}\tau^{p}_{t}w_{t}e_{j,s,t}n_{j,s,t}(1 + g_{NDC,t})^{R-s-1}\biggr]\delta_{R, t} \quad \forall s > R ``` where: - \begin{itemize} - \item $\tau^p$ is the pension contribution tax rate - \item $g_{NDC,t}$ the rate of growth applied to contributions. - \begin{itemize} - \item For example, In the Italian system, $g_{NDC,t}$ is the mean nominal GDP growth rate in the 5 years before seniority - \item i.e., $g_{NDC,t}=\prod_{j=i}^{R-1}\bar{g}_{j}$ - \item Note, this is not $g_y$. In the SS, it's $(\bar{g}_{y} + \bar{g}_{n})$, and in the transition path equilibrium, it's not a function of exogenous variables since the growth rate of nominal GDP is endogenous. - \end{itemize} - \item $\delta_{R, t}$ is the conversion coefficient at time $t$ and its calculation is detailed below. - \end{itemize} + + * $\tau^p$ is the pension contribution tax rate + * $g_{NDC,t}$ the rate of growth applied to contributions. + * For example, In the Italian system, $g_{NDC,t}$ is the mean nominal GDP growth rate in the 5 years before seniority + * i.e., $g_{NDC,t}=\prod_{j=i}^{R-1}\bar{g}_{j}$ + * Note, this is not $g_y$. In the SS, it's $(\bar{g}_{y} + \bar{g}_{n})$, and in the transition path equilibrium, it's not a function of exogenous variables since the growth rate of nominal GDP is endogenous. + * $\delta_{R, t}$ is the conversion coefficient at time $t$ and its calculation is detailed below. ```{math} \delta_{R} = (dir_{R} + ind_{R} - k)^{-1} From 7574da93003be02b35d873bb2cb96c7f814ea549 Mon Sep 17 00:00:00 2001 From: jdebacker Date: Fri, 26 Jul 2024 19:23:58 -0400 Subject: [PATCH 37/50] model period equiv for years of contributions parameter --- ogcore/pensions.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/ogcore/pensions.py b/ogcore/pensions.py index eb054a95a..795a67132 100644 --- a/ogcore/pensions.py +++ b/ogcore/pensions.py @@ -199,7 +199,10 @@ def DB_amount(w, e, n, j, p): DB (Numpy array): pension amount for each household """ L_inc_avg = np.zeros(0) + # Adjustment to turn years into model periods + # TODO: could add this to parameters.py at some point equiv_periods = int(round((p.S / 80.0) * p.avg_earn_num_years)) - 1 + equiv_yr_contrib = int(round((p.S / 80.0) * p.yr_contrib)) L_inc_avg_s = np.zeros(equiv_periods) if n.shape[0] < p.S: @@ -223,7 +226,7 @@ def DB_amount(w, e, n, j, p): DB, equiv_periods, p.alpha_db, - p.yr_contr, + equiv_yr_contrib, ) DB = DB[-per_rmn:] @@ -242,7 +245,7 @@ def DB_amount(w, e, n, j, p): DB, equiv_periods, p.alpha_db, - p.yr_contr, + equiv_yr_contrib, ) elif np.ndim(n) == 2: @@ -260,7 +263,7 @@ def DB_amount(w, e, n, j, p): DB, equiv_periods, p.alpha_db, - p.yr_contr, + equiv_yr_contrib, ) return DB @@ -509,6 +512,7 @@ def deriv_DB(w, e, per_rmn, p): supply """ equiv_periods = int(round((p.S / 80.0) * p.avg_earn_num_years)) - 1 + equiv_yr_contrib = int(round((p.S / 80.0) * p.yr_contrib)) if per_rmn < (p.S - p.retire + 1): d_theta = np.zeros(p.S) else: @@ -520,7 +524,7 @@ def deriv_DB(w, e, per_rmn, p): per_rmn, equiv_periods, p.alpha_db, - p.yr_contr, + equiv_yr_contrib, ) return d_theta From 48d7090971f6ce45b616b9af702964b421b7a50f Mon Sep 17 00:00:00 2001 From: jdebacker Date: Fri, 26 Jul 2024 20:24:14 -0400 Subject: [PATCH 38/50] have year contribution turn into model periods --- ogcore/pensions.py | 6 ++++-- tests/test_pensions.py | 34 ++++++++++++++++++---------------- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/ogcore/pensions.py b/ogcore/pensions.py index 795a67132..81b790788 100644 --- a/ogcore/pensions.py +++ b/ogcore/pensions.py @@ -202,7 +202,7 @@ def DB_amount(w, e, n, j, p): # Adjustment to turn years into model periods # TODO: could add this to parameters.py at some point equiv_periods = int(round((p.S / 80.0) * p.avg_earn_num_years)) - 1 - equiv_yr_contrib = int(round((p.S / 80.0) * p.yr_contrib)) + equiv_yr_contrib = int(round((p.S / 80.0) * p.yr_contrib)) - 1 L_inc_avg_s = np.zeros(equiv_periods) if n.shape[0] < p.S: @@ -512,7 +512,7 @@ def deriv_DB(w, e, per_rmn, p): supply """ equiv_periods = int(round((p.S / 80.0) * p.avg_earn_num_years)) - 1 - equiv_yr_contrib = int(round((p.S / 80.0) * p.yr_contrib)) + equiv_yr_contrib = int(round((p.S / 80.0) * p.yr_contrib)) - 1 if per_rmn < (p.S - p.retire + 1): d_theta = np.zeros(p.S) else: @@ -692,6 +692,8 @@ def deriv_DB_loop( another unit of labor supply """ d_theta = np.zeros(per_rmn) + print("Year contribution: ", yr_contr) + print("Average earnings years: ", avg_earn_num_years) num_per_retire = S - S_ret for s in range(per_rmn): d_theta[s] = w[s] * e[s] * alpha_db * (yr_contr / avg_earn_num_years) diff --git a/tests/test_pensions.py b/tests/test_pensions.py index 9833e2eca..bdbd47384 100644 --- a/tests/test_pensions.py +++ b/tests/test_pensions.py @@ -77,13 +77,14 @@ def test_replacement_rate_vals(n, w, factor, j, p_in, expected): p.alpha_db = 0.2 p.retire = 4 p.avg_earn_num_years = 50 -p.yr_contr = 4 +p.yr_contrib = 55 p.g_y = np.ones(p.T) * 0.03 j = 1 w = np.array([1.2, 1.1, 1.21, 1.0, 1.01, 0.99, 0.8]) e = np.array([1.1, 1.11, 0.9, 0.87, 0.87, 0.7, 0.6]) n = np.array([0.4, 0.45, 0.4, 0.42, 0.3, 0.2, 0.2]) equiv_periods = int(round((p.S / 80.0) * p.avg_earn_num_years)) - 1 +equiv_contrib_periods = int(round((p.S / 80.0) * p.yr_contrib)) - 1 L_inc_avg = np.zeros(0) L_inc_avg_s = np.zeros(equiv_periods) DB = np.zeros(p.S) @@ -103,7 +104,7 @@ def test_replacement_rate_vals(n, w, factor, j, p_in, expected): DB, equiv_periods, p.alpha_db, - p.yr_contr, + equiv_contrib_periods ) test_data = [(args1, DB_loop_expected1)] @@ -129,7 +130,7 @@ def test_DB_1dim_loop(args, DB_loop_expected): DB, last_career_yrs, rep_rate_py, - yr_contr, + yr_contrib, ) = args DB_loop = pensions.DB_1dim_loop( w, @@ -143,7 +144,7 @@ def test_DB_1dim_loop(args, DB_loop_expected): DB, last_career_yrs, rep_rate_py, - yr_contr, + yr_contrib, ) assert np.allclose(DB_loop, DB_loop_expected) @@ -153,7 +154,7 @@ def test_DB_1dim_loop(args, DB_loop_expected): p.retire = 4 per_rmn = p.S p.avg_earn_num_years = 50 -p.yr_contr = p.retire +p.yr_contrib = 55 p.alpha_db = 0.2 p.g_y = np.ones(p.T) * 0.03 w = np.array([1.2, 1.1, 1.21, 1, 1.01, 0.99, 0.8]) @@ -163,6 +164,7 @@ def test_DB_1dim_loop(args, DB_loop_expected): ) d_theta_empty = np.zeros_like(w) equiv_periods = int(round((p.S / 80.0) * p.avg_earn_num_years)) - 1 +equiv_contrib_periods = int(round((p.S / 80.0) * p.yr_contrib)) - 1 args3 = ( w, e, @@ -171,7 +173,7 @@ def test_DB_1dim_loop(args, DB_loop_expected): per_rmn, equiv_periods, p.alpha_db, - p.yr_contr, + equiv_contrib_periods, ) test_data = [(args3, deriv_DB_loop_expected)] @@ -182,9 +184,9 @@ def test_deriv_DB_loop(args, deriv_DB_loop_expected): """ Test of the pensions.deriv_DB_loop() function. """ - (w, e, S, retire, per_rmn, last_career_yrs, rep_rate_py, yr_contr) = args + (w, e, S, retire, per_rmn, last_career_yrs, rep_rate_py, yr_contrib) = args deriv_DB_loop = pensions.deriv_DB_loop( - w, e, S, retire, per_rmn, last_career_yrs, rep_rate_py, yr_contr + w, e, S, retire, per_rmn, last_career_yrs, rep_rate_py, yr_contrib ) assert np.allclose(deriv_DB_loop, deriv_DB_loop_expected) @@ -228,7 +230,7 @@ def test_deriv_PS_loop(args, deriv_PS_loop_expected): p.S = 7 p.retire = 4 p.avg_earn_num_years = 50 -p.yr_contr = p.retire +p.yr_contrib = 55 p.alpha_db = 0.2 p.g_y = np.ones(p.T) * 0.03 n_ddb1 = np.array([0.4, 0.45, 0.4, 0.42, 0.3, 0.2, 0.2]) @@ -244,7 +246,7 @@ def test_deriv_PS_loop(args, deriv_PS_loop_expected): p2.S = 7 p2.retire = 5 p2.last_career_yrs = 2 -p2.yr_contr = p2.retire +p2.yr_contrib = 55 p2.alpha_db = 0.2 p2.g_y = 0.03 n_ddb2 = np.array([0.45, 0.4, 0.42, 0.3, 0.2, 0.2]) @@ -252,7 +254,7 @@ def test_deriv_PS_loop(args, deriv_PS_loop_expected): e_ddb1 = np.array([1.11, 0.9, 0.87, 0.87, 0.7, 0.6]) per_rmn = n_ddb2.shape[0] d_theta_empty = np.zeros_like(per_rmn) -deriv_DB_expected2 = np.array([0.6105, 0.5445, 0.435, 0.43935, 0.0, 0.0]) +deriv_DB_expected2 = np.array([0.4884, 0.4356, 0.348, 0.35148, 0.0, 0.0]) args_ddb2 = (w_ddb1, e_ddb1, per_rmn, p2) test_data = [(args_ddb1, deriv_DB_expected1), (args_ddb2, deriv_DB_expected2)] @@ -387,7 +389,7 @@ def test_deriv_NDC(args, d_NDC_expected): p.S = 7 p.retire = 4 p.avg_earn_num_years = 50 -p.yr_contr = p.retire +p.yr_contrib = 55 p.alpha_db = 0.2 p.g_y = np.ones(p.T) * 0.03 w_db = np.array([1.2, 1.1, 1.21, 1.0, 1.01, 0.99, 0.8]) @@ -594,7 +596,7 @@ def test_delta_ret_loop(args, dir_delta_ret_expected): p.S = 7 p.retire = 4 p.avg_earn_num_years = 50 -p.yr_contr = p.retire +p.yr_contrib = 55 p.alpha_db = 0.2 p.g_y = np.ones(p.T) * 0.03 j = 1 @@ -609,7 +611,7 @@ def test_delta_ret_loop(args, dir_delta_ret_expected): p2.S = 7 p2.retire = 4 p.avg_earn_num_years = 50 -p2.yr_contr = p2.retire +p2.yr_contrib = 55 p2.alpha_db = 0.2 p2.g_y = np.ones(p2.T) * 0.03 j = 1 @@ -639,7 +641,7 @@ def test_delta_ret_loop(args, dir_delta_ret_expected): ] ) p2.e = e2 -DB_expected2 = np.array([0, 0, 0.289170525, 0.280624244, 0.272330544]) +DB_expected2 = np.array([0, 0, 0.30593337, 0.29689167, 0.2881172]) args2 = (w2, e2, n2, j, p2) test_data = [(args1, DB_expected1), (args2, DB_expected2)] @@ -665,7 +667,7 @@ def test_DB(args, DB_expected): p.retire = 4 per_rmn = p.S p.avg_earn_num_years = 50 -p.yr_contr = p.retire +p.yr_contrib = 55 p.alpha_db = 0.2 w_ddb = np.array([1.2, 1.1, 1.21, 1, 1.01, 0.99, 0.8]) e_ddb = np.array([1.1, 1.11, 0.9, 0.87, 0.87, 0.7, 0.6]) From fec65706bce2c7d0505120b562ace9f85a9a752e Mon Sep 17 00:00:00 2001 From: jdebacker Date: Fri, 26 Jul 2024 20:24:30 -0400 Subject: [PATCH 39/50] rename param --- ogcore/default_parameters.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ogcore/default_parameters.json b/ogcore/default_parameters.json index bf24960ff..ee153ff06 100644 --- a/ogcore/default_parameters.json +++ b/ogcore/default_parameters.json @@ -3015,7 +3015,7 @@ } } }, - "yr_contr": { + "yr_contrib": { "title": "Number of years of contributions made to defined benefits pension system.", "description": "Number of years of contributions made to defined benefits pension system.", "section_1": "Fiscal Policy Parameters", From 276a786c56df6618d1b86a4ca55cdc4a5d352cca Mon Sep 17 00:00:00 2001 From: jdebacker Date: Fri, 26 Jul 2024 20:25:28 -0400 Subject: [PATCH 40/50] remove typo --- docs/book/content/theory/stationarization.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/book/content/theory/stationarization.md b/docs/book/content/theory/stationarization.md index cfde149b6..9b21fb8c2 100644 --- a/docs/book/content/theory/stationarization.md +++ b/docs/book/content/theory/stationarization.md @@ -366,7 +366,7 @@ where $\frac{\partial \hat{\theta}_{j,u,t+u-s}}{\partial n_{j,s,t}}$ is given by ``` -#### Stationarized Defined Benefits Equations} +#### Stationarized Defined Benefits Equations Stationarized pension amount: From bb3abe9c2fd1122e6c5e0d9a752a8fc7f5d26506 Mon Sep 17 00:00:00 2001 From: jdebacker Date: Fri, 26 Jul 2024 20:34:03 -0400 Subject: [PATCH 41/50] add math to docstrings --- ogcore/pensions.py | 62 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 52 insertions(+), 10 deletions(-) diff --git a/ogcore/pensions.py b/ogcore/pensions.py index 81b790788..41dbd11d4 100644 --- a/ogcore/pensions.py +++ b/ogcore/pensions.py @@ -9,9 +9,12 @@ def replacement_rate_vals(nssmat, wss, factor_ss, j, p): - """ + r""" Calculates replacement rate values for the social security system. + .. math:: + \theta_{j,R,t+R} = \frac{PIA_{j,R,t+R} \times 12}{factor \times w_{t+R}} + Args: nssmat (Numpy array): initial guess at labor supply, size = SxJ new_w (scalar): steady state real wage rate @@ -106,10 +109,13 @@ def pension_amount(r, w, n, Y, theta, t, j, shift, method, e, factor, p): def SS_amount(w, n, theta, t, j, shift, method, e, p): - """ + r""" Calculate public pension benefit amounts for each household under a US-style social security system. + .. mathL:: + pension_{j,s,t} = \theta_j \times w_t \quad \forall s > R + Args: w (array_like): real wage rate n (Numpy array): labor supply @@ -185,9 +191,13 @@ def SS_amount(w, n, theta, t, j, shift, method, e, p): def DB_amount(w, e, n, j, p): - """ + r""" Calculate public pension from a defined benefits system. + .. math:: + pension{j,s,t} = \biggl[\frac{\sum_{s=R-ny}^{R-1}w_{t}e_{j,s,t} + n_{j,s,t}}{ny}\biggr]\times Cy \times \alpha_{DB} \quad \forall s > R + Args: w (array_like): real wage rate e (Numpy array): effective labor units @@ -270,10 +280,14 @@ def DB_amount(w, e, n, j, p): def NDC_amount(w, e, n, r, Y, j, p): - """ + r""" Calculate public pension from a notional defined contribution system. + .. math:: + pension{j,s,t} = \biggl[\sum_{s=E}^{R-1}\tau^{p}_{t}w_{t} + e_{j,s,t}n_{j,s,t}(1 + g_{NDC,t})^{R-s-1}\biggr]\delta_{R, t} \quad \forall s > R + Args: w (array_like): real wage rate e (Numpy array): effective labor units @@ -350,9 +364,12 @@ def NDC_amount(w, e, n, r, Y, j, p): def PS_amount(w, e, n, j, factor, p): - """ + r""" Calculate public pension from a points system. + .. math:: + pension{j,s,t} = \sum_{s=E}^{R-1}w_{t}e_{j,s,t}n_{j,s,t}\times v_{t} \quad \forall s > R + Args: w (array_like): real wage rate e (Numpy array): effective labor units @@ -459,9 +476,16 @@ def deriv_theta(r, w, e, Y, per_rmn, factor, p): def deriv_NDC(r, w, e, Y, per_rmn, p): - """ + r""" Change in NDC pension benefits for another unit of labor supply + .. math:: + \frac{\partial \theta_{j,u,t+u-s}}{\partial n_{j,s,t}} = + \begin{cases} + \tau^{p}_{t}w_{t}e_{j,s}(1+g_{NDC,t})^{u - s}\delta_{R,t}, & \text{if}\ s Date: Fri, 26 Jul 2024 20:34:16 -0400 Subject: [PATCH 42/50] space --- docs/book/content/theory/government.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/book/content/theory/government.md b/docs/book/content/theory/government.md index 086317787..33d62423d 100644 --- a/docs/book/content/theory/government.md +++ b/docs/book/content/theory/government.md @@ -447,7 +447,7 @@ Under a points system, the pension amount is given as: ```{math} :label: eqn:ps_pension - pension{j,s,t} = \sum_{s=E}^{R-1}w_{t}e_{j,s,t}n_{j,s,t}\times v_{t} \forall s > R + pension{j,s,t} = \sum_{s=E}^{R-1}w_{t}e_{j,s,t}n_{j,s,t}\times v_{t} \quad \forall s > R ``` where $v_{t}$ is the value of a point at time $t$ From 56d0c2e2044d410b653d458e7866507dbe59c537 Mon Sep 17 00:00:00 2001 From: jdebacker Date: Fri, 26 Jul 2024 20:41:46 -0400 Subject: [PATCH 43/50] api docs or new pension module and funcs --- docs/book/content/api/public_api.rst | 1 + docs/book/content/api/tax.rst | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/book/content/api/public_api.rst b/docs/book/content/api/public_api.rst index 5a158a83d..332bec63d 100644 --- a/docs/book/content/api/public_api.rst +++ b/docs/book/content/api/public_api.rst @@ -23,6 +23,7 @@ There is also a link to the source code for each documented member. parameter_plots parameter_tables parameters + pensions tax txfunc utils diff --git a/docs/book/content/api/tax.rst b/docs/book/content/api/tax.rst index e2c426acc..1866139da 100644 --- a/docs/book/content/api/tax.rst +++ b/docs/book/content/api/tax.rst @@ -9,6 +9,6 @@ ogcore.tax ------------------------------------------ .. automodule:: ogcore.tax - :members: replacement_rate_vals, ETR_wealth, MTR_wealth, ETR_income, - MTR_income, get_biz_tax, net_taxes, income_tax_liab, pension_amount, + :members: ETR_wealth, MTR_wealth, ETR_income, + MTR_income, get_biz_tax, net_taxes, income_tax_liab, wealth_tax_liab, bequest_tax_liab From 0b1cfaec0f2a8793df8b999fed888ef6dcca1582 Mon Sep 17 00:00:00 2001 From: jdebacker Date: Fri, 26 Jul 2024 20:50:10 -0400 Subject: [PATCH 44/50] format --- tests/test_pensions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_pensions.py b/tests/test_pensions.py index bdbd47384..b8f157438 100644 --- a/tests/test_pensions.py +++ b/tests/test_pensions.py @@ -104,7 +104,7 @@ def test_replacement_rate_vals(n, w, factor, j, p_in, expected): DB, equiv_periods, p.alpha_db, - equiv_contrib_periods + equiv_contrib_periods, ) test_data = [(args1, DB_loop_expected1)] From c62aaddf53a4284c850103e904bf7d59c97de36f Mon Sep 17 00:00:00 2001 From: jdebacker Date: Fri, 26 Jul 2024 20:54:28 -0400 Subject: [PATCH 45/50] format --- ogcore/tax.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ogcore/tax.py b/ogcore/tax.py index f7f360228..24a1d5724 100644 --- a/ogcore/tax.py +++ b/ogcore/tax.py @@ -281,7 +281,10 @@ def net_taxes( """ T_I = income_tax_liab(r, w, b, n, factor, t, j, method, e, etr_params, p) - pension = pensions.pension_amount(w, n, theta, t, j, shift, method, e, p) + # TODO: replace "1" with Y in the args below when want NDC functions + pension = pensions.pension_amount( + r, w, n, 1, theta, t, j, shift, method, e, factor, p + ) T_BQ = bequest_tax_liab(r, b, bq, t, j, method, p) T_W = wealth_tax_liab(r, b, t, j, method, p) From e49d547eed8591e0f8e22000b371328d69ba79b0 Mon Sep 17 00:00:00 2001 From: jdebacker Date: Fri, 26 Jul 2024 21:31:40 -0400 Subject: [PATCH 46/50] use pension not tax module for pension funcs --- ogcore/SS.py | 10 +++++----- ogcore/aggregates.py | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/ogcore/SS.py b/ogcore/SS.py index 7b932413c..c1716f3f1 100644 --- a/ogcore/SS.py +++ b/ogcore/SS.py @@ -4,7 +4,7 @@ import scipy.optimize as opt from dask import delayed, compute import dask.multiprocessing -from ogcore import tax, household, firm, utils, fiscal +from ogcore import tax, pensions, household, firm, utils, fiscal from ogcore import aggregates as aggr from ogcore.constants import SHOW_RUNTIME import os @@ -71,7 +71,7 @@ def euler_equation_solver(guesses, *args): b_s = np.array([0] + list(b_guess[:-1])) b_splus1 = b_guess - theta = tax.replacement_rate_vals(n_guess, w, factor, j, p) + theta = pensions.replacement_rate_vals(n_guess, w, factor, j, p) error1 = household.FOC_savings( r, @@ -275,7 +275,7 @@ def inner_loop(outer_loop_vars, p, client): b_splus1 = bssmat b_s = np.array(list(np.zeros(p.J).reshape(1, p.J)) + list(bssmat[:-1, :])) - theta = tax.replacement_rate_vals(nssmat, w, factor, None, p) + theta = pensions.replacement_rate_vals(nssmat, w, factor, None, p) num_params = len(p.etr_params[-1][0]) etr_params_3D = [ @@ -401,7 +401,7 @@ def inner_loop(outer_loop_vars, p, client): new_BQ = aggr.get_BQ(new_r_p, bssmat, None, p, "SS", False) new_bq = household.get_bq(new_BQ, None, p, "SS") tr = household.get_tr(TR, None, p, "SS") - theta = tax.replacement_rate_vals(nssmat, new_w, new_factor, None, p) + theta = pensions.replacement_rate_vals(nssmat, new_w, new_factor, None, p) # Find updated goods prices new_p_m = firm.get_pm(new_w, Y_vec, L_vec, p, "SS") @@ -724,7 +724,7 @@ def SS_solver( bqssmat = household.get_bq(BQss, None, p, "SS") trssmat = household.get_tr(TR_ss, None, p, "SS") ubissmat = p.ubi_nom_array[-1, :, :] / factor_ss - theta = tax.replacement_rate_vals(nssmat, wss, factor_ss, None, p) + theta = pensions.replacement_rate_vals(nssmat, wss, factor_ss, None, p) # Compute effective and marginal tax rates for all agents num_params = len(p.etr_params[-1][0]) diff --git a/ogcore/aggregates.py b/ogcore/aggregates.py index 5a7c9a9a4..431910b46 100644 --- a/ogcore/aggregates.py +++ b/ogcore/aggregates.py @@ -6,7 +6,7 @@ # Packages import numpy as np -from ogcore import tax +from ogcore import tax, pensions """ ------------------------------------------------------------------------------- @@ -347,8 +347,8 @@ def revenue( inc_pay_tax_liab = tax.income_tax_liab( r, w, b, n, factor, 0, None, method, e, etr_params, p ) - pension_benefits = tax.pension_amount( - w, n, theta, 0, None, False, method, e, p + pension_benefits = pensions.pension_amount( + r, w, n, Y, theta, 0, None, False, method, e, factor, p ) bq_tax_liab = tax.bequest_tax_liab(r, b, bq, 0, None, method, p) w_tax_liab = tax.wealth_tax_liab(r, b, 0, None, method, p) From bba5268aef2841b90c3817c214e830bb5af5b95b Mon Sep 17 00:00:00 2001 From: jdebacker Date: Sat, 27 Jul 2024 10:17:32 -0400 Subject: [PATCH 47/50] update parameter names in constant --- ogcore/constants.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ogcore/constants.py b/ogcore/constants.py index 533051873..877de6da8 100644 --- a/ogcore/constants.py +++ b/ogcore/constants.py @@ -234,8 +234,8 @@ "Shift parameter in government interest rate wedge", r"$\mu_{d,t}$", ], - "AIME_num_years": [ - "Number of years over which compute AIME", + "avg_earn_num_years": [ + "Number of years over which compute average earnings for pension benefit", r"$\texttt{AIME_num_years}$", ], "AIME_bkt_1": ["First AIME bracket threshold", r"$\texttt{AIME_bkt_1}$"], From 9be34deeedafc8f1d82d19c5fae62361825f5bfa Mon Sep 17 00:00:00 2001 From: jdebacker Date: Sat, 27 Jul 2024 10:24:36 -0400 Subject: [PATCH 48/50] fix df name --- ogcore/demographics.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/ogcore/demographics.py b/ogcore/demographics.py index 24fe75518..c223e2b60 100644 --- a/ogcore/demographics.py +++ b/ogcore/demographics.py @@ -7,13 +7,10 @@ # Import packages import os -import time import numpy as np -import json from io import StringIO import scipy.optimize as opt import pandas as pd -import matplotlib.pyplot as plt from ogcore.utils import get_legacy_session from ogcore import parameter_plots as pp @@ -402,8 +399,8 @@ def get_pop( end_year=start_year, ) initial_pop_sample = initial_pop_data[ - (pre_pop_data["age"] >= min_age) - & (pre_pop_data["age"] <= max_age) + (initial_pop_data["age"] >= min_age) + & (initial_pop_data["age"] <= max_age) ] initial_pop = initial_pop_sample.value.values initial_pop = pop_rebin(initial_pop, E + S) From cf3f5446b44d9075eb572f8cf793ee6a6854eae7 Mon Sep 17 00:00:00 2001 From: Richard Evans Date: Sun, 28 Jul 2024 07:47:44 -0600 Subject: [PATCH 49/50] Updated version and added numba to setup.py --- CHANGELOG.md | 7 +++++++ ogcore/__init__.py | 2 +- setup.py | 3 ++- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3314628b..1817cddc7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.11.13] - 2024-07-28 12:00:00 + +### Added + +- Added three new pension types to the model: (i) defined benefits system, (ii) notional defined contribution system, and (iii) points system. + ## [0.11.12] - 2024-07-26 01:00:00 ### Bug Fix @@ -265,6 +271,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Any earlier versions of OG-USA can be found in the [`OG-Core`](https://github.com/PSLmodels/OG-Core) repository [release history](https://github.com/PSLmodels/OG-Core/releases) from [v.0.6.4](https://github.com/PSLmodels/OG-Core/releases/tag/v0.6.4) (Jul. 20, 2021) or earlier. +[0.11.13]: https://github.com/PSLmodels/OG-Core/compare/v0.11.11...v0.11.13 [0.11.11]: https://github.com/PSLmodels/OG-Core/compare/v0.11.10...v0.11.11 [0.11.10]: https://github.com/PSLmodels/OG-Core/compare/v0.11.9...v0.11.10 [0.11.9]: https://github.com/PSLmodels/OG-Core/compare/v0.11.8...v0.11.9 diff --git a/ogcore/__init__.py b/ogcore/__init__.py index 7039682e2..234c048f7 100644 --- a/ogcore/__init__.py +++ b/ogcore/__init__.py @@ -20,4 +20,4 @@ from ogcore.txfunc import * from ogcore.utils import * -__version__ = "0.11.12" +__version__ = "0.11.13" diff --git a/setup.py b/setup.py index 70e57cab1..0b111a027 100755 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="ogcore", - version="0.11.12", + version="0.11.13", author="Jason DeBacker and Richard W. Evans", license="CC0 1.0 Universal (CC0 1.0) Public Domain Dedication", description="A general equilibribum overlapping generations model for fiscal policy analysis", @@ -28,6 +28,7 @@ "numpy", "scipy>=1.7.1", "pandas>=1.2.5", + "numba", "matplotlib", "dask>=2.30.0", "distributed>=2.30.1", From f44266f20ad3a91829d37b1e7ff90510c7c745cb Mon Sep 17 00:00:00 2001 From: Richard Evans Date: Sun, 28 Jul 2024 07:50:30 -0600 Subject: [PATCH 50/50] Added numba to requirements.txt --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 7dba69feb..2c3d07b77 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,5 +2,6 @@ taxcalc scipy pandas numpy +numba os numpydoc