forked from wch/retirement-simulation-dashboard
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathretirement.qmd
174 lines (137 loc) · 4.6 KB
/
retirement.qmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
---
title: "Retirement: simulating wealth with random returns, inflation and withdrawals"
format: dashboard
logo: retirement-logo.png
server: shiny
---
```{python}
#| context: setup
import math
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from shiny import render, reactive, ui
```
## Row {.flow}
```{python}
#| title: Scenario A
#| expandable: false
ui.input_slider("start_capital", "Initial investment", 1e5, 1e7, value=2e6, pre="$")
ui.input_slider("return_mean", "Average annual investment return", 0, 30, value=5, step=0.5, post="%")
ui.input_slider("inflation_mean", "Average annual inflation", 0, 20, value=2.5, step=0.5, post="%")
ui.input_slider("monthly_withdrawal", "Monthly withdrawals", 0, 50000, value=10000, pre="$")
```
```{python}
#| title: Scenario B
#| expandable: false
ui.input_slider("start_capital2", "Initial investment", 1e5, 1e7, value=2e6, pre="$")
ui.input_slider("return_mean2", "Average annual investment return", 0, 30, value=5, step=0.5, post="%")
ui.input_slider("inflation_mean2", "Average annual inflation", 0, 20, value=2.5, step=0.5, post="%")
ui.input_slider("monthly_withdrawal2", "Monthly withdrawals", 0, 50000, value=8000, step=500, pre="$")
```
## Row
```{python}
@render.plot()
def nav_1():
nav_df = run_simulation(
input.start_capital(),
input.return_mean() / 100,
# input.return_stdev() / 100,
.07,
input.inflation_mean() / 100,
# input.inflation_stdev() / 100,
.015,
input.monthly_withdrawal(),
30,
100
)
return make_plot(nav_df)
```
```{python}
@render.plot()
def nav_2():
nav_df = run_simulation(
input.start_capital2(),
input.return_mean2() / 100,
# input.return_stdev2() / 100,
.07,
input.inflation_mean2() / 100,
# input.inflation_stdev2() / 100,
.015,
input.monthly_withdrawal2(),
30,
100
)
return make_plot(nav_df)
```
```{python}
def create_matrix(rows, cols, mean, stdev):
x = np.random.randn(rows, cols)
x = mean + x * stdev
return x
def run_simulation(
start_capital,
return_mean,
return_stdev,
inflation_mean,
inflation_stdev,
monthly_withdrawal,
n_years,
n_simulations
):
# Convert annual values to monthly
n_months = 12 * n_years
monthly_return_mean = return_mean / 12
monthly_return_stdev = return_stdev / math.sqrt(12)
monthly_inflation_mean = inflation_mean / 12
monthly_inflation_stdev = inflation_stdev / math.sqrt(12)
# Simulate returns and inflation
monthly_returns = create_matrix(
n_months, n_simulations, monthly_return_mean, monthly_return_stdev
)
monthly_inflation = create_matrix(
n_months, n_simulations, monthly_inflation_mean, monthly_inflation_stdev
)
# Simulate withdrawals
nav = np.full((n_months + 1, n_simulations), float(start_capital))
for j in range(n_months):
nav[j + 1, :] = (
nav[j, :] *
(1 + monthly_returns[j, :] - monthly_inflation[j, :]) -
monthly_withdrawal
)
# Set nav values below 0 to NaN (Not a Number, which is equivalent to NA in R)
nav[nav < 0] = np.nan
# convert to millions
nav = nav / 1000000
return pd.DataFrame(nav)
def make_plot(nav_df):
# # For the histogram, we will fill NaNs with -1
nav_df_zeros = nav_df.ffill().fillna(0).iloc[-1, :]
# Define the figure and axes
fig = plt.figure()
# Create the top plot for time series on the first row that spans all columns
ax1 = plt.subplot2grid((2, 2), (0, 0), colspan=2)
# Create the bottom left plot for the percentage above zero
ax2 = plt.subplot2grid((2, 2), (1, 0), colspan=2)
for column in nav_df.columns:
ax1.plot(nav_df.index, nav_df[column], alpha=0.3)
ax1.spines['top'].set_visible(False)
ax1.spines['right'].set_visible(False)
ax1.title.set_text("Projected value of capital over 30 years")
ax1.set_xlabel("Months")
ax1.set_ylabel("Millions")
ax1.grid(True)
# Calculate the percentage of columns that are above zero for each date and plot (bottom left plot)
percent_above_zero = (nav_df > 0).sum(axis=1) / nav_df.shape[1] * 100
ax2.plot(nav_df.index, percent_above_zero, color='purple')
ax2.set_xlim(nav_df.index.min(), nav_df.index.max())
ax2.set_ylim(0, 100) # Percentage goes from 0 to 100
ax2.title.set_text("Percent of scenarios still paying")
ax2.spines['top'].set_visible(False)
ax2.spines['right'].set_visible(False)
ax2.set_xlabel("Months")
ax2.grid(True)
plt.tight_layout()
return fig
```