owlplanner 2025.5.5__py3-none-any.whl → 2025.5.15__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- owlplanner/__init__.py +3 -1
- owlplanner/config.py +2 -2
- owlplanner/plan.py +139 -274
- owlplanner/plotting/__init__.py +7 -0
- owlplanner/plotting/base.py +76 -0
- owlplanner/plotting/factory.py +32 -0
- owlplanner/plotting/matplotlib_backend.py +432 -0
- owlplanner/plotting/plotly_backend.py +980 -0
- owlplanner/rates.py +3 -14
- owlplanner/timelists.py +2 -7
- owlplanner/version.py +1 -1
- {owlplanner-2025.5.5.dist-info → owlplanner-2025.5.15.dist-info}/METADATA +10 -5
- owlplanner-2025.5.15.dist-info/RECORD +22 -0
- owlplanner/plots.py +0 -296
- owlplanner-2025.5.5.dist-info/RECORD +0 -18
- /owlplanner/{logging.py → mylogging.py} +0 -0
- {owlplanner-2025.5.5.dist-info → owlplanner-2025.5.15.dist-info}/WHEEL +0 -0
- {owlplanner-2025.5.5.dist-info → owlplanner-2025.5.15.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base classes for plot backends.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PlotBackend(ABC):
|
|
9
|
+
"""Abstract base class for plot backends."""
|
|
10
|
+
|
|
11
|
+
@abstractmethod
|
|
12
|
+
def jupyter_renderer(self, fig):
|
|
13
|
+
"""Render plot for Jupyter."""
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
@abstractmethod
|
|
17
|
+
def plot_histogram_results(self, objective, df, N, year_n, n_d=None, N_i=1, phi_j=None):
|
|
18
|
+
"""Show a histogram of values from historical data or Monte Carlo simulations."""
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
@abstractmethod
|
|
22
|
+
def plot_rates_correlations(self, name, tau_kn, N_n, rate_method, rate_frm=None, rate_to=None,
|
|
23
|
+
tag="", share_range=False):
|
|
24
|
+
"""Plot correlations between various rates."""
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
@abstractmethod
|
|
28
|
+
def plot_rates(self, name, tau_kn, year_n, year_frac_left, N_k, rate_method,
|
|
29
|
+
rate_frm=None, rate_to=None, tag=""):
|
|
30
|
+
"""Plot rate values used over the time horizon."""
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
@abstractmethod
|
|
34
|
+
def plot_rates_distributions(self, frm, to, SP500, BondsBaa, TNotes, Inflation, FROM):
|
|
35
|
+
"""Plot histograms of the rates distributions."""
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
@abstractmethod
|
|
39
|
+
def plot_gross_income(self, year_n, G_n, gamma_n, value, title, tax_brackets):
|
|
40
|
+
"""Plot gross income over time."""
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
@abstractmethod
|
|
44
|
+
def plot_profile(self, year_n, xi_n, title, inames):
|
|
45
|
+
"""Plot profile over time."""
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
@abstractmethod
|
|
49
|
+
def plot_net_spending(self, year_n, g_n, xi_n, xiBar_n, gamma_n, value, title, inames):
|
|
50
|
+
"""Plot net spending over time."""
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
@abstractmethod
|
|
54
|
+
def plot_asset_distribution(self, year_n, inames, b_ijkn, gamma_n, value, name, tag):
|
|
55
|
+
"""Plot asset distribution over time."""
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
@abstractmethod
|
|
59
|
+
def plot_allocations(self, year_n, inames, alpha_ijkn, ARCoord, title):
|
|
60
|
+
"""Plot allocations over time."""
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
@abstractmethod
|
|
64
|
+
def plot_accounts(self, year_n, savings_in, gamma_n, value, title, inames):
|
|
65
|
+
"""Plot accounts over time."""
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
@abstractmethod
|
|
69
|
+
def plot_sources(self, year_n, sources_in, gamma_n, value, title, inames):
|
|
70
|
+
"""Plot sources over time."""
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
@abstractmethod
|
|
74
|
+
def plot_taxes(self, year_n, T_n, M_n, gamma_n, value, title, inames):
|
|
75
|
+
"""Plot taxes over time."""
|
|
76
|
+
pass
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Factory for creating plot backends.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .base import PlotBackend
|
|
6
|
+
from .matplotlib_backend import MatplotlibBackend
|
|
7
|
+
from .plotly_backend import PlotlyBackend
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PlotFactory:
|
|
11
|
+
"""Factory for creating plot backends."""
|
|
12
|
+
|
|
13
|
+
@staticmethod
|
|
14
|
+
def createBackend(backend_type: str) -> PlotBackend:
|
|
15
|
+
"""
|
|
16
|
+
Create a plot backend of the specified type.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
backend_type: Type of backend to create ("matplotlib" or "plotly")
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
A PlotBackend instance
|
|
23
|
+
|
|
24
|
+
Raises:
|
|
25
|
+
ValueError: If backend_type is not a valid option
|
|
26
|
+
"""
|
|
27
|
+
if backend_type == "matplotlib":
|
|
28
|
+
return MatplotlibBackend()
|
|
29
|
+
elif backend_type == "plotly":
|
|
30
|
+
return PlotlyBackend()
|
|
31
|
+
else:
|
|
32
|
+
raise ValueError(f"Unknown backend type: {backend_type}")
|
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Matplotlib implementation of plot backend.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pandas as pd
|
|
7
|
+
import matplotlib.pyplot as plt
|
|
8
|
+
import matplotlib.ticker as tk
|
|
9
|
+
import io
|
|
10
|
+
import os
|
|
11
|
+
|
|
12
|
+
os.environ["JUPYTER_PLATFORM_DIRS"] = "1"
|
|
13
|
+
|
|
14
|
+
import seaborn as sbn # Noqa: E402
|
|
15
|
+
|
|
16
|
+
from .base import PlotBackend # Noqa: E402
|
|
17
|
+
from .. import utils as u # Noqa: E402
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class MatplotlibBackend(PlotBackend):
|
|
21
|
+
"""Matplotlib implementation of plot backend."""
|
|
22
|
+
|
|
23
|
+
def __init__(self):
|
|
24
|
+
"""Initialize the matplotlib backend."""
|
|
25
|
+
self.set_plot_style()
|
|
26
|
+
|
|
27
|
+
def jupyter_renderer(self, fig):
|
|
28
|
+
pass
|
|
29
|
+
# plt.show()
|
|
30
|
+
|
|
31
|
+
def set_plot_style(self):
|
|
32
|
+
"""Set the style for all matplotlib plots."""
|
|
33
|
+
plt.rcParams.update({'figure.autolayout': True})
|
|
34
|
+
plt.rcParams.update({'figure.figsize': (6, 4)})
|
|
35
|
+
plt.rcParams.update({'axes.grid': True})
|
|
36
|
+
plt.rcParams.update({'axes.grid.which': 'both'})
|
|
37
|
+
|
|
38
|
+
def _line_income_plot(self, x, series, style, title, yformat=r"\$k"):
|
|
39
|
+
"""Core line plotter function."""
|
|
40
|
+
fig, ax = plt.subplots()
|
|
41
|
+
|
|
42
|
+
for sname in series:
|
|
43
|
+
ax.plot(x, series[sname], label=sname, ls=style[sname])
|
|
44
|
+
|
|
45
|
+
ax.legend(loc="upper left", reverse=True, fontsize=8, framealpha=0.3)
|
|
46
|
+
ax.set_title(title)
|
|
47
|
+
ax.set_xlabel("year")
|
|
48
|
+
ax.set_ylabel(yformat)
|
|
49
|
+
ax.xaxis.set_major_locator(tk.MaxNLocator(integer=True))
|
|
50
|
+
if "k" in yformat:
|
|
51
|
+
ax.get_yaxis().set_major_formatter(tk.FuncFormatter(lambda x, p: format(int(x / 1000), ",")))
|
|
52
|
+
# Give range to y values in unindexed flat profiles.
|
|
53
|
+
ymin, ymax = ax.get_ylim()
|
|
54
|
+
if ymax - ymin < 5000:
|
|
55
|
+
ax.set_ylim((ymin * 0.95, ymax * 1.05))
|
|
56
|
+
|
|
57
|
+
return fig, ax
|
|
58
|
+
|
|
59
|
+
def _stack_plot(self, x, inames, title, irange, series, snames, location, yformat=r"\$k"):
|
|
60
|
+
"""Core function for stacked plots."""
|
|
61
|
+
nonzeroSeries = {}
|
|
62
|
+
for sname in snames:
|
|
63
|
+
for i in irange:
|
|
64
|
+
tmp = series[sname][i]
|
|
65
|
+
if sum(tmp) > 1.0:
|
|
66
|
+
nonzeroSeries[sname + " " + inames[i]] = tmp
|
|
67
|
+
|
|
68
|
+
if len(nonzeroSeries) == 0:
|
|
69
|
+
return None, None
|
|
70
|
+
|
|
71
|
+
fig, ax = plt.subplots()
|
|
72
|
+
|
|
73
|
+
ax.stackplot(x, nonzeroSeries.values(), labels=nonzeroSeries.keys(), alpha=0.6)
|
|
74
|
+
ax.legend(loc=location, reverse=True, fontsize=8, ncol=2, framealpha=0.5)
|
|
75
|
+
ax.set_title(title)
|
|
76
|
+
ax.set_xlabel("year")
|
|
77
|
+
ax.xaxis.set_major_locator(tk.MaxNLocator(integer=True))
|
|
78
|
+
if "k" in yformat:
|
|
79
|
+
ax.set_ylabel(yformat)
|
|
80
|
+
ax.get_yaxis().set_major_formatter(tk.FuncFormatter(lambda x, p: format(int(x / 1000), ",")))
|
|
81
|
+
elif yformat == "percent":
|
|
82
|
+
ax.set_ylabel("%")
|
|
83
|
+
ax.get_yaxis().set_major_formatter(tk.FuncFormatter(lambda x, p: format(int(100 * x), ",")))
|
|
84
|
+
else:
|
|
85
|
+
raise RuntimeError(f"Unknown yformat: {yformat}.")
|
|
86
|
+
|
|
87
|
+
return fig, ax
|
|
88
|
+
|
|
89
|
+
def plot_histogram_results(self, objective, df, N, year_n, n_d=None, N_i=1, phi_j=None):
|
|
90
|
+
"""Show a histogram of values from historical data or Monte Carlo simulations."""
|
|
91
|
+
description = io.StringIO()
|
|
92
|
+
|
|
93
|
+
pSuccess = u.pc(len(df) / N)
|
|
94
|
+
print(f"Success rate: {pSuccess} on {N} samples.", file=description)
|
|
95
|
+
title = f"$N$ = {N}, $P$ = {pSuccess}"
|
|
96
|
+
means = df.mean(axis=0, numeric_only=True)
|
|
97
|
+
medians = df.median(axis=0, numeric_only=True)
|
|
98
|
+
|
|
99
|
+
my = 2 * [year_n[-1]]
|
|
100
|
+
if N_i == 2 and n_d is not None and n_d < len(year_n):
|
|
101
|
+
my[0] = year_n[n_d - 1]
|
|
102
|
+
|
|
103
|
+
# Don't show partial bequest of zero if spouse is full beneficiary,
|
|
104
|
+
# or if solution led to empty accounts at the end of first spouse's life.
|
|
105
|
+
if (phi_j is not None and np.all((1 - phi_j) < 0.01)) or medians.iloc[0] < 1:
|
|
106
|
+
if medians.iloc[0] < 1:
|
|
107
|
+
print(f"Optimized solutions all have null partial bequest in year {my[0]}.", file=description)
|
|
108
|
+
df.drop("partial", axis=1, inplace=True)
|
|
109
|
+
means = df.mean(axis=0, numeric_only=True)
|
|
110
|
+
medians = df.median(axis=0, numeric_only=True)
|
|
111
|
+
|
|
112
|
+
df /= 1000
|
|
113
|
+
if len(df) > 0:
|
|
114
|
+
thisyear = year_n[0]
|
|
115
|
+
if objective == "maxBequest":
|
|
116
|
+
fig, axes = plt.subplots()
|
|
117
|
+
# Show both partial and final bequests in the same histogram.
|
|
118
|
+
sbn.histplot(df, multiple="dodge", kde=True, ax=axes)
|
|
119
|
+
legend = []
|
|
120
|
+
# Don't know why but legend is reversed from df.
|
|
121
|
+
for q in range(len(means) - 1, -1, -1):
|
|
122
|
+
dmedian = u.d(medians.iloc[q], latex=True)
|
|
123
|
+
dmean = u.d(means.iloc[q], latex=True)
|
|
124
|
+
legend.append(f"{my[q]}: $M$: {dmedian}, $\\bar{{x}}$: {dmean}")
|
|
125
|
+
plt.legend(legend, shadow=True)
|
|
126
|
+
plt.xlabel(f"{thisyear} $k")
|
|
127
|
+
plt.title(objective)
|
|
128
|
+
leads = [f"partial {my[0]}", f" final {my[1]}"]
|
|
129
|
+
elif len(means) == 2:
|
|
130
|
+
# Show partial bequest and net spending as two separate histograms.
|
|
131
|
+
fig, axes = plt.subplots(1, 2, figsize=(10, 5))
|
|
132
|
+
cols = ["partial", objective]
|
|
133
|
+
leads = [f"partial {my[0]}", objective]
|
|
134
|
+
for q in range(2):
|
|
135
|
+
sbn.histplot(df[cols[q]], kde=True, ax=axes[q])
|
|
136
|
+
dmedian = u.d(medians.iloc[q], latex=True)
|
|
137
|
+
dmean = u.d(means.iloc[q], latex=True)
|
|
138
|
+
legend = [f"$M$: {dmedian}, $\\bar{{x}}$: {dmean}"]
|
|
139
|
+
axes[q].set_label(legend)
|
|
140
|
+
axes[q].legend(labels=legend)
|
|
141
|
+
axes[q].set_title(leads[q])
|
|
142
|
+
axes[q].set_xlabel(f"{thisyear} $k")
|
|
143
|
+
else:
|
|
144
|
+
# Show net spending as single histogram.
|
|
145
|
+
fig, axes = plt.subplots()
|
|
146
|
+
sbn.histplot(df[objective], kde=True, ax=axes)
|
|
147
|
+
dmedian = u.d(medians.iloc[0], latex=True)
|
|
148
|
+
dmean = u.d(means.iloc[0], latex=True)
|
|
149
|
+
legend = [f"$M$: {dmedian}, $\\bar{{x}}$: {dmean}"]
|
|
150
|
+
plt.legend(legend, shadow=True)
|
|
151
|
+
plt.xlabel(f"{thisyear} $k")
|
|
152
|
+
plt.title(objective)
|
|
153
|
+
leads = [objective]
|
|
154
|
+
|
|
155
|
+
plt.suptitle(title)
|
|
156
|
+
|
|
157
|
+
for q in range(len(means)):
|
|
158
|
+
print(f"{leads[q]:>12}: Median ({thisyear} $): {u.d(medians.iloc[q])}", file=description)
|
|
159
|
+
print(f"{leads[q]:>12}: Mean ({thisyear} $): {u.d(means.iloc[q])}", file=description)
|
|
160
|
+
mmin = 1000 * df.iloc[:, q].min()
|
|
161
|
+
mmax = 1000 * df.iloc[:, q].max()
|
|
162
|
+
print(f"{leads[q]:>12}: Range: {u.d(mmin)} - {u.d(mmax)}", file=description)
|
|
163
|
+
nzeros = len(df.iloc[:, q][df.iloc[:, q] < 0.001])
|
|
164
|
+
print(f"{leads[q]:>12}: N zero solns: {nzeros}", file=description)
|
|
165
|
+
|
|
166
|
+
return fig, description
|
|
167
|
+
|
|
168
|
+
return None, description
|
|
169
|
+
|
|
170
|
+
def plot_rates_correlations(self, name, tau_kn, N_n, rate_method, rate_frm=None, rate_to=None,
|
|
171
|
+
tag="", share_range=False):
|
|
172
|
+
"""Plot correlations between various rates."""
|
|
173
|
+
rate_names = [
|
|
174
|
+
"S&P500 (incl. div.)",
|
|
175
|
+
"Baa Corp. Bonds",
|
|
176
|
+
"10-y T-Notes",
|
|
177
|
+
"Inflation",
|
|
178
|
+
]
|
|
179
|
+
|
|
180
|
+
df = pd.DataFrame()
|
|
181
|
+
for k, name in enumerate(rate_names):
|
|
182
|
+
data = 100 * tau_kn[k]
|
|
183
|
+
df[name] = data
|
|
184
|
+
|
|
185
|
+
g = sbn.PairGrid(df, diag_sharey=False, height=1.8, aspect=1)
|
|
186
|
+
if share_range:
|
|
187
|
+
minval = df.min().min() - 5
|
|
188
|
+
maxval = df.max().max() + 5
|
|
189
|
+
g.set(xlim=(minval, maxval), ylim=(minval, maxval))
|
|
190
|
+
g.map_upper(sbn.scatterplot)
|
|
191
|
+
g.map_lower(sbn.kdeplot)
|
|
192
|
+
g.map_diag(sbn.histplot, color="orange")
|
|
193
|
+
|
|
194
|
+
# Put zero axes on off-diagonal plots.
|
|
195
|
+
imod = len(rate_names) + 1
|
|
196
|
+
for i, ax in enumerate(g.axes.flat):
|
|
197
|
+
ax.axvline(x=0, color="grey", linewidth=1, linestyle=":")
|
|
198
|
+
if i % imod != 0:
|
|
199
|
+
ax.axhline(y=0, color="grey", linewidth=1, linestyle=":")
|
|
200
|
+
|
|
201
|
+
title = name + "\n"
|
|
202
|
+
title += f"Rates Correlations (N={N_n}) {rate_method}"
|
|
203
|
+
if rate_method in ["historical", "histochastic"]:
|
|
204
|
+
title += f" ({rate_frm}-{rate_to})"
|
|
205
|
+
|
|
206
|
+
if tag != "":
|
|
207
|
+
title += " - " + tag
|
|
208
|
+
|
|
209
|
+
g.figure.suptitle(title, y=1.08)
|
|
210
|
+
return g.figure
|
|
211
|
+
|
|
212
|
+
def plot_rates(self, name, tau_kn, year_n, year_frac_left, N_k,
|
|
213
|
+
rate_method, rate_frm=None, rate_to=None, tag=""):
|
|
214
|
+
"""Plot rate values used over the time horizon."""
|
|
215
|
+
fig, ax = plt.subplots()
|
|
216
|
+
title = name + "\nReturn & Inflation Rates (" + str(rate_method)
|
|
217
|
+
if rate_method in ["historical", "histochastic", "historical average"]:
|
|
218
|
+
title += f" {rate_frm}-{rate_to}"
|
|
219
|
+
title += ")"
|
|
220
|
+
|
|
221
|
+
if tag != "":
|
|
222
|
+
title += " - " + tag
|
|
223
|
+
|
|
224
|
+
rate_name = [
|
|
225
|
+
"S&P500 (incl. div.)",
|
|
226
|
+
"Baa Corp. Bonds",
|
|
227
|
+
"10-y T-Notes",
|
|
228
|
+
"Inflation",
|
|
229
|
+
]
|
|
230
|
+
ltype = ["-", "-.", ":", "--"]
|
|
231
|
+
|
|
232
|
+
for k in range(N_k):
|
|
233
|
+
# Don't plot partial rates for current year if mid-year.
|
|
234
|
+
if year_frac_left == 1:
|
|
235
|
+
data = 100 * tau_kn[k]
|
|
236
|
+
years = year_n
|
|
237
|
+
else:
|
|
238
|
+
data = 100 * tau_kn[k, 1:]
|
|
239
|
+
years = year_n[1:]
|
|
240
|
+
|
|
241
|
+
# Use ddof=1 to match pandas' statistical calculations from numpy.
|
|
242
|
+
label = (
|
|
243
|
+
rate_name[k] + " <" + "{:.1f}".format(np.mean(data)) + " +/- {:.1f}".format(np.std(data, ddof=1)) + "%>"
|
|
244
|
+
)
|
|
245
|
+
ax.plot(years, data, label=label, ls=ltype[k % N_k])
|
|
246
|
+
|
|
247
|
+
ax.xaxis.set_major_locator(tk.MaxNLocator(integer=True))
|
|
248
|
+
ax.legend(loc="best", reverse=False, fontsize=8, framealpha=0.7)
|
|
249
|
+
ax.set_title(title)
|
|
250
|
+
ax.set_xlabel("year")
|
|
251
|
+
ax.set_ylabel("%")
|
|
252
|
+
|
|
253
|
+
return fig
|
|
254
|
+
|
|
255
|
+
def plot_rates_distributions(self, frm, to, SP500, BondsBaa, TNotes, Inflation, FROM):
|
|
256
|
+
"""Plot histograms of the rates distributions."""
|
|
257
|
+
title = f"Rates from {frm} to {to}"
|
|
258
|
+
# Bring year values to indices.
|
|
259
|
+
frm -= FROM
|
|
260
|
+
to -= FROM
|
|
261
|
+
|
|
262
|
+
nbins = int((to - frm) / 4)
|
|
263
|
+
fig, ax = plt.subplots(1, 4, sharey=True, sharex=True, tight_layout=True)
|
|
264
|
+
|
|
265
|
+
dat0 = np.array(SP500[frm:to])
|
|
266
|
+
dat1 = np.array(BondsBaa[frm:to])
|
|
267
|
+
dat2 = np.array(TNotes[frm:to])
|
|
268
|
+
dat3 = np.array(Inflation[frm:to])
|
|
269
|
+
|
|
270
|
+
fig.suptitle(title)
|
|
271
|
+
ax[0].set_title("S&P500")
|
|
272
|
+
label = "<>: " + u.pc(np.mean(dat0), 2, 1)
|
|
273
|
+
ax[0].hist(dat0, bins=nbins, label=label)
|
|
274
|
+
ax[0].legend(loc="upper left", fontsize=8, framealpha=0.7)
|
|
275
|
+
|
|
276
|
+
ax[1].set_title("BondsBaa")
|
|
277
|
+
label = "<>: " + u.pc(np.mean(dat1), 2, 1)
|
|
278
|
+
ax[1].hist(dat1, bins=nbins, label=label)
|
|
279
|
+
ax[1].legend(loc="upper left", fontsize=8, framealpha=0.7)
|
|
280
|
+
|
|
281
|
+
ax[2].set_title("TNotes")
|
|
282
|
+
label = "<>: " + u.pc(np.mean(dat2), 2, 1)
|
|
283
|
+
ax[2].hist(dat2, bins=nbins, label=label)
|
|
284
|
+
ax[2].legend(loc="upper left", fontsize=8, framealpha=0.7)
|
|
285
|
+
|
|
286
|
+
ax[3].set_title("Inflation")
|
|
287
|
+
label = "<>: " + u.pc(np.mean(dat3), 2, 1)
|
|
288
|
+
ax[3].hist(dat3, bins=nbins, label=label)
|
|
289
|
+
ax[3].legend(loc="upper left", fontsize=8, framealpha=0.7)
|
|
290
|
+
|
|
291
|
+
return fig
|
|
292
|
+
|
|
293
|
+
def plot_gross_income(self, year_n, G_n, gamma_n, value, title, tax_brackets):
|
|
294
|
+
"""Plot gross income over time."""
|
|
295
|
+
style = {"taxable income": "-"}
|
|
296
|
+
if value == "nominal":
|
|
297
|
+
series = {"taxable income": G_n}
|
|
298
|
+
yformat = r"\$k (nominal)"
|
|
299
|
+
infladjust = gamma_n[:-1]
|
|
300
|
+
else:
|
|
301
|
+
series = {"taxable income": G_n / gamma_n[:-1]}
|
|
302
|
+
yformat = r"\$k (" + str(year_n[0]) + r"\$)"
|
|
303
|
+
infladjust = 1
|
|
304
|
+
fig, ax = self._line_income_plot(year_n, series, style, title, yformat)
|
|
305
|
+
# Overlay tax brackets
|
|
306
|
+
for key in tax_brackets:
|
|
307
|
+
data_adj = tax_brackets[key] * infladjust
|
|
308
|
+
ax.plot(year_n, data_adj, label=key, ls=":")
|
|
309
|
+
ax.grid(visible=True, which='both')
|
|
310
|
+
ax.legend(loc="upper left", reverse=True, fontsize=8, framealpha=0.3)
|
|
311
|
+
|
|
312
|
+
return fig
|
|
313
|
+
|
|
314
|
+
def plot_profile(self, year_n, xi_n, title, inames):
|
|
315
|
+
"""Plot profile over time."""
|
|
316
|
+
style = {"profile": "-"}
|
|
317
|
+
series = {"profile": xi_n}
|
|
318
|
+
|
|
319
|
+
return self._line_income_plot(year_n, series, style, title, yformat=r"$\xi$")[0]
|
|
320
|
+
|
|
321
|
+
def plot_net_spending(self, year_n, g_n, xi_n, xiBar_n, gamma_n, value, title, inames):
|
|
322
|
+
"""Plot net spending over time."""
|
|
323
|
+
style = {"net": "-", "target": ":"}
|
|
324
|
+
if value == "nominal":
|
|
325
|
+
series = {"net": g_n, "target": (g_n[0] / xi_n[0]) * xiBar_n}
|
|
326
|
+
yformat = r"\$k (nominal)"
|
|
327
|
+
else:
|
|
328
|
+
series = {"net": g_n / gamma_n[:-1], "target": (g_n[0] / xi_n[0]) * xi_n}
|
|
329
|
+
yformat = r"\$k (" + str(year_n[0]) + r"\$)"
|
|
330
|
+
|
|
331
|
+
return self._line_income_plot(year_n, series, style, title, yformat)[0]
|
|
332
|
+
|
|
333
|
+
def plot_asset_distribution(self, year_n, inames, b_ijkn, gamma_n, value, name, tag):
|
|
334
|
+
"""Plot asset distribution over time."""
|
|
335
|
+
if value == "nominal":
|
|
336
|
+
yformat = r"\$k (nominal)"
|
|
337
|
+
infladjust = 1
|
|
338
|
+
else:
|
|
339
|
+
yformat = r"\$k (" + str(year_n[0]) + r"\$)"
|
|
340
|
+
infladjust = gamma_n
|
|
341
|
+
years_n = np.array(year_n)
|
|
342
|
+
years_n = np.append(years_n, [years_n[-1] + 1])
|
|
343
|
+
y2stack = {}
|
|
344
|
+
jDic = {"taxable": 0, "tax-deferred": 1, "tax-free": 2}
|
|
345
|
+
kDic = {"stocks": 0, "C bonds": 1, "T notes": 2, "common": 3}
|
|
346
|
+
figures = []
|
|
347
|
+
for jkey in jDic:
|
|
348
|
+
stackNames = []
|
|
349
|
+
for kkey in kDic:
|
|
350
|
+
namek = kkey + " / " + jkey
|
|
351
|
+
stackNames.append(namek)
|
|
352
|
+
y2stack[namek] = np.zeros((len(inames), len(years_n)))
|
|
353
|
+
for i in range(len(inames)):
|
|
354
|
+
y2stack[namek][i][:] = b_ijkn[i][jDic[jkey]][kDic[kkey]][:] / infladjust
|
|
355
|
+
title = name + "\nAssets Distribution - " + jkey
|
|
356
|
+
if tag:
|
|
357
|
+
title += " - " + tag
|
|
358
|
+
fig, ax = self._stack_plot(years_n, inames, title, range(len(inames)),
|
|
359
|
+
y2stack, stackNames, "upper left", yformat)
|
|
360
|
+
figures.append(fig)
|
|
361
|
+
|
|
362
|
+
return figures
|
|
363
|
+
|
|
364
|
+
def plot_allocations(self, year_n, inames, alpha_ijkn, ARCoord, title):
|
|
365
|
+
"""Plot allocations over time."""
|
|
366
|
+
count = len(inames)
|
|
367
|
+
if ARCoord == "spouses":
|
|
368
|
+
acList = [ARCoord]
|
|
369
|
+
count = 1
|
|
370
|
+
elif ARCoord == "individual":
|
|
371
|
+
acList = [ARCoord]
|
|
372
|
+
elif ARCoord == "account":
|
|
373
|
+
acList = ["taxable", "tax-deferred", "tax-free"]
|
|
374
|
+
else:
|
|
375
|
+
raise ValueError(f"Unknown coordination {ARCoord}.")
|
|
376
|
+
figures = []
|
|
377
|
+
assetDic = {"stocks": 0, "C bonds": 1, "T notes": 2, "common": 3}
|
|
378
|
+
for i in range(count):
|
|
379
|
+
y2stack = {}
|
|
380
|
+
for acType in acList:
|
|
381
|
+
stackNames = []
|
|
382
|
+
for key in assetDic:
|
|
383
|
+
aname = key + " / " + acType
|
|
384
|
+
stackNames.append(aname)
|
|
385
|
+
y2stack[aname] = np.zeros((count, len(year_n)))
|
|
386
|
+
y2stack[aname][i][:] = alpha_ijkn[i, acList.index(acType), assetDic[key], : len(year_n)]
|
|
387
|
+
t = title + f" - {acType}"
|
|
388
|
+
fig, ax = self._stack_plot(year_n, inames, t, [i], y2stack, stackNames, "upper left", "percent")
|
|
389
|
+
figures.append(fig)
|
|
390
|
+
|
|
391
|
+
return figures
|
|
392
|
+
|
|
393
|
+
def plot_accounts(self, year_n, savings_in, gamma_n, value, title, inames):
|
|
394
|
+
"""Plot accounts over time."""
|
|
395
|
+
stypes = list(savings_in.keys())
|
|
396
|
+
year_n_full = np.append(year_n, [year_n[-1] + 1])
|
|
397
|
+
if value == "nominal":
|
|
398
|
+
yformat = r"\$k (nominal)"
|
|
399
|
+
savings = savings_in
|
|
400
|
+
else:
|
|
401
|
+
yformat = r"\$k (" + str(year_n[0]) + r"\$)"
|
|
402
|
+
savings = {k: v / gamma_n for k, v in savings_in.items()}
|
|
403
|
+
fig, ax = self._stack_plot(year_n_full, inames, title, range(len(inames)),
|
|
404
|
+
savings, stypes, "upper left", yformat)
|
|
405
|
+
|
|
406
|
+
return fig
|
|
407
|
+
|
|
408
|
+
def plot_sources(self, year_n, sources_in, gamma_n, value, title, inames):
|
|
409
|
+
"""Plot sources over time."""
|
|
410
|
+
stypes = list(sources_in.keys())
|
|
411
|
+
if value == "nominal":
|
|
412
|
+
yformat = r"\$k (nominal)"
|
|
413
|
+
sources = sources_in
|
|
414
|
+
else:
|
|
415
|
+
yformat = r"\$k (" + str(year_n[0]) + r"\$)"
|
|
416
|
+
sources = {k: v / gamma_n[:-1] for k, v in sources_in.items()}
|
|
417
|
+
fig, ax = self._stack_plot(year_n, inames, title, range(len(inames)), sources, stypes, "upper left", yformat)
|
|
418
|
+
|
|
419
|
+
return fig
|
|
420
|
+
|
|
421
|
+
def plot_taxes(self, year_n, T_n, M_n, gamma_n, value, title, inames):
|
|
422
|
+
"""Plot taxes over time."""
|
|
423
|
+
style = {"income taxes": "-", "Medicare": "-."}
|
|
424
|
+
if value == "nominal":
|
|
425
|
+
series = {"income taxes": T_n, "Medicare": M_n}
|
|
426
|
+
yformat = r"\$k (nominal)"
|
|
427
|
+
else:
|
|
428
|
+
series = {"income taxes": T_n / gamma_n[:-1], "Medicare": M_n / gamma_n[:-1]}
|
|
429
|
+
yformat = r"\$k (" + str(year_n[0]) + r"\$)"
|
|
430
|
+
fig, ax = self._line_income_plot(year_n, series, style, title, yformat)
|
|
431
|
+
|
|
432
|
+
return fig
|