lifelib's basic term in cashflower

lifelib is an open-source Python package designed for actuarial modelling. It's a significant contribution to the actuarial community, offering one of the few — if not the only — open-source actuarial cash flow models. This makes it a valuable tool for enhancing modelling skills.

The package includes multiple libraries, one of which covers basic life insurance products. In this post, we'll focus on BasicTerm_SE, a model for term insurance with in-force policies. lifelib is built on modelx, which handles model execution.

We'll also explore an alternative approach by modelling the same product using cashflower, another actuarial modelling package.


List of content:

  1. Input
    1. Model point set
    2. Assumptions
  2. Model
    1. Policyholder
    2. Economic
    3. Cash flows
    4. Policy count
    5. Present values
  3. Conclusion

Input

Model point set

The policy data is stored in a separate file named model_point_table.csv. We load this data into cashflower using the ModelPointSet class:

# input.py 
from cashflower import ModelPointSet
import pandas as pd

policy = ModelPointSet(data=pd.read_csv("input/model_point_table.csv"))

Each row in this table represents a policyholder, with the following attributes:

  • policy_id - unique identifier
  • age_at_entry - age at policy issue
  • sex - male or female
  • policy_term - policy term in years
  • policy_count - number of policies
  • sum_assured - sum assured
  • duration_mth - months elapsed from issue to t=0

Here's a preview of the data:

policy_id  age_at_entry sex  policy_term  policy_count  sum_assured  duration_mth
        1            47   M           10            86     622000.0             1
        2            29   M           20            56     752000.0           210
        3            51   F           10            83     799000.0            15
        4            32   F           20            72     422000.0           125
        5            28   M           15            99     605000.0            55
      ...           ...  ..          ...           ...          ...           ...
     9996            47   M           20            25     827000.0           157
     9997            30   M           15            81     826000.0           168
     9998            45   F           20            10     783000.0           146
     9999            39   M           20             9     302000.0            11
    10000            22   F           15            18     576000.0           166

This dataset defines the policies that will be used in our model. Each policyholder has a unique combination of attributes that influence the cash flow projections.

Assumptions

The model relies on several assumptions, including discount rates, mortality probabilities and premium rates, as well as fixed expense and inflation assumptions. These inputs define the financial and actuarial dynamics of the model.

# input.py
assumption = {
    "disc_rate_ann": pd.read_csv(disc_rate_ann_path, index_col="year"),
    "mort_table": pd.read_csv(mort_table_path, index_col="Age"),
    "premium_table": pd.read_csv(premium_table_path),
    "loading_prem": 0.5,
    "expense_acq": 300,
    "expense_maint": 60,
    "inflation_rate": 0.01,
}

Assumption tables

The model reads three key tables:

  • disc_rate_ann.csv - annual discount rates
  • mort_table.csv - mortality rates by age and year
  • premium_table.csv - premium rates based on age at entry and policy term
Annual discount rates

The discount rate table provides the zero-coupon spot rates for each future year. These rates determine how future cash flows are discounted to present value.

year  zero_spot
   0    0.00000
   1    0.00555
   2    0.00684
   3    0.00788
   4    0.00866
 ...        ...
 146    0.03025
 147    0.03033
 148    0.03041
 149    0.03049
 150    0.03056
Mortality table

The mortality table contains probabilities of death for different ages and policy durations. Each column represents a policy duration, while rows correspond to ages.

Age         0         1         2         3         4         5
 18  0.000231  0.000254  0.000280  0.000308  0.000338  0.000372
 19  0.000235  0.000259  0.000285  0.000313  0.000345  0.000379
 20  0.000240  0.000264  0.000290  0.000319  0.000351  0.000386
 21  0.000245  0.000269  0.000296  0.000326  0.000359  0.000394
 22  0.000250  0.000275  0.000303  0.000333  0.000367  0.000403
...       ...       ...       ...       ...       ...       ...
116  1.000000  1.000000  1.000000  1.000000  1.000000  1.000000
117  1.000000  1.000000  1.000000  1.000000  1.000000  1.000000
118  1.000000  1.000000  1.000000  1.000000  1.000000  1.000000
119  1.000000  1.000000  1.000000  1.000000  1.000000  1.000000
120  1.000000  1.000000  1.000000  1.000000  1.000000  1.000000
Premium rates

The premium rate table provides pricing for policies based on age at entry and policy term. These rates are used to determine policyholder contributions.

