Life & mortality¶
The IDE's actuarial tables as functions — the same code path as the chat's "build a life table at 4 %" — plus experience analysis, graduation, projection models, and a base-R / pure-numpy port of lifelib's BasicTerm_ME with Solvency II and IFRS 17 readings on top.
The illustrative basis
With no basis given, everything below runs on Scelo's illustrative
Gompertz–Makeham (A = 0.00022, B = 2.7e-6, c = 1.124), ages 20–110 —
and says so in its notes, every time. It is not a published standard
table. Swap in your own qx before relying on the figures; the note
disappears when you do.
Any basis in, one table out¶
Every table generator takes a basis first: nothing (illustrative
Makeham), a Makeham/Gompertz parameter set, a data frame with age + qx,
age + lx, or age + deaths + exposure, or a qx vector by age. Percent-
shaped rates (max > 1) are divided by 100 with a note; gaps are
interpolated linearly with a note; crude deaths ÷ exposure rates carry a
"graduate before using" note.
sc.life_table() # age qx px lx dx Lx Tx ex — 91 rows, ages 20–110
sc.life_table(my_qx_df) # your basis, same columns
sc.commutation(i=0.04) # age lx dx v^x Dx Nx Cx Mx Rx Sx
sc.factors(i=0.04, n=10) # äx ax Ax äx:10 A¹x:10 10Ex Ax:10
sc.annuity(65, i=0.04) # ä65 as one number (n=, due= for temporaries)
sc.assurance(40, i=0.04, n=20) # A¹40:20
sc.premium(i=0.04, product="term") # net premium per 1,000 SA, age × term grid
sc.epv([100]*10, 65, i=0.04) # EPV of any cashflow vector against the basis
age äx ax Ax äx:10 A¹x:10 10Ex Ax:10
0 20 23.795024 22.795024 0.084807 8.426126 0.002196 0.673722 0.675918
1 21 23.712744 22.712744 0.087971 8.425946 0.002248 0.673677 0.675925
— Annuity & assurance factors · Gompertz–Makeham (illustrative) · i = 4 % · n = 10
basis: Gompertz–Makeham (illustrative) · i = 4 %
· äx = Nx/Dx (annuity-due), ax = äx − 1, Ax = Mx/Dx …; äx:10 = (Nx − Nx+10)/Dx, …
The identities hold to machine precision and are asserted by the tests:
Ax = 1 − d·äx, Ax:n = A¹x:n + nEx, N₀ = ΣD, M₀ = ΣC. premium is
the pure equivalence-principle risk premium P = 1000·A/ä — no expense
loading, no profit margin, and the note says so. Column names use the
real actuarial glyphs (äx, A¹x:10); in R, back-tick them.
Small conversions ride along: mx_to_qx / qx_to_mx (uniform or
constant-force), survival, life_expectancy (curtate or complete),
close_table, nominal / effective / force / from_force.
Experience¶
sc.ae(experience_df) # actual vs expected by age band, + total row
sc.ae_test(actual=112, expected=98) # Poisson z-test on one cell
sc.exposure(df, "start", "end", event="died") # central exposure, split at birthdays
sc.graduate(crude_qx, h=100) # Whittaker–Henderson, on log qx by default
sc.experience("experience.csv") # the one-liner: A/E + graduation + life table
ae defaults the expected basis to the illustrative Makeham over the
observed ages; by= splits the study. graduate minimises
Σw(g−u)² + h·Σ(Δ²g)² with weights defaulting to exposure — check the
residual signs before adopting, as the notes remind you. exposure
computes policy-year central exposure age last birthday, deaths allocated
to the age at exit.
Projection models¶
Lee–Carter uses the standard constraints (Σbₓ = 1, Σkₜ = 0) and a
random-walk-with-drift forecast; the print header reports the drift and
how much the first SVD component explains. Kaplan–Meier codes an event
from 1 / true / yes / dead / claim / lapsed and puts its 95 % bounds on
the log(−log S) scale.
BasicTerm, SCR and CSM¶
A port of lifelib's BasicTerm_ME semantics that needs no Python-in-R
and no lifelib — monthly projection, then Solvency II and IFRS 17 read
off it. Feed it a model-point file (sc.sample("lifelib-mp") is shaped
right); model_points(df) builds one from a seriatim book.
charge marginal share
mortality 284602.417576 127373.887500 0.185206
lapse 584291.612329 518226.535282 0.753520
expense 9965.340683 5708.561200 0.008300
cat 82820.304745 36432.377775 0.052974
SCR 687741.361757 NaN NaN
diversification -273938.313576 NaN NaN
— Solvency II life SCR · 100 model points · BasicTerm projection
· SCR_life = 687,741 (undiversified 961,680); BEL … Lapse charge is the worst of
up / down / mass: down (584,292).
basictermtakes an assumptions set with fifteen dials (mortality A/B/c, lapse, expenses, discount rate, loading, plus the shock dials); override any subset. Its notes say plainly: illustrative assumptions, not a priced basis.scr_liferecomputes the projection under each Delegated-Regulation shock (+15 % mortality, −20 % longevity, lapse up / down / mass, +10 % expenses + 1 % inflation, 0.15 % cat), floors each ΔBEL at zero, takes the worst lapse, and aggregates with the SII life correlation matrix (SII_LIFE_CORR/SC_SII_LIFE_CORR, exported). Disability and revision are zero for term business.csmsetsCSM₀ = max(−FCF, 0); a positive FCF is reported as the loss component (the bundled sample is deliberately under-priced, so it shows one). Release follows coverage units discounted at the locked-in rate.
The real lifelib¶
When you want lifelib itself rather than the port:
normalise_model_points(df) maps any reasonably named file onto the
lifelib shape first (and is what lifelib_run uses). The library copy
lives under ~/.cache/scelo/lifelib/0.14.0 (or $SCELO_LIFELIB_HOME);
a different installed lifelib version warns rather than stops.
Function list¶
makeham gompertz qx life_table commutation factors annuity
assurance premium epv ae ae_test exposure graduate
lee_carter kaplan_meier model_points survival life_expectancy
close_table mx_to_qx qx_to_mx basicterm scr_life csm
lifelib_models lifelib_run lifelib_provenance lifelib_home
normalise_model_points experience — each with the sc_ twin in R.