Skip to main content

Life contingencies and actuarial mathematics in Python, with dynamic life tables and mortality forecasting

Project description

pylifecontingencies

A native Python port of the R lifecontingencies package, extended with dynamic life tables and mortality-rate forecasting (Lee-Carter, CBD M5).

No R runtime required. Pure NumPy + pandas at install time; rpy2 is only used in the validation test suite.


What it includes

  • Single-life actuarial present values: annuities, insurances, endowments, increasing/decreasing benefits, premiums, and reserves
  • Mortality and demographic utilities: pxt, qxt, mxt, Lxt, Tx, exn
  • Dynamic mortality forecasting: Lee-Carter, CBD M5, projected life tables, stochastic scenario support
  • Monte Carlo PV simulation via StochasticResult — k-thly payments, deferral, hist/plot visualisation, var/tvar risk metrics, pandas export
  • Parametric mortality graduation with GompertzMakeham and HeligmanPollard
  • Bundled data: 100+ mortality tables via load_table() / list_tables() — SOA ILT, BR-EMS series (2010–2021), AT, UP, RP, GAM, CSO, IBGE, and many historical tables; plus R-sourced parquets (soa08, AM92Lt, demoUsa, etc.)

Installation

Core package:

pip install pylifecontingencies

With SOA XTbML support:

pip install "pylifecontingencies[soa]"

For local development and release checks:

pip install -e ".[dev]"

Quick examples

Static actuarial values

from pylifecontingencies import load_table, ActuarialTable, axn, Axn, AExn

lt = load_table("soa_ilt")
at = ActuarialTable(lt, interest=0.06)

axn(at, x=40)          # whole-life annuity-due
Axn(at, x=40, n=20)    # 20-year term insurance
AExn(at, x=40, n=20)   # 20-year endowment insurance

Bundled R tables

from pylifecontingencies import load_table, list_columns

list_columns("demoUsa")
lt_usa = load_table("demoUsa", column="USSS2007M")
lt_soa = load_table("soa08")

BR-EMS tables

from pylifecontingencies import load_table, ActuarialTable, axn, qxt

# BR-EMS Sobrevivencia 2021, male
lt_br = load_table("br_emssb_2021_m")
at_br = ActuarialTable(lt_br, interest=0.03)

qxt(lt_br, x=40, t=10)   # 10-year death probability at age 40
axn(at_br, x=65)         # whole-life annuity-due at age 65

Stochastic simulation

from pylifecontingencies import simulate_pv

# Annual term insurance
r = simulate_pv(at, x=40, n=20, benefit="term", n_sim=50_000, random_state=42)
r.mean, r.std, r.quantile(0.95)

# Monthly annuity (k=12 payments per year)
r_monthly = simulate_pv(at, x=40, n=20, benefit="annuity", k=12, n_sim=50_000)

# 10-year deferred whole-life annuity (pension starting at age 50)
r_deferred = simulate_pv(at, x=40, benefit="annuity", m=10, n_sim=50_000)

# Visualisation
r.hist()          # histogram with mean and 95 % CI lines
r.plot()          # empirical CDF

# Risk metrics (Solvency II)
r.var(0.995)      # Value at Risk at 99.5 %
r.tvar(0.995)     # Tail VaR (Expected Shortfall) at 99.5 %

# Export samples to pandas for custom analysis
df = r.to_dataframe()   # DataFrame with column "pv"
df["pv"].describe()

Mortality-law fitting

import numpy as np
from pylifecontingencies import available_mortality_laws, fit_mortality_law

fit = fit_mortality_law(lt, "gompertz_makeham", ages=np.arange(40, 90))
fit.params_dict, fit.rmse
available_mortality_laws()

Development and release

GitHub Actions workflows live under .github/workflows/:

  • ci.yml runs the main pytest matrix on Python 3.10, 3.11, and 3.12, plus coverage and distribution builds.
  • r-parity.yml installs R + lifecontingencies and runs the parity tests that compare Python results against the R package.
  • publish.yml builds and publishes to PyPI on tags matching v* after the test suite passes.

For publishing, configure GitHub Trusted Publishing for the PyPI project, then create a version tag such as:

git tag v0.1.0
git push origin v0.1.0

Quick start — static life table

from pylifecontingencies import load_table, ActuarialTable
from pylifecontingencies import axn, Axn, Exn, AExn, IAxn, DAxn, exn

# Load a bundled table
lt = load_table("soa_ilt")            # SOA Illustrative Life Table
at = ActuarialTable(lt, interest=0.06)

# Whole-life annuity-due at age 40
axn(at, x=40)                         # ä_40

# 20-year term insurance
Axn(at, x=40, n=20)                   # A^1_{40:20|}

