import numpy as np
import pandas as pd
import os
import matplotlib.pyplot as plt
import matplotlib
import matplotlib.ticker as mticker
from matplotlib.lines import Line2D
from ogcore.constants import GROUP_LABELS
from ogcore import utils, txfunc
from ogcore.constants import DEFAULT_START_YEAR, VAR_LABELS
# Line styles cycled over when plotting one line per income group j
# alongside a colormap over years (see plot_fert_rates)
J_LINESTYLES = [
"-",
"--",
"-.",
":",
(0, (5, 1)),
(0, (3, 1, 1, 1)),
(0, (1, 1)),
(0, (5, 2, 1, 2)),
(0, (3, 2, 1, 2, 1, 2)),
]
def _check_ndim(arr, name, allowed_ndims):
"""
Check that a demographic object has one of the allowed numbers of
dimensions. Objects are allowed either without an income-group
(J) dimension (backward compatible with earlier versions of
OG-Core) or with J as the last dimension.
Args:
arr (array_like): object to check
name (str): name of the object, used in the error message
allowed_ndims (tuple): allowed numbers of dimensions
Returns:
arr (NumPy array): the object as a NumPy array of floats
"""
arr = np.asarray(arr, dtype=float)
assert arr.ndim in allowed_ndims, (
name
+ " must have "
+ " or ".join(str(n) for n in allowed_ndims)
+ " dimensions (with income groups j as the last dimension, if "
+ "present), got shape "
+ str(arr.shape)
)
return arr
def _avg_over_J(rates, omega=None):
"""
Average demographic rates over the income-group (J) dimension,
which is assumed to be the last axis of ``rates``.
Args:
rates (NumPy array): array of rates whose last axis indexes
income groups j
omega (NumPy array or None): population distribution with the
same shape as ``rates`` used as weights. The weights are
normalized within each cell of the leading dimensions (e.g.,
within each age) so that they sum to one across j. If
None, a simple (unweighted) mean across j is used.
Returns:
avg (NumPy array): rates averaged over j (last axis removed)
"""
rates = np.asarray(rates, dtype=float)
if omega is None:
return rates.mean(axis=-1)
omega = np.asarray(omega, dtype=float)
assert omega.shape == rates.shape, (
"omega must have the same shape as the rates being averaged, "
+ "got "
+ str(omega.shape)
+ " and "
+ str(rates.shape)
)
omega_sum = omega.sum(axis=-1, keepdims=True)
# where an age has zero population, fall back to equal weights
weights = np.divide(
omega,
omega_sum,
out=np.full(omega.shape, 1.0 / omega.shape[-1]),
where=omega_sum != 0,
)
return (rates * weights).sum(axis=-1)
def _sum_over_J(dist):
"""
Collapse a population distribution over the income-group (J)
dimension (assumed to be the last axis) by summing, which yields
the marginal distribution over the remaining dimensions.
Args:
dist (NumPy array): population distribution whose last axis
indexes income groups j
Returns:
marginal (NumPy array): distribution summed over j
"""
return np.asarray(dist, dtype=float).sum(axis=-1)
def _plot_age_profile(
ax,
arr,
label,
by_J,
x=None,
omega=None,
collapse="avg",
transform=None,
**plot_kwargs,
):
"""
Plot an age profile that may or may not have an income-group (J)
dimension.
If ``arr`` is one-dimensional (S,), a single line is plotted. If
``arr`` is two-dimensional (S x J) and ``by_J`` is True, one line
is plotted for each income group j and labeled with the j value.
If ``arr`` is (S x J) and ``by_J`` is False, the array is first
collapsed over j (summed for distributions, population-weighted
averaged for rates) and a single line is plotted.
Args:
ax (Matplotlib axes object): axes to plot on
arr (NumPy array): age profile, (S,) or (S x J)
label (str): legend label for the series (", j=k" is appended
when plotting by income group)
by_J (bool): whether to plot a separate line for each j
x (array_like or None): x-axis values, if None then the index
of ``arr`` is used
omega (NumPy array or None): population distribution of the
same shape as ``arr`` used to weight the average across
j when ``collapse`` is "avg"
collapse (str): "sum" to sum over j (for distributions) or
"avg" to take the population-weighted average over j (for
rates) when ``by_J`` is False
transform (callable or None): function applied to each (S,)
series just before it is plotted (e.g., to compute
cumulative survival rates from mortality rates)
plot_kwargs (dict): additional keyword arguments passed to
``ax.plot``
Returns:
None
"""
assert collapse in ["sum", "avg"], "collapse must be 'sum' or 'avg'"
arr = np.asarray(arr, dtype=float)
assert arr.ndim in [1, 2], (
"Expected an age profile of shape (S,) or (S, J), got shape "
+ str(arr.shape)
)
if transform is None:
def transform(y):
return y
if arr.ndim == 2:
if by_J:
series = [
(arr[:, j], label + ", j=" + str(j))
for j in range(arr.shape[-1])
]
elif collapse == "sum":
series = [(_sum_over_J(arr), label)]
else:
series = [(_avg_over_J(arr, omega), label)]
else:
series = [(arr, label)]
for y, lab in series:
if x is None:
ax.plot(transform(y), label=lab, **plot_kwargs)
else:
ax.plot(x, transform(y), label=lab, **plot_kwargs)
[docs]
def plot_imm_rates(
imm_rates,
start_year=DEFAULT_START_YEAR,
years_to_plot=[DEFAULT_START_YEAR],
include_title=False,
source="United Nations, World Population Prospects",
path=None,
by_J=False,
omega=None,
):
"""
Plot immigration rates from the data
Args:
imm_rates (NumPy array): immigration rates for each year and
age (T x S) or for each year, age, and income group
(T x S x J)
start_year (int): first year of data
years_to_plot (list): list of years to plot
include_title (bool): whether to include a title in the plot
source (str): data source for immigration rates
path (str): path to save figure to, if None then figure
is returned
by_J (bool): if imm_rates has an income-group dimension, plot
a separate line for each income group j (labeled by j).
If False, the rates are averaged across income groups so
that one line is plotted per year.
omega (NumPy array): population distribution with the same
shape as imm_rates, used to weight the average across
income groups when by_J is False. If None, a simple mean
across income groups is used.
Returns:
fig (Matplotlib plot object): plot of immigration rates
"""
imm_rates = _check_ndim(imm_rates, "imm_rates", (2, 3))
if omega is not None:
omega = _check_ndim(omega, "omega", (imm_rates.ndim,))
plot_by_J = by_J and imm_rates.ndim == 3
fig, ax = plt.subplots()
for y in years_to_plot:
i = y - start_year
omega_i = None if omega is None else omega[i]
_plot_age_profile(
ax,
imm_rates[i],
"Year " + str(y),
by_J,
omega=omega_i,
collapse="avg",
**({} if plot_by_J else {"c": "blue"}),
)
plt.xlabel(r"Age $s$")
plt.ylabel(r"Immigration rate $i_{s}$")
plt.legend(loc="upper left")
plt.text(
-5,
-0.05,
"Source: " + source,
fontsize=9,
)
if include_title:
plt.title("Immigration Rates by Age")
# Save or return figure
if path:
output_path = os.path.join(path, "imm_rates")
plt.savefig(output_path, bbox_inches="tight", dpi=300)
plt.close()
else:
fig.show()
return fig
[docs]
def plot_mort_rates(
p_list,
labels=[""],
years=[DEFAULT_START_YEAR],
survival_rates=False,
include_title=False,
path=None,
by_J=False,
):
"""
Create a plot of mortality rates from OG-Core parameterization.
Args:
p_list (list): list of parameters objects
labels (list): list of labels for the legend
years (list): list of years to plot
survival_rates (bool): whether to plot survival rates instead
of mortality rates
include_title (bool): whether to include a title in the plot
path (string): path to save figure to
by_J (bool): if mortality rates vary by income group, plot a
separate line for each income group j (labeled by j). If
False, the rates are averaged across income groups using
the population weights in omega so that one line is
plotted per year and parameters object.
Returns:
fig (Matplotlib plot object): plot of mortality rates
"""
p0 = p_list[0]
for p in p_list:
_check_ndim(p.rho, "rho", (2, 3))
_check_ndim(p.omega, "omega", (2, 3))
age_per = np.linspace(p0.E, p0.E + p0.S, p0.S)
fig, ax = plt.subplots()
if survival_rates:
def transform(rho_t):
return np.cumprod(1 - rho_t)
else:
transform = None
for y in years:
t = y - p0.start_year
for i, p in enumerate(p_list):
omega_t = None
if p.rho.ndim == 3 and not by_J:
# population weights used to average over j
if p.omega.ndim == 3:
omega_t = p.omega[t, :, :]
else:
omega_t = np.tile(p.lambdas.reshape(1, p.J), (p.S, 1))
_plot_age_profile(
ax,
p.rho[t],
labels[i] + " " + str(y),
by_J,
x=age_per,
omega=omega_t,
collapse="avg",
transform=transform,
)
plt.xlabel(r"Age $s$ (model periods)")
if survival_rates:
plt.ylabel(r"Cumulative Survival Rates")
plt.legend(loc="lower left")
title = "Survival Rates"
else:
plt.ylabel(r"Mortality Rates $\rho_{s}$")
plt.legend(loc="upper left")
title = "Mortality Rates"
ticks_loc = ax.get_yticks().tolist()
ax.yaxis.set_major_locator(mticker.FixedLocator(ticks_loc))
ax.set_yticklabels(["{:,.0%}".format(x) for x in ticks_loc])
if include_title:
plt.title(title)
if path is None:
return fig
else:
if survival_rates:
fig_path = os.path.join(path, "survival_rates")
else:
fig_path = os.path.join(path, "mortality_rates")
plt.savefig(fig_path, dpi=300)
[docs]
def plot_pop_growth(
p,
start_year=DEFAULT_START_YEAR,
num_years_to_plot=150,
include_title=False,
path=None,
):
"""
Create a plot of population growth rates by year.
Args:
p (OG-Core Specifications class): parameters object
start_year (integer): year to begin plotting
num_years_to_plot (integer): number of years to plot
include_title (bool): whether to include a title in the plot
path (string): path to save figure to
Returns:
fig (Matplotlib plot object): plot of immigration rates
"""
assert isinstance(start_year, int)
assert isinstance(num_years_to_plot, int)
year_vec = np.arange(start_year, start_year + num_years_to_plot)
start_index = start_year - p.start_year
fig, ax = plt.subplots()
# g_n stores the pre-time-path boundary growth separately in
# g_n_preTP; prepend it to recover the year-aligned growth path.
g_n_full = np.append(p.g_n_preTP, p.g_n)
plt.plot(
year_vec,
g_n_full[start_index : start_index + num_years_to_plot],
)
plt.xlabel(r"Year $t$")
plt.ylabel(r"Population Growth Rate $g_{n, t}$")
ticks_loc = ax.get_yticks().tolist()
ax.yaxis.set_major_locator(mticker.FixedLocator(ticks_loc))
ax.set_yticklabels(["{:,.2%}".format(x) for x in ticks_loc])
if include_title:
plt.title("Population Growth Rates")
if path is None:
return fig
else:
fig_path = os.path.join(path, "pop_growth_rates")
plt.savefig(fig_path, dpi=300)
[docs]
def plot_population(
p, years_to_plot=["SS"], include_title=False, path=None, by_J=False
):
"""
Plot the distribution of the population over age for various years.
Args:
p (OG-Core Specifications class): parameters object
years_to_plot (list): list of years to plot, 'SS' will denote
the steady-state period
include_title (bool): whether to include a title in the plot
path (string): path to save figure to
by_J (bool): if the population distribution has an
income-group dimension, plot a separate line for each
income group j (labeled by j). If False, the distribution
is summed across income groups so that one line showing
the overall population distribution by age is plotted per
year.
Returns:
fig (Matplotlib plot object): plot of population distribution
"""
for i, v in enumerate(years_to_plot):
assert isinstance(v, int) | (v == "SS")
if isinstance(v, int):
assert v >= p.start_year
_check_ndim(p.omega_SS, "omega_SS", (1, 2))
_check_ndim(p.omega, "omega", (2, 3))
age_vec = np.arange(p.E, p.S + p.E)
fig, ax = plt.subplots()
for i, v in enumerate(years_to_plot):
if v == "SS":
pop_dist = p.omega_SS
else:
pop_dist = p.omega[v - p.start_year]
_plot_age_profile(
ax, pop_dist, str(v) + " pop.", by_J, x=age_vec, collapse="sum"
)
plt.xlabel(r"Age $s$")
plt.ylabel(r"Pop. dist'n $\omega_{s}$")
plt.legend(loc="lower left")
if include_title:
plt.title("Population Distribution by Year")
if path is None:
return fig
else:
fig_path = os.path.join(path, "pop_distribution")
plt.savefig(fig_path, dpi=300)
[docs]
def plot_ability_profiles(
p, p2=None, t=None, log_scale=False, include_title=False, path=None
):
"""
Create a plot of earnings ability profiles.
Args:
p (OG-Core Specifications class): parameters object
t (int): model period for year, if None, then plot ability
matrix for SS
log_scale (bool): whether to plot in log points
include_title (bool): whether to include a title in the plot
path (string): path to save figure to
Returns:
fig (Matplotlib plot object): plot of earnings ability profiles
"""
if t is None:
t = -1
age_vec = np.arange(p.starting_age, p.starting_age + p.S)
fig, ax = plt.subplots()
cm = plt.get_cmap("coolwarm")
ax.set_prop_cycle(color=[cm(1.0 * i / p.J) for i in range(p.J)])
for j in range(p.J):
if log_scale:
plt.plot(age_vec, np.log(p.e[t, :, j]), label=GROUP_LABELS[p.J][j])
else:
plt.plot(age_vec, p.e[t, :, j], label=GROUP_LABELS[p.J][j])
if p2 is not None:
for j in range(p.J):
if log_scale:
plt.plot(
age_vec,
np.log(p2.e[t, :, j]),
linestyle="--",
label=GROUP_LABELS[p.J][j],
)
else:
plt.plot(
age_vec,
p2.e[t, :, j],
linestyle="--",
label=GROUP_LABELS[p.J][j],
)
plt.xlabel(r"Age")
if log_scale:
plt.ylabel(r"ln(Earnings ability)")
else:
plt.ylabel(r"Earnings ability")
plt.legend(loc=9, bbox_to_anchor=(0.5, -0.15), ncols=5)
if include_title:
plt.title("Lifecycle Profiles of Effective Labor Units")
if path is None:
return fig
else:
fig_path = os.path.join(path, "ability_profiles")
plt.savefig(fig_path, bbox_inches="tight", dpi=300)
[docs]
def plot_elliptical_u(p, plot_MU=True, include_title=False, path=None):
"""
Create a plot of showing the fit of the elliptical utility function.
Args:
p (OG-Core Specifications class): parameters object
plot_MU (boolean): whether plot marginal utility or utility in
levels
path (string): path to save figure to
Returns:
fig (Matplotlib plot object): plot of elliptical vs CFE utility
"""
theta = 1 / p.frisch
N = 101
n_grid = np.linspace(0.01, 0.8, num=N)
if plot_MU:
CFE = (1.0 / p.ltilde) * ((n_grid / p.ltilde) ** theta)
ellipse = (
1.0
* p.b_ellipse
* (1.0 / p.ltilde)
* (
(1.0 - (n_grid / p.ltilde) ** p.upsilon)
** ((1.0 / p.upsilon) - 1.0)
)
* (n_grid / p.ltilde) ** (p.upsilon - 1.0)
)
else:
CFE = ((n_grid / p.ltilde) ** (1 + theta)) / (1 + theta)
k = 1.0 # we don't estimate k, so not in parameters
ellipse = (
p.b_ellipse
* ((1 - ((n_grid / p.ltilde) ** p.upsilon)) ** (1 / p.upsilon))
+ k
)
fig, ax = plt.subplots()
plt.plot(n_grid, CFE, label="Constant Frisch elasticity")
plt.plot(n_grid, ellipse, label="Elliptical disutility")
if include_title:
if plot_MU:
plt.title("Marginal Utility of CFE and Elliptical")
else:
plt.title("Constant Frisch Elasticity vs. Elliptical Utility")
plt.xlabel(r"Labor Supply $n_{j,s,t}$")
if plot_MU:
plt.ylabel(r"Marginal disutility")
else:
plt.ylabel(r"Disutility")
plt.legend(loc="upper left")
plt.grid(color="gray", linestyle=":", linewidth=1, alpha=0.5)
if path is None:
return fig
else:
fig_path = os.path.join(path, "ellipse_v_CFE")
plt.savefig(fig_path, dpi=300)
[docs]
def plot_chi_n(
p_list,
labels=[""],
years_to_plot=[DEFAULT_START_YEAR],
include_title=False,
path=None,
):
"""
Create a plot of showing the values of the chi_n parameters.
Args:
p_list (list): parameters objects
labels (list): labels for legend
years_to_plot (list): list of years to plot
include_title (boolean): whether to include a title in the plot
path (string): path to save figure to
Returns:
fig (Matplotlib plot object): plot of chi_n parameters
"""
p0 = p_list[0]
age = np.linspace(p0.starting_age, p0.ending_age, p0.S)
fig, ax = plt.subplots()
for y in years_to_plot:
for i, p in enumerate(p_list):
plt.plot(
age,
p.chi_n[y - p.start_year, :],
label=labels[i] + " " + str(y),
)
if include_title:
plt.title("Utility Weight on the Disutility of Labor Supply")
plt.xlabel("Age, $s$")
plt.ylabel(r"$\chi^{n}_{s}$")
if path is None:
return fig
else:
fig_path = os.path.join(path, "chi_n_values")
plt.savefig(fig_path, dpi=300)
[docs]
def plot_fert_rates(
fert_rates_list,
labels=[""],
include_title=False,
source="United Nations, World Population Prospects",
path=None,
by_J=False,
omega_list=None,
):
"""
Plot fertility rates from the data
Args:
fert_rates_list (list): list of Numpy arrays of fertility rates
by age (S,) or by age and income group (S x J), one array
per series (e.g., per year)
labels (list): list of labels for the legend
include_title (bool): whether to include a title in the plot
source (str): data source for fertility rates
path (str): path to save figure to, if None then figure
is returned
by_J (bool): if the fertility rates have an income-group
dimension, plot a separate line for each income group j
(labeled by j). If False, the rates are averaged across
income groups so that one line is plotted per series.
omega_list (list): list of Numpy arrays of the population
distribution, each with the same shape as the corresponding
element of fert_rates_list, used to weight the average
across income groups when by_J is False. If None, a
simple mean across income groups is used.
Returns:
fig (Matplotlib plot object): plot of fertility rates
"""
fig, ax = plt.subplots()
num_series = len(fert_rates_list)
assert num_series == len(labels), (
"Number of series must match number of labels"
)
if omega_list is not None:
assert len(omega_list) == num_series, (
"Number of series must match number of omega arrays"
)
fert_rates_list = [
_check_ndim(f, "fert_rates_list[" + str(i) + "]", (1, 2))
for i, f in enumerate(fert_rates_list)
]
if omega_list is not None:
omega_list = [
_check_ndim(
om, "omega_list[" + str(i) + "]", (fert_rates_list[i].ndim,)
)
for i, om in enumerate(omega_list)
]
plot_by_J = by_J and any(f.ndim == 2 for f in fert_rates_list)
if num_series > 4:
cm = plt.get_cmap("coolwarm")
# Attempt to convert labels to numeric values for colormap scaling
try:
label_values = [int(label) for label in labels]
except (ValueError, TypeError):
label_values = list(range(num_series))
vmin, vmax = min(label_values), max(label_values)
norm = plt.Normalize(vmin=vmin, vmax=vmax)
max_J = 1
for i, fert_rates in enumerate(fert_rates_list):
color = cm(norm(label_values[i]))
if fert_rates.ndim == 2 and plot_by_J:
# one line per j: color denotes the series (year),
# line style denotes the income group
max_J = max(max_J, fert_rates.shape[-1])
for j in range(fert_rates.shape[-1]):
ax.plot(
fert_rates[:, j],
color=color,
linestyle=J_LINESTYLES[j % len(J_LINESTYLES)],
label=str(labels[i]) + ", j=" + str(j),
)
else:
omega_i = None if omega_list is None else omega_list[i]
_plot_age_profile(
ax,
fert_rates,
str(labels[i]),
by_J=False,
omega=omega_i,
collapse="avg",
color=color,
)
sm = plt.cm.ScalarMappable(cmap=cm, norm=norm)
sm.set_array([])
plt.colorbar(sm, ax=ax, label="Year")
if plot_by_J:
# legend keyed on line style to identify income groups
handles = [
Line2D(
[0],
[0],
color="black",
linestyle=J_LINESTYLES[j % len(J_LINESTYLES)],
label="j=" + str(j),
)
for j in range(max_J)
]
ax.legend(handles=handles, loc="upper right")
else:
for i, fert_rates in enumerate(fert_rates_list):
omega_i = None if omega_list is None else omega_list[i]
_plot_age_profile(
ax,
fert_rates,
str(labels[i]),
by_J,
omega=omega_i,
collapse="avg",
)
ax.legend(loc="upper right")
if include_title:
ax.set_title("Fertility rates by age ($f_{s}$)", fontsize=20)
ax.set_xlabel(r"Age $s$")
ax.set_ylabel(r"Fertility rate $f_{s}$")
ax.text(
-5,
-0.023,
"Source: " + source,
fontsize=9,
)
# Save or return figure
if path:
output_path = os.path.join(path, "fert_rates")
plt.savefig(output_path, bbox_inches="tight", dpi=300)
plt.close()
else:
fig.show()
return fig
[docs]
def plot_mort_rates_data(
mort_rates,
start_year=DEFAULT_START_YEAR,
years_to_plot=[DEFAULT_START_YEAR],
source="United Nations, World Population Prospects",
path=None,
by_J=False,
omega=None,
):
"""
Plots mortality rates from the data.
Args:
mort_rates (array_like): mortality rates for each year and
age (T x S) or for each year, age, and income group
(T x S x J)
start_year (int): first year of data
years_to_plot (list): list of years to plot
source (str): data source for mortality rates
path (str): path to save figure to, if None then figure
is returned
by_J (bool): if mort_rates has an income-group dimension, plot
a separate line for each income group j (labeled by j).
If False, the rates are averaged across income groups so
that one line is plotted per year.
omega (NumPy array): population distribution with the same
shape as mort_rates, used to weight the average across
income groups when by_J is False. If None, a simple mean
across income groups is used.
Returns:
fig (Matplotlib plot object): plot of mortality rates
"""
mort_rates = _check_ndim(mort_rates, "mort_rates", (2, 3))
if omega is not None:
omega = _check_ndim(omega, "omega", (mort_rates.ndim,))
plot_by_J = by_J and mort_rates.ndim == 3
fig, ax = plt.subplots()
for y in years_to_plot:
i = y - start_year
omega_i = None if omega is None else omega[i]
_plot_age_profile(
ax,
mort_rates[i],
"Year " + str(y),
by_J,
omega=omega_i,
collapse="avg",
**({} if plot_by_J else {"c": "blue"}),
)
plt.xlabel(r"Age $s$")
plt.ylabel(r"Mortality rate $rho_{s}$")
plt.legend(loc="upper left")
plt.text(
-5,
-0.223,
"Source: " + source,
fontsize=9,
)
plt.tight_layout(rect=(0, 0.035, 1, 1))
# Save or return figure
if path:
output_path = os.path.join(path, "mort_rates")
plt.savefig(output_path, dpi=300)
plt.close()
else:
fig.show()
return fig
[docs]
def plot_g_n(p_list, label_list=[""], include_title=False, path=None):
"""
Create a plot of population growth rates from OG-Core parameterization.
Args:
p_list (list): list of OG-Core Specifications objects
label_list (list): list of labels for the legend
include_title (bool): whether to include a title in the plot
path (string): path to save figure to
Returns:
fig (Matplotlib plot object): plot of immigration rates
"""
p0 = p_list[0]
years = np.arange(p0.start_year, p0.start_year + p0.T)
fig, ax = plt.subplots()
for i, p in enumerate(p_list):
plt.plot(
years,
np.append(p.g_n_preTP, p.g_n[: p.T - 1]),
label=label_list[i],
)
plt.xlabel(r"Year $s$ (model periods)")
plt.ylabel(r"Population Growth Rate $g_{n,t}$")
if label_list[0] != "":
plt.legend(loc="upper right")
ticks_loc = ax.get_yticks().tolist()
ax.yaxis.set_major_locator(mticker.FixedLocator(ticks_loc))
ax.set_yticklabels(["{:,.0%}".format(x) for x in ticks_loc])
if include_title:
plt.title("Population Growth Rates")
if path is None:
return fig
else:
fig_path = os.path.join(path, "pop_growth_rates")
plt.savefig(fig_path, dpi=300)
[docs]
def plot_omega_fixed(
age_per_EpS, omega_SS_orig, omega_SSfx, E, S, path=None, by_J=False
):
"""
Plot the steady-state population distribution implied by the data
on fertility and mortality rates versus the the steady-state
population distribution after adjusting immigration rates so that
the stationary distribution is achieved a reasonable number of
model periods.
Args:
age_per_EpS (array_like): list of ages over which to plot
population distribution
omega_SS_orig (Numpy array): population distribution in SS
without adjustment to immigration rates, by age (E+S,) or
by age and income group (E+S x J)
omega_SSfx (Numpy array): population distribution in SS
after adjustment to immigration rates, by age (E+S,) or
by age and income group (E+S x J)
E (int): age at which household becomes economically active
S (int): number of years which household is economically active
path (str): path to save figure to, if None then figure
is returned
by_J (bool): if the distributions have an income-group
dimension, plot a separate line for each income group j
(labeled by j). If False, the distributions are summed
across income groups so that one line is plotted for each
distribution.
Returns:
fig (Matplotlib plot object): plot of SS population distribution
before and after adjustment to immigration rates
"""
omega_SS_orig = _check_ndim(omega_SS_orig, "omega_SS_orig", (1, 2))
omega_SSfx = _check_ndim(omega_SSfx, "omega_SSfx", (1, 2))
fig, ax = plt.subplots()
_plot_age_profile(
ax,
omega_SS_orig,
"Original Dist'n",
by_J,
x=age_per_EpS,
collapse="sum",
)
_plot_age_profile(
ax, omega_SSfx, "Fixed Dist'n", by_J, x=age_per_EpS, collapse="sum"
)
plt.title("Original steady-state population distribution vs. fixed")
plt.xlabel(r"Age $s$")
plt.ylabel(r"Pop. dist'n $\omega_{s}$")
plt.xlim((0, E + S + 1))
plt.legend(loc="upper right")
# Save or return figure
if path:
output_path = os.path.join(path, "OrigVsFixSSpop")
plt.savefig(output_path, dpi=300)
plt.close()
else:
return fig
[docs]
def plot_imm_fixed(
age_per_EpS,
imm_rates_orig,
imm_rates_adj,
E,
S,
path=None,
by_J=False,
omega=None,
):
"""
Plot the immigration rates implied by the data on population,
mortality, and fertility versus the adjusted immigration rates
needed to achieve a stationary distribution of the population in a
reasonable number of model periods.
Args:
age_per_EpS (array_like): list of ages over which to plot
population distribution
imm_rates_orig (Numpy array): immigration rates by age (E+S,)
or by age and income group (E+S x J)
imm_rates_adj (Numpy array): adjusted immigration rates by age
(E+S,) or by age and income group (E+S x J)
E (int): age at which household becomes economically active
S (int): number of years which household is economically active
path (str): path to save figure to, if None then figure
is returned
by_J (bool): if the immigration rates have an income-group
dimension, plot a separate line for each income group j
(labeled by j). If False, the rates are averaged across
income groups so that one line is plotted for each set of
rates.
omega (NumPy array): population distribution with the same
shape as the immigration rates, used to weight the average
across income groups when by_J is False. If None, a
simple mean across income groups is used.
Returns:
fig (Matplotlib plot object): plot of immigration rates found
from residuals and the adjusted rates to hit SS sooner
"""
imm_rates_orig = _check_ndim(imm_rates_orig, "imm_rates_orig", (1, 2))
imm_rates_adj = _check_ndim(imm_rates_adj, "imm_rates_adj", (1, 2))
if omega is not None:
omega = _check_ndim(omega, "omega", (1, 2))
fig, ax = plt.subplots()
_plot_age_profile(
ax,
imm_rates_orig,
"Original Imm. Rates",
by_J,
x=age_per_EpS,
omega=omega,
collapse="avg",
)
_plot_age_profile(
ax,
imm_rates_adj,
"Adj. Imm. Rates",
by_J,
x=age_per_EpS,
omega=omega,
collapse="avg",
)
plt.title("Original immigration rates vs. adjusted")
plt.xlabel(r"Age $s$")
plt.ylabel(r"Imm. rates $i_{s}$")
plt.xlim((0, E + S + 1))
plt.legend(loc="upper center")
# Save or return figure
if path:
output_path = os.path.join(path, "OrigVsAdjImm")
plt.savefig(output_path, dpi=300)
plt.close()
else:
return fig
[docs]
def plot_population_path(
age_per_EpS,
omega_path_lev,
omega_SSfx,
start_year,
year1,
year2,
S,
path=None,
by_J=False,
):
"""
Plot the distribution of the population over age for various years.
Args:
age_per_EpS (array_like): list of ages over which to plot
population distribution
omega_path_lev (Numpy array): number of households by age
(T+S x E+S) or by age and income group (T+S x E+S x J)
over the transition path
omega_SSfx (Numpy array): population distribution by age (E+S,)
or by age and income group (E+S x J) in the SS
start_year (int): first year of data (so can get index of year1
and year2)
year1 (int): first year of data to plot
year2 (int): second year of data to plot
S (int): number of years which household is economically active
path (str): path to save figure to, if None then figure
is returned
by_J (bool): if the population distributions have an
income-group dimension, plot a separate line for each
income group j (labeled by j). If False, the distributions
are summed across income groups so that one line showing
the overall population distribution by age is plotted for
each point in time.
Returns:
fig (Matplotlib plot object): plot of population distribution
at points along the time path
"""
omega_path_lev = _check_ndim(omega_path_lev, "omega_path_lev", (2, 3))
omega_SSfx = _check_ndim(omega_SSfx, "omega_SSfx", (1, 2))
fig, ax = plt.subplots()
periods = [
(year1 - start_year, str(year1) + " pop."),
(year2 - start_year, str(year2) + " pop."),
(int(0.5 * S), "T=" + str(int(0.5 * S)) + " pop."),
(int(S), "T=" + str(int(S)) + " pop."),
]
for t, label in periods:
# population distribution across all (age, income group) cells
pop_dist = omega_path_lev[t] / omega_path_lev[t].sum()
_plot_age_profile(
ax, pop_dist, label, by_J, x=age_per_EpS, collapse="sum"
)
_plot_age_profile(
ax, omega_SSfx, "Adj. SS pop.", by_J, x=age_per_EpS, collapse="sum"
)
plt.title("Population distribution at points in time path")
plt.xlabel(r"Age $s$")
plt.ylabel(r"Pop. dist'n $\omega_{s}$")
plt.legend(loc="lower left")
# Save or return figure
if path:
output_path = os.path.join(path, "PopDistPath")
plt.savefig(output_path, dpi=300)
plt.close()
else:
return fig
[docs]
def gen_3Dscatters_hist(df, s, t, output_dir):
"""
Create 3-D scatterplots and corresponding 3D histogram of ETR, MTRx,
and MTRy as functions of labor income and capital income with
truncated data in the income dimension
Args:
df (Pandas DataFrame): 11 variables with N observations of tax
rates
s (int): age of individual, >= 21
t (int): year of analysis, >= 2016
path (str): output directory for saving plot files
Returns:
None
"""
from ogcore.txfunc import MAX_INC_GRAPH, MIN_INC_GRAPH
# Truncate the data
df_trnc = df[
(df["total_labinc"] > MIN_INC_GRAPH)
& (df["total_labinc"] < MAX_INC_GRAPH)
& (df["total_capinc"] > MIN_INC_GRAPH)
& (df["total_capinc"] < MAX_INC_GRAPH)
]
inc_lab = df_trnc["total_labinc"]
inc_cap = df_trnc["total_capinc"]
etr_data = df_trnc["etr"]
mtrx_data = df_trnc["mtr_labinc"]
mtry_data = df_trnc["mtr_capinc"]
# Plot 3D scatterplot of ETR data
fig = plt.figure()
ax = fig.add_subplot(111, projection="3d")
ax.scatter(inc_lab, inc_cap, etr_data, c="r", marker="o")
ax.set_xlabel("Total Labor Income")
ax.set_ylabel("Total Capital Income")
ax.set_zlabel("ETR")
plt.title(
"ETR, Lab. Inc., and Cap. Inc., Age=" + str(s) + ", Year=" + str(t)
)
filename = "ETR_age_" + str(s) + "_Year_" + str(t) + "_data.png"
fullpath = os.path.join(output_dir, filename)
fig.savefig(fullpath, bbox_inches="tight", dpi=300)
plt.close()
# Plot 3D histogram for all data
fig = plt.figure()
ax = fig.add_subplot(111, projection="3d")
bin_num = int(30)
hist, xedges, yedges = np.histogram2d(inc_lab, inc_cap, bins=bin_num)
hist = hist / hist.sum()
x_midp = xedges[:-1] + 0.5 * (xedges[1] - xedges[0])
y_midp = yedges[:-1] + 0.5 * (yedges[1] - yedges[0])
elements = (len(xedges) - 1) * (len(yedges) - 1)
ypos, xpos = np.meshgrid(y_midp, x_midp)
xpos = xpos.flatten()
ypos = ypos.flatten()
zpos = np.zeros(elements)
dx = (xedges[1] - xedges[0]) * np.ones_like(bin_num)
dy = (yedges[1] - yedges[0]) * np.ones_like(bin_num)
dz = hist.flatten()
ax.bar3d(xpos, ypos, zpos, dx, dy, dz, color="b", zsort="average")
ax.set_xlabel("Total Labor Income")
ax.set_ylabel("Total Capital Income")
ax.set_zlabel("Percent of obs.")
plt.title(
"Histogram by lab. inc., and cap. inc., Age="
+ str(s)
+ ", Year="
+ str(t)
)
filename = "Hist_Age_" + str(s) + "_Year_" + str(t) + ".png"
fullpath = os.path.join(output_dir, filename)
fig.savefig(fullpath, bbox_inches="tight", dpi=300)
plt.close()
# Plot 3D scatterplot of MTRx data
fig = plt.figure()
ax = fig.add_subplot(111, projection="3d")
ax.scatter(inc_lab, inc_cap, mtrx_data, c="r", marker="o")
ax.set_xlabel("Total Labor Income")
ax.set_ylabel("Total Capital Income")
ax.set_zlabel("Marginal Tax Rate, Labor Inc.)")
plt.title(
"MTR Labor Income, Lab. Inc., and Cap. Inc., Age="
+ str(s)
+ ", Year="
+ str(t)
)
filename = "MTRx_Age_" + str(s) + "_Year_" + str(t) + "_data.png"
fullpath = os.path.join(output_dir, filename)
fig.savefig(fullpath, bbox_inches="tight", dpi=300)
plt.close()
# Plot 3D scatterplot of MTRy data
fig = plt.figure()
ax = fig.add_subplot(111, projection="3d")
ax.scatter(inc_lab, inc_cap, mtry_data, c="r", marker="o")
ax.set_xlabel("Total Labor Income")
ax.set_ylabel("Total Capital Income")
ax.set_zlabel("Marginal Tax Rate (Capital Inc.)")
plt.title(
"MTR Capital Income, Cap. Inc., and Cap. Inc., Age="
+ str(s)
+ ", Year="
+ str(t)
)
filename = "MTRy_Age_" + str(s) + "_Year_" + str(t) + "_data.png"
fullpath = os.path.join(output_dir, filename)
fig.savefig(fullpath, bbox_inches="tight", dpi=300)
plt.close()
# Garbage collection
del df, df_trnc, inc_lab, inc_cap, etr_data, mtrx_data, mtry_data
[docs]
def txfunc_graph(
s,
t,
df,
X,
Y,
txrates,
rate_type,
tax_func_type,
params_to_plot,
output_dir,
):
"""
This function creates a 3D plot of the fitted tax function against
the data.
Args:
s (int): age of individual, >= 21
t (int): year of analysis, >= 2016
df (Pandas DataFrame): 11 variables with N observations of tax
rates
X (Pandas DataSeries): labor income
Y (Pandas DataSeries): capital income
Y (Pandas DataSeries): tax rates from the data
rate_type (str): type of tax rate: mtrx, mtry, etr
tax_func_type (str): functional form of tax functions
params_to_plot (array_like or function): tax function parameters or
nonparametric function
path (str): output directory for saving plot files
Returns:
None
"""
cmap1 = matplotlib.colormaps.get_cmap("summer")
# Make comparison plot with full income domains
fig = plt.figure()
ax = fig.add_subplot(111, projection="3d")
ax.scatter(X, Y, txrates, c="r", marker="o")
ax.set_xlabel("Total Labor Income")
ax.set_ylabel("Total Capital Income")
if rate_type == "etr":
tx_label = "ETR"
elif rate_type == "mtrx":
tx_label = "MTRx"
elif rate_type == "mtry":
tx_label = "MTRy"
ax.set_zlabel(tx_label)
plt.title(
tx_label
+ " vs. Predicted "
+ tx_label
+ ": Age="
+ str(s)
+ ", Year="
+ str(t)
)
gridpts = 50
X_vec = np.exp(np.linspace(np.log(5), np.log(X.max()), gridpts))
Y_vec = np.exp(np.linspace(np.log(5), np.log(Y.max()), gridpts))
X_grid, Y_grid = np.meshgrid(X_vec, Y_vec)
txrate_grid = txfunc.get_tax_rates(
params_to_plot,
X_grid,
Y_grid,
None,
tax_func_type,
rate_type,
for_estimation=False,
)
ax.plot_surface(X_grid, Y_grid, txrate_grid, cmap=cmap1, linewidth=0)
filename = tx_label + "_age_" + str(s) + "_Year_" + str(t) + "_vsPred.png"
fullpath = os.path.join(output_dir, filename)
fig.savefig(fullpath, bbox_inches="tight", dpi=300)
plt.close()
# Make comparison plot with truncated income domains
df_trnc_gph = df[
(df["total_labinc"] > 5)
& (df["total_labinc"] < 800000)
& (df["total_capinc"] > 5)
& (df["total_capinc"] < 800000)
]
X_gph = df_trnc_gph["total_labinc"]
Y_gph = df_trnc_gph["total_capinc"]
if rate_type == "etr":
txrates_gph = df_trnc_gph["etr"]
elif rate_type == "mtrx":
txrates_gph = df_trnc_gph["mtr_labinc"]
elif rate_type == "mtry":
txrates_gph = df_trnc_gph["mtr_capinc"]
fig = plt.figure()
ax = fig.add_subplot(111, projection="3d")
ax.scatter(X_gph, Y_gph, txrates_gph, c="r", marker="o")
ax.set_xlabel("Total Labor Income")
ax.set_ylabel("Total Capital Income")
ax.set_zlabel(tx_label)
plt.title(
"Truncated "
+ tx_label
+ ", Lab. Inc., and Cap. "
+ "Inc., Age="
+ str(s)
+ ", Year="
+ str(t)
)
gridpts = 50
X_vec = np.exp(np.linspace(np.log(5), np.log(X_gph.max()), gridpts))
Y_vec = np.exp(np.linspace(np.log(5), np.log(Y_gph.max()), gridpts))
X_grid, Y_grid = np.meshgrid(X_vec, Y_vec)
txrate_grid = txfunc.get_tax_rates(
params_to_plot,
X_grid,
Y_grid,
None,
tax_func_type,
rate_type,
for_estimation=False,
)
ax.plot_surface(X_grid, Y_grid, txrate_grid, cmap=cmap1, linewidth=0)
filename = (
tx_label + "trunc_age_" + str(s) + "_Year_" + str(t) + "_vsPred.png"
)
fullpath = os.path.join(output_dir, filename)
fig.savefig(fullpath, bbox_inches="tight", dpi=300)
plt.close()
[docs]
def txfunc_sse_plot(age_vec, sse_mat, start_year, varstr, output_dir, round):
"""
Plot sum of squared errors of tax functions over age for each year
of budget window.
Args:
age_vec (numpy array): vector of ages, length S
sse_mat (Numpy array): SSE for each estimated tax function,
size is BW x S
start_year (int): first year of budget window
varstr (str): name of tax function being evaluated
path (str): path to save graph to
round (int): which round of sweeping for outliers (0, 1, or 2)
Returns:
None
"""
fig, ax = plt.subplots()
BW = sse_mat.shape[0]
for y in range(BW):
plt.plot(age_vec, sse_mat[y, :], label=str(start_year + y))
plt.legend(loc="upper left")
titletext = (
"Sum of Squared Errors by age and Tax Year"
+ " minus outliers (Round "
+ str(round)
+ "): "
+ varstr
)
plt.title(titletext)
plt.xlabel(r"age $s$")
plt.ylabel(r"SSE")
graphname = "SSE_" + varstr + "_Round" + str(round)
output_path = os.path.join(output_dir, graphname)
plt.savefig(output_path, bbox_inches="tight", dpi=300)
plt.close()
[docs]
def plot_income_data(
ages, abil_midp, abil_pcts, emat, t=None, path=None, filesuffix=""
):
"""
This function graphs ability matrix in 3D, 2D, log, and nolog
Args:
ages (Numpy array) ages represented in sample, length S
abil_midp (Numpy array): midpoints of income percentile bins in
each ability group
abil_pcts (Numpy array): percent of population in each lifetime
income group, length J
emat (Numpy array): effective labor units by age and lifetime
income group, size TxSxJ
t (int): model period for year, if None, then plot SS values
filesuffix (str): suffix to be added to plot files
Returns:
None
"""
if t is None:
t = -1
J = abil_midp.shape[0]
abil_mesh, age_mesh = np.meshgrid(abil_midp, ages)
cmap1 = matplotlib.colormaps["summer"]
if path:
# Make sure that directory is created
utils.mkdirs(path)
if J == 1:
# Plot of 2D, J=1 in levels
plt.figure()
plt.plot(ages, emat[t, :, :])
filename = "ability_2D_lev" + filesuffix
fullpath = os.path.join(path, filename)
plt.savefig(fullpath, dpi=300)
plt.close()
# Plot of 2D, J=1 in logs
plt.figure()
plt.plot(ages, np.log(emat[t, :, :]))
filename = "ability_2D_log" + filesuffix
fullpath = os.path.join(path, filename)
plt.savefig(fullpath, dpi=300)
plt.close()
else:
# Plot of 3D, J>1 in levels
fig10, ax10 = plt.subplots(subplot_kw={"projection": "3d"})
ax10.plot_surface(
age_mesh,
abil_mesh,
emat[t, :, :],
rstride=8,
cstride=1,
cmap=cmap1,
)
ax10.set_xlabel(r"age-$s$")
ax10.set_ylabel(r"ability type -$j$")
ax10.set_zlabel(r"ability $e_{j,s}$")
filename = "ability_3D_lev" + filesuffix
fullpath = os.path.join(path, filename)
plt.savefig(fullpath, dpi=300)
plt.close()
# Plot of 3D, J>1 in logs
fig11, ax11 = plt.subplots(subplot_kw={"projection": "3d"})
ax11.plot_surface(
age_mesh,
abil_mesh,
np.log(emat[t, :, :]),
rstride=8,
cstride=1,
cmap=cmap1,
)
ax11.set_xlabel(r"age-$s$")
ax11.set_ylabel(r"ability type -$j$")
ax11.set_zlabel(r"log ability $log(e_{j,s})$")
filename = "ability_3D_log" + filesuffix
fullpath = os.path.join(path, filename)
plt.savefig(fullpath, dpi=300)
plt.close()
if J <= 10: # Restricted because of line and marker types
# Plot of 2D lines from 3D version in logs
ax = plt.subplot(111)
linestyles = np.array(
[
"-",
"--",
"-.",
":",
]
)
markers = np.array(["x", "v", "o", "d", ">", "|"])
pct_lb = 0
for j in range(J):
this_label = (
str(int(np.rint(pct_lb)))
+ " - "
+ str(int(np.rint(pct_lb + 100 * abil_pcts[j])))
+ "%"
)
pct_lb += 100 * abil_pcts[j]
if j <= 3:
ax.plot(
ages,
np.log(emat[t, :, j]),
label=this_label,
linestyle=linestyles[j],
color="black",
)
elif j > 3:
ax.plot(
ages,
np.log(emat[t, :, j]),
label=this_label,
marker=markers[j - 4],
color="black",
)
ax.axvline(x=80, color="black", linestyle="--")
box = ax.get_position()
ax.set_position([box.x0, box.y0, box.width * 0.8, box.height])
ax.legend(loc="center left", bbox_to_anchor=(1, 0.5))
ax.set_xlabel(r"age-$s$")
ax.set_ylabel(r"log ability $log(e_{j,s})$")
filename = "ability_2D_log" + filesuffix
fullpath = os.path.join(path, filename)
plt.savefig(fullpath, dpi=300)
plt.close()
else:
if J <= 10: # Restricted because of line and marker types
# Plot of 2D lines from 3D version in logs
ax = plt.subplot(111)
linestyles = np.array(
[
"-",
"--",
"-.",
":",
]
)
markers = np.array(["x", "v", "o", "d", ">", "|"])
pct_lb = 0
for j in range(J):
this_label = (
str(int(np.rint(pct_lb)))
+ " - "
+ str(int(np.rint(pct_lb + 100 * abil_pcts[j])))
+ "%"
)
pct_lb += 100 * abil_pcts[j]
if j <= 3:
ax.plot(
ages,
np.log(emat[t, :, j]),
label=this_label,
linestyle=linestyles[j],
color="black",
)
elif j > 3:
ax.plot(
ages,
np.log(emat[t, :, j]),
label=this_label,
marker=markers[j - 4],
color="black",
)
ax.axvline(x=80, color="black", linestyle="--")
box = ax.get_position()
ax.set_position([box.x0, box.y0, box.width * 0.8, box.height])
ax.legend(loc="center left", bbox_to_anchor=(1, 0.5))
ax.set_xlabel(r"age-$s$")
ax.set_ylabel(r"log ability $log(e_{j,s})$")
return ax
[docs]
def plot_2D_taxfunc(
year,
start_year,
tax_param_list,
age=None,
E=21, # Age at which agents become economically active in the model
tax_func_type=["DEP"],
rate_type="etr",
over_labinc=True,
other_inc_val=1000,
max_inc_amt=1000000,
data_list=None,
labels=["1st Functions"],
title=None,
path=None,
):
"""
This function plots OG-Core tax functions in two dimensions.
The tax rates are plotted over capital or labor income, as
entered by the user.
Args:
year (int): year of policy tax functions represent
start_year (int): first year tax functions estimated for in
tax_param_list elements
tax_param_list (list): list of arrays containing tax function
parameters
age (int): age for tax functions to plot, use None if tax
function parameters were not age specific
tax_func_type (list): list of strings in ["DEP", "DEP_totalinc",
"GS", "linear"] and specifies functional form of tax functions
in tax_param_list
rate_type (str): string that is in ["etr", "mtrx", "mtry"] and
determines the type of tax rate that is plotted
over_labinc (bool): indicates that x-axis of the plot is over
labor income, if False then plot is over capital income
other_inc_val (scalar): dollar value at which to hold constant
the amount of income that is not represented on the x-axis
max_inc_amt (scalar): largest income amount to represent on the
x-axis of the plot
data_list (list): list of DataFrames with data to scatter plot
with tax functions, needs to be of format output from
ogcore.get_micro_data.get_data
labels (list): list of labels for tax function parameters
title (str): title for the plot
path (str): path to which to save plot, if None then figure
returned
Returns:
fig (Matplotlib plot object): plot of tax functions
"""
# Check that inputs are valid
assert isinstance(start_year, int)
assert isinstance(year, int)
assert year >= start_year
# if list of tax function types less than list of params, assume
# all the same functional form
if len(tax_func_type) < len(tax_param_list):
tax_func_type = [tax_func_type[0]] * len(tax_param_list)
for i, v in enumerate(tax_func_type):
assert v in ["DEP", "DEP_totalinc", "GS", "linear", "mono", "mono2D"]
assert rate_type in ["etr", "mtrx", "mtry"]
assert len(tax_param_list) == len(labels)
# Set age and year to look at
if age is not None:
assert isinstance(age, int)
assert age >= E
# Note: assumed age is given in E + model periods (but age
# below is also assumed to be calendar years)
s = age - E
else:
s = 0 # if not age-specific, all ages have the same values
t = year - start_year
# create rate_key to correspond to keys in tax func dicts
rate_key = "tfunc_" + rate_type + "_params_S"
# Set income range to plot over (min income value hard coded to 5)
inc_sup = np.exp(np.linspace(np.log(5), np.log(max_inc_amt), 100))
# Set income value for other income
inc_fix = other_inc_val
if over_labinc:
key1 = "total_labinc"
X = inc_sup
Y = inc_fix
else:
key1 = "total_capinc"
X = inc_fix
Y = inc_sup
# get tax rates for each point in the income support and plot
fig, ax = plt.subplots()
for i, tax_params in enumerate(tax_param_list):
tax_params = tax_params[rate_key][t][s]
rates = txfunc.get_tax_rates(
tax_params,
X,
Y,
None,
tax_func_type[i],
rate_type,
for_estimation=False,
)
plt.plot(inc_sup, rates, label=labels[i])
# plot raw data (if passed)
if data_list is not None:
rate_type_dict = {
"etr": "etr",
"mtrx": "mtr_labinc",
"mtry": "mtr_capinc",
}
# censor data to range of the plot
for d, data in enumerate(data_list):
data_to_plot = data[str(year)].copy()
if age is not None:
data_to_plot.drop(
data_to_plot[data_to_plot["age"] != age].index,
inplace=True,
)
# other censoring
data_to_plot.drop(
data_to_plot[data_to_plot[key1] > max_inc_amt].index,
inplace=True,
)
# other censoring used in txfunc.py
data_to_plot = txfunc.tax_data_sample(data_to_plot)
# set number of bins to 100 or bins of $1000 dollars
n_bins = min(100, np.floor_divide(max_inc_amt, 1000))
# need to compute weighted averages by group...
def weighted_mean(x, cols, w="weight"):
try:
return pd.Series(
np.average(x[cols], weights=x[w], axis=0), cols
)
except ZeroDivisionError:
return 0
data_to_plot["inc_bin"] = pd.cut(data_to_plot[key1], n_bins)
groups = data_to_plot.groupby("inc_bin", observed=True).apply(
weighted_mean, [rate_type_dict[rate_type], key1]
)
plt.scatter(
groups[key1], groups[rate_type_dict[rate_type]], alpha=0.1
)
# add legend, labels, etc to plot
plt.legend(loc="center right")
if title:
plt.title(title)
if over_labinc:
plt.xlabel(r"Labor income")
else:
plt.xlabel(r"Capital income")
plt.ylabel(VAR_LABELS[rate_type])
if path is None:
return fig
else:
plt.savefig(path, dpi=300)