age_at_entry  policy_term  premium_rate
          20           10      0.000046
          20           15      0.000052
          20           20      0.000057
          21           10      0.000048
          21           15      0.000054
         ...          ...           ...
          58           15      0.000433
          58           20      0.000557
          59           10      0.000362
          59           15      0.000471
          59           20      0.000609

Other assumptions

In addition to the tables, the model incorporates fixed expense and pricing assumptions:

  • premium loading = 50% - additional charge on base premium
  • acquisition expense = 300 - one-time cost per policy at issue
  • maintenance expense = 60 - annual ongoing cost per policy
  • inflation rate = 1% - used for expense escalation over time

These assumptions provide the necessary foundation for projecting policyholder cash flows and ensuring the model reflects real-world pricing and risk factors.

Model

Policyholder

Policyholder's attributes

We determine the policyholder's time in force, age and projection length.

@variable()
def duration_mth(t):
    if t == 0:
        return model_point.get("duration_mth")
    else:
        return duration_mth(t-1) + 1


@variable()
def duration(t):
    return duration_mth(t) // 12


@variable()
def age(t):
    return model_point.get("age_at_entry") + duration(t)


@variable()
def proj_len():
    return max(12 * model_point.get("policy_term") - model_point.get("duration_mth") + 1, 0)

Premium calculation

The premium per policy is determined based on the sum assured and the premium rate from the assumption table.

@variable()
def premium_pp():
    df = assumption["premium_table"]
    age_at_entry = model_point.get("age_at_entry")
    policy_term = model_point.get("policy_term")
    premium_rate = df.loc[(df['age_at_entry'] == age_at_entry) &
                          (df['policy_term'] == policy_term), 'premium_rate'].values[0]
    return round(model_point.get("sum_assured") * premium_rate, 2)

Lapse and mortality rates

The lapse rate is assumed to decrease with duration but remains above a minimum threshold.

@variable()
def lapse_rate(t):
    return max(0.1 - 0.02 * duration(t), 0.02)

Mortality rates are read from the mortality table. The function avoids redundant reads for performance optimization.

@variable()
def mort_rate(t):
    if t > 0 and age(t-1) == age(t) and (duration(t-1) == duration(t) or duration(t) > 5):
        return mort_rate(t-1)
    age_t = int(max(min(age(t), 120), 18))
    duration_t = str(int(max(min(duration(t), 5), 0)))
    return assumption["mort_table"].loc[age_t, duration_t]

@variable()
def mort_rate_mth(t):
    return 1-(1-mort_rate(t))**(1/12)

Economic

We define the discount rates and inflation factor used in the model.

Discount rates

The annual discount rate is retrieved from the assumption table, avoiding unnecessary lookups within the same year.

