Plotting Support

Currently, RetirementPlanners.jl provides two specialized plotting functions: plot_gradient and plot_sensitivity. Each plotting function is illustrated below using the code detailed in the basic example and the advanced example. For ease of presentation, only key elements of the code are visible by default. The code to setup the simulation can be revealed by clicking on the arrow button.

Gradient Plot

The function plot_gradient is used to plot the distribution of a quantity across time. Some examples include, net worth and total income. This functionality is useful in cases involving thousands of simulations, which would lead to overplotting. plot_gradient overcomes this challenge by representing variability as a density gradient, where darker regions correspond to more likely trajectories. plot_gradient is conditionally loaded into your active session when RetirementPlanners and Plots are loaded. The example below shows a gradient density plot for 1000 simulations. The full set of code can be seen by expanding the hidden code under Show Code.

Show Code
using Distributions
using Plots
using RetirementPlanners

# montly contribution
contribution = (50_000 / 12) * 0.15
# configuration options
config = (
    # time step in years
    Δt = 1 / 12,
    # start age of simulation
    start_age = 27,
    # duration of simulation in years
    duration = 58,
    # initial investment amount
    start_amount = 10_000,
    # withdraw parameters
    kw_withdraw = (withdraws = Transaction(;
        start_age = 60,
        amount = AdaptiveWithdraw(;
            min_withdraw = 2200,
            percent_of_real_growth = 0.15,
            income_adjustment = 0.0,
            volitility = 0.05
        )
    ),),
    # invest parameters
    kw_invest = (investments = Transaction(;
        start_age = 27,
        end_age = 60,
        amount = Normal(contribution, 100)
    ),),
    # interest parameters
    kw_market = (
        # dynamic model of the stock market
        gbm = VarGBM(;
            # non-recession parameters
            αμ = 0.070,
            ημ = 0.010,
            ασ = 0.035,
            ησ = 0.010,
            # recession parameters
            αμᵣ = -0.05,
            ημᵣ = 0.010,
            ασᵣ = 0.035,
            ησᵣ = 0.010
        ),
        recessions = Transaction(; start_age = 0, end_age = 0)
    ),
    # inflation parameters
    kw_inflation = (gbm = VarGBM(; αμ = 0.035, ημ = 0.005, ασ = 0.005, ησ = 0.0025),),
    # income parameters
    kw_income = (income_sources = Transaction(; start_age = 67, amount = 2000),)
)
# setup retirement model
model = Model(; config...)
times = get_times(model)
n_reps = 1000
n_steps = length(times)
logger = Logger(; n_steps, n_reps)
simulate!(model, logger, n_reps)

# plot of survival probability as a function of time
survival_probs = mean(logger.net_worth .> 0, dims = 2)
696×1 Matrix{Float64}:
 1.0
 1.0
 1.0
 1.0
 1.0
 1.0
 1.0
 1.0
 1.0
 1.0
 ⋮
 0.595
 0.594
 0.592
 0.591
 0.589
 0.588
 0.588
 0.586
 0.585
income_plot = plot_gradient(
    times,
    logger.total_income;
    xlabel = "Age",
    ylabel = "Total Income",
    xlims = (config.kw_withdraw.withdraws.start_age, times[end]),
    n_lines = 0,
    color = :blue
)
Example block output

Sensitivity Plot

In many cases, it is informative to perform a sensitivity analysis of your retirement strategy. For example, you might want to know to what extent your net worth varies according to changes in investment amount and number of years investing. The function plot_sensitivity uses a contour plot visualize the effect of two variables on another variable.

In the code block below, invest amount and number of years investing are varied independently across a range of values and the survival probability at the end of the simulation is color coded from low in red to high in green. As you might expect, you are more likely run out of money by withdrawing more and investing less. The benefit of a sensitivity analysis is that it provides details about the magnitude of these changes.

As shown in the configuration below, we specificy a vector of values for the variables we wish to vary. The configuration is passed to a function called grid_search, which runs the simulation for all combinations of the two variables. Click the arrow to reveal the details.

