Interactive Plots & Investment Predictions

Projections are usually wrong, lets make some interactive ones together
jupyter
viz
finance
Author

Nicholas Lillywhite

Published

January 11, 2022

I’m a big fan of the value (Graham, Dodd, Bogle) and FIRE investment philosophies. I’ve been a happy customer of Pearler and their ‘get rich slow’ platform. An important set of projections to know is your savings rate and your projected retirement age, there are a few calculators online which all have aspects that I like but often good at one thing not the other. For example, 1. Networthify’s Calculator has a nice visualisation and some comparison data as well as some control over some assumptions but you don’t see your progression over the years. 2. Playing with Fire has some other great assumptions and a graph over time but not controls over the compounding periods and is missing other assumptions 3. Aussie Firebug has the most custom / transparent viz but its all excel/gdrive and the viz is limited. 4. Engaging Data has the best in my opinion but is maybe a little busy? Maybe I could replicate this but improve the data-ink ratio (I pray to Tufte, lord of viz) 5. Wallet Burst has a great UI but I can’t see the dark tooltip on hover… 6. Data Driven Money has some lovely formulas that I need and I like that they’ve used seaborn and made their plots pretty.

I’d like to make my own as all the calculators feel ‘close’ but not quite what I want, it might be fun to add a ‘financial crash’ option to see what would happen if in year ‘t’ the bottom fell out of the market, how would it set you back etc. I also thought this might be an interesting exercise to share transparently as well if others are interested in how they might build their own.

I also really like the MoneySmart calculator interface, particularly the choice of deposit frequency

Interactive Charting Quickie

To start, lets get a dummy interactive chart going so that we get the mechanics of having some interactive widgets that call some function which create our plots.

Lets start with ipywidgets & matplotlib which is easy to begin with, stackoverflow strikes again:

import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
import ipywidgets as widgets


def plot_func(freq):
    x = np.linspace(0, 2*np.pi)
    y = np.sin(x * freq)
    plt.plot(x, y)


widgets.interact(plot_func, freq=widgets.FloatSlider(value=7.5,
                                                     min=1,
                                                     max=15,
                                                     step=0.1))

# Apologies if this doesn't render online on my blog, altair is much happier with fastpages.
<function __main__.plot_func(freq)>

This gets us started but its not quite the feel I’m looking for, its janky on the re-render. Once again Altair is beautiful and straightforward. Lets use their transform calculate method & selection.

# datum is a way to access the data object itself, this I think is part of the vega expressions syntax
import altair as alt
from altair import datum

# Lets just grab a range as dummy values
df = pd.DataFrame({
    'x': np.arange(100)
})

# This is our range creation
range_binder = alt.binding_range(min=0, max=5, step=0.01, name='X Value')

# This is our selector which we then use to calculate y values & we use our range_binder to set the values of the selctor.
selector = alt.selection_single(name="selector",   # name our object
                                fields=['x_mod'],  # name our field
                                bind=range_binder,  # add in our range_binder
                                init={'x_mod': 3.1})  # set an initial value

alt.Chart(df).mark_line().encode(
    x='x:Q',
    y='y:Q'
).transform_calculate(
    y="sin(datum.x * selector.x_mod)").properties(title='Interactive Chart!').add_selection(selector)

Compound Interest Calculations

Ok now we’ve got to setup the plots to produce the compound interest, we can then combine the interactivity of the plots above with the compound calculations we do below

import random

# Compound Interest Formula
# A = P(1 + r/n)**(nt)

# Compound Interest with Contributions | Thanks datadrivenmoney, my #6 inspiration
# A = P(1 + r/n)**nt + M((1 +r/n)^nt -1)/(r/n)

# Lets setup a starter dataframe with some assumptions locked in and some basic features

years = 20
principal = 20000
ror = 0.08
ror_low = 0.06
ror_high = 0.1
periods = 12
contributions = 5200

fire_df = pd.DataFrame({
    "year": range(1, years),
    "contributions": contributions*periods})

fire_df["net_cont"] = fire_df.contributions.cumsum()

fire_df["net_worth"] = principal * (1+(ror/periods))**(periods*fire_df.year) + \
    contributions*(((1+ror/periods)**(periods*fire_df.year))-1)/(ror/periods)

fire_df["low_net_worth"] = principal * (1+(ror_low/periods))**(periods*fire_df.year) + \
    contributions * \
    (((1+ror_low/periods)**(periods*fire_df.year))-1)/(ror_low/periods)
fire_df["high_net_worth"] = principal * (1+(ror_high/periods))**(periods*fire_df.year) + \
    contributions * \
    (((1+ror_high/periods)**(periods*fire_df.year))-1)/(ror_high/periods)

fire_df["investment_return"] = fire_df.net_worth - fire_df.net_cont - principal

x = alt.X("year:Q")
tooltip = ["net_cont", "net_worth", "low_net_worth",
           "high_net_worth", "year", "investment_return"]


fire_df["noise"] = fire_df.net_worth * \
    [(ror+(random.randint(-10, 10)))/100 + 1 for year in range(1, years)]