@variable()
def discount_ann(t):
    if t > 0 and (t-1)//12 == t//12:
        return discount_ann(t-1)
    return assumption["disc_rate_ann"].loc[t//12, 'zero_spot']

The monthly discount rate is derived from the annual rate.

@variable()
def discount(t):
    return (1 + discount_ann(t))**(-t/12)

Inflation factor

The inflation factor grows exponentially based on the inflation rate assumption.

@variable()
def inflation_factor(t):
    return (1 + assumption["inflation_rate"])**(t/12)

Cash flows

The model distinguishes between inflows (premiums) and outflows (claims, commissions, and expenses). The net cash flow is the difference between these components.

Inflows

Premiums are received monthly as long as the policy is active.

@variable()
def premiums(t):
    if t >= proj_len():
        return 0
    return premium_pp() * pols_if_at_bef_decr(t)

Outflows

Claims are paid when policyholders pass away.

@variable()
def claims(t):
    if t >= proj_len():
        return 0
    return model_point.get("sum_assured") * pols_death(t)

Commissions are paid in full when the policy is sold.

@variable()
def commissions(t):
    if duration(t) == 0:
        return premiums(t)
    else:
        return 0

Expenses consist of acquisition costs at policy inception and ongoing maintenance costs, adjusted for inflation.

@variable()
def expenses(t):
    if t >= proj_len():
        return 0

    return (assumption["expense_acq"] * pols_new_biz(t) + pols_if_at_bef_decr(t) * assumption["expense_maint"]/12
            * inflation_factor(t))

Net cash flow

Net cash flow is the difference between inflows and outflows.

@variable()
def net_cf(t):
    return premiums(t) - claims(t) - expenses(t) - commissions(t)

Policy count

This section tracks the number of policies in force (IF) at different stages, considering new business, lapses, deaths and maturities.

Initial policy count

At the start of the projection, policies may already exist.

@variable()
def pols_if_init():
    if duration_mth(0) > 0:
        return model_point.get("policy_count")
    else:
        return 0

Policies before decrements

The number of policies before lapses, death and maturities is the sum of last period's in-force policies and new business.

@variable()
def pols_if_at_bef_decr(t):
    return pols_if_at_bef_nb(t) + pols_new_biz(t)

Decrements (lapses and deaths)

Lapses occur based on the lapse rate.

@variable()
def pols_lapse(t):
    return (pols_if_at_bef_decr(t) - pols_death(t)) * (1 - (1 - lapse_rate(t)) ** (1 / 12))

Deaths occur based on the mortality rate.

@variable()
def pols_death(t):
    return pols_if_at_bef_decr(t) * mort_rate_mth(t)

Policies before maturities

After accounting for lapses and deaths, we determine policies before maturities.

@variable()
def pols_if_at_bef_mat(t):
    if t == 0:
        return pols_if_init()
    else:
        return pols_if_at_bef_decr(t-1) - pols_lapse(t-1) - pols_death(t-1)

Maturities

Policies mature when they reach the end of their term.

@variable()
def pols_maturity(t):
    if duration_mth(t) == model_point.get("policy_term") * 12:
        return pols_if_at_bef_mat(t)
    else:
        return 0

Policies before new business

This represents policies before adding new business for the period.

@variable()
def pols_if_at_bef_nb(t):
    return pols_if_at_bef_mat(t) - pols_maturity(t)

New business

New policies are added when they are first issued.

@variable()
def pols_new_biz(t):
    if duration_mth(t) == 0:
        return model_point.get("policy_count")
    else:
        return 0

Final policies in force

The final number of in-force policies after all adjustments.

@variable()
def pols_if(t):
    return pols_if_at_bef_mat(t)

Present values

Present value (PV) calculations discount future cash flows to today's value. These help in assessing the financial position of a policy over time.

Present value of policies in force

This represents the discounted sum of future in-force policies.

@variable()
def pv_pols_if(t):
    if t == settings["T_MAX_CALCULATION"]:
        return pols_if(t) * discount(t)
    return pols_if(t) * discount(t) + pv_pols_if(t+1)

Present value of premiums

The total value of all future premium payments, discounted to today.

@variable()
def pv_premiums(t):
    if t == settings["T_MAX_CALCULATION"]:
        return premiums(t) * discount(t)
    return premiums(t) * discount(t) + pv_premiums(t+1)

Present value of claims

The total expected claim payouts, adjusted for the time value of money.

@variable()
def pv_claims(t):
    if t == settings["T_MAX_CALCULATION"]:
        return claims(t) * discount(t)
    return claims(t) * discount(t) + pv_claims(t+1)

Present value of commissions

Total commissions payable over time, discounted to present value.

@variable()
def pv_commissions(t):
    if t == settings["T_MAX_CALCULATION"]:
        return commissions(t) * discount(t)
    return commissions(t) * discount(t) + pv_commissions(t+1)

Present value of expenses

The total expected expenses incurred, adjusted for discounting.

@variable()
def pv_expenses(t):
    if t == settings["T_MAX_CALCULATION"]:
        return expenses(t) * discount(t)
    return expenses(t) * discount(t) + pv_expenses(t+1)

Present value of net cash flows

The net position of the policy, factoring in all cash flows.

@variable()
def pv_net_cf(t):
    return pv_premiums(t) - pv_claims(t) - pv_expenses(t) - pv_commissions(t)

Net premium per policy

The premium required to cover expected claims, assuming no profit or loss.

@variable()
def net_premium_pp():
    if math.isclose(pv_pols_if(0), 0):
        return 0
    return pv_claims(0) / pv_pols_if(0)

Conclusion

In this post, we built a structured cash flow model, step by step, covering everything from policyholder attributes to present values. We started by defining key variables, such as policy duration and age, and moved on to economic factors like discount rates and inflation. We then outlined cash flows, policy counts, and finally, the present values that form the foundation for financial calculations.

This model serves as a clear and systematic way to project cash flows for insurance policies. The logic behind it is based on lifelib, an excellent open-source library for actuarial modelling. A huge thank you to the developers of lifelib for their work.

If you have any thoughts or questions, feel free to share them in the comments!

Read also:

Log in to add your comment.

Comments

future_actuary (2025-03-01)

Great post!