Skip to content

Instantly share code, notes, and snippets.

@jbradberry
Last active May 21, 2021 17:41
Show Gist options
  • Save jbradberry/187217afc150a85788ab47ba8821a7b9 to your computer and use it in GitHub Desktop.
Save jbradberry/187217afc150a85788ab47ba8821a7b9 to your computer and use it in GitHub Desktop.
Retirement simulation
1976 0.1560
1977 0.0300
1978 0.0140
1979 0.0190
1980 0.0270
1981 0.0630
1982 0.3260
1983 0.0840
1984 0.1515
1985 0.2211
1986 0.1526
1987 0.0276
1988 0.0789
1989 0.1453
1990 0.0896
1991 0.1600
1992 0.0740
1993 0.0975
1994 -0.0292
1995 0.1847
1996 0.0363
1997 0.0965
1998 0.0869
1999 -0.0082
2000 0.1163
2001 0.0844
2002 0.1026
2003 0.0410
2004 0.0434
2005 0.0243
2006 0.0433
2007 0.0697
2008 0.0524
2009 0.0593
2010 0.0654
2011 0.0784
2012 0.0421
2013 -0.0202
2014 0.0597
2015 0.0055
2016 0.0265
Display the source blob
Display the rendered blob
Raw
{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"import datetime\n",
"\n",
"from IPython.display import display\n",
"import ipywidgets as widgets\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"import matplotlib.patches as mpatches\n",
"import matplotlib.lines as mlines\n",
"\n",
"%matplotlib inline"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"stocks = pd.read_csv('./stock_returns.csv', header=None, names=['year', 'yield'], index_col=0, squeeze=True)\n",
"bonds = pd.read_csv('./bond_returns.csv', header=None, names=['year', 'yield'], index_col=0, squeeze=True)"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [],
"source": [
"starting_year = datetime.date.today().year\n",
"\n",
"social_security = 2000\n",
"full_soc_sec = 67\n",
"\n",
"inflation = 1.0322"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"CPU times: user 2.19 s, sys: 175 ms, total: 2.37 s\n",
"Wall time: 2.38 s\n"
]
}
],
"source": [
"%%time\n",
"\n",
"# Generate one million simulation chains of the next 100 years of stock and bond yields.\n",
"sim_stocks = pd.DataFrame(\n",
" np.random.choice(stocks, size=(1000000, 100), replace=True),\n",
" columns=np.arange(starting_year + 1, starting_year + 101)\n",
")\n",
"\n",
"sim_bonds = pd.DataFrame(\n",
" np.random.choice(bonds, size=(1000000, 100), replace=True),\n",
" columns=np.arange(starting_year + 1, starting_year + 101)\n",
")"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [],
"source": [
"def bond_function(allocation, birth_year, year):\n",
" if allocation == 'Age as bond percentage':\n",
" return min((year - birth_year) / 100, 1.)\n",
" return 1. - int(allocation[:2]) / 100\n",
"\n",
"def run_simulation(birth_year, retirement_age, starting_value, contribution_rate, burn_rate, allocation):\n",
" bond_allocation = pd.Series({\n",
" year: bond_function(allocation, birth_year, year) for year in sim_stocks.columns\n",
" })\n",
" contributions = pd.Series({\n",
" year: contribution_rate * (year - birth_year < retirement_age) for year in sim_stocks.columns\n",
" })\n",
" spending = pd.Series({\n",
" year: (burn_rate * (year - birth_year >= retirement_age) -\n",
" 12 * social_security * (year - birth_year >= full_soc_sec))\n",
" for year in sim_stocks.columns\n",
" })\n",
"\n",
" results = pd.DataFrame({starting_year: pd.Series(starting_value for _ in range(1000000))})\n",
" for year in sim_stocks.columns:\n",
" yield_ = 1. + (1 - bond_allocation[year]) * sim_stocks[year] + bond_allocation[year] * sim_bonds[year]\n",
" results[year] = yield_ * (results[year - 1] + contributions[year] - spending[year]) / inflation\n",
"\n",
" fig, axs = plt.subplots(2, 1, sharex=True, figsize=(12, 16))\n",
"\n",
" ax = axs[0]\n",
" df = pd.DataFrame({\n",
" year: results[year].describe([.03, .5, .97])[['97%', '50%', '3%']]\n",
" for year in sim_stocks.columns\n",
" }).T\n",
" df.columns = ['97th percentile', '50th percentile', '3rd percentile']\n",
" df.plot(ax=ax, logy=True)\n",
" ax.set_title('Value of Retirement Fund')\n",
" ax.set_xlabel('Year')\n",
" ax.set_ylabel('Value (log USD)')\n",
"\n",
" ax = axs[1]\n",
" s = results.lt(0).sum() / 1e6\n",
" s.plot(subplots=True, ax=ax)\n",
" ax.axhline(0.03, color='red', linestyle='-.')\n",
" ax.legend(handles=[\n",
" mlines.Line2D([], [], label='Chance of running out of money (%)'),\n",
" mlines.Line2D([], [], color='red', linestyle='-.', label='3% threshold')\n",
" ])\n",
" ax.set_title('Risk of Ruin')\n",
" ax.set_xlabel('Year')\n",
" ax.set_ylabel('Probability of negative value')\n",
" \n",
" for ax in axs:\n",
" ax.axvline(birth_year + full_soc_sec, color='blue', linestyle=':')\n",
" ax.axvspan(birth_year + retirement_age, birth_year + 100, facecolor='green', alpha=0.15)\n",
"\n",
" handles = [\n",
" mpatches.Patch(color='green', alpha=0.15, label='Retirement up to age 100'),\n",
" mlines.Line2D([], [], color='blue', linestyle=':', label='Full Social Security benefits')\n",
" ]\n",
" fig.legend(handles=handles, bbox_to_anchor=(0.95, 0.92), borderaxespad=0)"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {
"scrolled": false
},
"outputs": [
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "47906b1da6584db8a9231bd2009d9fcc",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
"interactive(children=(BoundedIntText(value=1977, description='Birth Year:', layout=Layout(width='50%'), max=20…"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/plain": [
"<function __main__.run_simulation(birth_year, retirement_age, starting_value, contribution_rate, burn_rate, allocation)>"
]
},
"execution_count": 6,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"layout = widgets.Layout(width='50%')\n",
"style = {'description_width': '150px'}\n",
"\n",
"birth_widget = widgets.BoundedIntText(\n",
" value=1977,\n",
" min=starting_year - 100,\n",
" max=starting_year,\n",
" step=1,\n",
" description='Birth Year:',\n",
" disabled=False,\n",
" layout=layout,\n",
" style=style\n",
")\n",
"retirement_widget = widgets.BoundedIntText(\n",
" value=67,\n",
" min=40,\n",
" max=80,\n",
" step=1,\n",
" description='Retirement Age:',\n",
" disabled=False,\n",
" layout=layout,\n",
" style=style\n",
")\n",
"starting_widget = widgets.IntText(\n",
" value=100000,\n",
" step=100,\n",
" description='Starting Value:', disabled=False, layout=layout, style=style\n",
")\n",
"contribution_widget = widgets.IntText(\n",
" value=25000,\n",
" step=100,\n",
" description='Yearly Contribution:', disabled=False, layout=layout, style=style\n",
")\n",
"burn_widget = widgets.IntText(\n",
" value=65000,\n",
" step=100,\n",
" description='Retirement Burn Rate:', disabled=False, layout=layout, style=style\n",
")\n",
"allocation_widget = widgets.Dropdown(\n",
" options=[\n",
" 'Age as bond percentage',\n",
" '80 / 20 stocks vs bonds',\n",
" '60 / 40 stocks vs bonds'\n",
" ],\n",
" value='Age as bond percentage',\n",
" description='Allocation Strategy:',\n",
" disabled=False,\n",
" layout=layout,\n",
" style=style\n",
")\n",
"\n",
"widgets.interact_manual(\n",
" run_simulation,\n",
" birth_year=birth_widget, retirement_age=retirement_widget, starting_value=starting_widget,\n",
" contribution_rate=contribution_widget, burn_rate=burn_widget, allocation=allocation_widget,\n",
")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.8.7"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
1928 0.4381
1929 -0.0830
1930 -0.2512
1931 -0.4384
1932 -0.0864
1933 0.4998
1934 -0.0119
1935 0.4674
1936 0.3194
1937 -0.3534
1938 0.2928
1939 -0.0110
1940 -0.1067
1941 -0.1277
1942 0.1917
1943 0.2506
1944 0.1903
1945 0.3582
1946 -0.0843
1947 0.0520
1948 0.0570
1949 0.1830
1950 0.3081
1951 0.2368
1952 0.1815
1953 -0.0121
1954 0.5256
1955 0.3260
1956 0.0744
1957 -0.1046
1958 0.4372
1959 0.1206
1960 0.0034
1961 0.2664
1962 -0.0881
1963 0.2261
1964 0.1642
1965 0.1240
1966 -0.0997
1967 0.2380
1968 0.1081
1969 -0.0824
1970 0.0356
1971 0.1422
1972 0.1876
1973 -0.1431
1974 -0.2590
1975 0.3700
1976 0.2383
1977 -0.0698
1978 0.0651
1979 0.1852
1980 0.3174
1981 -0.0470
1982 0.2042
1983 0.2234
1984 0.0615
1985 0.3124
1986 0.1849
1987 0.0581
1988 0.1654
1989 0.3148
1990 -0.0306
1991 0.3023
1992 0.0749
1993 0.0997
1994 0.0133
1995 0.3720
1996 0.2268
1997 0.3310
1998 0.2834
1999 0.2089
2000 -0.0903
2001 -0.1185
2002 -0.2197
2003 0.2836
2004 0.1074
2005 0.0483
2006 0.1561
2007 0.0548
2008 -0.3655
2009 0.2594
2010 0.1482
2011 0.0210
2012 0.1589
2013 0.3215
2014 0.1352
2015 0.0136
2016 0.1174
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment