"""Define base class for actuarial math, with utility helpers and constants
MIT License. Copyright (c) 2022-2023 Terence Lim
"""
import math
import numpy as np
import scipy
import scipy.misc
import scipy.differentiate
import scipy.integrate
import scipy.optimize
import matplotlib.pyplot as plt
from typing import Callable, Any, Tuple, List
#plt.style.use('seaborn-dark') # 'ggplot'
[docs]class Actuarial(object):
"""Define constants and common utility functions
Constants:
VARIANCE : select variance as the statistical moment to calculate
WHOLE : indicates that term of insurance or annuity is Whole Life
"""
# constants
VARIANCE = -2
WHOLE = -999
_VARIANCE = VARIANCE
_WHOLE = WHOLE
_TOL = 1e-6
_verbose = 0
_MAXAGE = 100 # default oldest age
_MINAGE = 0 # default youngest age
#
# Helpers for numerical computations
#
[docs] @staticmethod
def isclose(r: float, target: float = 0., abs_tol=1e-6) -> bool:
"""Is close to zero or target value
Args:
r : value to test if close to zero or target
target : target value, default is 0.0
"""
return math.isclose(r, target, abs_tol=abs_tol)
[docs] @staticmethod
def integral(fun: Callable[[float], float],
lower: float, upper: float) -> float:
"""Compute integral of the function between lower and upper limits
Args:
fun : function to integrate
upper : upper limit
lower : lower limit
"""
y = scipy.integrate.quad(fun, lower, upper, full_output=1)
return y[0]
[docs] @staticmethod
def derivative(fun: Callable[[float], float], x: float) -> float:
"""Compute derivative of the function at a value
Args:
fun : function to compute derivative
x : value to compute derivative at
Examples:
>>> print(Actuarial.derivative(fun=lambda x: x/50, x=25))
"""
return float(scipy.differentiate.derivative(fun, x=x, initial_step=1, maxiter=1, preserve_shape=True).df)
# return scipy.misc.derivative(fun, x0=x, dx=1)
[docs] @staticmethod
def solve(fun: Callable[[float], float], target: float,
grid: float | Tuple | List, mad: bool = False) -> float:
"""Solve root, or parameter that minimizes absolute value, of a function
Args:
fun : function to compute output given input values
target : target value of function output
grid : initial range of guesses
root : whether to solve root (True), or minimize absolute function (False)
Returns:
value s.t. output of function fun(value) ~ target
Examples:
>>> print(Actuarial.solve(fun=lambda omega: 1/omega,
>>> target=0.05, grid=[1, 100]))
"""
if mad: # minimize absolute difference
f = lambda t: abs(fun(t) - target)
return scipy.optimize.minimize_scalar(f, grid).x
else: # solve root
f = lambda x: fun(x) - target
if isinstance(grid, (list, tuple)):
grid = min([(abs(f(x)), x) # guess can be list of guesses
for x in np.linspace(min(grid), max(grid), 5)])[1]
output = scipy.optimize.fsolve(f, [grid], full_output=True)
fun(output[0][0]) # call again with final in case want side effect
return output[0][0]
[docs] def add_term(self, t: int, n: int) -> int:
"""Add two terms, either term may be Whole Life
Args:
t : first term to add
n : second term to add
"""
if t == self.WHOLE or n == self.WHOLE:
return self.WHOLE # adding any term to WHOLE is still WHOLE
return t + n
[docs] def max_term(self, x: int, t: int, u: int = 0) -> int:
"""Decrease term t if adding deferral period u to (x) exceeds maxage
Args:
x : age
t : term of insurance or annuity, after deferral period
u : term deferred
Returns:
value of term t adjusted by deferral and maxage s.t. maxage not exceeded
"""
if t < 0 or x + t + u > self._MAXAGE:
return self._MAXAGE - (x + u)
return t
if __name__ == "__main__":
actuarial = Actuarial()
def as_term(t): return "WHOLE_LIFE" if t == Actuarial.WHOLE else t
for a,b in [(3, Actuarial.WHOLE), (3, 2), (3, -1)]:
print(f"({as_term(a)}) + ({as_term(b)}) =",
as_term(actuarial.add_term(a, b)))
print(Actuarial.solve(fun=lambda omega: 1/omega,
target=0.05,
grid=[1, 100]))
print(Actuarial.derivative(fun=lambda x: x/50, x=25))