# montly contribution 
contribute(x, r) = (x / 12) * r
salary = 50_000

withdraws = map(
    a -> Transaction(;
        start_age = a,
        amount = AdaptiveWithdraw(;
            min_withdraw = 2200,
            percent_of_real_growth = 0.15,
            income_adjustment = 0.0,
            volitility = 0.05
        )
    ),
    55:65
)

investments = [
    Transaction(; start_age = 27, end_age = a, amount = Normal(contribute(5e4, p), 100))
    for p ∈ 0.10:0.05:0.30 for a ∈ 55:65
]

# configuration options
config = (
    log_times = 70:5:85,
    # time step in years 
    Δt = 1 / 12,
    # start age of simulation 
    start_age = 27,
    # duration of simulation in years
    duration = 58,
    # initial investment amount 
    start_amount = 10_000,
    # withdraw parameters 
    kw_withdraw = (; withdraws),
    # invest parameters
    kw_invest = (; investments),
    # interest parameters
    kw_market = (gbm = VarGBM(; αμ = 0.070, ημ = 0.010, ασ = 0.035, ησ = 0.010),),
    # inflation parameters
    kw_inflation = (gbm = VarGBM(; αμ = 0.035, ημ = 0.005, ασ = 0.005, ησ = 0.0025),),
    # income parameters 
    kw_income = (income_sources = Transaction(; start_age = 67, amount = 2000),)
)
Show Code
using DataFrames
using Distributions
using Random
using RetirementPlanners
using StatsPlots

Random.seed!(6522)
# montly contribution
contribute(x, r) = (x / 12) * r
salary = 50_000

withdraws = map(
    a -> Transaction(;
        start_age = a,
        amount = AdaptiveWithdraw(;
            min_withdraw = 2200,
            percent_of_real_growth = 0.15,
            income_adjustment = 0.0,
            volitility = 0.05
        )
    ),
    55:65
)

investments = [
    Transaction(; start_age = 27, end_age = a, amount = Normal(contribute(5e4, p), 100))
    for p ∈ 0.10:0.05:0.30 for a ∈ 55:65
]

# configuration options
config = (
    log_times = 70:5:85,
    # time step in years
    Δt = 1 / 12,
    # start age of simulation
    start_age = 27,
    # duration of simulation in years
    duration = 58,
    # initial investment amount
    start_amount = 10_000,
    # withdraw parameters
    kw_withdraw = (; withdraws),
    # invest parameters
    kw_invest = (; investments),
    # interest parameters
    kw_market = (gbm = VarGBM(; αμ = 0.070, ημ = 0.010, ασ = 0.035, ησ = 0.010),),
    # inflation parameters
    kw_inflation = (gbm = VarGBM(; αμ = 0.035, ημ = 0.005, ασ = 0.005, ησ = 0.0025),),
    # income parameters
    kw_income = (income_sources = Transaction(; start_age = 67, amount = 2000),)
)

yoked_values =
    [Pair((:kw_withdraw, :withdraws, :start_age), (:kw_invest, :investments, :end_age))]
results = grid_search(Model, Logger, 1000, config; yoked_values);
df = to_dataframe(Model(; config...), results)
df.survived = df.net_worth .> 0
df.retirement_age = map(x -> x.end_age, df.invest_investments)
df.mean_investment = map(x -> x.amount.μ, df.invest_investments)
220000-element Vector{Float64}:
  416.66666666666674
  416.66666666666674
  416.66666666666674
  416.66666666666674
  416.66666666666674
  416.66666666666674
  416.66666666666674
  416.66666666666674
  416.66666666666674
  416.66666666666674
    ⋮
 1250.0
 1250.0
 1250.0
 1250.0
 1250.0
 1250.0
 1250.0
 1250.0
 1250.0
plot_sensitivity(
    df,
    [:retirement_age, :mean_investment],
    :survived,
    xlabel = "Age",
    ylabel = "Invest Amount",
    colorbar_title = "Surival Probability"
)
Example block output

