diff --git a/docs/book/content/calibration/exogenous_parameters.md b/docs/book/content/calibration/exogenous_parameters.md index c5d2c9e..f9b0239 100644 --- a/docs/book/content/calibration/exogenous_parameters.md +++ b/docs/book/content/calibration/exogenous_parameters.md @@ -60,10 +60,10 @@ kernelspec: | $\texttt{use_zeta}$ | Whether to distribute bequests between lifetime income groups | 0.00E+00 | | $\zeta$ | Distribution of bequests | Too large to report here, see default parameters JSON | | $Z_{t}$ | Total factor productivity | Too large to report here, see default parameters JSON | -| $\gamma$ | Capital share of income | [0.401...0.401] | +| $\gamma$ | Capital share of income | [0.618...0.618] | | $\varepsilon$ | Elasticity of substitution between capital and labor | [1.000...1.000] | | $\delta$ | Capital depreciation rate | 0.050 | -| $g_{y}$ | Growth rate of labor augmenting technological progress | 0.00E+00 | +| $g_{y}$ | Growth rate of labor augmenting technological progress | 0.060 | | $\texttt{tax_func_type}$ | Functional form used for income tax functions | linear | | $\texttt{analytical_mtrs}$ | Whether use analytical MTRs or estimate MTRs | 0.00E+00 | | $\texttt{age_specific}$ | Whether use age-specific tax functions | 0.00E+00 | @@ -77,14 +77,14 @@ kernelspec: | $P$ | Coefficient on level term in wealth tax function | [0.000...0.000] | | $\texttt{budget_balance}$ | Whether have a balanced budget in each period | 0.00E+00 | | $\texttt{baseline_spending}$ | Whether level of spending constant between the baseline and reform runs | 0.00E+00 | -| $\alpha^{T}_{t}$ | Transfers as a share of GDP | [0.041...0.041] | +| $\alpha^{T}_{t}$ | Transfers as a share of GDP | [0.004...0.004] | | $\eta_{j,s,t}$ | Distribution of transfers | Too large to report here, see default parameters JSON | -| $\alpha^{G}_{t}$ | Government spending as a share of GDP | [0.267...0.267] | +| $\alpha^{G}_{t}$ | Government spending as a share of GDP | [0.055...0.055] | | $t_{G1}$ | Model period in which budget closure rule starts | 20 | | $t_{G2}$ | Model period in which budget closure rule ends | 256 | | $\rho_{G}$ | Budget closure rule smoothing parameter | 0.100 | | $\bar{\alpha}_{D}$ | Steady-state Debt-to-GDP ratio | 1.200 | -| $\alpha_{D,0}$ | Initial period Debt-to-GDP ratio | 0.740 | +| $\alpha_{D,0}$ | Initial period Debt-to-GDP ratio | 0.327 | | $\tau_{d,t}$ | Scale parameter in government interest rate wedge | [0.245...0.245] | | $\mu_{d,t}$ | Shift parameter in government interest rate wedge | [-0.034...-0.034] | | $\texttt{avg_earn_num_years}$ | Number of years over which compute average earnings for pension benefit | 35 | @@ -97,9 +97,9 @@ kernelspec: | $\texttt{PIA_minpayment}$ | Minimum PIA payment | 0.00E+00 | | $\theta_{adj,t}$ | Adjustment to replacement rate | [1.000...1.000] | | $r^{*}_{t}$ | World interest rate | [0.040...0.040] | -| $D_{f,0}$ | Share of government debt held by foreigners in initial period | 0.237 | -| $\zeta_{D, t}$ | Share of new debt issues purchased by foreigners | [0.237...0.237] | -| $\zeta_{K, t}$ | Share of excess capital demand satisfied by foreigners | [0.900...0.900] | +| $D_{f,0}$ | Share of government debt held by foreigners in initial period | 0.420 | +| $\zeta_{D, t}$ | Share of new debt issues purchased by foreigners | [0.120...0.120] | +| $\zeta_{K, t}$ | Share of excess capital demand satisfied by foreigners | [0.650...0.650] | | $\xi$ | Dampening parameter for TPI | 0.400 | | $\texttt{maxiter}$ | Maximum number of iterations for TPI | 250 | | $\texttt{mindist_SS}$ | SS solution tolerance | 1.00E-09 | diff --git a/docs/book/content/calibration/macro.md b/docs/book/content/calibration/macro.md index 1588c27..c3fd9d2 100644 --- a/docs/book/content/calibration/macro.md +++ b/docs/book/content/calibration/macro.md @@ -9,21 +9,21 @@ As the default rate of labor augmenting technological change, $g_y$, we use a va ### Foreign holding of government debt in the initial period -The path of foreign holding of domestic debt is endogenous, but the initial period stock of debt held by foreign investors is exogenous. We set this parameter, `initial_foreign_debt_ratio` to 0.95, consistent with [this report from the Ministry of Finance](https://www.mofed.gov.et/media/filer_public/9b/92/9b9264db-1a2b-4cd5-aa7d-f0307d67b4ce/public_sector_debt_statistical_bulletin_no_50.pdf). +The path of foreign holding of domestic debt is endogenous, but the initial period stock of debt held by foreign investors is exogenous. We set this parameter, `initial_foreign_debt_ratio`, to 0.42 using the Ministry of Finance Public Sector Debt Portfolio Analysis for FY2023/24, where external debt is USD 28.89 billion out of USD 68.86 billion of total public debt. ### Foreign purchases of newly issued debt -We set $\zeta_D = 0.95$, the same as initial holdings of government debt by foreigners. +We set $\zeta_D = 0.12$ using the same FY2023/24 Ministry of Finance debt portfolio analysis. In that year, the change in total public debt was about USD 5.53 billion while the change in external debt was about USD 0.64 billion, implying that roughly 11.6% of new debt issuance was external. ### Foreign holdings of excess capital -We set $\zeta_K = 0.95$. Note, this parameter is harder to pin down from the data as foreign purchases on "excess" capital demand is not typically directly measured or reported. A value of 0.95 implies a high degree of openness to international capital flows. +We set $\zeta_K = 0.65$. Note, this parameter is harder to pin down from the data as foreign purchases on "excess" capital demand is not typically directly measured or reported. A value of 0.65 implies a relatively open economy while still allowing for a sizable domestic share of excess capital demand. ## Government Debt, Spending and Transfers ### Government Debt -The path of government debt is endogenous. But the initial value and the steady-state (long-run) value are exogenous. To avoid converting between model units and dollars, we calibrate the initial debt to GDP ratio, rather than the dollar value of the debt. This is the model parameter $\alpha_D$ and the parameter name in [`ogeth_default_parameters.json`](https://github.com/EAPD-DRB/OG-ETH/blob/main/ogeth/ogeth_default_parameters.json) is `initial_debt_ratio`. We compute this from the ratio of publicly held debt outstanding to GDP. Based on the 2019 value reported by the World Bank, the initial debt-to-GDP ratio in Ethiopia is 0.314.[^macro_wb_DY] +The path of government debt is endogenous. But the initial value and the steady-state (long-run) value are exogenous. To avoid converting between model units and dollars, we calibrate the initial debt-to-GDP ratio, rather than the dollar value of debt. This is the model parameter $\alpha_D$ and the parameter name in [`ogeth_default_parameters.json`](https://github.com/EAPD-DRB/OG-ETH/blob/main/ogeth/ogeth_default_parameters.json) is `initial_debt_ratio`. We set `initial_debt_ratio = 0.327` using the IMF WEO gross general government debt series for Ethiopia, where FY2023/24 mapped to calendar year 2024 is 32.66% of GDP. #### Interest rates on government debt @@ -37,23 +37,46 @@ We assume that there is a wedge between the real rate of return on private capit where $\tau_d$ is the scale parameter and $\mu_d$ is the level shift parameter. We set the values of these two parameters to 0.245 and -0.034, respectively. These are found by using the estimated relationship between corporate and sovereign yields in {cite}`LMW2023` (Table 8, Column 2) and simulating a series of corporate yields given a series of sovereign yields between 2% and 12%. We then estimate the scale and level shift parameters that best fit these simulated data using ordinary least squares. +We use this emerging-markets relationship because a calibration based on readily available US corporate and sovereign yield data would be a poor proxy for Ethiopia. The goal of these parameters is not to capture a country-specific live spread series, but to impose a reasonable wedge between private and government borrowing rates using evidence from a broader sample of emerging markets. + +These values are fixed calibrated defaults in [`ogeth_default_parameters.json`](https://github.com/EAPD-DRB/OG-ETH/blob/main/ogeth/ogeth_default_parameters.json); they are not refreshed from live data during calibration updates. The following Python reproduces the one-time calculation used to obtain them: + +```python +import numpy as np +import statsmodels.api as sm + +sov_y = np.arange(20, 120) / 10 +corp_yhat = 8.199 - (2.975 * sov_y) + (0.478 * sov_y**2) +corp_yhat = sm.add_constant(corp_yhat) +res = sm.OLS(sov_y, corp_yhat).fit() + +r_gov_shift = -res.params[0] / 100 +r_gov_scale = res.params[1] + +print(r_gov_shift) # -0.03376625043803517 +print(r_gov_scale) # 0.24484763593657818 +``` + +We store these defaults rounded to five decimal places in the packaged JSON: +`r_gov_shift = -0.03377` and `r_gov_scale = 0.24485`. + ### Aggregate transfers -Aggregate (non-Social Security) transfers to households are set as a share of GDP with the parameter $\alpha_T$. We exclude Social Security from transfers since it is modeled specifically. We compute this from the IMF GFS Statement of Operations data as -
$\alpha_T = (\texttt{G27\_T} - \texttt{G271\_T}) / 100$
-For Ethiopia, the available IMF GFS percent-of-GDP series for this calibration are published under the `Budgetary central government` sector (`S1311B`), so the OG-ETH calibration uses that sector rather than mechanically reusing another country repo's sector code. Using the 2024 IMF values for `G27_T` and `G271_T`, the default calibration sets $\alpha_T = 0.0$. +Aggregate (non-Social Security) transfers to households are set as a share of GDP with the parameter $\alpha_T$. We exclude Social Security from transfers since it is modeled specifically. In OG-ETH, the relevant concept is government-financed, non-pension transfers paid to households. For Ethiopia, the IMF GFS `S1311B` social-benefits series (`G27_T` and `G271_T`) do not line up well with that concept in the calibration year because they miss the main FY2024/25 government cash contributions to the rural PSNP and urban UPSNP. + +Instead, the default calibration uses the IMF program target for Government Contributions to Productive Safety Net Programme cash transfers in FY2024/25 and scales it by the IMF nominal GDP series for the same fiscal year. The FY2024/25 transfer target is 51.4 billion birr and nominal GDP is 14,856 billion birr, implying $\alpha_T = 51.4 / 14{,}856 \approx 0.00346$, which we round to 0.0035. This choice is also consistent with the World Bank development policy financing documents that place the government contribution to rural and urban safety nets at about 0.4% of GDP in FY2024/25. Using the 2024 calibration year, the default calibration therefore sets $\alpha_T = 0.0035$. ### Government expenditures Government spending on goods and services are also set as a share of GDP with the parameter $\alpha_G$. We define government spending as:
Government Spending = Total Outlays - Transfers - Net Interest on Debt - Social Security
-In IMF GFS Statement of Operations terms, we compute -
$\alpha_G = (\texttt{G2\_T} - \texttt{G24\_T} - \texttt{G27\_T}) / 100$
-Using Ethiopia's 2024 `S1311B` IMF GFS percent-of-GDP series, the default calibration sets $\alpha_G = 0.0453$. +For Ethiopia, the concept behind $\alpha_G$ is closer to government consumption and public-goods spending than to a budgetary-central-government outlay residual. Because the IMF GFS data available for Ethiopia are published for `Budgetary central government` (`S1311B`), while the model concept is broader and closer to general government spending on goods and services, the default calibration uses the World Bank indicator `NE.CON.GOVT.ZS` instead of mechanically reusing the IMF construction used elsewhere. + +Using the World Bank's 2024 value for general government final consumption expenditure, the default calibration sets $\alpha_G = 0.0552$. (SecLWI_footnotes)= ## Footnotes The following are the footnotes for this section. -[^macro_wb_DY]: See https://data.worldbank.org/country/ethiopia, accessed Nov. 17, 2025. +[^macro_wb_DY]: The macro debt and transfer updates above use FY2023/24 or FY2024/25 sources mapped to the repo's 2024 calibration year when that is the closest official match. diff --git a/docs/create_doc_figures.py b/docs/create_doc_figures.py index 4195508..42efad0 100644 --- a/docs/create_doc_figures.py +++ b/docs/create_doc_figures.py @@ -11,7 +11,6 @@ from ogcore import parameter_plots as pp from ogcore import demographics as demog - CUR_DIR = os.path.dirname(os.path.realpath(__file__)) UN_COUNTRY_CODE = "231" plot_path = os.path.join(CUR_DIR, "book", "content", "calibration", "images") diff --git a/environment.yml b/environment.yml index 12dfdf5..17d890c 100644 --- a/environment.yml +++ b/environment.yml @@ -27,7 +27,6 @@ dependencies: - fsspec - aiohttp - xlwt -- statsmodels - linearmodels - black - jupyter diff --git a/ogeth/input_output.py b/ogeth/input_output.py index d30b78f..5748c53 100644 --- a/ogeth/input_output.py +++ b/ogeth/input_output.py @@ -3,7 +3,6 @@ import os from ogeth.constants import CONS_DICT, PROD_DICT - CUR_DIR = os.path.dirname(os.path.realpath(__file__)) sam_path = os.path.join(CUR_DIR, "data", "IFPRI_SAM_ETH_2022_SAM.csv") diff --git a/ogeth/macro_params.py b/ogeth/macro_params.py index 51fb341..0620250 100644 --- a/ogeth/macro_params.py +++ b/ogeth/macro_params.py @@ -1,16 +1,13 @@ """ -This module uses data from World Bank WDI, World Bank Quarterly Public -Sector Debt (QPSD) database, the IMF, and UN ILO to find values for -parameters for the OG-ETH model that rely on macro data for calibration. +This module uses World Bank WDI, UN ILO, and documented fiscal-source +values to update OG-ETH macro calibration parameters. """ # imports import pandas as pd -import numpy as np import requests import datetime from io import StringIO -from pathlib import Path def _fetch_wb_data(indicators, country_iso, start_year, end_year, source): @@ -90,135 +87,81 @@ def _fetch_wb_data(indicators, country_iso, start_year, end_year, source): return data -# IMF GFS coverage differs by country. For Ethiopia, the percent-of-GDP -# Statement of Operations series used for alpha_T and alpha_G are published -# under budgetary central government (S1311B), not the broader S1311 sector. -IMF_GFS_SECTOR_BY_COUNTRY = {"ETH": "S1311B"} +# Ethiopia calibrates alpha_G from World Bank general government final +# consumption expenditure because that better matches the OG-ETH concept of +# government spending on goods, services, and public goods than the available +# budgetary central government IMF GFS outlay series. +WB_ALPHA_G_SERIES = ( + "General government final consumption expenditure (% of GDP)" +) +# Ethiopia's default long-run productivity-growth calibration uses the +# post-2005 growth regime rather than the full World Bank history. +GDP_GROWTH_START_YEAR = 2006 -def _get_imf_gfs_sector(country_iso): - """ - Return the IMF GFS sector code to use for a country's alpha queries. - """ - return IMF_GFS_SECTOR_BY_COUNTRY.get(country_iso.upper(), "S1311") - -def _get_imf_macro_params(country_iso, target_year, data_path=None): +def _get_world_bank_alpha_g(wb_data, target_year): """ - Fetch IMF GFS data and compute alpha_T and alpha_G. - - Args: - country_iso (str): ISO alpha-3 country code - target_year (int): preferred calibration year - data_path (str | Path | None): optional path to save IMF CSV data - - Returns: - dict: IMF-derived macro parameters + Return alpha_G from World Bank government consumption data. """ - sector = _get_imf_gfs_sector(country_iso) - required_indicators = {"G2_T", "G24_T", "G27_T", "G271_T"} - data_path = Path(data_path) if data_path is not None else None - response = requests.get( - ( - "https://api.imf.org/external/sdmx/3.0/data/dataflow/" - f"IMF.STA/GFS_SOO/12.0.0/{country_iso}.{sector}.G2M.*.POGDP_PT.A" - ), - timeout=30, - ) - response.raise_for_status() - try: - payload = response.json() - data = payload["data"] - structure = data["structures"][0] - data_set = data["dataSets"][0] - series_dimensions = structure["dimensions"]["series"] - observation_years = [ - value.get("id", value.get("value")) - for value in structure["dimensions"]["observation"][0]["values"] - ] - except (ValueError, KeyError, IndexError, TypeError) as exc: - raise ValueError( - "Empty or malformed IMF response for GFS_SOO" - ) from exc - - records = [] - for series_key, series in data_set["series"].items(): - dimension_indexes = [int(idx) for idx in series_key.split(":")] - labels = { - dim["id"]: dim["values"][idx].get( - "id", dim["values"][idx].get("value") - ) - for dim, idx in zip(series_dimensions, dimension_indexes) - } - indicator = labels.get("INDICATOR") - if indicator not in required_indicators: - continue - for observation_key, observation in series.get( - "observations", {} - ).items(): - value = observation[0] - records.append( - { - "year": observation_years[int(observation_key)], - "indicator": indicator, - "value": value, - "country_iso": country_iso, - "sector": sector, - "dataset": "IMF.STA:GFS_SOO(12.0.0)", - } - ) - - imf_data = pd.DataFrame(records) - if imf_data.empty: - raise ValueError("Empty or malformed IMF response for GFS_SOO") - - imf_data["year"] = pd.to_numeric(imf_data["year"], errors="coerce") - imf_data["value"] = pd.to_numeric(imf_data["value"], errors="coerce") - imf_data = imf_data.dropna(subset=["year", "value"]) + if WB_ALPHA_G_SERIES not in wb_data.columns: + return None - if data_path is not None: - data_path.parent.mkdir(parents=True, exist_ok=True) - imf_data.sort_values(["indicator", "year"]).to_csv( - data_path, index=False - ) - print(f"IMF data saved to {data_path}") - - available = ( - imf_data.pivot_table( - index="year", - columns="indicator", - values="value", - aggfunc="first", - ) - .sort_index() - .dropna(subset=sorted(required_indicators)) - ) - available = available.loc[available.index <= int(target_year)] - - if available.empty: + if isinstance(wb_data.index, pd.MultiIndex): + years = wb_data.index.get_level_values(-1) + else: + years = wb_data.index + + alpha_g_series = pd.Series( + wb_data[WB_ALPHA_G_SERIES].values, + index=pd.to_numeric(years, errors="coerce"), + ).dropna() + alpha_g_series = alpha_g_series[alpha_g_series.index <= int(target_year)] + if alpha_g_series.empty: raise ValueError( - "No complete IMF data available for " - f"{country_iso} sector {sector} up to {target_year}" + "No World Bank government consumption data available up to " + f"{target_year}" ) selected_year = ( int(target_year) - if int(target_year) in available.index - else int(available.index.max()) + if int(target_year) in alpha_g_series.index + else int(alpha_g_series.index.max()) ) + value = alpha_g_series.loc[selected_year] / 100 if selected_year != int(target_year): print( - f"Warning: No IMF data for {target_year}. " - f"Using last available year: {selected_year}" + f"No World Bank alpha_G data in {target_year}. Using last " + f"available year {selected_year}: alpha_G={value:.4f}" ) + return [value] - values = available.loc[selected_year] - return { - "alpha_T": [(values["G27_T"] - values["G271_T"]) / 100], - "alpha_G": [ - (values["G2_T"] - values["G24_T"] - values["G27_T"]) / 100 - ], - } + +def _get_world_bank_g_y_annual(wb_data, data_start_date): + """ + Compute average GDP-per-capita growth using the Ethiopia-specific + calibration window rather than the full available history. + """ + series_name = "GDP per capita (constant 2015 US$)" + if series_name not in wb_data.columns: + return None + + if isinstance(wb_data.index, pd.MultiIndex): + years = wb_data.index.get_level_values(-1) + else: + years = wb_data.index + + growth_start_year = max( + int(data_start_date.year), + GDP_GROWTH_START_YEAR, + ) + gdp_pc_series = pd.Series( + wb_data[series_name].values, + index=pd.to_numeric(years, errors="coerce"), + ).sort_index(ascending=False) + gdp_pc_series = gdp_pc_series[gdp_pc_series.index >= growth_start_year] + g_y_series = gdp_pc_series.pct_change(-1) + return g_y_series.mean() if not g_y_series.isna().all() else None def get_macro_params( @@ -226,8 +169,6 @@ def get_macro_params( data_end_date=datetime.datetime(2024, 12, 31), country_iso="ETH", update_from_api=False, - imf_data_year=None, - imf_data_path=None, ): """ Compute values of parameters that are derived from macro data @@ -236,9 +177,6 @@ def get_macro_params( data_start_date (datetime): start date for data data_end_date (datetime): end date for data country_iso (str): ISO code for country - imf_data_year (int | None): IMF target year override. Defaults to - data_end_date.year when None. - imf_data_path (str | Path | None): optional path to save IMF CSV data Returns: macro_parameters (dict): dictionary of parameter values @@ -251,18 +189,18 @@ def get_macro_params( """ # Dictionaries of variables and their corresponding World Bank codes # Annual data - wb_a_variable_dict = { + wb_growth_variable_dict = { "GDP per capita (constant 2015 US$)": "NY.GDP.PCAP.KD", # "Real GDP (constant 2015 US$)": "NY.GDP.MKTP.KD", # "Nominal GDP (current US$)": "NY.GDP.MKTP.CD", # "General government final consumption expenditure (current US$)": "NE.CON.GOVT.CD", } + wb_alpha_g = None if update_from_api: try: - # Pull annual series from the World Bank v2 API - wb_data_a = _fetch_wb_data( - wb_a_variable_dict, + wb_growth_data = _fetch_wb_data( + wb_growth_variable_dict, country_iso, data_start_date.year, data_end_date.year, @@ -270,14 +208,9 @@ def get_macro_params( ) # Compute annual GDP growth safely - if "GDP per capita (constant 2015 US$)" in wb_data_a.columns: - g_y_series = wb_data_a[ - "GDP per capita (constant 2015 US$)" - ].pct_change(-1) - - # If all values are NaN, return None - macro_parameters["g_y_annual"] = ( - g_y_series.mean() if not g_y_series.isna().all() else None + if "GDP per capita (constant 2015 US$)" in wb_growth_data.columns: + macro_parameters["g_y_annual"] = _get_world_bank_g_y_annual( + wb_growth_data, data_start_date ) else: print( @@ -289,10 +222,21 @@ def get_macro_params( ) except Exception: print("Failed to retrieve data from World Bank") - print("Will not update the following parameters:") - print( - "[initial_debt_ratio, initial_foreign_debt_ratio, zeta_D, g_y]" + print("Will not update g_y_annual") + + try: + wb_alpha_g_data = _fetch_wb_data( + {WB_ALPHA_G_SERIES: "NE.CON.GOVT.ZS"}, + country_iso, + data_start_date.year, + data_end_date.year, + source=2, + ) + wb_alpha_g = _get_world_bank_alpha_g( + wb_alpha_g_data, data_end_date.year ) + except Exception: + wb_alpha_g = None else: print("Not updating from World Bank API") @@ -352,30 +296,25 @@ def get_macro_params( print("Not updating from ILOSTAT API") """ - Calibrate parameters from IMF and other sources + Calibrate parameters from documented fiscal sources """ if update_from_api: - try: - imf_year = ( - data_end_date.year if imf_data_year is None else imf_data_year - ) - macro_parameters.update( - _get_imf_macro_params( - country_iso, - imf_year, - data_path=imf_data_path, - ) - ) - print( - f"alpha_T updated from IMF data: {macro_parameters['alpha_T']}" - ) + # Ethiopia's alpha_T is calibrated from FY2024/25 government + # PSNP/UPSNP cash transfers, not from IMF GFS social-benefits + # series. Source: IMF Country Report 25/188: + # 51.4 / 14,856 ~= 0.0035, with World Bank DPO P181591 as a + # consistent ~0.4% of GDP cross-check. + print("Not updating alpha_T from API for Ethiopia") + + if wb_alpha_g is not None: + macro_parameters["alpha_G"] = wb_alpha_g print( - f"alpha_G updated from IMF data: {macro_parameters['alpha_G']}" + "alpha_G updated from World Bank government consumption " + f"data: {macro_parameters['alpha_G']}" ) - except Exception: - print("Failed to retrieve data from IMF") - print("Will not update alpha_T, alpha_G") + else: + print("Will not update alpha_G") # initial_debt_ratio, gross general government debt as a fraction of GDP # source: from the IMF WEO, Series ETH.GGXWDG_NGDP.A — Gross general government debt (% of GDP). @@ -397,45 +336,7 @@ def get_macro_params( # We use the latest year. macro_parameters["zeta_D"] = [0.12] - """" - Estimate the discount on sovereign yields relative to private debt - Follow the methodology in Li, Magud, Werner, Witte (2021) - available at: - https://www.imf.org/en/Publications/WP/Issues/2021/06/04/The-Long-Run-Impact-of-Sovereign-Yields-on-Corporate-Yields-in-Emerging-Markets-50224 - discussion is here: https://github.com/EAPD-DRB/OG-ZAF/issues/22 - Steps: - 1) Generate modelled corporate yields (corp_yhat) for a range of - sovereign yields (sov_y) using the estimated equation in col 2 of - table 8 (and figure 3). 2) Estimate the OLS using sovereign yields - as the dependent variable - """ - - try: - import statsmodels.api as sm - - # # estimate r_gov_shift and r_gov_scale - sov_y = np.arange(20, 120) / 10 - corp_yhat = 8.199 - (2.975 * sov_y) + (0.478 * sov_y**2) - corp_yhat = sm.add_constant(corp_yhat) - mod = sm.OLS( - sov_y, - corp_yhat, - ) - res = mod.fit() - # First term is the constant and needs to be divided by 100 to have - # the correct unit. Second term is the coefficient - macro_parameters["r_gov_shift"] = [-res.params[0] / 100] - macro_parameters["r_gov_scale"] = [res.params[1]] - print( - f"r_gov_shift updated from IMF data: {macro_parameters['r_gov_shift']}" - ) - print( - f"r_gov_scale updated from IMF data: {macro_parameters['r_gov_scale']}" - ) - except Exception: - print("Failed to compute r_gov_shift, r_gov_scale") - print("Will not update r_gov_shift, r_gov_scale") else: - print("Not updating alpha_T, alpha_G, r_gov_shift, r_gov_scale") + print("Not updating alpha_T, alpha_G") return macro_parameters diff --git a/ogeth/ogeth_default_parameters.json b/ogeth/ogeth_default_parameters.json index 5ad048e..a2f3797 100644 --- a/ogeth/ogeth_default_parameters.json +++ b/ogeth/ogeth_default_parameters.json @@ -1187,10 +1187,10 @@ "tG1": 20, "tG2": 256, "alpha_T": [ - 0.0 + 0.0035 ], "alpha_G": [ - 0.04525629819754754 + 0.05515691 ], "alpha_I": [ 0.0 @@ -1214,10 +1214,10 @@ "initial_debt_ratio": 0.327, "initial_Kg_ratio": 0.0, "r_gov_scale": [ - 0.24484763593657818 + 0.24485 ], "r_gov_shift": [ - -0.03376625043803517 + -0.03377 ], "cit_rate": [ [ diff --git a/setup.py b/setup.py index 7629f31..8943313 100644 --- a/setup.py +++ b/setup.py @@ -38,7 +38,6 @@ "requests", "xlwt", "openpyxl>=3.1.2", - "statsmodels", "linearmodels", "wheel", "black", diff --git a/tests/test_input_output.py b/tests/test_input_output.py index eac3782..c180ebd 100644 --- a/tests/test_input_output.py +++ b/tests/test_input_output.py @@ -9,7 +9,6 @@ from ogeth import input_output as io - HH_COLS = [ "hhd-r1", "hhd-r2", diff --git a/tests/test_macro_params.py b/tests/test_macro_params.py index 5a14acd..0649fda 100644 --- a/tests/test_macro_params.py +++ b/tests/test_macro_params.py @@ -2,11 +2,6 @@ Tests of macro_params.py module """ -import datetime -import sys -import types - -import pandas as pd import pytest import requests @@ -35,99 +30,6 @@ def raise_for_status(self): ) -def _imf_payload(indicator_year_values, country="ETH", sector="S1311B"): - years = sorted( - { - int(year) - for observations in indicator_year_values.values() - for year in observations.keys() - } - ) - indicators = list(indicator_year_values.keys()) - return { - "meta": {}, - "data": { - "dataSets": [ - { - "structure": 0, - "action": "Replace", - "series": { - f"0:0:0:{indicator_idx}:0:0": { - "attributes": [0, None, 0], - "observations": { - str(years.index(int(year))): [value] - for year, value in observations.items() - }, - } - for indicator_idx, observations in enumerate( - indicator_year_values.values() - ) - }, - } - ], - "structures": [ - { - "dimensions": { - "series": [ - {"id": "COUNTRY", "values": [{"id": country}]}, - {"id": "SECTOR", "values": [{"id": sector}]}, - {"id": "GFS_GRP", "values": [{"id": "G2M"}]}, - { - "id": "INDICATOR", - "values": [ - {"id": indicator} - for indicator in indicators - ], - }, - { - "id": "TYPE_OF_TRANSFORMATION", - "values": [{"id": "POGDP_PT"}], - }, - {"id": "FREQUENCY", "values": [{"id": "A"}]}, - ], - "observation": [ - { - "id": "TIME_PERIOD", - "values": [ - {"value": str(year)} for year in years - ], - } - ], - }, - "attributes": { - "series": [ - { - "id": "SCALE", - "values": [{"id": "0"}], - }, - {"id": "DECIMALS_DISPLAYED", "values": []}, - {"id": "OVERLAP", "values": [{"id": "OL"}]}, - ], - "observation": [ - {"id": "PRECISION", "values": []}, - { - "id": "DERIVATION_TYPE", - "values": [{"id": "O"}], - }, - {"id": "STATUS", "values": []}, - {"id": "NATURE_OF_DATA", "values": []}, - { - "id": "BASES_OF_RECORDING_CASH_NON_CASH", - "values": [], - }, - { - "id": "BASES_OF_RECORDING_GROSS_NET", - "values": [{"id": "NP"}], - }, - {"id": "VALUATION", "values": [{"id": "_Z"}]}, - ], - }, - } - ], - }, - } - - def _wb_payload(observations): return [ { @@ -151,6 +53,9 @@ def _wb_payload(observations): "NY.GDP.PCAP.KD": _wb_payload( [("2024", 100.0), ("2023", 80.0), ("2022", 64.0)] ), + "NE.CON.GOVT.ZS": _wb_payload( + [("2024", 5.515691), ("2023", 6.317772), ("2022", 7.361735)] + ), } @@ -159,7 +64,6 @@ def _mock_requests_get( requested_urls, *, ilo_text=None, - imf_json=None, wb_payloads=None, ): payloads = _DEFAULT_WB_PAYLOADS if wb_payloads is None else wb_payloads @@ -173,53 +77,11 @@ def fake_get(url, params=None, headers=None, timeout=None): return MockResponse( text=ilo_text or "time,obs_value\n2024,38.209\n2023,38.0\n" ) - if "api.imf.org" in url: - return MockResponse( - json_data=imf_json - or _imf_payload( - { - "G2_T": { - 2023: 6.121621434173129, - 2024: 5.117707506327274, - }, - "G24_T": { - 2023: 0.5526694185117545, - 2024: 0.5920776865725198, - }, - "G27_T": {2023: 0.0, 2024: 0.0}, - "G271_T": {2023: 0.0, 2024: 0.0}, - } - ) - ) raise AssertionError(f"Unexpected URL requested in test: {url}") monkeypatch.setattr(macro_params.requests, "get", fake_get) -def _mock_statsmodels(monkeypatch, params=None): - fake_statsmodels = types.ModuleType("statsmodels") - fake_api = types.ModuleType("statsmodels.api") - - def add_constant(values): - return values - - class OLS: - def __init__(self, endog, exog): - self.endog = endog - self.exog = exog - - def fit(self): - result = types.SimpleNamespace() - result.params = params or [3.376625043803517, 0.24484763593657818] - return result - - fake_api.add_constant = add_constant - fake_api.OLS = OLS - fake_statsmodels.api = fake_api - monkeypatch.setitem(sys.modules, "statsmodels", fake_statsmodels) - monkeypatch.setitem(sys.modules, "statsmodels.api", fake_api) - - def test_get_macro_params_update_from_api_false_returns_empty_dict(): test_dict = macro_params.get_macro_params(update_from_api=False) @@ -229,7 +91,6 @@ def test_get_macro_params_update_from_api_false_returns_empty_dict(): def test_get_macro_params_update_from_api_true(monkeypatch): requested_urls = [] - _mock_statsmodels(monkeypatch) _mock_requests_get(monkeypatch, requested_urls) test_dict = macro_params.get_macro_params(update_from_api=True) @@ -237,9 +98,6 @@ def test_get_macro_params_update_from_api_true(monkeypatch): assert isinstance(test_dict, dict) assert sorted(test_dict.keys()) == sorted( [ - "r_gov_shift", - "r_gov_scale", - "alpha_T", "alpha_G", "initial_debt_ratio", "g_y_annual", @@ -253,105 +111,33 @@ def test_get_macro_params_update_from_api_true(monkeypatch): assert test_dict["zeta_D"] == [0.12] assert test_dict["g_y_annual"] == pytest.approx(0.25) assert test_dict["gamma"] == [pytest.approx(0.61791)] - assert test_dict["alpha_T"] == [pytest.approx(0.0)] - assert test_dict["alpha_G"] == [pytest.approx(0.04525629819754754)] - assert test_dict["r_gov_shift"] == [pytest.approx(-0.03376625043803517)] - assert test_dict["r_gov_scale"] == [pytest.approx(0.24484763593657818)] - assert any( - ".ETH.S1311B.G2M." in url or "/ETH.S1311B.G2M." in url - for url in requested_urls - ) + assert "alpha_T" not in test_dict + assert test_dict["alpha_G"] == [pytest.approx(0.05515691)] + assert not any("api.imf.org" in url for url in requested_urls) -def test_get_imf_macro_params_uses_eth_budgetary_sector(monkeypatch): +def test_alpha_g_omitted_when_world_bank_returns_empty(monkeypatch): + # Simulate the World Bank alpha_G series returning no observations + # while the GDP-per-capita series is complete. The split fetches keep + # g_y_annual intact; alpha_T is not sourced from any API for Ethiopia. requested_urls = [] - _mock_requests_get(monkeypatch, requested_urls) - - result = macro_params._get_imf_macro_params("ETH", 2024) - - assert result["alpha_T"] == [pytest.approx(0.0)] - assert result["alpha_G"] == [pytest.approx(0.04525629819754754)] - assert any("/ETH.S1311B.G2M.*.POGDP_PT.A" in url for url in requested_urls) - - -def test_get_imf_macro_params_overwrites_saved_file(monkeypatch, tmp_path): - requested_urls = [] - _mock_requests_get(monkeypatch, requested_urls) - - data_file = tmp_path / "imf_gfs_soo_eth_s1311b_g2m_pogdp_pt_a.csv" - result = macro_params._get_imf_macro_params( - "ETH", 2024, data_path=data_file - ) - - assert result["alpha_G"] == [pytest.approx(0.04525629819754754)] - assert data_file.exists() - - requested_urls.clear() _mock_requests_get( monkeypatch, requested_urls, - imf_json=_imf_payload( - { - "G2_T": {2024: 5.0}, - "G24_T": {2024: 0.5}, - "G27_T": {2024: 0.0}, - "G271_T": {2024: 0.0}, - } - ), - ) - - refreshed = macro_params._get_imf_macro_params( - "ETH", 2024, data_path=data_file - ) - - assert refreshed != result - saved_data = pd.read_csv(data_file) - saved_2024 = saved_data[saved_data["year"] == 2024].set_index("indicator") - assert saved_2024.loc["G2_T", "value"] == pytest.approx(5.0) - assert saved_2024.loc["G24_T", "value"] == pytest.approx(0.5) - - -def test_get_imf_macro_params_falls_back_to_last_available_year(monkeypatch): - requested_urls = [] - _mock_requests_get( - monkeypatch, - requested_urls, - imf_json=_imf_payload( - { - "G2_T": {2024: 5.117707506327274}, - "G24_T": {2024: 0.5920776865725198}, - "G27_T": {2024: 0.0}, - "G271_T": {2024: 0.0}, - } - ), - ) - - result = macro_params._get_imf_macro_params("ETH", 2025) - - assert result["alpha_T"] == [pytest.approx(0.0)] - assert result["alpha_G"] == [pytest.approx(0.04525629819754754)] - - -def test_get_macro_params_passes_imf_year_override(monkeypatch): - requested_urls = [] - _mock_statsmodels(monkeypatch) - _mock_requests_get( - monkeypatch, - requested_urls, - imf_json=_imf_payload( - { - "G2_T": {2023: 6.121621434173129}, - "G24_T": {2023: 0.5526694185117545}, - "G27_T": {2023: 0.0}, - "G271_T": {2023: 0.0}, - } - ), + wb_payloads={ + "NY.GDP.PCAP.KD": _wb_payload( + [("2024", 100.0), ("2023", 80.0), ("2022", 64.0)] + ), + "NE.CON.GOVT.ZS": [ + {"page": 1, "pages": 1, "per_page": "10000", "total": 0}, + [], + ], + }, ) - test_dict = macro_params.get_macro_params( - update_from_api=True, - imf_data_year=2023, - data_end_date=datetime.datetime(2024, 12, 31), - ) + test_dict = macro_params.get_macro_params(update_from_api=True) - assert test_dict["alpha_G"] == [pytest.approx(0.05568952015661374)] + assert "alpha_T" not in test_dict + assert "alpha_G" not in test_dict + assert test_dict["g_y_annual"] == pytest.approx(0.25) + assert not any("api.imf.org" in url for url in requested_urls)