using ActuaryUtilities
using DataFrames
using Setfield
A simple example of how one could define a nested projection system. Includes the following examples: - Outer loop policy projections only - Outer + Inner loop policy projections with padded cashflows determining reserves and capital - Outer + Inner loop with a stochastic interest rate for the reserves
In this notebook, we define a term life policy, implement the mechanics for “outer” projected values, as well as “inner” projections so that we can determine a projection-based reserve. This is done with both a deterministic and stochastic “inner” loop.
Policy Data & Methods
We will use a simple Term policy as an example for the mechanics. We don’t need to, but to illustrate that we could easily implement different product types, we first define an abstract type
for Policy
s, and then the specific Term
implementation.
The type annotations aren’t strictly necessary, but knowing the types in advance helps Julia specialize the code for it.
abstract type Policy end
struct Term <: Policy
inforce::Float64 # count of inforce
term::Int # length of benefit period (in months)
premium::Float64
face::Float64
end
For consistency across different calculated amounts, we will keep the function signature consistent, even if not all of the arguments are used:
(policy,assumptions,time) -> result
premiums(pol::Term, assumptions, time) = pol.inforce * pol.premium / 12
qx(pol::Term, assumptions, time) = assumptions.q / 12
deaths(pol::Term, assumptions, time) = pol.inforce * qx(pol, assumptions, time)
claims(pol::Term, assumptions, time) = deaths(pol, assumptions, time) * pol.face
claims (generic function with 1 method)
Projection Assumptions
We define some global assumptions that get passed around. It’s good practice (and performant) to pass variables into functions instead of just referring to global variable values.
assumptions = (
q=0.012,
int_reserve=0.02,
capital_factor=0.1, # rate * reserves
)
(q = 0.012, int_reserve = 0.02, capital_factor = 0.1)
Inner-loop assumption
In this example, we’re assuming just a PADed mortality rate for the inner loop. We take the assumption set and use Setfield.@set
to return a new immutable named tuple with just that value modified:
innerloop_assumption(outer_assump) = @set outer_assump.q *= 1.2
innerloop_assumption (generic function with 1 method)
Projection Logic
The architecture takes inspiration from web server architecture where data is passed through multiple processing steps before being returned. The logic is contained within a function called project
, which:
- Project a single timestep and create a tuple of values:
(policy, assumptions, result)
- Apply the function
additional_processing
which takes as an argument(policy, assumptions, result)
. additional_processing
can then define an “inner” loop, which could just be to apply theproject
with a modified set of assumptions. In this way, one or more “inner” loops can be defined.- The final
additional_processing
function should return whatever you want to return as a result.
By default, the additional_processing
will simply return the last argument, result
and therefore will not have any inner loops.
"""
project(policy,assumptions;start_time=1,additional_processing=res)
The kwarg `additional_processing` defines an intermediate processing step where one can take the current model state and perform additional work, including nested projections. If left out of the arguments, the default for `additional_processing` is `res`, where `res` is (pol,assumptions, result)->result (ie will just return the model point's results with no additional work being done).
"""
function project(
pol::Term,
assumptions;
start_time=1,
additional_processing=(pol, assumptions, result) -> result
)
# alias the assumptions to A for brevity
A = assumptions
# iterate over the policy from the start time to the end of the policy's term
map(start_time:pol.term) do t
# calculate components of the projection
timestep = t
premium = premiums(pol, A, t)
q = qx(pol, A, t)
death = deaths(pol, A, t)
claim = claims(pol, A, t)
net_cf = premium - claim
inforce = pol.inforce - death
pol = @set pol.inforce = inforce
# return a vector of name tuples with the results
result = (;
timestep,
premium,
death,
claim,
net_cf,
inforce,
q,
)
# apply additional processing function
additional_processing(pol, A, result)
end
end
project
# Function signature: (policy, assumptions, result) -> updated result
function run_inner(policy, assumptions, result)
additional_results = if result.timestep + 1 <= policy.term
A = innerloop_assumption(assumptions)
p = project(policy, A; start_time=result.timestep + 1)
# calculate the reserves as the present value of the
# cashflows within the inner loop projections
# discounted at the reserve interest rate
reserves = -pv(A.int_reserve, [modelpoint.net_cf for modelpoint in p])
capital = reserves * A.capital_factor
(; reserves, capital)
else
reserves = 0.0
capital = 0.0
(; reserves, capital)
end
return merge(result, additional_results)
end
run_inner (generic function with 1 method)
And a stochastic version:
# Function signature: (policy, assumptions, result) -> updated result
function run_inner_stochastic(policy, assumptions, result)
additional_results = if result.timestep + 1 <= policy.term
A = innerloop_assumption(assumptions)
p = project(policy, A; start_time=result.timestep + 1)
# simple stochastic interest rate
n = 100
reserves = let
i = A.int_reserve
f = pv(i + 0.005 * randn(), [modelpoint.net_cf for modelpoint in p])
-sum(f for _ in 1:n) / n
end
capital = reserves * A.capital_factor
(; reserves, capital)
else
reserves = 0.0
capital = 0.0
(; reserves, capital)
end
return merge(result, additional_results)
end
run_inner_stochastic (generic function with 1 method)
Projections
First, define a sample policy:
p = Term(1.0, 120, 1300.0, 100_000.0)
Term(1.0, 120, 1300.0, 100000.0)
A projection without any additional processing:
project(p, assumptions) |> DataFrame
Row | timestep | premium | death | claim | net_cf | inforce | q |
---|---|---|---|---|---|---|---|
Int64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | |
1 | 1 | 108.333 | 0.001 | 100.0 | 8.33333 | 0.999 | 0.001 |
2 | 2 | 108.225 | 0.000999 | 99.9 | 8.325 | 0.998001 | 0.001 |
3 | 3 | 108.117 | 0.000998001 | 99.8001 | 8.31667 | 0.997003 | 0.001 |
4 | 4 | 108.009 | 0.000997003 | 99.7003 | 8.30836 | 0.996006 | 0.001 |
5 | 5 | 107.901 | 0.000996006 | 99.6006 | 8.30005 | 0.99501 | 0.001 |
6 | 6 | 107.793 | 0.00099501 | 99.501 | 8.29175 | 0.994015 | 0.001 |
7 | 7 | 107.685 | 0.000994015 | 99.4015 | 8.28346 | 0.993021 | 0.001 |
8 | 8 | 107.577 | 0.000993021 | 99.3021 | 8.27517 | 0.992028 | 0.001 |
9 | 9 | 107.47 | 0.000992028 | 99.2028 | 8.2669 | 0.991036 | 0.001 |
10 | 10 | 107.362 | 0.000991036 | 99.1036 | 8.25863 | 0.990045 | 0.001 |
11 | 11 | 107.255 | 0.000990045 | 99.0045 | 8.25037 | 0.989055 | 0.001 |
12 | 12 | 107.148 | 0.000989055 | 98.9055 | 8.24212 | 0.988066 | 0.001 |
13 | 13 | 107.04 | 0.000988066 | 98.8066 | 8.23388 | 0.987078 | 0.001 |
⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ |
109 | 109 | 97.2377 | 0.000897579 | 89.7579 | 7.47983 | 0.896682 | 0.001 |
110 | 110 | 97.1405 | 0.000896682 | 89.6682 | 7.47235 | 0.895785 | 0.001 |
111 | 111 | 97.0434 | 0.000895785 | 89.5785 | 7.46487 | 0.894889 | 0.001 |
112 | 112 | 96.9463 | 0.000894889 | 89.4889 | 7.45741 | 0.893994 | 0.001 |
113 | 113 | 96.8494 | 0.000893994 | 89.3994 | 7.44995 | 0.8931 | 0.001 |
114 | 114 | 96.7525 | 0.0008931 | 89.31 | 7.4425 | 0.892207 | 0.001 |
115 | 115 | 96.6558 | 0.000892207 | 89.2207 | 7.43506 | 0.891315 | 0.001 |
116 | 116 | 96.5591 | 0.000891315 | 89.1315 | 7.42762 | 0.890424 | 0.001 |
117 | 117 | 96.4626 | 0.000890424 | 89.0424 | 7.4202 | 0.889533 | 0.001 |
118 | 118 | 96.3661 | 0.000889533 | 88.9533 | 7.41278 | 0.888644 | 0.001 |
119 | 119 | 96.2697 | 0.000888644 | 88.8644 | 7.40536 | 0.887755 | 0.001 |
120 | 120 | 96.1735 | 0.000887755 | 88.7755 | 7.39796 | 0.886867 | 0.001 |
And an example which uses a PADed inner loop to determine the resserves and capital:
project(p, assumptions; additional_processing=run_inner) |> DataFrame
Row | timestep | premium | death | claim | net_cf | inforce | q | reserves | capital |
---|---|---|---|---|---|---|---|---|---|
Int64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | |
1 | 1 | 108.333 | 0.001 | 100.0 | 8.33333 | 0.999 | 0.001 | 504.61 | 50.461 |
2 | 2 | 108.225 | 0.000999 | 99.9 | 8.325 | 0.998001 | 0.001 | 503.148 | 50.3148 |
3 | 3 | 108.117 | 0.000998001 | 99.8001 | 8.31667 | 0.997003 | 0.001 | 501.668 | 50.1668 |
4 | 4 | 108.009 | 0.000997003 | 99.7003 | 8.30836 | 0.996006 | 0.001 | 500.169 | 50.0169 |
5 | 5 | 107.901 | 0.000996006 | 99.6006 | 8.30005 | 0.99501 | 0.001 | 498.652 | 49.8652 |
6 | 6 | 107.793 | 0.00099501 | 99.501 | 8.29175 | 0.994015 | 0.001 | 497.117 | 49.7117 |
7 | 7 | 107.685 | 0.000994015 | 99.4015 | 8.28346 | 0.993021 | 0.001 | 495.561 | 49.5561 |
8 | 8 | 107.577 | 0.000993021 | 99.3021 | 8.27517 | 0.992028 | 0.001 | 493.986 | 49.3986 |
9 | 9 | 107.47 | 0.000992028 | 99.2028 | 8.2669 | 0.991036 | 0.001 | 492.391 | 49.2391 |
10 | 10 | 107.362 | 0.000991036 | 99.1036 | 8.25863 | 0.990045 | 0.001 | 490.775 | 49.0775 |
11 | 11 | 107.255 | 0.000990045 | 99.0045 | 8.25037 | 0.989055 | 0.001 | 489.138 | 48.9138 |
12 | 12 | 107.148 | 0.000989055 | 98.9055 | 8.24212 | 0.988066 | 0.001 | 487.479 | 48.7479 |
13 | 13 | 107.04 | 0.000988066 | 98.8066 | 8.23388 | 0.987078 | 0.001 | 485.799 | 48.5799 |
⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ |
109 | 109 | 97.2377 | 0.000897579 | 89.7579 | 7.47983 | 0.896682 | 0.001 | 101.795 | 10.1795 |
110 | 110 | 97.1405 | 0.000896682 | 89.6682 | 7.47235 | 0.895785 | 0.001 | 93.3884 | 9.33884 |
111 | 111 | 97.0434 | 0.000895785 | 89.5785 | 7.46487 | 0.894889 | 0.001 | 84.8223 | 8.48223 |
112 | 112 | 96.9463 | 0.000894889 | 89.4889 | 7.45741 | 0.893994 | 0.001 | 76.0936 | 7.60936 |
113 | 113 | 96.8494 | 0.000893994 | 89.3994 | 7.44995 | 0.8931 | 0.001 | 67.199 | 6.7199 |
114 | 114 | 96.7525 | 0.0008931 | 89.31 | 7.4425 | 0.892207 | 0.001 | 58.1351 | 5.81351 |
115 | 115 | 96.6558 | 0.000892207 | 89.2207 | 7.43506 | 0.891315 | 0.001 | 48.8986 | 4.88986 |
116 | 116 | 96.5591 | 0.000891315 | 89.1315 | 7.42762 | 0.890424 | 0.001 | 39.4858 | 3.94858 |
117 | 117 | 96.4626 | 0.000890424 | 89.0424 | 7.4202 | 0.889533 | 0.001 | 29.8932 | 2.98932 |
118 | 118 | 96.3661 | 0.000889533 | 88.9533 | 7.41278 | 0.888644 | 0.001 | 20.1172 | 2.01172 |
119 | 119 | 96.2697 | 0.000888644 | 88.8644 | 7.40536 | 0.887755 | 0.001 | 10.1541 | 1.01541 |
120 | 120 | 96.1735 | 0.000887755 | 88.7755 | 7.39796 | 0.886867 | 0.001 | 0.0 | 0.0 |
And a stochastic example:
project(p, assumptions; additional_processing=run_inner_stochastic) |> DataFrame
Row | timestep | premium | death | claim | net_cf | inforce | q | reserves | capital |
---|---|---|---|---|---|---|---|---|---|
Int64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | |
1 | 1 | 108.333 | 0.001 | 100.0 | 8.33333 | 0.999 | 0.001 | 495.008 | 49.5008 |
2 | 2 | 108.225 | 0.000999 | 99.9 | 8.325 | 0.998001 | 0.001 | 551.777 | 55.1777 |
3 | 3 | 108.117 | 0.000998001 | 99.8001 | 8.31667 | 0.997003 | 0.001 | 456.478 | 45.6478 |
4 | 4 | 108.009 | 0.000997003 | 99.7003 | 8.30836 | 0.996006 | 0.001 | 437.01 | 43.701 |
5 | 5 | 107.901 | 0.000996006 | 99.6006 | 8.30005 | 0.99501 | 0.001 | 681.328 | 68.1328 |
6 | 6 | 107.793 | 0.00099501 | 99.501 | 8.29175 | 0.994015 | 0.001 | 512.651 | 51.2651 |
7 | 7 | 107.685 | 0.000994015 | 99.4015 | 8.28346 | 0.993021 | 0.001 | 538.304 | 53.8304 |
8 | 8 | 107.577 | 0.000993021 | 99.3021 | 8.27517 | 0.992028 | 0.001 | 848.208 | 84.8208 |
9 | 9 | 107.47 | 0.000992028 | 99.2028 | 8.2669 | 0.991036 | 0.001 | 523.956 | 52.3956 |
10 | 10 | 107.362 | 0.000991036 | 99.1036 | 8.25863 | 0.990045 | 0.001 | 404.1 | 40.41 |
11 | 11 | 107.255 | 0.000990045 | 99.0045 | 8.25037 | 0.989055 | 0.001 | 508.197 | 50.8197 |
12 | 12 | 107.148 | 0.000989055 | 98.9055 | 8.24212 | 0.988066 | 0.001 | 468.548 | 46.8548 |
13 | 13 | 107.04 | 0.000988066 | 98.8066 | 8.23388 | 0.987078 | 0.001 | 456.656 | 45.6656 |
⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ |
109 | 109 | 97.2377 | 0.000897579 | 89.7579 | 7.47983 | 0.896682 | 0.001 | 104.005 | 10.4005 |
110 | 110 | 97.1405 | 0.000896682 | 89.6682 | 7.47235 | 0.895785 | 0.001 | 97.0335 | 9.70335 |
111 | 111 | 97.0434 | 0.000895785 | 89.5785 | 7.46487 | 0.894889 | 0.001 | 83.7453 | 8.37453 |
112 | 112 | 96.9463 | 0.000894889 | 89.4889 | 7.45741 | 0.893994 | 0.001 | 75.1751 | 7.51751 |
113 | 113 | 96.8494 | 0.000893994 | 89.3994 | 7.44995 | 0.8931 | 0.001 | 65.4564 | 6.54564 |
114 | 114 | 96.7525 | 0.0008931 | 89.31 | 7.4425 | 0.892207 | 0.001 | 59.3372 | 5.93372 |
115 | 115 | 96.6558 | 0.000892207 | 89.2207 | 7.43506 | 0.891315 | 0.001 | 49.5258 | 4.95258 |
116 | 116 | 96.5591 | 0.000891315 | 89.1315 | 7.42762 | 0.890424 | 0.001 | 39.476 | 3.9476 |
117 | 117 | 96.4626 | 0.000890424 | 89.0424 | 7.4202 | 0.889533 | 0.001 | 29.9494 | 2.99494 |
118 | 118 | 96.3661 | 0.000889533 | 88.9533 | 7.41278 | 0.888644 | 0.001 | 20.2168 | 2.02168 |
119 | 119 | 96.2697 | 0.000888644 | 88.8644 | 7.40536 | 0.887755 | 0.001 | 10.1431 | 1.01431 |
120 | 120 | 96.1735 | 0.000887755 | 88.7755 | 7.39796 | 0.886867 | 0.001 | 0.0 | 0.0 |
Endnotes
Further Work
This example is simple, but could be greatly optimized to reduce intermediate variable allocations, refine the timing of cashflows, add additional decrements, handle different types of Policy
s, abstract some of the projection mechanics into an Iterable
object, etc.
Disclaimer
Created as a proof of concept and not indended to be interpreted as a meaningful projection.