The example below shows how to make a grid of plots at multiple time points by passing a vector or unit range to the age keyword. To ensure each subplot has the same scale, pass lower and upper bounds to the clims keyword.

plot_sensitivity(
    df,
    [:retirement_age, :mean_investment],
    :survived,
    xlabel = "Age",
    ylabel = "Invest Amount",
    colorbar_title = "Surival Probability",
    colorbar = false,
    clims = (0, 1),
    age = 70:5:85
)
Example block output
All Code
###############################################################################################################
#                                           load dependencies
###############################################################################################################
cd(@__DIR__)
using Pkg
Pkg.activate("..")
using Distributions
using DataFrames
using Plots
using RetirementPlanners
using StatsPlots
###############################################################################################################
#                                           setup simulation
###############################################################################################################
# montly contribution 
contribute(x, r) = (x / 12) * r
salary = 50_000

withdraws = map(
    a -> Transaction(;
        start_age = a,
        amount = AdaptiveWithdraw(;
            min_withdraw = 2200,
            percent_of_real_growth = 0.15,
            income_adjustment = 0.0,
            volitility = 0.05
        )
    ),
    55:65
)

investments = [
    Transaction(; start_age = 27, end_age = a, amount = Normal(contribute(5e4, p), 100))
    for p ∈ 0.10:0.05:0.30 for a ∈ 55:65
]

# configuration options
config = (
    # time step in years 
    Δt = 1 / 12,
    # start age of simulation 
    start_age = 27,
    # duration of simulation in years
    duration = 58,
    # initial investment amount 
    start_amount = 10_000,
    # withdraw parameters 
    kw_withdraw = (; withdraws),
    # invest parameters
    kw_invest = (; investments),
    # interest parameters
    kw_market = (
        gbm = VarGBM(;
            αμ = 0.070,
            ημ = 0.010,
            ασ = 0.035,
            ησ = 0.010,
            αμᵣ = -0.05,
            ημᵣ = 0.010,
            ασᵣ = 0.035,
            ησᵣ = 0.010
        ),
        Transaction(; start_age = 0, end_age = 0)
    ),
    # inflation parameters
    kw_inflation = (gbm = VarGBM(; αμ = 0.035, ημ = 0.005, ασ = 0.005, ησ = 0.0025),),
    # income parameters 
    kw_income = (income_sources = Transaction(; start_age = 67, amount = 2000),)
)
###############################################################################################################
#                                           run simulation
###############################################################################################################
yoked_values =
    [Pair((:kw_withdraw, :withdraws, :start_age), (:kw_invest, :investments, :end_age))]
results = grid_search(Model, Logger, 1000, config; yoked_values);
df = to_dataframe(Model(; config...), results)
df.survived = df.net_worth .> 0
df.retirement_age = map(x -> x.end_age, df.invest_investments)
df.mean_investment = map(x -> x.amount.μ, df.invest_investments)
df1 = combine(groupby(df, [:retirement_age, :mean_investment, :time]), :net_worth => mean)
df2 = combine(groupby(df, [:retirement_age, :mean_investment, :time]), :survived => mean)
###############################################################################################################
#                                            plot results 
###############################################################################################################
@df df1 plot(
    :time,
    :net_worth_mean,
    group = (:retirement_age, :mean_investment),
    ylims = (0, 2e6),
    legend = false,
    legendtitle = "withdraw age",
    grid = false,
    xlabel = "Age",
    ylabel = "Mean Net Worth"
)

@df df2 plot(
    :time,
    :survived_mean,
    group = (:retirement_age, :mean_investment),
    ylims = (0, 1),
    grid = false,
    xlabel = "Age",
    layout = (4, 1),
    legend = :bottomleft,
    ylabel = "Survival Probability"
)

plot_sensitivity(
    df,
    [:retirement_age, :mean_investment],
    :survived,
    xlabel = "Age",
    ylabel = "Invest Amount",
    colorbar_title = "Surival Probability"
)

As shown below, you can also create a grid of contour plots in which the rows correspond to a variable of interest and the columns correspond to specified time points.