# 20-year endowment insurance
AExn(at, x=40, n=20)                  # A_{40:20|}

# 20-year pure endowment
Exn(at, x=40, n=20)                   # _20 E_40

# Increasing whole-life insurance
IAxn(at, x=40)                        # (IA)_40

# 20-year decreasing term
DAxn(at, x=40, n=20)                  # (DA)^1_{40:20|}

# Curtate future lifetime expectation
exn(at, x=40)                         # e_40

Semi-annual payments (k=2)

axn(at, x=40, n=20, k=2)             # ä^(2)_{40:20|} via UDD
Axn(at, x=40, n=20, k=2)             # A^(2)_{40:20|} via UDD

Quick start — dynamic life tables

from pylifecontingencies.dynamic import MortalityRates, LeeCarter, ProjectedLifeTable

# Build a rates surface from a DataFrame (ages as index, years as columns, values = log(mx))
rates = MortalityRates.from_dataframe(df_log_mx)

# Fit Lee-Carter
lc = LeeCarter().fit(rates)
print(lc.ax)   # age-specific levels
print(lc.bx)   # age-specific sensitivities
print(lc.kt)   # period index

# Forecast 50 years ahead with 95% bootstrap prediction interval
forecast = lc.forecast(horizon=50, n_bootstrap=500, ci=0.95)

# Build a cohort life table for someone born in 1985
cohort_lt = ProjectedLifeTable(forecast, birth_year=1985).to_life_table()
at_cohort = ActuarialTable(cohort_lt, interest=0.03)
axn(at_cohort, x=40)   # cohort-true annuity at 40

Stochastic PV simulation

from pylifecontingencies import load_table, ActuarialTable, simulate_pv

lt = load_table("soa_ilt")
at = ActuarialTable(lt, interest=0.03)

# Annual term insurance — PV distribution
r = simulate_pv(at, x=40, n=20, benefit="term", n_sim=50_000, random_state=42)
print(r.mean, r.std)
print(r.quantile(0.95))

# Monthly annuity (k=12 payments per year, UDD)
r_monthly = simulate_pv(at, x=40, n=20, benefit="annuity", k=12, n_sim=50_000, random_state=42)

# 10-year deferred whole-life annuity (pension, m=10 deferral)
r_deferred = simulate_pv(at, x=40, benefit="annuity", m=10, n_sim=50_000, random_state=42)

# Combine: monthly pension starting in 10 years
r_pension = simulate_pv(at, x=40, n=20, benefit="annuity", k=12, m=10, n_sim=50_000)

# Export to pandas for custom analysis
df = r.to_dataframe()   # DataFrame with column "pv"
df["pv"].hist(bins=30)
df["pv"].describe()

# Convenience method on the table object
r_ann = at.simulate_pv(x=40, n=20, benefit="annuity", k=12, n_sim=10_000)

Supported benefit types: term, whole / whole_life, annuity, pure_endowment, endowment, increasing, decreasing.

k > 1 (fractional payments) and m > 0 (deferral) are supported for annuity. m > 0 is also supported for all insurance benefit types.


Mortality-law graduation

import numpy as np
from pylifecontingencies import (
    available_mortality_laws,
    load_table,
    fit_mortality_law,
    GompertzMakeham,
    HeligmanPollard,
)

lt = load_table("soa_ilt")

# String dispatch
gm_fit = fit_mortality_law(lt, "gompertz_makeham", ages=np.arange(40, 90))
print(gm_fit.params_dict)
print(gm_fit.rmse, gm_fit.aic)

# Explicit law object
hp_fit = fit_mortality_law(lt, HeligmanPollard(), ages=np.arange(1, 90))
df_fit = hp_fit.to_dataframe()
available_mortality_laws()

MortalityLawFit stores fitted parameters, observed/fitted q_x, residuals, and goodness-of-fit metrics (loglik, AIC, BIC, RMSE, MAE). The current implementation is Python-only. R packages can be used as inspiration for future models, but they are not required at runtime.

from pylifecontingencies import available_mortality_laws

available_mortality_laws()

Interest-rate utilities

from pylifecontingencies import InterestRate

ir = InterestRate(i=0.05)
ir.v           # 0.952...  discount factor
ir.delta       # 0.04879...  force of interest
ir.d           # 0.04762...  annual discount rate
ir.i_m(12)     # monthly nominal rate
ir.d_m(12)     # monthly nominal discount rate

InterestRate.from_delta(0.05)    # from force of interest
InterestRate.from_discount(0.04) # from annual discount rate

Demographic functions

from pylifecontingencies import pxt, qxt, dxt, mxt, Lxt, Tx

lt = load_table("soa_ilt")
pxt(lt, x=40, t=10)   # _10 p_40
qxt(lt, x=40, t=10)   # _10 q_40
exn(lt, x=40)         # e_40  (curtate)

