Getting started
Let's run a few simulations to get a feel of the package functionality.
First, we have to choose a Model
to simulate. There are a few to choose from, essentially divided in two categories: TermLifeModel
s, representing term life insurance, and UniversalLifeModel
s, representing universal life insurance. For this example, we will take a simple term life model based on lifelib, reimplemented in Julia. There are two implementations we can choose from: a memoized implementation, and an iterative implementation. The memoized implementation replicates 1:1 the design of the corresponding Python library, and has a less polished interface. For this reason, we will take the iterative implementation, LifelibBasiclife
, which will allow us to showcase most of the data structures relevant in context of this package.
using LifeSimulator, Dates
model = LifelibBasiclife()
LifelibBasiclife{TabularMortality, TimeVaryingLapse{LifeSimulator.var"#7#12"}}(TabularMortality([0.0002310671048780701 0.0002541738153658771 … 0.0003383053482519825 0.0003721358830771808; 0.0002353200126583699 0.000258852013924207 … 0.0003445320305331195 0.0003789852335864315; … ; 1.0 1.0 … 1.0 1.0; 1.0 1.0 … 1.0 1.0]), TimeVaryingLapse{LifeSimulator.var"#7#12"}(LifeSimulator.var"#7#12"()), 0.5, 300.0, 60.0, 0.6, 0.01, [0.0, 0.00555, 0.006840000000000001, 0.00788, 0.00866, 0.00937, 0.00997, 0.0105, 0.01098, 0.01144 … 0.02984, 0.02992, 0.03001, 0.03009, 0.03017, 0.03025, 0.03033000000000001, 0.03041000000000001, 0.03049, 0.03056000000000001])
Second, we define a bunch of policies that we want to simulate forward in time. Such policies represent life insurance products. Ideally, we would simulate individual products, that is, separate contracts for different customers. However, for efficiency and scalability reasons, such insurance products are implemented as sets of products. That is, a contract is weighted by a number of customers holding this type of contract. those are called PolicySet
s. We can generate some randomly using Base.rand
:
rand(PolicySet, 500)
500-element Vector{PolicySet}:
PolicySet(Policy(FEMALE, Dates.Year(64), true, 329659.0, 281105.0, Dates.Month(0), Dates.Year(8), LifeSimulator.Product(LifeSimulator.PREMIUM_LEVEL, 3, 0.05), 0.0), 127.6)
PolicySet(Policy(FEMALE, Dates.Year(52), true, 516840.0, 483254.0, Dates.Month(0), Dates.Year(5), LifeSimulator.Product(LifeSimulator.PREMIUM_LEVEL, nothing, 0.1), 0.0), 768.6)
PolicySet(Policy(FEMALE, Dates.Year(26), false, 318423.0, 282440.0, Dates.Month(0), Dates.Year(5), LifeSimulator.Product(LifeSimulator.PREMIUM_SINGLE, 1, 0.0), 0.0), 530.0)
PolicySet(Policy(FEMALE, Dates.Year(23), false, 349410.0, 326094.0, Dates.Month(0), Dates.Year(6), LifeSimulator.Product(LifeSimulator.PREMIUM_SINGLE, 1, 0.0), 0.0), 303.0)
PolicySet(Policy(FEMALE, Dates.Year(24), false, 307777.0, 270966.0, Dates.Month(0), Dates.Year(17), LifeSimulator.Product(LifeSimulator.PREMIUM_SINGLE, nothing, 0.0), 0.0), 800.9)
PolicySet(Policy(FEMALE, Dates.Year(48), false, 533958.0, 501667.0, Dates.Month(0), Dates.Year(10), LifeSimulator.Product(LifeSimulator.PREMIUM_SINGLE, nothing, 0.0), 0.0), 778.4)
PolicySet(Policy(MALE, Dates.Year(66), true, 453810.0, 379776.0, Dates.Month(0), Dates.Year(19), LifeSimulator.Product(LifeSimulator.PREMIUM_LEVEL, 3, 0.05), 0.0), 506.1)
PolicySet(Policy(MALE, Dates.Year(44), false, 428148.0, 339256.0, Dates.Month(0), Dates.Year(19), LifeSimulator.Product(LifeSimulator.PREMIUM_SINGLE, nothing, 0.0), 0.0), 852.3)
PolicySet(Policy(FEMALE, Dates.Year(38), true, 393871.0, 362671.0, Dates.Month(0), Dates.Year(17), LifeSimulator.Product(LifeSimulator.PREMIUM_LEVEL, 3, 0.05), 0.0), 688.8)
PolicySet(Policy(MALE, Dates.Year(39), true, 497008.0, 423093.0, Dates.Month(0), Dates.Year(16), LifeSimulator.Product(LifeSimulator.PREMIUM_LEVEL, nothing, 0.1), 0.0), 716.8)
⋮
PolicySet(Policy(MALE, Dates.Year(25), true, 589239.0, 520329.0, Dates.Month(0), Dates.Year(12), LifeSimulator.Product(LifeSimulator.PREMIUM_LEVEL, 3, 0.05), 0.0), 254.4)
PolicySet(Policy(MALE, Dates.Year(41), false, 398206.0, 386186.0, Dates.Month(0), Dates.Year(7), LifeSimulator.Product(LifeSimulator.PREMIUM_SINGLE, 1, 0.0), 0.0), 348.5)
PolicySet(Policy(MALE, Dates.Year(60), true, 620037.0, 522310.0, Dates.Month(0), Dates.Year(19), LifeSimulator.Product(LifeSimulator.PREMIUM_LEVEL, 3, 0.05), 0.0), 765.4)
PolicySet(Policy(FEMALE, Dates.Year(58), true, 639747.0, 545324.0, Dates.Month(0), Dates.Year(7), LifeSimulator.Product(LifeSimulator.PREMIUM_LEVEL, nothing, 0.1), 0.0), 97.8)
PolicySet(Policy(MALE, Dates.Year(44), false, 682005.0, 614163.0, Dates.Month(0), Dates.Year(8), LifeSimulator.Product(LifeSimulator.PREMIUM_SINGLE, 1, 0.0), 0.0), 867.1)
PolicySet(Policy(FEMALE, Dates.Year(39), false, 577004.0, 524182.0, Dates.Month(0), Dates.Year(11), LifeSimulator.Product(LifeSimulator.PREMIUM_SINGLE, nothing, 0.0), 0.0), 53.7)
PolicySet(Policy(FEMALE, Dates.Year(20), true, 598547.0, 583689.0, Dates.Month(0), Dates.Year(19), LifeSimulator.Product(LifeSimulator.PREMIUM_LEVEL, 3, 0.05), 0.0), 735.2)
PolicySet(Policy(FEMALE, Dates.Year(21), true, 494277.0, 431371.0, Dates.Month(0), Dates.Year(17), LifeSimulator.Product(LifeSimulator.PREMIUM_LEVEL, 3, 0.05), 0.0), 855.0)
PolicySet(Policy(MALE, Dates.Year(45), true, 477603.0, 423260.0, Dates.Month(0), Dates.Year(11), LifeSimulator.Product(LifeSimulator.PREMIUM_LEVEL, nothing, 0.1), 0.0), 790.1)
But for this example, we'll use a small set of fixed policies to guarantee the consistency of results across runs (which will allow us to reliably interpret what we obtain). We'll stick to the default values for the most part.
policies = [
PolicySet(Policy(term = Year(20), age = Year(20), premium = 200_000), 100),
PolicySet(Policy(term = Year(20), age = Year(45), premium = 600_000), 80),
PolicySet(Policy(term = Year(10), age = Year(70), premium = 400_000), 50),
]
3-element Vector{PolicySet}:
PolicySet(Policy(MALE, Dates.Year(20), false, 500000.0, 200000.0, Dates.Month(0), Dates.Year(20), LifeSimulator.Product(LifeSimulator.PREMIUM_SINGLE, nothing, 0.0), 0.0), 100.0)
PolicySet(Policy(MALE, Dates.Year(45), false, 500000.0, 600000.0, Dates.Month(0), Dates.Year(20), LifeSimulator.Product(LifeSimulator.PREMIUM_SINGLE, nothing, 0.0), 0.0), 80.0)
PolicySet(Policy(MALE, Dates.Year(70), false, 500000.0, 400000.0, Dates.Month(0), Dates.Year(10), LifeSimulator.Product(LifeSimulator.PREMIUM_SINGLE, nothing, 0.0), 0.0), 50.0)
Now that we have a model and policies to evolve over time, we can carry out a simulation using simulate
over a specified time range.
First, as we're just experimenting, we can simulate a single step (which is equivalent to a single month) and print what happened during that time. The data structure that will be provided to our custom callback function will be a SimulationEvents
, and we can just print it out for now.
n = 1 # number of timesteps
simulate(model, policies, n) do events
display(events)
end;
SimulationEvents:
● Time range: months 0 to 1
● Total deaths: 0.020593317125309207 over 3 policy sets
● Total lapses: 2.0103905008136698 over 3 policy sets
● Number of policies starting at the beginning of month 0: 230.0
● Policy expirations: ~0.0 (0 policy sets)
● Claims: ~10300
● Expenses: ~69000
This SimulationEvents
data structure has information about deaths, lapses, new and expired policies, claims and expenses. This is all useful to compute cash flows and miscellaneous costs involved for the insurance company providing the insurance products.
In fact, the computation of cash flows is usually the main point of interest of such simulations, which warranted its implementation in this package: enters CashFlow
.
Now, instead of printing the raw SimulationEvents
, we can print the associated CashFlow
quite simply:
simulate(model, policies, n) do events
cf = CashFlow(events, model)
display(cf)
end;
-79300 ⇆ ↑ 0.0, ↓ -79300 (13% claims, 87% expenses)
We got a negative cashflow, from the perspective of the insurance company. Why is that? In our model, establishing new contracts (policies) has a fixed cost, which is part of the expenses reported by the SimulationEvents
during printing earlier.
But note that we only computed cash flows due to the various events that occurred during the simulation. We have not computed cash flows related to active policies, and therefore, our value for the net cash flow is incomplete! Let's fix that. We will need to manually build a Simulation
object so we can reference during the computation of cash flows, and use simulate!
to mutate this simulation in-place. Note that the simulate
function essentially does the same thing, it's just that it won't give you access to the simulation object itself.
simulation = Simulation(model, policies)
simulate!(simulation, n) do events
cf = CashFlow(simulation) # premiums, policy upkeep costs, commissions
cf += CashFlow(events, model) # claims, costs for new policies
display(cf)
end;
+3.481e+07 ⇆ ↑ +8.722e+07 (100% premiums), ↓ -5.241e+07 (<1% claims, >99% commissions, <1% expenses)
Note how the net cashflow is now positive: the premiums balance out the costs incurred by policy acquisitions to the insurance company, as well as claims made during that period. It even turns out that commissions on said premiums now actually make most of the cash flow; indeed, by default there is a 60% commission rate for the first year.
Instead of building a Simulation
, and then computing cash flows manually, convenience functions are provided when the sole interest of the simulation is to compute cash flows. Let's for example simulate 5 months now and see what we get:
n = 5
CashFlow(model, policies, n)
+1.728e+08 ⇆ ↑ +4.323e+08 (100% premiums), ↓ -2.595e+08 (<1% claims, >99% commissions, <1% expenses)
CashFlow(model, policies, n) do cashflow
display(cashflow)
end;
+3.512e+07 ⇆ ↑ +8.8e+07 (100% premiums), ↓ -5.288e+07 (<1% claims, >99% commissions, <1% expenses)
+3.488e+07 ⇆ ↑ +8.722e+07 (100% premiums), ↓ -5.234e+07 (<1% claims, >99% commissions)
+3.457e+07 ⇆ ↑ +8.645e+07 (100% premiums), ↓ -5.188e+07 (<1% claims, >99% commissions)
+3.426e+07 ⇆ ↑ +8.569e+07 (100% premiums), ↓ -5.142e+07 (<1% claims, >99% commissions)
+3.396e+07 ⇆ ↑ +8.493e+07 (100% premiums), ↓ -5.097e+07 (<1% claims, >99% commissions)
For term life insurance products, it will be normal for the company to have a decreasing revenue over time, as the premium remains fixed while mortality increases. An exception is typically made for the first year, during which commissions to agents are generally paid a large percentage of the premium.
This page was generated using Literate.jl.