income_plots = plot_sensitivity(
    df,
    [:retirement_age, :mean_investment],
    :total_income,
    :mean_growth_rate;
    row_label = "growth:",
    xlabel = "Retirement Age",
    ylabel = "Invest Amount",
    colorbar_title = "Mean Total Income",
    clims = (2000, 6000),
    age = 70:5:85,
    size = (1200, 600)
)

The complete code for this example can be found below by expanding the tab labeled All Code.

All Code
###############################################################################################################
#                                           load dependencies
###############################################################################################################
cd(@__DIR__)
using Pkg
Pkg.activate("..")
using Distributions
using DataFrames
using Plots
using RetirementPlanners
using Revise
using StatsPlots
###############################################################################################################
#                                           setup simulation
###############################################################################################################
# montly contribution 
contribute(x, r) = (x / 12) * r
# enter your salary here
salary = 150_000

# withdraws = map(
#     a -> Transaction(;
#         start_age = a,
#         amount = Normal(2200, 200)
#     ),
#     55:65
# )

withdraws = map(
    a -> Transaction(;
        start_age = a,
        amount = AdaptiveWithdraw(;
            min_withdraw = 2200,
            percent_of_real_growth = 0.15,
            income_adjustment = 0.0,
            volitility = 0.05
        )
    ),
    55:65
)

investments = [
    Transaction(; start_age = 49, end_age = a, amount = Normal(contribute(salary, p), 100))
    for p ∈ 0.10:0.05:0.30 for a ∈ 55:65
]

gbm = map(
    αμ -> VarGBM(;
        αμ,
        ημ = 0.010,
        ασ = 0.035,
        ησ = 0.010,
        αμᵣ = -0.05,
        ημᵣ = 0.010,
        ασᵣ = 0.035,
        ησᵣ = 0.010
    ),
    0.05:0.025:0.10
)

# configuration options
config = (
    # time step in years 
    Δt = 1 / 12,
    # start age of simulation 
    start_age = 49,
    # duration of simulation in years
    duration = 40,
    # initial investment amount 
    start_amount = 300_000,
    # withdraw parameters 
    kw_withdraw = (; withdraws),
    # invest parameters
    kw_invest = (; investments),
    # interest parameters
    kw_market = (; gbm,),
    # inflation parameters
    kw_inflation = (gbm = VarGBM(; αμ = 0.035, ημ = 0.005, ασ = 0.005, ησ = 0.0025),),
    # income parameters 
    kw_income = (income_sources = Transaction(; start_age = 67, amount = 2000),)
)
###############################################################################################################
#                                           run simulation
###############################################################################################################
n_reps = 1000
yoked_values =
    [Pair((:kw_withdraw, :withdraws, :start_age), (:kw_invest, :investments, :end_age))]
results = grid_search(Model, Logger, n_reps, config; yoked_values);
df = to_dataframe(Model(; config...), results)
df.survived = df.net_worth .> 0
df.retirement_age = map(x -> x.end_age, df.invest_investments)
df.mean_investment = map(x -> x.amount.μ, df.invest_investments)
df.mean_growth_rate = map(x -> x.αμ, df.market_gbm)
###############################################################################################################
#                                            plot results 
###############################################################################################################
@time survival_plots = plot_sensitivity(
    df,
    [:retirement_age, :mean_investment],
    :survived,
    :mean_growth_rate;
    row_label = "growth:",
    xlabel = "Retirement Age",
    ylabel = "Invest Amount",
    clims = (0, 1),
    colorbar_title = "Survival Probability",
    age = 70:5:85,
    size = (1200, 600)
)

income_plots = plot_sensitivity(
    df,
    [:retirement_age, :mean_investment],
    :total_income,
    :mean_growth_rate;
    row_label = "growth:",
    xlabel = "Retirement Age",
    ylabel = "Invest Amount",
    colorbar_title = "Mean Total Income",
    clims = (2000, 6000),
    age = 70:5:85,
    size = (1200, 600)
)