🔷 4.1 Introduction
This chapter presents the Monte Carlo simulation results of the hybrid pension participation model. The objective is to evaluate long-term retirement accumulation and pension benefit outcomes under stochastic contribution behaviour, actuarial mortality assumptions, and compound investment growth.
The simulation models individuals entering the pension system at age 18, contributing irregularly throughout their working life, and retiring at age 64. Mortality assumptions are derived from the Standard Ultimate Life Table, and an annual effective investment return of 8% is assumed.
4.2 Model Parameters
# Demographic settings
age_start <- 18
age_retire <- 64
age_max <- 100
# Financial assumption
i_annual <- 0.08
i_m <- (1 + i_annual)^(1/12) - 1
v_m <- 1 / (1 + i_m)
# Simulation size
N <- 10000
i_m
## [1] 0.00643403
4.3 Mortality Assumptions
Annual mortality probabilities are obtained from the Standard Ultimate Life Table. Since the table begins at age 20, ages 18 and 19 are approximated using the mortality rate at age 20.
Annual mortality rates are converted to monthly probabilities using:
\[ q_{x,m} = 1 - (1 - q_x)^{1/12} \]
mort <- data.frame(
age = 20:100,
qx = c(
0.000250,0.000253,0.000257,0.000262,0.000267,0.000273,
0.000280,0.000287,0.000296,0.000305,0.000315,0.000327,
0.000341,0.000356,0.000372,0.000391,0.000412,0.000436,
0.000463,0.000493,0.000527,0.000565,0.000608,0.000656,
0.000710,0.000771,0.000839,0.000916,0.001003,0.001100,
0.001209,0.001331,0.001469,0.001623,0.001797,0.001993,
0.002212,0.002459,0.002736,0.003048,0.003398,0.003792,
0.004234,0.004730,0.005288,0.005915,0.006619,0.007409,
0.008297,0.009294,0.010413,0.011670,0.013081,0.014664,
0.016440,0.018433,0.020668,0.023175,0.025984,0.029132,
0.032658,0.036607,0.041025,0.045968,0.051493,0.057665,
0.064554,0.072237,0.080798,0.090326,0.100917,0.112675,
0.125708,0.140128,0.156052,0.173599,0.192887,0.214030,
0.237134,0.262294,0.289584
)
)
qx_lookup <- function(age){
a <- floor(age)
if (a %in% c(18,19)) a <- 20
if (a > 100) a <- 100
mort$qx[mort$age == a]
}
q_monthly <- function(age){
qA <- qx_lookup(age)
1 - (1 - qA)^(1/12)
}
# Check pattern
q_monthly(20)
## [1] 2.083572e-05
q_monthly(60)
## [1] 0.0002836086
q_monthly(100)
## [1] 0.02808998
4.4 Contribution Behaviour
Contribution activity is stochastic. For each age group, the probability of contributing in a given month is defined as follows:
pi_by_age <- function(age){
if (age >= 18 && age <= 25) return(0.65)
if (age >= 26 && age <= 35) return(0.75)
if (age >= 36 && age <= 45) return(0.80)
if (age >= 46 && age <= 55) return(0.78)
if (age >= 56 && age <= 63) return(0.70)
return(0)
}
# Visualise contribution probability by age
ages <- 18:63
probs <- sapply(ages, pi_by_age)
plot(ages, probs, type="l",
main="Monthly Contribution Probability by Age",
xlab="Age", ylab="Probability")
4.5 Contribution Amount Distribution
Contribution amounts are assumed to follow a Lognormal distribution.
mean_contrib <- 3000
sd_contrib <- 2000
var_y <- sd_contrib^2
sigma2 <- log(1 + var_y/(mean_contrib^2))
mu <- log(mean_contrib) - 0.5*sigma2
sigma <- sqrt(sigma2)
# Plot distribution
sim_amounts <- rlnorm(10000, mu, sigma)
hist(sim_amounts, breaks=50,
main="Simulated Monthly Contribution Distribution",
xlab="Contribution (KSh)")
4.6 Simulation Procedure
simulate_one <- function(id){
balance <- 0
age <- age_start
month_index <- 0
total_contrib <- 0
months_contributed <- 0
retired_balance <- NA
reached_retirement <- FALSE
while(age < age_max){
# Mortality
if(runif(1) < q_monthly(age)){
break
}
# Retirement
if(age >= age_retire){
retired_balance <- balance
reached_retirement <- TRUE
break
}
# Contribution decision
contrib <- 0
if(runif(1) < pi_by_age(age)){
contrib <- rlnorm(1, mu, sigma)
total_contrib <- total_contrib + contrib
months_contributed <- months_contributed + 1
}
balance <- (balance + contrib) * (1 + i_m)
month_index <- month_index + 1
age <- age_start + month_index/12
}
data.frame(
id = id,
total_contributions = total_contrib,
months_contributed = months_contributed,
retirement_balance = retired_balance,
reached_retirement = reached_retirement
)
}
results_df <- do.call(rbind, lapply(1:N, simulate_one))
retirees <- subset(results_df, reached_retirement == TRUE)
length(retirees$retirement_balance)
## [1] 9478
annuity_due <- function(){
surv <- 1
a <- 0
age <- age_retire
for(k in 0:((age_max - age_retire)*12)){
a <- a + (v_m^k) * surv
surv <- surv * (1 - q_monthly(age))
age <- age + 1/12
if(surv < 1e-12) break
}
a
}
a64 <- annuity_due()
retirees$annual_pension <- 12 * (retirees$retirement_balance / a64)
set.seed(100)
sample_table <- retirees[
sample(1:nrow(retirees), 10),
c("id",
"months_contributed",
"total_contributions",
"retirement_balance",
"annual_pension")
]
knitr::kable(sample_table, digits = 2,
caption = "Table 4.X: Sample of Simulated Individual Outcomes")
| id | months_contributed | total_contributions | retirement_balance | annual_pension | |
|---|---|---|---|---|---|
| 4003 | 4003 | 373 | 1135019 | 10113344 | 980828.1 |
| 530 | 530 | 383 | 1208650 | 11622211 | 1127163.3 |
| 3630 | 3630 | 363 | 1041417 | 9373629 | 909087.9 |
| 3909 | 3909 | 357 | 1077269 | 8648986 | 838809.4 |
| 4322 | 4322 | 356 | 984510 | 9634130 | 934352.1 |
| 8312 | 8312 | 349 | 1027075 | 9642916 | 935204.3 |
| 3233 | 3233 | 365 | 1070283 | 10216210 | 990804.3 |
| 8632 | 8632 | 352 | 1024324 | 9778932 | 948395.6 |
| 9131 | 9131 | 363 | 1058448 | 10290297 | 997989.6 |
| 2095 | 2095 | 369 | 1114897 | 10147170 | 984108.6 |
`
# --- Summary Tables for Chapter 4 ---
# Vector of retirement balances (only those who reached retirement)
ret_bal <- retirees$retirement_balance
# Table 4.1: Survival to retirement
table_survival <- data.frame(
Total_Simulated = N,
Reached_Retirement = length(ret_bal),
Died_Before_Retirement = N - length(ret_bal),
Survival_Rate = length(ret_bal) / N
)
# helper for stats
stats_table <- function(x){
data.frame(
Statistic = c("N", "Mean", "Median", "Std Dev", "Min", "P10", "P25", "P75", "P90", "Max"),
Value = c(
length(x),
mean(x),
median(x),
sd(x),
min(x),
as.numeric(quantile(x, 0.10)),
as.numeric(quantile(x, 0.25)),
as.numeric(quantile(x, 0.75)),
as.numeric(quantile(x, 0.90)),
max(x)
)
)
}
# Table 4.2: Retirement balances
table_retbal <- stats_table(ret_bal)
# Table 4.3: Annual pension
table_pension <- stats_table(retirees$annual_pension)
knitr::kable(table_survival, digits = 4, caption = "Table 4.1: Survival to Retirement")
| Total_Simulated | Reached_Retirement | Died_Before_Retirement | Survival_Rate |
|---|---|---|---|
| 10000 | 9478 | 522 | 0.9478 |
knitr::kable(table_retbal, digits = 2, caption = "Table 4.2: Summary Statistics of Retirement Balances (KSh)")
| Statistic | Value |
|---|---|
| N | 9478.0 |
| Mean | 10335467.2 |
| Median | 10317399.3 |
| Std Dev | 639148.7 |
| Min | 8106685.4 |
| P10 | 9524447.1 |
| P25 | 9890345.2 |
| P75 | 10764616.1 |
| P90 | 11160169.1 |
| Max | 13137636.9 |
knitr::kable(table_pension, digits = 2, caption = "Table 4.3: Summary Statistics of Annual Pension (KSh)")
| Statistic | Value |
|---|---|
| N | 9478.00 |
| Mean | 1002370.34 |
| Median | 1000618.05 |
| Std Dev | 61986.91 |
| Min | 786215.16 |
| P10 | 923714.73 |
| P25 | 959200.83 |
| P75 | 1043990.72 |
| P90 | 1082352.86 |
| Max | 1274134.71 |
hist(ret_bal, breaks = 50,
main = "Distribution of Retirement Balances",
xlab = "Retirement Balance (KSh)")
hist(retirees$annual_pension, breaks = 50,
main = "Distribution of Annual Pension",
xlab = "Annual Pension (KSh/year)")
4.10 Interpretation of Results
The simulation results demonstrate substantial dispersion in retirement outcomes due to stochastic contribution behaviour and investment compounding.
Key observations include:
These findings confirm that irregular participation significantly affects long-term pension adequacy.