"""
-------------------------------------------------------------------------------
Functions for generating demographic objects necessary for the OG-USA model. A
list of UN official 3-digit country codes and corresponding 3-character country
abbreviations is available at https://unstats.un.org/unsd/methodology/m49/
-------------------------------------------------------------------------------
"""
# Import packages
import os
import numpy as np
from io import StringIO
import scipy.optimize as opt
import pandas as pd
from ogcore.utils import get_legacy_session
from ogcore import parameter_plots as pp
START_YEAR = 2024
END_YEAR = 2024
UN_COUNTRY_CODE = "840" # UN code for USA
# create output director for figures
CUR_PATH = os.path.split(os.path.abspath(__file__))[0]
OUTPUT_DIR = os.path.join(CUR_PATH, "..", "data", "OUTPUT", "Demographics")
if os.access(OUTPUT_DIR, os.F_OK) is False:
os.makedirs(OUTPUT_DIR)
"""
------------------------------------------------------------------------
Define functions
------------------------------------------------------------------------
"""
[docs]
def get_un_data(
variable_code,
country_id=UN_COUNTRY_CODE,
start_year=START_YEAR,
end_year=END_YEAR,
):
"""
This function retrieves data from the United Nations Data Portal API
for UN population data (see
https://population.un.org/dataportal/about/dataapi)
Args:
variable_code (str): variable code for UN data
country_id (str): country id for UN data
start_year (int): start year for UN data
end_year (int): end year for UN data
Returns:
df (Pandas DataFrame): DataFrame of UN data
"""
target = (
"https://population.un.org/dataportalapi/api/v1/data/indicators/"
+ variable_code
+ "/locations/"
+ country_id
+ "/start/"
+ str(start_year)
+ "/end/"
+ str(end_year)
+ "?format=csv"
)
# Check for a file named "un_api_token.txt" in the current directory
if os.path.exists(os.path.join("un_api_token.txt")):
with open(os.path.join("un_api_token.txt"), "r") as file:
UN_TOKEN = file.read().strip()
else: # if file not exist, prompt user for token
try:
UN_TOKEN = input(
"Please enter your UN API token "
"(press return if you do not have one): "
)
# write the UN_TOKEN to a file to find in the future
with open(os.path.join("un_api_token.txt"), "w") as file:
file.write(UN_TOKEN)
except EOFError:
UN_TOKEN = ""
# get data from url
payload = {}
# Accept a token with or without a leading "Bearer " prefix so the
# header isn't doubled into "Bearer Bearer <token>".
UN_TOKEN = UN_TOKEN.strip()
if UN_TOKEN.lower().startswith("bearer "):
UN_TOKEN = UN_TOKEN[len("bearer ") :].strip()
headers = {"Authorization": "Bearer " + UN_TOKEN}
response = get_legacy_session().get(target, headers=headers, data=payload)
# Check if the request was successful before processing
if response.status_code == 200:
csvStringIO = StringIO(response.text)
df = pd.read_csv(csvStringIO, sep="|", header=1)
# keep just what is needed from data
df = df[df.Variant == "Median"]
df = df[df.Sex == "Both sexes"][["TimeLabel", "AgeLabel", "Value"]]
df.rename(
{"TimeLabel": "year", "AgeLabel": "age", "Value": "value"},
axis=1,
inplace=True,
)
# The "100+" top age bin appears only in some series (mortality,
# population), so the age column may parse as int (no "100+") or
# str. Normalize to str so the replacement works under pandas
# >=3.0's strict string dtype, then cast the column to int.
df.age = df.age.astype(str)
df.loc[df.age == "100+", "age"] = "100"
df.age = df.age.astype(int)
df.year = df.year.astype(int)
df = df[df.age < 100] # need to drop 100+ age category
else:
# Read from UN GH Repo:
print(
"Failed to retrieve population data from UN. Reading "
+ " from https://github.com/EAPD-DRB/Population-Data "
+ "instead of UN WPP API"
)
country_dict = {
"840": "USA",
"710": "ZAF",
"458": "MYS",
"356": "IND",
"826": "UK",
"360": "IDN",
"608": "PHL",
"764": "THA",
"076": "BRA",
"410": "KOR",
"231": "ETH",
}
un_variable_dict = {
"68": "fertility_rates",
"80": "mortality_rates",
"47": "population",
}
country = country_dict[country_id]
variable = un_variable_dict[variable_code]
url = (
"https://raw.githubusercontent.com/EAPD-DRB/"
+ "Population-Data/main/"
+ "Data/{c}/UN_{v}_data.csv".format(c=country, v=variable)
)
df = pd.read_csv(url)
# keep just the years requested
df = df[(df.year >= start_year) & (df.year <= end_year)]
# Do we still want to keep the status code for failures?
# print(
# "Failed to retrieve population data. HTTP status code: "
# f"{response.status_code}"
# )
# assert False
return df
[docs]
def get_fert(
totpers=100,
min_age=0,
max_age=99,
country_id=UN_COUNTRY_CODE,
start_year=START_YEAR,
end_year=END_YEAR,
graph=False,
plot_path=None,
download_path=None,
):
"""
This function generates a vector of fertility rates by model period
age that corresponds to the fertility rate data by age in years.
Args:
totpers (int): total number of agent life periods (E+S), >= 3
min_age (int): age in years at which agents are born, >= 0
max_age (int): age in years at which agents die with certainty,
>= 4, < 100 (max age in UN data is 99, 100+ i same group)
country_id (str): country id for UN data
start_year (int): start year for UN data
end_year (int): end year for UN data
graph (bool): =True if want graphical output
plot_path (str): path to save fertility rate plot
download_path (str): path to save fertility rate data
Returns:
fert_rates (Numpy array): fertility rates for each year of data
and model age
fig (Matplotlib Figure): figure object if graph=True and plot_path=None
"""
# initialize fert rates array
fert_rates_2D = np.zeros((end_year + 1 - start_year, totpers))
# Read UN data
df = get_un_data(
"68", country_id=country_id, start_year=start_year, end_year=end_year
)
# CLean and rebin data
for y in range(start_year, end_year + 1):
df_y = df[(df.age >= min_age) & (df.age <= max_age) & (df.year == y)]
# put in vector
fert_rates = df_y.value.values
# fill in with zeros for ages < 15 and > 49
# NOTE: this assumes min_year < 15 and max_age > 49
fert_rates = np.append(fert_rates, np.zeros(max_age - 49))
fert_rates = np.append(np.zeros(15 - min_age), fert_rates)
# divide by 1000 because fertility rates are number of births per
# 1000 woman and we want births per person (might update to account
# from fraction men more correctly - below assumes 50/50 men and women)
fert_rates = fert_rates / 2000
# Rebin data in the case that model period not equal to one calendar
# year
fert_rates = pop_rebin(fert_rates, totpers)
fert_rates_2D[y - start_year, :] = fert_rates
if download_path:
np.savetxt(
os.path.join(download_path, "fert_rates.csv"),
fert_rates_2D,
delimiter=",",
)
# Create plots if needed
if graph:
if start_year == end_year:
years_list = [str(start_year)]
fert_rates_list = [fert_rates_2D[0, :]]
else:
years_list = [str(x) for x in np.arange(start_year, end_year + 1)]
fert_rates_list = [
fert_rates_2D[i, :] for i in range(fert_rates_2D.shape[0])
]
if plot_path is not None:
pp.plot_fert_rates(
fert_rates_list,
labels=years_list,
path=plot_path,
)
return fert_rates_2D
else:
fig = pp.plot_fert_rates(
fert_rates_list,
labels=years_list,
)
return fert_rates_2D, fig
else:
return fert_rates_2D
[docs]
def get_mort(
totpers=100,
min_age=0,
max_age=99,
country_id=UN_COUNTRY_CODE,
start_year=START_YEAR,
end_year=END_YEAR,
graph=False,
plot_path=None,
download_path=None,
):
"""
This function generates a vector of mortality rates by model period
age.
Args:
totpers (int): total number of agent life periods (E+S), >= 3
min_age (int): age in years at which agents are born, >= 0
max_age (int): age in years at which agents die with certainty,
>= 4, < 100 (max age in UN data is 99, 100+ i same group)
country_id (str): country id for UN data
start_year (int): start year for UN data
end_year (int): end year for UN data
graph (bool): =True if want graphical output
plot_path (str): path to save mortality rate plot
download_path (str): path to save mortality rate data
Returns:
mort_rates (Numpy array) mortality rates for each year of data
and model age
infmort_rate_vec (Numpy array): infant mortality rates for each
fig (Matplotlib Figure): figure object if graph=True and plot_path=None
"""
mort_rates_2D = np.zeros((end_year + 1 - start_year, totpers))
infmort_rate_vec = np.zeros(end_year + 1 - start_year)
# Read UN data
df = get_un_data(
"80", country_id=country_id, start_year=start_year, end_year=end_year
)
# CLean and rebin data
for y in range(start_year, end_year + 1):
df_y = df[(df.age >= min_age) & (df.age <= max_age) & (df.year == y)]
# put in vector
mort_rates_data = df_y.value.values
# In UN data, mortality rates for 0 year olds are the infant
# mortality rates
infmort_rate = mort_rates_data[0]
# Rebin data in the case that model period not equal to one calendar
# year
# make mort rates those from age 1-100 and set to 1 for age 100
mort_rates_data = np.append(mort_rates_data[1:], 1.0)
mort_rates = pop_rebin(mort_rates_data, totpers)
# put in 2D array
mort_rates_2D[y - start_year, :] = mort_rates
infmort_rate_vec[y - start_year] = infmort_rate
if download_path:
np.savetxt(
os.path.join(download_path, "mort_rates.csv"),
mort_rates_2D,
delimiter=",",
)
np.savetxt(
os.path.join(download_path, "infmort_rates.csv"),
infmort_rate_vec,
delimiter=",",
)
# Create plots if needed
if graph:
if start_year == end_year:
years_to_plot = [start_year]
else:
years_to_plot = [start_year, end_year]
if plot_path is not None:
pp.plot_mort_rates_data(
mort_rates_2D,
start_year,
years_to_plot,
path=plot_path,
)
return mort_rates_2D, infmort_rate_vec
else:
fig = pp.plot_mort_rates_data(
mort_rates_2D,
start_year,
years_to_plot,
)
return mort_rates_2D, infmort_rate_vec, fig
else:
return mort_rates_2D, infmort_rate_vec
[docs]
def get_pop(
E=20,
S=80,
min_age=0,
max_age=99,
infer_pop=False,
fert_rates=None,
mort_rates=None,
infmort_rates=None,
imm_rates=None,
initial_pop=None,
country_id=UN_COUNTRY_CODE,
start_year=START_YEAR,
end_year=END_YEAR,
download_path=None,
):
"""
Retrieves the population distribution data from the UN data API
Args:
E (int): number of model periods in which agent is not
economically active, >= 1
S (int): number of model periods in which agent is economically
active, >= 3
min_age (int): age in years at which agents are born, >= 0
max_age (int): age in years at which agents die with certainty,
>= 4, < 100 (max age in UN data is 99, 100+ i same group)
infer_pop (bool): =True if want to infer the population
from the given fertility, mortality, and immigration rates
fert_rates (Numpy array): fertility rates for each year of data
and model age
mort_rates (Numpy array): mortality rates for each year of data
and model age
infmort_rates (Numpy array): infant mortality rates for each year
of data
imm_rates (Numpy array): immigration rates for reach year of data
and model age
initial_pop_data (Pandas DataFrame): initial population data
for the first year of model calibration (start_year)
country_id (str): country id for UN data
start_year (int): start year data
end_year (int): end year for data
download_path (str): path to save population distribution data
Returns:
pop_2D (Numpy array): population distribution over T0 periods
"""
# Generate time path of the nonstationary population distribution
# Get path up to end of data year
pop_2D = np.zeros((end_year + 2 - start_year, E + S))
if infer_pop:
if initial_pop is None:
initial_pop_data = get_un_data(
"47",
country_id=country_id,
start_year=start_year,
end_year=start_year,
)
initial_pop_sample = initial_pop_data[
(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)
# Check that have all necessary inputs to infer the population
# distribution
assert not [
x
for x in (fert_rates, mort_rates, infmort_rates, imm_rates)
if x is None
]
len_pop_dist = end_year + 1 - start_year
pop_2D = np.zeros((len_pop_dist, E + S))
# set initial population distribution in the counterfactual to
# the first year of the user provided distribution
pop_2D[0, :] = initial_pop
for t in range(1, len_pop_dist):
# find newborns next period
newborns = np.dot(fert_rates[t - 1, :], pop_2D[t - 1, :])
pop_2D[t, 0] = (1 - infmort_rates[t - 1]) * newborns + imm_rates[
t - 1, 0
] * pop_2D[t - 1, 0]
pop_2D[t, 1:] = (
pop_2D[t - 1, :-1] * (1 - mort_rates[t - 1, :-1])
+ pop_2D[t - 1, 1:] * imm_rates[t - 1, 1:]
)
else:
# Read UN data
pop_data = get_un_data(
"47",
country_id=country_id,
start_year=start_year,
# note go to + 2 because needed to infer immigration
# for end_year
end_year=end_year + 2,
)
# CLean and rebin data
for y in range(start_year, end_year + 2):
pop_data_sample = pop_data[
(pop_data["age"] >= min_age)
& (pop_data["age"] <= max_age)
& (pop_data["year"] == y)
]
pop = pop_data_sample.value.values
# Generate the current population distribution given that E+S might
# be less than max_age-min_age+1
pop_EpS = pop_rebin(pop, E + S)
pop_2D[y - start_year, :] = pop_EpS
if download_path:
np.savetxt(
os.path.join(download_path, "population_distribution.csv"),
pop_2D,
delimiter=",",
)
return pop_2D
[docs]
def pop_rebin(curr_pop_dist, totpers_new):
"""
For cases in which totpers (E+S) is less than the number of periods
in the population distribution data, this function calculates a new
population distribution vector with totpers (E+S) elements.
Args:
curr_pop_dist (Numpy array): population distribution over N
periods
totpers_new (int): number of periods to which we are
transforming the population distribution, >= 3
Returns:
curr_pop_new (Numpy array): new population distribution over
totpers (E+S) periods that approximates curr_pop_dist
"""
# Number of periods in original data
assert totpers_new >= 3
# Number of periods in original data
totpers_orig = len(curr_pop_dist)
if int(totpers_new) == totpers_orig:
curr_pop_new = curr_pop_dist
elif int(totpers_new) < totpers_orig:
num_sub_bins = float(10000)
curr_pop_sub = np.repeat(
np.float64(curr_pop_dist) / num_sub_bins, num_sub_bins
)
len_subbins = (np.float64(totpers_orig * num_sub_bins)) / totpers_new
curr_pop_new = np.zeros(totpers_new, dtype=np.float64)
end_sub_bin = 0
for i in range(totpers_new):
beg_sub_bin = int(end_sub_bin)
end_sub_bin = int(np.rint((i + 1) * len_subbins))
curr_pop_new[i] = curr_pop_sub[beg_sub_bin:end_sub_bin].sum()
# Return curr_pop_new to single precision float (float32)
# datatype
curr_pop_new = np.float32(curr_pop_new)
return curr_pop_new
[docs]
def get_imm_rates(
totpers=100,
min_age=0,
max_age=99,
fert_rates=None,
mort_rates=None,
infmort_rates=None,
pop_dist=None,
country_id=UN_COUNTRY_CODE,
start_year=START_YEAR,
end_year=END_YEAR,
graph=False,
plot_path=None,
download_path=None,
):
"""
Calculate immigration rates by age as a residual given population
levels in different periods, then output average calculated
immigration rate. We have to replace the first mortality rate in
this function in order to adjust the first implied immigration rate
Args:
totpers (int): total number of agent life periods (E+S), >= 3
min_age (int): age in years at which agents are born, >= 0
max_age (int): age in years at which agents die with certainty,
>= 4
fert_rates (Numpy array): fertility rates for each year of data
and model age
mort_rates (Numpy array): mortality rates for each year of data
and model age
infmort_rates (Numpy array): infant mortality rates for each year
of data
pop_dist (Numpy array): population distribution over T0+1 periods
country_id (str): country id for UN data
start_year (int): start year for UN data
end_year (int): end year for UN data
graph (bool): =True if want graphical output
plot_path (str): path to save figure to
download_path (str): path to save immigration rate data
Returns:
imm_rates_2D (Numpy array):immigration rates that correspond to
each year of data and period of life, length E+S
"""
imm_rates_2D = np.zeros((end_year + 1 - start_year, totpers))
if fert_rates is None:
# get fert rates from UN data from initial year to data year
fert_rates = get_fert(
totpers, min_age, max_age, country_id, start_year, end_year
)
else:
# ensure that user provided fert_rates and mort rates of same size
assert fert_rates.shape == mort_rates.shape
if mort_rates is None:
# get mort rates from UN data from initial year to data year
mort_rates, infmort_rates = get_mort(
totpers, min_age, max_age, country_id, start_year, end_year
)
else:
# ensure that user provided fert_rates and mort rates of same size
assert fert_rates.shape == mort_rates.shape
assert infmort_rates is not None
assert infmort_rates.shape[0] == mort_rates.shape[0]
if pop_dist is None:
# need to read UN population data
df = get_un_data(
"47",
country_id=country_id,
start_year=start_year,
end_year=end_year + 2,
)
pop_dist = np.zeros((end_year + 2 - start_year, totpers))
for y in range(start_year, end_year + 1):
pop_t = df[
(df.age < 100) & (df.age >= 0) & (df.year == y)
].value.values
pop_t = pop_rebin(pop_t, totpers)
pop_dist[y - start_year, :] = pop_t
# Make sure shape conforms
assert pop_dist.shape[1] == mort_rates.shape[1]
assert pop_dist.shape[0] == end_year - start_year + 2
for y in range(start_year, end_year + 1):
pop_t = pop_dist[y - start_year, :]
pop_tp1 = pop_dist[y + 1 - start_year, :]
# initialize imm_rate vector
imm_rates = np.zeros(totpers)
# back out imm rates by age for each year
newborns = np.dot(fert_rates[y - start_year, :], pop_t)
# new born imm_rate
imm_rates[0] = (
pop_tp1[0] - (1 - infmort_rates[y - start_year]) * newborns
) / pop_t[0]
# all other age imm_rates
imm_rates[1:] = (
pop_tp1[1:] - (1 - mort_rates[y - start_year, :-1]) * pop_t[:-1]
) / pop_t[1:]
imm_rates_2D[y - start_year, :] = imm_rates
if download_path:
np.savetxt(
os.path.join(download_path, "immigration_rates.csv"),
imm_rates_2D,
delimiter=",",
)
# Create plots if needed
if graph:
if start_year == end_year:
years_to_plot = [start_year]
else:
years_to_plot = [start_year, end_year]
if plot_path is not None:
pp.plot_imm_rates(
imm_rates_2D,
start_year,
years_to_plot,
path=plot_path,
)
return imm_rates_2D
else:
fig = pp.plot_imm_rates(
imm_rates_2D,
start_year,
years_to_plot,
)
return imm_rates_2D, fig
else:
return imm_rates_2D
[docs]
def immsolve(imm_rates, *args):
"""
This function generates a vector of errors representing the
difference in two consecutive periods stationary population
distributions. This vector of differences is the zero-function
objective used to solve for the immigration rates vector, similar to
the original immigration rates vector from get_imm_rates(), that
sets the steady-state population distribution by age equal to the
population distribution in period int(1.5*S)
Args:
imm_rates (Numpy array):immigration rates that correspond to
each period of life, length E+S
args (tuple): (fert_rates, mort_rates, infmort_rates, omega_cur,
g_n_SS)
Returns:
omega_errs (Numpy array): difference between omega_new and
omega_cur_pct, length E+S
"""
fert_rates, mort_rates, infmort_rates, omega_cur_lev, g_n_SS = args
omega_cur_pct = omega_cur_lev / omega_cur_lev.sum()
totpers = len(fert_rates)
OMEGA = np.zeros((totpers, totpers))
OMEGA[0, :] = (1 - infmort_rates) * fert_rates + np.hstack(
(imm_rates[0], np.zeros(totpers - 1))
)
OMEGA[1:, :-1] += np.diag(1 - mort_rates[:-1])
OMEGA[1:, 1:] += np.diag(imm_rates[1:])
omega_new = np.dot(OMEGA, omega_cur_pct) / (1 + g_n_SS)
omega_errs = omega_new - omega_cur_pct
return omega_errs
def _logistic(x):
"""
Numerically stable logistic transform.
"""
return 1 / (1 + np.exp(-np.clip(x, -700, 700)))
def _income_shares_and_midpoints(income_percentiles):
"""
Convert income group population shares into centered percentile midpoints.
"""
income_shares = np.asarray(income_percentiles, dtype=float).ravel()
if income_shares.ndim != 1 or income_shares.size < 1:
raise ValueError("income_percentiles must be a one-dimensional array.")
if np.any(income_shares <= 0):
raise ValueError("income_percentiles must contain positive values.")
income_shares = income_shares / income_shares.sum()
percentile_midpoints = (
100 * (np.cumsum(income_shares) - 0.5 * income_shares) - 50
)
return income_shares, percentile_midpoints
def _extend_time_path(arr, num_periods):
"""
Extend or trim the first dimension of an array to num_periods.
"""
if arr.shape[0] == num_periods:
return arr
if arr.shape[0] > num_periods:
return arr[:num_periods]
extension = np.repeat(arr[-1:, ...], num_periods - arr.shape[0], axis=0)
return np.concatenate((arr, extension), axis=0)
def _format_age_gradient(gradient, num_periods, E, S, name):
"""
Put an age gradient into a num_periods x (E+S) array.
A length-S vector is interpreted as applying to economically active ages
only, with zero gradients for younger ages.
"""
totpers = E + S
if gradient is None:
return np.zeros((num_periods, totpers))
gradient = np.asarray(gradient, dtype=float)
if gradient.ndim == 0:
return np.full((num_periods, totpers), gradient)
if gradient.ndim == 1:
if gradient.shape[0] == 1:
full_gradient = np.full(totpers, gradient.item())
elif gradient.shape[0] == S:
full_gradient = np.concatenate((np.zeros(E), gradient))
elif gradient.shape[0] == totpers:
full_gradient = gradient
else:
raise ValueError(
f"{name} must have length 1, S={S}, or E+S={totpers}."
)
return np.tile(full_gradient.reshape(1, totpers), (num_periods, 1))
if gradient.ndim == 2:
if gradient.shape[1] == S:
gradient = np.concatenate(
(np.zeros((gradient.shape[0], E)), gradient), axis=1
)
elif gradient.shape[1] != totpers:
raise ValueError(
f"{name} must have second dimension S={S} or E+S={totpers}."
)
return _extend_time_path(gradient, num_periods)
raise ValueError(f"{name} must be a scalar, vector, or 2D array.")
def _format_infmort_gradient(gradient, num_periods):
"""
Put an infant mortality gradient into a num_periods vector.
"""
if gradient is None:
return np.zeros(num_periods)
gradient = np.asarray(gradient, dtype=float)
if gradient.ndim == 0:
return np.full(num_periods, gradient)
if gradient.ndim == 1:
if gradient.shape[0] == 1:
return np.full(num_periods, gradient.item())
return _extend_time_path(gradient.reshape(-1, 1), num_periods).ravel()
raise ValueError("infmort_gradient must be a scalar or vector.")
def _format_imm_shares(imm_pctiles, num_periods, E, S, income_shares):
"""
Format immigrant income shares as num_periods x (E+S) x J.
"""
totpers = E + S
J = income_shares.shape[0]
if imm_pctiles is None:
return np.tile(
income_shares.reshape(1, 1, J), (num_periods, totpers, 1)
)
imm_shares = np.asarray(imm_pctiles, dtype=float)
if imm_shares.ndim == 1:
if imm_shares.shape[0] != J:
raise ValueError(f"imm_pctiles must have J={J} elements.")
imm_shares = np.tile(
imm_shares.reshape(1, 1, J), (num_periods, totpers, 1)
)
elif imm_shares.ndim == 2:
if imm_shares.shape[-1] != J:
raise ValueError(f"imm_pctiles last dimension must be J={J}.")
if imm_shares.shape[0] == S:
young_shares = np.tile(income_shares.reshape(1, J), (E, 1))
imm_shares = np.concatenate((young_shares, imm_shares), axis=0)
elif imm_shares.shape[0] != totpers:
raise ValueError(
f"imm_pctiles first dimension must be S={S} or E+S={totpers}."
)
imm_shares = np.tile(
imm_shares.reshape(1, totpers, J), (num_periods, 1, 1)
)
elif imm_shares.ndim == 3:
if imm_shares.shape[-1] != J:
raise ValueError(f"imm_pctiles last dimension must be J={J}.")
if imm_shares.shape[1] == S:
young_shares = np.tile(
income_shares.reshape(1, 1, J), (imm_shares.shape[0], E, 1)
)
imm_shares = np.concatenate((young_shares, imm_shares), axis=1)
elif imm_shares.shape[1] != totpers:
raise ValueError(
f"imm_pctiles second dimension must be S={S} or E+S={totpers}."
)
imm_shares = _extend_time_path(imm_shares, num_periods)
else:
raise ValueError(
"imm_pctiles must be a vector, 2D array, or 3D array."
)
if np.any(imm_shares < 0):
raise ValueError("imm_pctiles must be nonnegative.")
denom = imm_shares.sum(axis=-1, keepdims=True)
if np.any(denom <= 0):
raise ValueError("imm_pctiles must sum to a positive value across J.")
return imm_shares / denom
def _within_age_weights(pop_by_age_j, income_shares):
"""
Compute J weights within each age, using income_shares if an age is empty.
"""
age_totals = pop_by_age_j.sum(axis=-1, keepdims=True)
default_weights = np.tile(
income_shares.reshape(1, income_shares.shape[0]),
(pop_by_age_j.shape[0], 1),
)
return np.divide(
pop_by_age_j,
age_totals,
out=default_weights,
where=age_totals > 0,
)
def _mean_preserving_logit_rates(mean_rates, slopes, weights, pct_midpoints):
"""
Create J-specific rates bounded in [0, 1] with weighted mean mean_rates.
"""
mean_rates = np.asarray(mean_rates, dtype=float)
slopes = np.asarray(slopes, dtype=float)
weights = np.asarray(weights, dtype=float)
rates = np.zeros(weights.shape)
for idx in np.ndindex(mean_rates.shape):
mean_rate = mean_rates[idx]
slope = slopes[idx]
w = weights[idx]
w = w / w.sum()
if mean_rate <= 0:
rates[idx] = 0.0
elif mean_rate >= 1:
rates[idx] = 1.0
elif np.isclose(slope, 0.0):
rates[idx] = mean_rate
else:
def mean_error(intercept):
return (
np.dot(w, _logistic(intercept + slope * pct_midpoints))
- mean_rate
)
intercept = opt.brentq(mean_error, -700, 700)
rates[idx] = _logistic(intercept + slope * pct_midpoints)
return rates
[docs]
def expand_pop_obj_J(
omega_path_lev,
omega_path_S,
omega_SSfx,
fert_rates,
mort_rates,
infmort_rates,
imm_rates,
mort_rates_S,
imm_rates_mat,
E,
S,
g_n_SS,
fixper,
income_percentiles=None,
fert_gradient=None,
mort_gradient=None,
infmort_gradient=None,
imm_pctiles=None,
):
"""
Expand aggregate demographic objects to age x income-group objects.
The aggregate population path and rates are left unchanged. If
income_percentiles is None and no income-specific inputs are
provided, the aggregate objects are broadcast across a single
income group (J=1). Otherwise, income_percentiles gives the initial
population distribution across J for every age and the income
shares of newborns in every period.
Args:
omega_path_lev (Numpy array): T+S x E+S aggregate population levels.
omega_path_S (Numpy array): T+S x S aggregate active-age population
shares.
omega_SSfx (Numpy array): fixed full-life population distribution.
fert_rates (Numpy array): T+S x E+S fertility rates.
mort_rates (Numpy array): T+S x E+S mortality rates.
infmort_rates (Numpy array): T+S infant mortality rates.
imm_rates (Numpy array): T+S x E+S immigration rates, including the
adjusted post-fixper rates.
mort_rates_S (Numpy array): T+S x S mortality rates for active ages.
imm_rates_mat (Numpy array): T+S x S immigration rates for active ages.
E (int): number of non-economically active periods.
S (int): number of economically active periods.
g_n_SS (float): steady-state population growth rate.
fixper (int): period at which the fixed steady-state distribution is
imposed.
income_percentiles (array_like): population shares for each J
group; defaults to a single income group when no
income-specific inputs are supplied.
fert_gradient (array_like): log-odds fertility slopes by age.
mort_gradient (array_like): log-odds mortality slopes by age.
infmort_gradient (array_like): log-odds infant mortality slopes.
imm_pctiles (array_like): immigrant income shares by period,
age, and J.
Returns:
dict: demographic objects with the same keys needed by get_pop_objs.
"""
omega_SS = omega_SSfx[-S:] / omega_SSfx[-S:].sum()
income_inputs = (
fert_gradient,
mort_gradient,
infmort_gradient,
imm_pctiles,
)
all_income_inputs_none = all(x is None for x in income_inputs)
if income_percentiles is None:
assert all_income_inputs_none, (
"income_percentiles must be provided when using "
+ "income-specific inputs."
)
# No income heterogeneity requested: broadcast the aggregate
# objects across a single income group.
income_percentiles = [100]
income_shares, pct_midpoints = _income_shares_and_midpoints(
income_percentiles
)
J = income_shares.shape[0]
num_periods, totpers = omega_path_lev.shape
assert totpers == E + S
if all_income_inputs_none:
return {
"omega_path_S": omega_path_S.reshape(
omega_path_S.shape[0], omega_path_S.shape[1], 1
)
* income_shares.reshape(1, 1, J),
"omega_SS": omega_SS.reshape(omega_SS.shape[0], 1)
* income_shares.reshape(1, J),
"mort_rates_S": np.tile(
mort_rates_S.reshape(
mort_rates_S.shape[0], mort_rates_S.shape[1], 1
),
(1, 1, J),
),
"imm_rates_mat": np.tile(
imm_rates_mat.reshape(
imm_rates_mat.shape[0], imm_rates_mat.shape[1], 1
),
(1, 1, J),
),
}
fert_slopes = _format_age_gradient(
fert_gradient, num_periods, E, S, "fert_gradient"
)
mort_slopes = _format_age_gradient(
mort_gradient, num_periods, E, S, "mort_gradient"
)
infmort_slopes = _format_infmort_gradient(infmort_gradient, num_periods)
imm_shares = _format_imm_shares(
imm_pctiles, num_periods, E, S, income_shares
)
# Use the aggregate fixed distribution after fixper, matching the
# age-only steady-state logic already computed above.
target_pop = np.array(omega_path_lev, dtype=float, copy=True)
fixed_full_dist = omega_SSfx / omega_SSfx.sum()
if fixper < num_periods:
total_pop = target_pop[fixper].sum()
target_pop[fixper] = total_pop * fixed_full_dist
for t in range(fixper + 1, num_periods):
total_pop *= 1 + g_n_SS
target_pop[t] = total_pop * fixed_full_dist
pop_path_J = np.zeros((num_periods, totpers, J))
fert_rates_J = np.zeros((num_periods, totpers, J))
mort_rates_J = np.zeros((num_periods, totpers, J))
imm_rates_J = np.zeros((num_periods, totpers, J))
infmort_rates_J = np.zeros((num_periods, J))
pop_path_J[0] = target_pop[0, :, None] * income_shares.reshape(1, J)
fixed_pop_dist_J = None
for t in range(num_periods):
pop_t_J = pop_path_J[t]
age_weights = _within_age_weights(pop_t_J, income_shares)
fert_rates_J[t] = _mean_preserving_logit_rates(
fert_rates[t], fert_slopes[t], age_weights, pct_midpoints
)
mort_rates_J[t] = _mean_preserving_logit_rates(
mort_rates[t], mort_slopes[t], age_weights, pct_midpoints
)
infmort_rates_J[t] = _mean_preserving_logit_rates(
np.array([infmort_rates[t]]),
np.array([infmort_slopes[t]]),
income_shares.reshape(1, J),
pct_midpoints,
)[0]
births = (fert_rates_J[t] * pop_t_J).sum()
newborns = births * income_shares
pre_imm_pop = np.zeros((totpers, J))
pre_imm_pop[0] = (1 - infmort_rates_J[t]) * newborns
pre_imm_pop[1:] = pop_t_J[:-1] * (1 - mort_rates_J[t, :-1])
if t + 1 < num_periods:
target_next = target_pop[t + 1]
else:
newborns_agg = np.dot(fert_rates[t], target_pop[t])
target_next = np.zeros(totpers)
target_next[0] = (1 - infmort_rates[t]) * newborns_agg + imm_rates[
t, 0
] * target_pop[t, 0]
target_next[1:] = (
target_pop[t, :-1] * (1 - mort_rates[t, :-1])
+ imm_rates[t, 1:] * target_pop[t, 1:]
)
if t == fixper:
fixed_pop_dist_J = pop_t_J / pop_t_J.sum()
if fixed_pop_dist_J is not None and t >= fixper:
target_next_J = fixed_pop_dist_J * target_next.sum()
imm_flow_J = target_next_J - pre_imm_pop
pop_next_J = target_next_J
else:
imm_flow = target_next - pre_imm_pop.sum(axis=1)
imm_flow_J = imm_flow[:, None] * imm_shares[t]
pop_next_J = pre_imm_pop + imm_flow_J
imm_rates_J[t] = np.divide(
imm_flow_J,
pop_t_J,
out=np.zeros_like(imm_flow_J),
where=pop_t_J != 0,
)
if t + 1 < num_periods:
if np.any(pop_next_J < -1e-8):
raise ValueError(
"Income-specific demographic inputs imply a negative "
"population in at least one age-income cell."
)
pop_path_J[t + 1] = np.maximum(pop_next_J, 0.0)
active_pop_J = pop_path_J[:, E:, :]
omega_path_S_J = active_pop_J / active_pop_J.sum(axis=(1, 2)).reshape(
num_periods, 1, 1
)
omega_SS_J = omega_path_S_J[fixper]
assert np.allclose(omega_path_S_J.sum(axis=2), omega_path_S)
assert np.allclose(omega_SS_J.sum(axis=1), omega_SS)
return {
"omega_path_S": omega_path_S_J,
"omega_SS": omega_SS_J,
"mort_rates_S": mort_rates_J[:, E:, :],
"imm_rates_mat": imm_rates_J[:, E:, :],
}
[docs]
def get_pop_objs(
E=20,
S=80,
T=320,
min_age=0,
max_age=99,
fert_rates=None,
mort_rates=None,
infmort_rates=None,
imm_rates=None,
infer_pop=False,
pop_dist=None,
fert_gradient=None,
mort_gradient=None,
infmort_gradient=None,
imm_pctiles=None,
income_percentiles=None,
country_id=UN_COUNTRY_CODE,
initial_data_year=START_YEAR - 1,
final_data_year=START_YEAR + 2,
GraphDiag=True,
download_path=None,
):
"""
This function produces the demographics objects to be used in the
OG-USA model package.
Args:
E (int): number of model periods in which agent is not
economically active, >= 1
S (int): number of model periods in which agent is economically
active, >= 3
T (int): number of periods to be simulated in TPI, > 2*S
min_age (int): age in years at which agents are born, >= 0
max_age (int): age in years at which agents die with certainty,
>= 4, < 100 (max age in UN data is 99, 100+ i same group)
fert_rates (array_like): user provided fertility rates, dimensions
are T0 x E+S
mort_rates (array_like): user provided mortality rates, dimensions
are T0 x E+S
infmort_rates (array_like): user provided infant mortality rates,
length T0
imm_rates (array_like): user provided immigration rates, dimensions
are T0 x E+S
infer_pop (bool): =True if want to infer the population
pop_dist (array_like): user provided population distribution,
dimensions are T0+1 x E+S
fert_gradient (array_like): user provided fertility rate gradient,
dimensions are S, represents the log-odds slope in the
fertility rate
per percentile of the lifetime income distribution.
mort_gradient (array_like): user provided mortality rate gradient,
dimensions are S, represents the log-odds slope in the
mortality rate
per percentile of the lifetime income distribution.
infmort_gradient (array_like): user provided infant mortality
rate gradient, dimensions are S, represents the log-odds
slope in the infant mortality rate per percentile of the
lifetime income distribution.
imm_pctiles (array_like): user provided lifetime income distribution
for new immigrants, shape is num_per x S x J, where num_per
is the number of years between initial and final_data_year
income_percentiles (array_like): user provided income percentiles,
dimensions are J, the number of lifetime income groups;
defaults to a single income group (J=1) when no
income-specific inputs are supplied
country_id (str): country id for UN data
initial_data_year (int): initial year of data to use
(not relevant if have user provided data)
final_data_year (int): final year of data to use,
T0=initial_year-final_year + 1
pop_dist (array_like): user provided population distribution, last
dimension is of length E+S
GraphDiag (bool): =True if want graphical output and printed
diagnostics
Returns:
pop_dict (dict): includes:
omega_path_S (Numpy array), time path of the population
distribution from the current state to the steady-state,
size T+S x S
g_n_SS (scalar): steady-state population growth rate
omega_SS (Numpy array): normalized steady-state population
distribution, length S
surv_rates (Numpy array): survival rates that correspond to
each model period of life, length S
mort_rates (Numpy array): mortality rates that correspond to
each model period of life, length S
g_n_path (Numpy array): population growth rates over the time
path, length T + S
"""
start_data_year = initial_data_year - 1 # grab data from one year
T = T + 1 # add one period to T to account for period -1 pop
# before initial so have pre-start year population distribution
# TODO: this function does not generalize with T.
# It assumes one model period is equal to one calendar year in the
# time dimension (it does adjust for S, however)
T0 = (
final_data_year - initial_data_year + 1
) # number of periods until constant fertility and mortality rates
print(
"Demographics data: Initial Data year = ",
initial_data_year,
", Final Data year = ",
final_data_year,
)
assert E + S <= max_age - min_age + 1
assert initial_data_year >= 2012 and initial_data_year <= 2100 - 1
assert final_data_year >= 2012 and final_data_year <= 2100 - 1
# Ensure that the last year of data used is before SS transition assumed
# Really, it will need to be well before this
assert final_data_year > initial_data_year
assert final_data_year < initial_data_year + T
assert (
T > 2 * T0
) # ensure time path 2x as long as allows rates to fluctuate
if imm_rates is not None and pop_dist is None:
assert (
infer_pop is True
) # if pass immigration rates, need to infer population
# Get fertility rates if not provided
if fert_rates is None:
# get fert rates from UN data from initial year to data year
fert_rates = get_fert(
E + S,
min_age,
max_age,
country_id,
start_data_year,
final_data_year,
download_path=download_path,
)
else:
# ensure that user provided fert_rates are of the correct shape
assert fert_rates.shape[0] == T0
assert fert_rates.shape[-1] == E + S
# Extrapolate fertility rates for the rest of the transition path
# the implicit assumption is that they are constant after the
# last year of UN or user provided data
fert_rates = np.concatenate(
(
fert_rates,
np.tile(
fert_rates[-1, :].reshape(1, E + S),
(T + S - fert_rates.shape[0], 1),
),
),
axis=0,
)
# Get mortality rates if not provided
if mort_rates is None:
# get mort rates from UN data from initial year to data year
mort_rates, infmort_rates = get_mort(
E + S,
min_age,
max_age,
country_id,
start_data_year,
final_data_year,
download_path=download_path,
)
else:
# ensure that user provided mort_rates are of the correct shape
assert mort_rates.shape[0] == T0
assert mort_rates.shape[-1] == E + S
assert infmort_rates is not None
assert infmort_rates.shape[0] == mort_rates.shape[0]
# Extrapolate mortality rates for the rest of the transition path
# the implicit assumption is that they are constant after the
# last year of UN or user provided data
mort_rates = np.concatenate(
(
mort_rates,
np.tile(
mort_rates[-1, :].reshape(1, E + S),
(T + S - mort_rates.shape[0], 1),
),
),
axis=0,
)
infmort_rates = np.concatenate(
(
infmort_rates,
np.tile(infmort_rates[-1], (T + S - infmort_rates.shape[0])),
)
)
mort_rates_S = mort_rates[:, E:]
# Get population distribution if not provided
# or if just provide initial pop and infer_pop=True
if (pop_dist is None) or (pop_dist is not None and infer_pop is True):
if infer_pop:
if pop_dist is not None:
initial_pop = pop_dist[0, :].reshape(1, pop_dist.shape[-1])
else:
initial_pop = None
pop_2D = get_pop(
E,
S,
min_age,
max_age,
infer_pop,
fert_rates,
mort_rates,
infmort_rates,
imm_rates,
initial_pop,
country_id,
start_data_year,
final_data_year,
download_path=download_path,
)
else:
pop_2D = get_pop(
E,
S,
min_age,
max_age,
country_id=country_id,
start_year=start_data_year,
end_year=final_data_year,
download_path=download_path,
)
else:
# Check first dims of pop_dist as input by user
assert pop_dist.shape[0] == T0 + 1 # population needs to be
# one year longer in order to find immigration rates
assert pop_dist.shape[-1] == E + S
# Create 2D array of population distribution
pop_2D = np.zeros((T0 + 1, E + S))
for t in range(T0 + 1):
pop_EpS = pop_rebin(pop_dist[t, :], E + S)
pop_2D[t, :] = pop_EpS
# Get immigration rates if not provided
if imm_rates is None:
imm_rates_orig = get_imm_rates(
E + S,
min_age,
max_age,
fert_rates,
mort_rates,
infmort_rates,
pop_2D,
country_id,
start_data_year,
final_data_year,
download_path=download_path,
)
else:
# ensure that user provided imm_rates are of the correct shape
assert imm_rates.shape[0] == T0
assert imm_rates.shape[-1] == E + S
imm_rates_orig = imm_rates
# Extrapolate immigration rates for the rest of the transition path
# the implicit assumption is that they are constant after the
# last year of UN or user provided data
imm_rates_orig = np.concatenate(
(
imm_rates_orig,
np.tile(
imm_rates_orig[-1, :].reshape(1, E + S),
(T + S - imm_rates_orig.shape[0], 1),
),
),
axis=0,
)
# If the population distribution was given, check it for consistency
# with the fertility, mortality, and immigration rates
len_pop_dist = pop_2D.shape[0]
pop_counter_2D = np.zeros((len_pop_dist, E + S))
# set initial population distribution in the counterfactual to
# the first year of the user provided distribution
pop_counter_2D[0, :] = pop_2D[0, :]
for t in range(1, len_pop_dist):
# find newborns next period
newborns = np.dot(fert_rates[t - 1, :], pop_counter_2D[t - 1, :])
pop_counter_2D[t, 0] = (
1 - infmort_rates[t - 1]
) * newborns + imm_rates_orig[t - 1, 0] * pop_counter_2D[t - 1, 0]
pop_counter_2D[t, 1:] = (
pop_counter_2D[t - 1, :-1] * (1 - mort_rates[t - 1, :-1])
+ pop_counter_2D[t - 1, 1:] * imm_rates_orig[t - 1, 1:]
)
# Check that counterfactual pop dist is close to pop dist given
assert np.allclose(pop_counter_2D, pop_2D)
# Create the transition matrix for the population distribution
# from T0 going forward (i.e., past when we have data on forecasts)
OMEGA_orig = np.zeros((E + S, E + S))
OMEGA_orig[0, :] = (1 - infmort_rates[-1]) * fert_rates[-1, :] + np.hstack(
(imm_rates_orig[-1, 0], np.zeros(E + S - 1))
)
OMEGA_orig[1:, :-1] += np.diag(1 - mort_rates[-1, :-1])
OMEGA_orig[1:, 1:] += np.diag(imm_rates_orig[-1, 1:])
# Solve for steady-state population growth rate and steady-state
# population distribution by age using eigenvalue and eigenvector
# decomposition
eigvalues, eigvectors = np.linalg.eig(OMEGA_orig)
g_n_SS = (eigvalues[np.isreal(eigvalues)].real).max() - 1
eigvec_raw = eigvectors[
:, (eigvalues[np.isreal(eigvalues)].real).argmax()
].real
omega_SS_orig = eigvec_raw / eigvec_raw.sum()
# Generate time path of the population distribution after final
# year of data
omega_path_lev = np.zeros((T + S, E + S))
pop_curr = pop_2D[T0 - 1, :]
omega_path_lev[:T0, :] = pop_2D[:T0, :]
for per in range(T0, T + S):
pop_next = np.dot(OMEGA_orig, pop_curr)
omega_path_lev[per, :] = pop_next.copy()
pop_curr = pop_next.copy()
# Force the population distribution after 1.5*S periods to be the
# steady-state distribution by adjusting immigration rates, holding
# constant mortality, fertility, and SS growth rates
imm_tol = 1e-14
fixper = int(1.5 * S)
assert fixper > T0 # ensure that we are fixing period after data
omega_SSfx = omega_path_lev[fixper, :] / omega_path_lev[fixper, :].sum()
imm_objs = (
fert_rates[fixper, :],
mort_rates[fixper, :],
infmort_rates[fixper],
omega_path_lev[fixper, :],
g_n_SS,
)
imm_fulloutput = opt.fsolve(
immsolve,
imm_rates_orig[fixper, :],
args=(imm_objs),
full_output=True,
xtol=imm_tol,
)
imm_rates_adj = imm_fulloutput[0]
imm_diagdict = imm_fulloutput[1]
omega_path_S = omega_path_lev[:, -S:] / (
omega_path_lev[:, -S:].sum(axis=1).reshape((T + S, 1))
)
omega_path_S[fixper:, :] = np.tile(
omega_path_S[fixper, :].reshape((1, S)), (T + S - fixper, 1)
)
g_n_path = np.zeros(T + S)
g_n_path[:-1] = (
omega_path_lev[1:, -S:].sum(axis=1)
- omega_path_lev[:-1, -S:].sum(axis=1)
) / omega_path_lev[:-1, -S:].sum(axis=1)
g_n_path[fixper + 1 :] = g_n_SS
imm_rates_full = np.concatenate(
(
imm_rates_orig[:fixper, :],
np.tile(imm_rates_adj.reshape(1, E + S), (T + S - fixper, 1)),
),
axis=0,
)
imm_rates_mat = imm_rates_full[:, E:]
if GraphDiag:
# Check whether original SS population distribution is close to
# the period-T population distribution
omegaSSmaxdif = np.absolute(
omega_SS_orig - (omega_path_lev[T, :] / omega_path_lev[T, :].sum())
).max()
if omegaSSmaxdif > 0.0003:
print(
"POP. WARNING: Max. abs. dist. between original SS "
+ "pop. dist'n and period-T pop. dist'n is greater than"
+ " 0.0003. It is "
+ str(omegaSSmaxdif)
+ "."
)
else:
print(
"POP. SUCCESS: orig. SS pop. dist is very close to "
+ "period-T pop. dist'n. The maximum absolute "
+ "difference is "
+ str(omegaSSmaxdif)
+ "."
)
# Plot the adjusted steady-state population distribution versus
# the original population distribution. The difference should be
# small
omegaSSvTmaxdiff = np.absolute(omega_SS_orig - omega_SSfx).max()
if omegaSSvTmaxdiff > 0.0003:
print(
"POP. WARNING: The maximum absolute difference "
+ "between any two corresponding points in the original"
+ " and adjusted steady-state population "
+ "distributions is"
+ str(omegaSSvTmaxdiff)
+ ", "
+ "which is greater than 0.0003."
)
else:
print(
"POP. SUCCESS: The maximum absolute difference "
+ "between any two corresponding points in the original"
+ " and adjusted steady-state population "
+ "distributions is "
+ str(omegaSSvTmaxdiff)
)
# Print whether or not the adjusted immigration rates solved the
# zero condition
immtol_solved = np.absolute(imm_diagdict["fvec"].max()) < imm_tol
if immtol_solved:
print(
"POP. SUCCESS: Adjusted immigration rates solved "
+ "with maximum absolute error of "
+ str(np.absolute(imm_diagdict["fvec"].max()))
+ ", which is less than the tolerance of "
+ str(imm_tol)
)
else:
print(
"POP. WARNING: Adjusted immigration rates did not "
+ "solve. Maximum absolute error of "
+ str(np.absolute(imm_diagdict["fvec"].max()))
+ " is greater than the tolerance of "
+ str(imm_tol)
)
# Test whether the steady-state growth rates implied by the
# adjusted OMEGA matrix equals the steady-state growth rate of
# the original OMEGA matrix
OMEGA2 = np.zeros((E + S, E + S))
OMEGA2[0, :] = (1 - infmort_rates[-1]) * fert_rates[-1, :] + np.hstack(
(imm_rates_adj[0], np.zeros(E + S - 1))
)
OMEGA2[1:, :-1] += np.diag(1 - mort_rates[-1, :-1])
OMEGA2[1:, 1:] += np.diag(imm_rates_adj[1:])
eigvalues2, eigvectors2 = np.linalg.eig(OMEGA2)
g_n_SS_adj = (eigvalues[np.isreal(eigvalues2)].real).max() - 1
if np.max(np.absolute(g_n_SS_adj - g_n_SS)) > 10 ** (-8):
print(
"FAILURE: The steady-state population growth rate"
+ " from adjusted OMEGA is different (diff is "
+ str(g_n_SS_adj - g_n_SS)
+ ") than the steady-"
+ "state population growth rate from the original"
+ " OMEGA."
)
elif np.max(np.absolute(g_n_SS_adj - g_n_SS)) <= 10 ** (-8):
print(
"SUCCESS: The steady-state population growth rate"
+ " from adjusted OMEGA is close to (diff is "
+ str(g_n_SS_adj - g_n_SS)
+ ") the steady-"
+ "state population growth rate from the original"
+ " OMEGA."
)
# Do another test of the adjusted immigration rates. Create the
# new OMEGA matrix implied by the new immigration rates. Plug in
# the adjusted steady-state population distribution. Hit is with
# the new OMEGA transition matrix and it should return the new
# steady-state population distribution
omega_new = np.dot(OMEGA2, omega_SSfx)
omega_errs = np.absolute(omega_new - omega_SSfx)
print(
"The maximum absolute difference between the adjusted "
+ "steady-state population distribution and the "
+ "distribution generated by hitting the adjusted OMEGA "
+ "transition matrix is "
+ str(omega_errs.max())
)
# Plot the original immigration rates versus the adjusted
# immigration rates
immratesmaxdiff = np.absolute(imm_rates_orig - imm_rates_adj).max()
print(
"The maximum absolute distance between any two points "
+ "of the original immigration rates and adjusted "
+ "immigration rates is "
+ str(immratesmaxdiff)
)
# plots
age_per_EpS = np.arange(1, E + S + 1)
pp.plot_omega_fixed(
age_per_EpS, omega_SS_orig, omega_SSfx, E, S, path=OUTPUT_DIR
)
pp.plot_imm_fixed(
age_per_EpS,
imm_rates_orig[fixper - 1, :],
imm_rates_adj,
E,
S,
path=OUTPUT_DIR,
)
pp.plot_population_path(
age_per_EpS,
omega_path_lev,
omega_SSfx,
initial_data_year,
initial_data_year,
initial_data_year,
S,
path=OUTPUT_DIR,
)
pop_objs = expand_pop_obj_J(
omega_path_lev,
omega_path_S,
omega_SSfx,
fert_rates,
mort_rates,
infmort_rates,
imm_rates_full,
mort_rates_S,
imm_rates_mat,
E,
S,
g_n_SS,
fixper,
income_percentiles=income_percentiles,
fert_gradient=fert_gradient,
mort_gradient=mort_gradient,
infmort_gradient=infmort_gradient,
imm_pctiles=imm_pctiles,
)
# Return objects in a dictionary
pop_dict = {
"omega": pop_objs["omega_path_S"][1:, :, :],
"g_n_ss": g_n_SS,
"omega_SS": pop_objs["omega_SS"],
"rho": pop_objs["mort_rates_S"][1:, :, :],
"g_n": g_n_path[1:],
"imm_rates": pop_objs["imm_rates_mat"][1:, :, :],
"omega_S_preTP": pop_objs["omega_path_S"][0, :, :],
"imm_rates_preTP": pop_objs["imm_rates_mat"][0, :, :],
"rho_preTP": pop_objs["mort_rates_S"][0, :, :],
"g_n_preTP": g_n_path[0],
}
return pop_dict