Last active
May 21, 2021 17:41
-
-
Save jbradberry/187217afc150a85788ab47ba8821a7b9 to your computer and use it in GitHub Desktop.
Retirement simulation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"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 | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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