Source code for actuarialmath.survival

"""Survival models - Computes survival and mortality functions

MIT License. Copyright (c) 2022-2023 Terence Lim
"""
from typing import Callable, Tuple, Any, List
import math
import numpy as np
from actuarialmath import Life

[docs]class Survival(Life): """Set and derive basic survival and mortality functions""" _RADIX = 100000 # default initial number of lives in life table
[docs] def set_survival(self, S: Callable[[int,float,float], float] | None = None, f: Callable[[int,float,float], float] | None = None, l: Callable[[int,float], float] | None = None, mu: Callable[[int,float], float] | None = None, minage: int = 0, maxage: int = 1000) -> "Survival": """Construct the basic survival and mortality functions given any one form Args: S : probability [x]+s survives t years f : or lifetime density function of [x]+s after t years l : or number of lives aged (x+t) mu : or force of mortality at age (x+t) maxage : maximum age minage : minimum age Examples: >>> B, c = 0.00027, 1.1 >>> def S(x,s,t): return (math.exp(-B * c**(x+s) * (c**t - 1)/math.log(c))) >>> life = Survival().set_survival(S=S) >>> def ell(x,s): return (1 - (x+s) / 60)**(1 / 3) >>> life = Survival().set_survival(l=ell) """ assert(any([S, f, l, mu])), "One form of survival function must be specified" self._MAXAGE = maxage self._MINAGE = minage self.S = None # survival probability: Prob(T_[x]+s > t) self.f = None # lifetime density: f_[x]+s(t) ~ Prob[([x]+s) dies at t] self.l = None # number of lives aged [x]+s: l_[x]+s self.mu = None # force of mortality: mu_(x+t) def S_from_l(x: int, s, t: float) -> float: """Derive survival probability from number of lives""" return (self.l(x, s+t) / self.l(x, s)) if self.l(x, s) else 0. def mu_from_l(x: int, t: float) -> float: """Derive force of mortality from number of lives""" return -self.derivative(lambda s: self.l(x, s), t) / self.l(x,t) def f_from_l(x: int, s, t: float) -> float: """Derive lifetime density function from number of lives""" return -self.derivative(lambda t: self.l(x, s+t), t) def mu_from_S(x: int, t: float) -> float: """Derive force of mortality from survival probability""" return -self.derivative(lambda s: self.S(x, 0, s), t) / self.S(x,0,t) def f_from_S(x: int, s, t: float) -> float: """Derive lifetime density function from survival probability""" return -self.derivative(lambda t: self.S(x, s, t), t) def S_from_mu(x: int, s, t: float) -> float: """Derive survival probability from force of mortality""" return math.exp(-self.integral(lambda t: self.mu(x, s+t), lower=0, upper=t)) def S_from_f(x: int, s, t: float) -> float: """Derive survival probability from lifetime density function""" return 1 - self.integral(lambda t: self.f(x, s, t), lower=0, upper=t) def f_from_mu(x: int, s, t: float) -> float: """Derive lifetime density function from force of mortality""" return self.S(x, s, t) * self.mu(x, s+t) def mu_from_f(x: int, t: float) -> float: """Derive force of mortality from lifetime density function""" return self.f(x, 0, t) / self.S(x, 0, t) # derive and set all forms of basic survival and mortality functions if l is not None: assert callable(l), "l must be callable" self.S = S_from_l self.mu = mu_from_l self.f = f_from_l if S is not None: assert callable(S), "S must be callable" self.mu = mu_from_S self.f = f_from_S if mu is not None: assert callable(mu), "mu must be callable" self.S = S_from_mu self.f = f_from_mu if f is not None: assert callable(f), "f must be callable" self.S = S_from_f self.mu = mu_from_f self.l = l or self.l self.S = S or self.S self.f = f or self.f self.mu = mu or self.mu return self
# # Actuarial forms of survival and mortality functions, at integer ages #
[docs] def l_x(self, x: int, s: int = 0) -> float: """Number of lives at integer age [x]+s: l_[x]+s Args: x : age of selection s : years after selection """ assert x >= 0, "x must be non-negative" assert s >= 0, "s must be non-negative" if self.l is not None: return self.l(x, s) return self._RADIX * self.p_x(x=self._MINAGE, s=0, t=s+x-self._MINAGE)
[docs] def d_x(self, x: int, s: int = 0) -> float: """Number of deaths at integer age [x]+s: d_[x]+s Args: x : age of selection s : years after selection """ assert x >= 0, "x must be non-negative" assert s >= 0, "s must be non-negative" return self.l_x(x=x, s=s) - self.l_x(x=x, s=s+1)
[docs] def p_x(self, x: int, s: int = 0, t: int = 1) -> float: """Probability that [x]+s lives another t years: : t_p_[x]+s Args: x : age of selection s : years after selection t : survives at least t years """ assert x >= 0, "x must be non-negative" assert s >= 0, "s must be non-negative" if self.S is not None: return self.S(x, s, t) raise Exception("No functions implemented to compute survival")
[docs] def q_x(self, x: int, s: int = 0, t: int = 1, u: int = 0) -> float: """Probability that [x]+s lives for u, but not t+u years: u|t_q_[x]+s Args: x : age of selection s : years after selection u : survives u years, then t : dies within next t years Examples: >>> def ell(x,s): return (1-((x+s)/250)) if x+s < 40 else (1-((x+s)/100)**2) >>> q = Survival().set_survival(l=ell).q_x(30, t=20) """ assert x >= 0, "x must be non-negative" assert s >= 0, "s must be non-negative" return self.p_x(x, s=s, t=u) - self.p_x(x, s=s, t=t+u)
[docs] def f_x(self, x: int, s: int = 0, t: int = 0) -> float: """Lifetime density function of [x]+s after t years: f_[x]+s(t) Args: x : age of selection s : years after selection t : dies at year t Examples: >>> B, c = 0.00027, 1.1 >>> def S(x,s,t): return (math.exp(-B * c**(x+s) * (c**t - 1)/math.log(c))) >>> f = Survival().set_survival(S=S).f_x(x=50, t=10) """ assert x >= 0, "x must be non-negative" assert s >= 0, "s must be non-negative" if self.f is not None: return self.f(x, s, t) return self.p_x(x, s=s, t=t) * self.mu_x(x, s=s, t=t)
[docs] def mu_x(self, x: int, s: int = 0, t: int = 0) -> float: """Force of mortality of [x] at s+t years: mu_[x](s+t) Args: x : age of selection s : years after selection t : force of mortality at year t Examples: >>> def ell(x, s): return (1 - (x+s) / 60)**(1 / 3) >>> print(Survival().set_survival(l=ell).mu_x(35)) """ assert x >= 0, "x must be non-negative" assert s >= 0, "s must be non-negative" if self.mu is not None: return self.mu(x, s+t) return self.f_x(x, s=s, t=t) / self.p_x(x, s=s, t=t)
# def survival_curve(self, x: int, s: int = 0, stop: int = 0) -> Tuple[List, List]: # """Construct curve of survival probabilities at each integer age # # Args: # x : age of selection # s : years after selection # stop : end at time t, inclusive # # Returns: # lists of lifetime and survival probability from t, S_x(t) respectively # """ # stop = stop or self._MAXAGE - (x + s) # steps = range(stop + 1) # return steps, [self.p_x(x=x, s=s, t=t) for t in steps] if __name__ == "__main__": print("SOA Question 2.3: (A) 0.0483") B, c = 0.00027, 1.1 def S(x,s,t): return (math.exp(-B * c**(x+s) * (c**t - 1)/math.log(c))) f = Survival().set_survival(S=S).f_x(x=50, t=10) print(f) print("SOA Question 2.6: (C) 13.3") def ell(x,s): return (1 - (x+s) / 60)**(1 / 3) mu = Survival().set_survival(l=ell).mu_x(35) * 1000 print(mu) print("SOA Question 2.7: (B) 0.1477") def ell(x,s): return (1 - ((x+s)/250)) if x+s < 40 else (1 - ((x+s)/100)**2) q = Survival().set_survival(l=ell).q_x(30, t=20) print(q) print("CAS41-F99:12: k = 41") def fun(k): return Survival().set_survival(l=lambda x,s: 100*(k - (x+s)/2)**(2/3))\ .mu_x(50) print(Survival.solve(fun, target=1/48, grid=50))