Bundled tables

Over 100 mortality tables are shipped with the package and discoverable at runtime:

from pylifecontingencies import list_tables, load_table

list_tables()          # returns all available names
lt = load_table("at_2000_female")
lt = load_table("ibge_2020_homens")
lt = load_table("up_94_male")

CSV tables (automatically discovered)

Selected groups:

Group Examples
SOA soa_ilt
AT (Annuity 2000) at_2000_female, at_2000_male, at_49_female, at_49_male, at_83_female_basic, …
UP / RP (Group annuity) up_84_f, up_84_m, up_94_female, up_94_male, rp_2000_female, rp_2000male, …
GAM gam_71_female, gam_71_male, gam_83_female_suav_10, gam_94_female, …
CSO cso_41, cso_58, cso58_female, cso58_male, cso80
BR-EMS (Sobrevivência) br_emssb_2010_m/f, br_emssb_2015_m/f, br_emssb_2021_m/f
BR-EMS (Mortalidade) br_emsmt_2010_m/f, br_emsmt_2015_m/f, br_emsmt_2021_m/f
IBGE ibge_2006_ambos_os_sexos, ibge_2020_homens, ibge_2020_mulheres, …
Historical american_experience, bentzien, muller, rentiers_francais, zimmermann, …

All CSV tables have age and qx columns; x_min is inferred automatically from the first age in the file.

Multi-column R tables

Some bundled parquet datasets contain several sub-tables in one file. Use list_columns(name) to inspect the available columns and load_table(..., column=...) to select one:

from pylifecontingencies import load_table, list_columns

list_columns("demoUsa")
# ['USSS2007M', 'USSS2007F', 'USSS2000M', 'USSS2000F', 'USSS1990M', 'USSS1990F']

lt_usa = load_table("demoUsa", column="USSS2007M")
lt_ger = load_table("demoGermany", column="qxMale")

Single-column parquet tables such as soa08, AM92Lt, and AF92Lt can be loaded directly:

lt = load_table("soa08")

Additional tables (AM92, AF92, demoUsa, etc.) can be imported from R using the provided conversion script:

# requires R + lifecontingencies + rpy2 + pyarrow
python scripts/convert_rda_to_parquet.py

Comparison with R lifecontingencies

R Python
axn(at, x=40, n=20) axn(at, x=40, n=20)
Axn(at, x=40, n=20) Axn(at, x=40, n=20)
Exn(at, x=40, n=20) Exn(at, x=40, n=20)
exn(lt, x=40) exn(lt, x=40)
pxt(lt, x=40, t=5) pxt(lt, x=40, t=5)
new("lifetable", x=..., lx=..., name=...) LifeTable(lx, x_min=0, name=...)
new("actuarialtable", ..., interest=0.06) ActuarialTable(lt, interest=0.06)

Scope and roadmap

Current: Single-life EPVs, stochastic PV simulation (k-thly payments, deferral, visualisation, VaR/TVaR, pandas export), mortality-law fitters, interest-rate utilities, demographic functions, bundled tables, Lee-Carter and CBD M5 mortality forecasting.

Planned next: Multi-decrement tables, Renshaw-Haberman and APC forecasting models, and broader stochastic simulation equivalents to rLifeContingencies.

License

GPL-2.0 — matching the upstream lifecontingencies R package.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pylifecontingencies-0.5.0.tar.gz (146.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pylifecontingencies-0.5.0-py3-none-any.whl (181.6 kB view details)

Uploaded Python 3

File details

Details for the file pylifecontingencies-0.5.0.tar.gz.

File metadata

  • Download URL: pylifecontingencies-0.5.0.tar.gz
  • Upload date:
  • Size: 146.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for pylifecontingencies-0.5.0.tar.gz
Algorithm Hash digest
SHA256 5b80137dd6a81b461734e063ed7bb84403b46e70a8b099bdba2a145110351b05
MD5 af1f75384918e1347a19b25e7ba62ab8
BLAKE2b-256 7f1bdbfcdbcc6e554a5e400d36956bc5245aba57f8fc80dd51193c66358ba6ce

See more details on using hashes here.

File details

Details for the file pylifecontingencies-0.5.0-py3-none-any.whl.

File metadata

File hashes

Hashes for pylifecontingencies-0.5.0-py3-none-any.whl
Algorithm Hash digest
SHA256 a15e3f8a557b9984793c0b7d5e4109059ab2c9e7a3da5a8c075842ee100692ba
MD5 e4fd15dc2cdacfbafb70a7478856eddf
BLAKE2b-256 ca758671201a321529e2af7cca3df7462ac4b5397e60586cb0117d63126b53c7

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page