# Add this plot in if you'd like to see some noise in the full return which might appear more realistic
# a = alt.Chart(fire_df,).mark_area(opacity=0.3,
#                                 color="blue").encode(
#     x=x,
#     y=alt.Y("noise:Q",title="Noisy Return"),
#     tooltip=tooltip)

b = alt.Chart(fire_df, title="Net Contributions + 8% APR Returns with +2%/-2% Alternate Scenarios").mark_area(opacity=0.3,
                                                                                                              color="darkgreen").encode(
    x=x,
    y=alt.Y("net_cont:Q", title="Contributions"),
    tooltip=tooltip)

c = alt.Chart(fire_df).mark_area(opacity=0.3,
                                 color="blue").encode(
    x=x,
    y=alt.Y("net_worth:Q", title="Net Worth"),
    tooltip=tooltip)

d = alt.Chart(fire_df).mark_area(opacity=0.3,
                                 color="darkblue").encode(
    x=x,
    y=alt.Y("low_net_worth:Q", title="Low Return"),
    tooltip=tooltip)

e = alt.Chart(fire_df).mark_area(opacity=0.3,
                                 color="red").encode(
    x=x,
    y=alt.Y("high_net_worth:Q", title="High Return"),
    tooltip=tooltip)

f = b+c+d+e  # +a
f

Ok this is exciting, we’ve got various returns and our net contributions all outlined and shown on our viz, lets add in the interactivity we had on the first vizualisation and we should have a nice FIRE graph to play around with

years = 40
df = pd.DataFrame({
    'x': np.arange(years)
})

df = df[1:]

# This is our range creation
range_binder = alt.binding_range(
    min=0, max=0.15, step=0.01, name='Rate of Return')

# This is our selector which we then use to calculate y values & we use our range_binder to set the values of the selctor.
selector = alt.selection_single(name="selector",   # name our object
                                fields=['x_mod'],  # name our field
                                bind=range_binder,  # add in our range_binder
                                init={'x_mod': 0.06})  # set an initial value

# RoR 2 selector
range_binder2 = alt.binding_range(
    min=0, max=0.15, step=0.01, name='Rate of Return 2')
selector2 = alt.selection_single(name="selector2",
                                 fields=['x_mod'],
                                 bind=range_binder2,
                                 init={'x_mod': 0.08})

# Principal selector
p_range_binder = alt.binding_range(
    min=0, max=500000, step=10000, name='Principal')
p_selector = alt.selection_single(name="p_selector",
                                  fields=['principal'],
                                  bind=p_range_binder,
                                  init={'principal': 10000})

# Monthly Contribution Selector
contrib_binder = alt.binding_range(
    min=0, max=8000, step=100, name='Monthly Contributions')
contrib_selector = alt.selection_single(name="contrib_selector",
                                        fields=['contrib'],
                                        bind=contrib_binder,
                                        init={'contrib': 1000})


# We've gotta do some string mangling to stuff our variables into the vega expression required for the calculate transform but its not too deadly

x = alt.X("x:Q", title="Years")
y = alt.Y("y:Q", title="$ Investment Value")

# Lets just do 20 years
a = alt.Chart(df).mark_area(opacity=0.3,
                            color="darkblue").encode(
    x=x,
    y=y, tooltip=[x, y]
).transform_calculate(
    y="p_selector.principal*pow((1+(selector.x_mod/12)), 12*datum.x) + contrib_selector.contrib*(pow((1+selector.x_mod/12),12*datum.x)-1)/(selector.x_mod/12)").properties(title='Interactive Rates of Return').add_selection(
    selector).add_selection(p_selector).add_selection(contrib_selector)

b = alt.Chart(df).mark_area(opacity=0.3,
                            color="blue").encode(
    x=x,
    y=y, tooltip=[x, y]
).transform_calculate(
    y="p_selector.principal*pow((1+(selector2.x_mod/12)), 12*datum.x) + contrib_selector.contrib*(pow((1+selector2.x_mod/12),12*datum.x)-1)/(selector2.x_mod/12)").add_selection(selector2)

c = alt.Chart(df).mark_area(opacity=0.3, color="darkblue").encode(
    x=x,
    y=y, tooltip=[x, y]).transform_calculate(y="contrib_selector.contrib*12*datum.x")

d = b+a+c
d

Play Around

Ok so we’ve now got a nice plot of our investments over time that we can adjust multiple rates of return, principal starting value and the contributions.

Playing around with the values, you can see that monthly contributions and return rate are by far the biggest factors. People overemphasize getting a big initial investment when wanting to invest their money but actually their rate of return and ability to continue to contribute is what matters most. Going from no monthly contributions to just \$100 changes your initial $10000 value at 40 years to over \$500,000 from \$220,000 despite only contributing an extra \$44,000.

Obviously this isn’t investment advice, we’re playing around with plots on a computer screen but the power of compounding is in your fingertips to play with and I think its fantastic. The more I use Altair, the more I like it. I’m sure I could refactor my code to improve the restating of the baseplot etc but I’ll do that at a later date, this should suffice in showing you how to integrate interactivity and inputs into your plots live.