Getting Started#
This guide walks you through the first steps with Lactuca: installing the library, loading an actuarial table, and running common probability and valuation calculations. No prior knowledge of the package is assumed.
Professional use: Results depend on inputs and assumptions chosen by the user. See the End User License Agreement §10.2–10.3 for the full professional responsibility and regulatory disclaimer.
Installation#
Lactuca runs on Windows, macOS, and Linux. Python ≥ 3.12 is required.
pip install lactuca
All other dependencies (NumPy ≥ 2.3, Pandas ≥ 2.3, SciPy ≥ 1.16, Polars ≥ 1.34) are installed automatically.
Activation#
A valid license is required before import lactuca can run calculations.
See Activation Guide for environment variables, the CLI (python -m lactuca), and the
free trial.
Bundled tables (one-time setup)#
Table payloads ship inside the package but must be written to disk as .ltk files before use.
Run this once per environment (or after upgrading Lactuca):
from lactuca.tables.data import Tables
Tables.install() # all bundled tables → ./actuarial_tables/ by default
Install only what you need: Tables.install("PER2020_Ind_1o"). The default directory is
actuarial_tables/ under your current working directory; change it with
lactuca.config.tables_path. See Bundled Actuarial Tables for the complete catalogue and
Managing bundled tables for install options.
Quick example#
Note
Requires an activated license (Activation Guide) and bundled tables on disk (see above).
from lactuca import LifeTable, äx
# First-order individual annuitant table (PER 2020, born 1969)
lt = LifeTable(table_name="PER2020_Ind_1o", sex="m", cohort=1969)
# 15-year temporary life annuity-due (annual payments, 3% interest)
annuity = lt.äx(x=50, n=15, m=1, ir=0.03)
print(f"Life annuity value: {annuity:.4f}")
# The same calculation via the functional API:
annuity = äx(table=lt, x=50, n=15, m=1, ir=0.03)
print(f"Life annuity value: {annuity:.4f}")
How Lactuca is organised#
Lactuca is structured as five layers. As a user you interact exclusively with the Public API layer; the layers below it are handled automatically:
Actuarial table types#
Lactuca provides three table classes that mirror the standard actuarial taxonomy:
Class |
Decrement |
Typical use |
|---|---|---|
|
|
Life insurance, pensions, annuities |
|
|
Income protection, long-term care (dependency) |
|
|
Pension fund turnover, policy lapses |
All three are classified along two orthogonal dimensions (see Table Taxonomy):
Insured structure — Aggregate tables apply uniform rates to all insured persons; Select-Ultimate tables distinguish recently underwritten lives from the general portfolio.
Temporal nature — Static tables use a fixed rate schedule with no improvement projection; Generational tables project rates forward from a base year using improvement factors tied to the insured’s birth cohort. Generational tables require the
cohort=parameter.
See Bundled Actuarial Tables for every bundled table identifier.
Loading a table#
Before performing any calculation, ensure the table file exists on disk (see
Managing bundled tables) and create a table object. All constructors share the
same two required arguments: the table identifier (a string) and the sex code
("m" for male, "f" for female).
Static life table#
Static tables need no cohort. The object is ready to use immediately:
from lactuca import LifeTable
# PASEM 2020, 1st order — Spanish life-risk / mortality table (conservative)
pasem = LifeTable("PASEM2020_Rel_1o", "m") # male
pasem_f = LifeTable("PASEM2020_Rel_1o", "f") # female
print(f"pasem.sex: {pasem.sex}, pasem_f.sex: {pasem_f.sex}")
Generational life table (cohort required)#
For generational tables, pass the insured’s birth year via cohort=. The improvement
factors are applied automatically:
from lactuca import LifeTable
# PER 2020 individual, 1st order — Spanish pension annuitants (born 1969)
per = LifeTable("PER2020_Ind_1o", "m", cohort=1969)
# DAV 2004 R aggregate, 2nd order — German private annuitants (born 1960)
dav = LifeTable("DAV2004R_Agg_2o", "m", cohort=1960)
# GAM 1994 with Scale AA — US group annuity with projected improvement (born 1955)
gam = LifeTable("GAM94_AA", "m", cohort=1955)
print(f"per.cohort: {per.cohort}, dav.cohort: {dav.cohort}, gam.cohort: {gam.cohort}")
Select-ultimate table#
Select-Ultimate tables distinguish recently underwritten lives (select period) from the
general portfolio (ultimate period). Pass duration= to specify years-since-underwriting;
"ult" explicitly selects the ultimate column:
from lactuca import LifeTable
# DAV 2004 R Select-Ultimate (generational) — 2 years since underwriting
dav_su = LifeTable("DAV2004R_SelUlt_1o", "m", cohort=1960, duration=2)
# Same table — ultimate column (select period exhausted)
dav_ult = LifeTable("DAV2004R_SelUlt_1o", "m", cohort=1960, duration="ult")
print(f"dav_su.duration: {dav_su.duration}, dav_ult.duration: {dav_ult.duration}")
Some tables follow a Duration-0 convention where the first select year is indexed as
duration=0 rather than duration=1. The AM92/AF92 series (UK) is one such table.
Check table.start_duration to see the minimum valid duration for any table:
from lactuca import LifeTable
# AM92/AF92 — UK select-ultimate (static), Duration-0 convention
am92_d0 = LifeTable("AM92_AF92", "m", duration=0) # freshly underwritten
am92_d1 = LifeTable("AM92_AF92", "m", duration=1) # 1 year since underwriting
am92_ult = LifeTable("AM92_AF92", "m", duration="ult") # ultimate column
print(f"start_duration: {am92_d0.start_duration}") # 0
print(f"select_period: {am92_d0.select_period}") # 2
print(f"qx(17) [duration=0]: {am92_d0.qx(17):.6f}") # 0.000427
The duration= parameter is supported by all three table classes.
See Table Taxonomy for the full Select-Ultimate type classification.
Disability table#
DisabilityTable models the annual probability of transitioning from an active (healthy)
state into disability. It is the standard tool for income-protection and long-term care
products. The constructor signature is identical to LifeTable; the only difference is
that the file must contain disability-incidence columns (ix_m, ix_f, or ix_u):
from lactuca import DisabilityTable
# PEAI 2007 IAP individual — Spanish disability incidence (ages 18–64, period)
disability = DisabilityTable("PEAI2007_IAP_Ind", "m")
print(f"Disability ix at age 18: {disability.ix(18):.8f}")
Exit (withdrawal) table#
ExitTable models the annual probability of voluntarily leaving the insured portfolio
— policy surrender, employment termination, or any other cause of exit that is neither
death nor disability. The constructor signature again mirrors LifeTable; the table
file must contain exit-rate columns (ox_m, ox_f, or ox_u):
from lactuca import ExitTable
exit_tbl = ExitTable("DummyEXIT", "m")
print(f"Exit ox at age 18: {exit_tbl.ox(18):.4f}")
Unisex blend#
All three table classes (LifeTable, DisabilityTable, ExitTable) support sex="u"
with a unisex_blend weight in [0.0, 1.0] to blend male and female decrement rates.
The formula applied is \(q_u = w \cdot q_m + (1 - w) \cdot q_f\), where \(w\) = unisex_blend
(0.0 = all female, 0.5 = equal blend, 1.0 = all male):
from lactuca import LifeTable, DisabilityTable, ExitTable
# Life table — 60 % male / 40 % female blend (PASEM 2020)
pasem_u = LifeTable("PASEM2020_Rel_1o", "u", unisex_blend=0.6)
# Disability table — 50/50 blend
disability_u = DisabilityTable("PEAI2007_IAP_Ind", "u", unisex_blend=0.5)
# Exit table — 40 % male / 60 % female blend
exit_u = ExitTable("DummyEXIT", "u", unisex_blend=0.4)
print(f"pasem_u.unisex_blend: {pasem_u.unisex_blend}")
print(f"disability_u.unisex_blend: {disability_u.unisex_blend}")
print(f"exit_u.unisex_blend: {exit_u.unisex_blend}")
Note
unisex_blend requires the table to carry both male and female rates (qx_m and qx_f).
Tables that include only one sex — for example the Chilean CMF tables (CB_H_2020 ♂,
RV_M_2020 ♀) or test tables like DummyLIFE_1Sm — cannot be blended and will raise a
ValueError.
Inspect a loaded table#
Once a table is loaded, its key attributes are accessible as properties. You can also create both male and female instances in a single call by passing a tuple of sex codes as the second argument — each instance is fully independent:
from lactuca import LifeTable
ltm, ltf = LifeTable("PASEM2020_Rel_1o", ("m", "f")) # returns two independent instances
print(ltm.summary()) # formatted overview of the table
print(ltm.table_name) # "PASEM2020_Rel_1o"
print(ltm.table_type) # "life"
print(ltm.omega) # terminal age ω
print(f"ltm.sex = {ltm.sex}, ltf.sex = {ltf.sex}")
For zip-mode broadcast rules, Cartesian-product grids (cartesian=True),
dict returns (return_dict=True), lactuca.TableKey lookup, and heterogeneous
table pairing in zip mode, see Using Actuarial Tables (Heterogeneous base tables (zip mode),
Cartesian-product creation).
Accessing raw table data with TableSource#
Every LifeTable links to its TableSource metadata and decrement arrays via the .table property
(which returns a TableSource object with metadata and the raw decrement arrays):
from lactuca import LifeTable
pasem = LifeTable("PASEM2020_Rel_1o", "m")
ts = pasem.table # TableSource instance
print(ts.table_name) # "PASEM2020_Rel_1o"
print(ts.valid_sexes) # ['f', 'm']
print(ts.data.head()) # first rows of the Polars DataFrame
You can also load a TableSource directly when you only need to inspect a file:
from lactuca import TableSource
ts = TableSource("PASEM2020_Rel_1o") # .ltk extension optional
print(ts) # formatted summary of all metadata
For the full attribute reference see lactuca.TableSource.
Default interest rate#
LifeTable carries an optional interest_rate attribute
that serves as the default for all annuity, insurance, and endowment calculations.
It can be set at construction time or assigned later:
from lactuca import LifeTable, InterestRate
# At construction time
pasem_3pct = LifeTable("PASEM2020_Rel_1o", "m", interest_rate=0.03)
print(f"pasem_3pct.interest_rate = {pasem_3pct.interest_rate}")
pasem = LifeTable("PASEM2020_Rel_1o", "m")
print(f"pasem.interest_rate = {pasem.interest_rate}")
# Or assign afterwards
pasem.interest_rate = 0.03
print(f"pasem.interest_rate = {pasem.interest_rate}")
# Variable rate curve as default
pasem.interest_rate = InterestRate(terms=[5, 5], rates=[0.02, 0.025, 0.035])
print(f"pasem.interest_rate = {pasem.interest_rate}")
Passing ir= explicitly in any method call overrides the table default for that call
only; interest_rate is left unchanged:
from lactuca import LifeTable
lt = LifeTable("PER2020_Ind_1o", "m", cohort=1961)
lt.interest_rate = 0.03
a_default = lt.äx(65) # uses 0.03
a_override = lt.äx(65, ir=0.05) # uses 0.05; interest_rate still 0.03
print(f"a_default = {a_default:.6f}")
print(f"a_override = {a_override:.6f}")
When creating multiple instances at once with vectorial syntax, interest_rate= is
assigned to every instance in the same call — see Default interest rate in vectorial creation in
Using Actuarial Tables. The same vectorial constructor also supports
cartesian=True and return_dict=True for study grids and structured lookup — see
Cartesian-product creation.
Survival and mortality probabilities#
The simplest call returns a single probability for a single age. The same methods also accept multiple ages at once or no argument at all to return the complete vector for every age in the table.
Single integer age#
from lactuca import LifeTable
lt = LifeTable("PASEM2010", "u", unisex_blend=0.45)
# Annual survival probability: ₁p₅₀
p50 = lt.px(50)
print(f"p50 = {p50:.6f}")
# Annual mortality probability: q₅₀ (= 1 − p₅₀ for life tables)
q50 = lt.qx(50)
print(f"q50 = {q50:.6f}")
All ages — complete table vector#
Call without arguments to retrieve probabilities for every age from 0 through ω. The result is a NumPy array indexed by age:
from lactuca import LifeTable
import matplotlib.pyplot as plt
lt = LifeTable("PASEM2010", "u", unisex_blend=0.45)
all_px = lt.px() # NDArray, length ω + 1; all_px[50] == p50
all_qx = lt.qx() # NDArray, same length
# Example: plot qx on a log scale
plt.semilogy(all_qx, label="PASEM 2010 Unisex")
plt.xlabel("Age")
plt.ylabel("$q_x$")
plt.legend()
plt.show()
Note
This plot example requires matplotlib, which is not a core Lactuca dependency.
Install with pip install matplotlib if needed.
Multiple ages at once#
Pass a list or NumPy array to compute probabilities for several ages simultaneously. The return value is a 1-D NumPy array in the same order as the input:
from lactuca import LifeTable
lt = LifeTable("PASEM2010", "f")
ages = [40, 50, 60, 65, 70]
px_arr = lt.px(ages) # NumPy array of 5 survival probabilities
qx_arr = lt.qx(ages) # NumPy array of 5 mortality probabilities
print(px_arr)
print(qx_arr)
Survivors and deaths: lx and dx#
Two fundamental table quantities complement the probability methods: \(\ell_x\) (number of survivors at exact age \(x\) from a radix cohort) and \(d_x = l_x - l_{x+1}\) (deaths between consecutive integer ages). Fractional ages are supported via \(l_x\) interpolation. For the full definitions — including the mid-year lives \(L_x\) and its role in life expectancy — see Actuarial Formulas.
from lactuca import LifeTable
pasem = LifeTable("PASEM2020_Rel_1o", "m")
# Survivors and deaths at integer ages
lx_65 = pasem.lx(65)
dx_65 = pasem.dx(65)
# Complete vectors for all ages (integers 0 … ω)
all_lx = pasem.lx() # NDArray, length ω + 1
all_dx = pasem.dx() # NDArray, length ω + 1
# Multiple ages at once
lx_arr = pasem.lx([50, 55, 60, 65])
dx_arr = pasem.dx([50, 55, 60, 65])
Fractional ages#
All probability methods accept non-integer ages. Lactuca interpolates \(\ell_x\) between
integer ages using the method set in config.lx_interpolation: "linear" (equivalent
to UDD; default) or "exponential" (constant force of mortality). See
lx Interpolation for the full derivation and comparison:
from lactuca import LifeTable
pasem = LifeTable("PASEM2020_Rel_1o", "m")
# Survival from exact age 50.5 to exact age 51.5
p50_half = pasem.px(50.5)
# Quarter-year grid
frac_ages = [50.0, 50.25, 50.50, 50.75]
p50_quarters = pasem.px(frac_ages) # NumPy array of 4 values
Sub-annual frequency#
Use the m parameter to obtain the per-period probability for payment frequency m.
Accepted values are 1, 2, 3, 4, 6, 12, 14, 24, 26, 52, 365 (not every integer
between 1 and 365). Common values include 2, 4, 6, 12, 26, 52, and 365; the full list
is in Calculation Modes:
from lactuca import LifeTable
pasem = LifeTable("PASEM2020_Rel_1o", "m")
# Monthly survival probability at age 50: (1/12)p₅₀
p50_monthly = pasem.px(50, m=12)
# Quarterly mortality probability
q50_quarterly = pasem.qx(50, m=4)
Multi-period survival and mortality#
tpx and tqx compute \({}_{t}p_x\) and \({}_{t}q_x\) for an arbitrary duration t
(keyword-only argument). With t=1 they are equivalent to px and qx:
from lactuca import LifeTable
pasem = LifeTable("PASEM2020_Rel_1o", "m")
# 10-year survival: ₁₀p₅₀
ten_px = pasem.tpx(50, t=10)
# 5-year mortality: ₅q₄₀
five_qx = pasem.tqx(40, t=5)
# Fractional t — 18-month survival from age 50
p50_18m = pasem.tpx(50, t=1.5)
Pass an array to t to compute a whole survival curve in a single call:
from lactuca import LifeTable
import numpy as np
pasem = LifeTable("PASEM2020_Rel_1o", "m")
t_values = np.arange(1, 21) # t = 1, 2, ..., 20
survival_curve = pasem.tpx(50, t=t_values) # 1-D array, length 20
Pass arrays to both x and t to get a 2-D matrix (rows = starting ages,
columns = durations):
from lactuca import LifeTable
pasem = LifeTable("PASEM2020_Rel_1o", "m")
ages = [50, 55, 60, 65]
durations = [5, 10, 15, 20]
tpx_matrix = pasem.tpx(ages, t=durations) # shape (4, 4)
Life expectancy#
ex() computes the complete (entire) expectation of life \(\mathring{e}_x\) at integer
ages via the discrete approximation \(\mathring{e}_x = T_x / l_x\), where
\(T_x = \sum_{k=x}^{\omega-1} L_k\) and \(L_k \approx (l_k + l_{k+1})/2\) under UDD.
ex_continuous() evaluates the same formula at fractional starting ages using the
trapezoidal rule with m sub-intervals per year (default m=12).
For the complete mathematical derivation see Actuarial Formulas:
from lactuca import LifeTable
pasem = LifeTable("PASEM2020_Rel_1o", "m")
# Complete life expectancy at integer age 65: ẽ₆₅
e_65 = pasem.ex(65)
print(f"Complete life expectancy at 65: {e_65:.2f} years")
# Complete life expectancy at a fractional age (ex_continuous requires non-integer ages)
e_65_cont = pasem.ex_continuous(65.5, m=12)
print(f"Complete life expectancy at 65.5: {e_65_cont:.2f} years")
Curtate expectation \(e_x\) — ex_curtate#
Textbooks often denote curtate future lifetime by plain \(e_x =
\sum_{k=1}^{\omega-x} {}_kp_x\) (whole years survived). Use
ex_curtate() for this quantity; ex() returns the complete
\(\mathring{e}_x = T_x/l_x\) instead — typically about \(0.5\) years
higher under UDD (\(\mathring{e}_x \approx e_x + \tfrac{1}{2}\)). See
Curtate e_x — ex_curtate in Commutation Functions and
Life expectancy in Actuarial Formulas.
Commutation functions#
Commutation functions — \(D_x\), \(N_x\), \(C_x\), \(M_x\) and their higher-order sums \(S_x\),
\(R_x\) — absorb discount factors and survival probabilities into reusable, age-indexed
building blocks. They enable classical closed-form expressions such as
\(\ddot{a}_x = N_x / D_x\) and \(A_x = M_x / D_x\), and are available as standalone
methods for custom calculations and for verifying results against classical formulas.
Note that Lactuca’s annuity and insurance methods (äx, ax, Ax, etc.) do not
use commutation functions internally — all calculation modes, including
discrete_precision, always evaluate exact payment-grid summation over survival
probabilities and discount factors. For the full dependency chain and usage, see
Commutation Functions and Calculation Modes.
from lactuca import LifeTable
ir = 0.03
pasem = LifeTable("PASEM2020_Rel_1o", "m", interest_rate=ir)
# Dx, Nx, Cx, Mx at age 50 with 3% annual effective interest
Dx_val = pasem.Dx(50)
Nx_val = pasem.Nx(50)
Cx_val = pasem.Cx(50)
Mx_val = pasem.Mx(50)
print(f"Dx(50) = {Dx_val:.4f}")
print(f"Nx(50) = {Nx_val:.4f}")
print(f"Cx(50) = {Cx_val:.6f}")
print(f"Mx(50) = {Mx_val:.4f}")
Interest rates#
InterestRate encapsulates an effective annual rate used for discounting and
accumulation. It supports constant rates, piecewise term structures (yield curves),
and named scenario collections. Any LifeTable method accepts either an
InterestRate object or a plain float in the ir= argument; a plain float is
wrapped automatically. For the complete API — discount factors, accumulation,
nominal-rate conversions, and scenarios — see Interest Rates.
Multi-scenario containers resolve the active scenario at each calculation call; see
Scenarios with LifeTable and batch methods in Interest Rates.
Constant rate#
from lactuca import InterestRate
# 3% annual effective rate
ir = InterestRate(0.03)
Piecewise term structure#
from lactuca import InterestRate
# 5 years at 2%, next 5 years at 2.5%, then 3.5% thereafter
ir_term = InterestRate(terms=[5, 5], rates=[0.02, 0.025, 0.035])
# Discount factor for t = 7 years
v7 = ir_term.vn(7)
print(f"v(7) = {v7:.6f}")
Growth rates#
GrowthRate models benefit escalation at policy-year anniversaries (not each
sub-annual payment). It is not limited to escalating benefits: common
applications include pension CPI indexation, salary-scale projections, guaranteed
annual increases, and any context where the benefit amount changes from one
anniversary to the next. Like InterestRate, it supports constant rates, piecewise
schedules, geometric or arithmetic growth, and multi-scenario containers. For the
complete API, see Growth Rates.
Constant growth#
from lactuca import GrowthRate
# Geometric (compound) growth: F(t) = (1+g)^t
gr_geom = GrowthRate(0.02)
print(f"geometric factor(3) = {gr_geom.factor(3):.4f}") # (1.02)^3 ≈ 1.0612
# Arithmetic (linear additive) growth: F(t) = 1 + g·t
gr_arith = GrowthRate(0.02, growth_type='a')
print(f"arithmetic factor(3) = {gr_arith.factor(3):.4f}") # 1 + 0.02 × 3 = 1.06
Piecewise schedule#
from lactuca import GrowthRate
# 1% for the first year, 2% thereafter (e.g. CPI step schedule)
gr = GrowthRate(rates=[0.01, 0.02], terms=[1])
print(f"factor(1) = {gr.factor(1):.6f}") # 1.01 — anniversary 1
print(f"factor(2) = {gr.factor(2):.6f}") # 1.0302 — 1.01 × 1.02
print(f"factor(3) = {gr.factor(3):.6f}") # 1.050804 — 1.01 × 1.02²
Configuration#
Lactuca exposes a global singleton config that controls all calculations in the
session: calculation mode (discrete vs. continuous), interpolation method for
fractional ages, numerical rounding precision per output type, and the file-system
path for custom table files. Changes take effect immediately and persist until
explicitly reset or the process restarts.
from lactuca import config
# Calculation mode
# Options: 'discrete_precision', 'discrete_simplified',
# 'continuous_precision', 'continuous_simplified'
config.calculation_mode = "discrete_precision"
# Interpolation method for fractional ages: 'linear' or 'exponential'
config.lx_interpolation = "linear"
# Rounding precision for individual output types
config.decimals.qx = 6 # mortality probability
config.decimals.Dx = 4 # commutation function Dx
config.decimals.annuities = 8 # annuity present values
# Days per year convention for date calculations
config.days_per_year = 365.25
# Path to custom bundled table files (`.ltk`); defaults to the package tables directory
# config.tables_path = "/path/to/my/custom/tables"
config.reset_to_defaults() # restore defaults after the illustrative settings above
For the full list of settings, TOML-file format, and the persistence API, see Configuration.
Life annuities#
A life annuity pays a recurring benefit while the annuitant survives. Lactuca
provides four standard variants — whole life, temporary, deferred, and continuous
— at any payment frequency \(m\) and with optional benefit escalation (gr=). When
interest_rate is set on the table, the ir= argument can be omitted.
Whole life annuities#
from lactuca import LifeTable
lt = LifeTable("PER2020_Ind_1o", "m", cohort=1961, interest_rate=0.03)
# Whole life annuity-due (annual payments): äx(65)
adue = lt.äx(65)
# Monthly payments: äx^(12)(65)
amonthly = lt.äx(65, m=12)
# Annuity-immediate (payment at end of period): ax(65)
aimmediate = lt.ax(65)
# Geometrically escalating (2% p.a.): plain float is auto-wrapped as GrowthRate(0.02)
agrowth = lt.äx(65, gr=0.02)
Temporary annuities#
from lactuca import LifeTable, GrowthRate
lt = LifeTable("PER2020_Ind_1o", "m", cohort=1961, interest_rate=0.03)
# 20-year temporary annuity-due: ä65:20|
atemp = lt.äx(65, n=20)
# 15-year temporary, monthly payments: ä50:15|^(12)
atemp_monthly = lt.äx(50, n=15, m=12)
# With m=12, a 2% growth step applies once per policy year (all 12 monthly payments
# in year k share factor (1+g)^k), not each month — see {doc}`growth_rates_guide`.
# Simplest growth: plain float auto-wrapped as GrowthRate(0.02) — geometric compound
atemp_gr_float = lt.äx(65, n=20, gr=0.02)
# Equivalent explicit form: GrowthRate object (geometric, 2% p.a.)
atemp_geom = lt.äx(65, n=20, gr=GrowthRate(0.02))
# Arithmetic salary-scale (flat 2 pp additive per year)
atemp_arith = lt.äx(65, n=20, gr=GrowthRate(0.02, growth_type='a'))
Deferred annuities#
from lactuca import LifeTable, config
config.decimals.annuities = 4
lt = LifeTable("PER2020_Ind_1o", "m", cohort=1971, interest_rate=0.03)
# 10-year deferred whole life annuity: 10|äx(55)
adef = lt.äx(55, d=10)
print(adef) # → 13.4328
# 5-year deferred, 20-year temporary: 5|ä60:20|
adef_temp = lt.äx(60, n=20, d=5)
print(adef_temp) # → 12.2533
config.reset_to_defaults()
For deferred insurances, fractional deferment, multi-frequency examples, and multi-life deferred products (joint annuities and first-death insurances), see Deferred Life Contingencies.
Continuous annuities#
Continuous mode is activated globally via config.calculation_mode:
from lactuca import LifeTable, config
lt = LifeTable("PER2020_Ind_1o", "m", cohort=1961, interest_rate=0.03)
config.calculation_mode = "continuous_precision"
# Continuous whole life annuity: āx(65)
acont = lt.äx(65)
# Continuous temporary: ā65:20|
acont_temp = lt.äx(65, n=20)
config.calculation_mode = "discrete_precision" # restore default
The default mode is "discrete_precision". Changing it affects all subsequent calls
in the session until explicitly reset. For all available modes and their trade-offs,
see Calculation Modes and Configuration.
Life insurances#
Whole life and term insurance#
from lactuca import LifeTable
lt = LifeTable("PASEM2020_Rel_1o", "m", interest_rate=0.03)
# Discrete whole life insurance: Ax(50)
Ax_val = lt.Ax(50)
# 20-year term insurance: A¹x:20|(50)
Aterm = lt.Ax(50, n=20)
# Geometrically escalating term insurance (sum insured grows 2% per anniversary year)
Aterm_geom = lt.Ax(50, n=20, gr=0.02)
Continuous insurance#
from lactuca import LifeTable, config
pasem = LifeTable("PASEM2020_Rel_1o", "m")
config.calculation_mode = "continuous_precision"
# Continuous whole life: Āx(50)
Ax_cont = pasem.Ax(50, ir=0.03)
config.calculation_mode = "discrete_precision"
Pure endowment#
from lactuca import LifeTable
pasem = LifeTable("PASEM2020_Rel_1o", "m")
# 10-year pure endowment: 10Ex(40)
nEx_val = pasem.nEx(40, n=10, ir=0.03)
Multi-life annuities (äxy, äjoint, …) and first-death insurances (Axy, Axyz,
Afirst) are covered in Joint-Life Calculations. Last-survivor products are
derivable via inclusion-exclusion (no dedicated method).
Disability tables#
A disability (incidence) table models the annual rate at which active lives transition
into a disabled state. All constructor parameters — sex=, cohort=, duration=,
unisex_blend= — follow the same conventions as LifeTable.
DisabilityTable exposes the disability incidence probability via ix(), which
accepts the same argument forms as qx() on a life table:
from lactuca import DisabilityTable
# PEAI 2007 IAP individual (Spanish disability table, period; ages 18–64)
dt = DisabilityTable("PEAI2007_IAP_Ind", "m")
# Annual disability incidence at age 40: ix(40)
ix_val = dt.ix(40)
# Multiple ages at once
ix_arr = dt.ix([40.1012, 45.25, 50, 55])
# Complete incidence vector for all ages
all_ix = dt.ix()
# Monthly disability incidence: _(1/12)ix^(12)(40)
ix_monthly = dt.ix(40, m=12)
Exit tables#
Exit tables model the withdrawal or lapse rate — the probability that a policyholder
or pension-scheme member leaves the active portfolio for a reason other than death
(e.g., voluntary surrender or employment termination). Constructor parameters follow
the same conventions as LifeTable and DisabilityTable.
ExitTable exposes the withdrawal probability via ox(), with the same call patterns
as qx() and ix():
from lactuca import ExitTable
et = ExitTable("DummyEXIT", "m")
# Annual withdrawal probability at age 45
ox_val = et.ox(45)
# Multiple ages
ox_arr = et.ox([30.21, 40.30, 50])
# Complete withdrawal vector for all ages
all_ox = et.ox()
Functional API#
All calculation methods are also available as standalone module-level functions in
lactuca.functional — useful in pipelines and custom workflows. The functional
form takes the table instance as the first argument; all remaining arguments are
identical to the method call:
from lactuca.functional import äx, ax, Ax, nEx, lx, qx, ix, ox
from lactuca import LifeTable, DisabilityTable, ExitTable
ir = 0.03
lt_ann = LifeTable("PER2020_Ind_1o", "m", cohort=1961, interest_rate=ir)
lt_ins = LifeTable("PASEM2020_Rel_1o", "m", interest_rate=ir)
dt = DisabilityTable("PEAI2007_IAP_Ind", "m")
et = ExitTable("DummyEXIT", "m")
äx(lt_ann, 65) # whole-life annuity-due (longevity / PER)
ax(lt_ann, 65) # whole-life immediate annuity
Ax(lt_ins, 50) # whole-life insurance (life-risk / PASEM)
nEx(lt_ins, 40, n=10) # 10-year pure endowment
lx(lt_ann, 65) # survivors at age 65
qx(lt_ins, 50) # annual mortality probability at 50
ix(dt, 40) # annual disability incidence at 40
ox(et, 45) # annual withdrawal probability at 45
For the complete function reference, see Functional API.
Time shift (ts) for off-anniversary reserves#
The ts parameter records how many years have elapsed since contract
inception; the calculation proceeds as if that shift has already occurred. Its
primary use is computing reserves at a valuation date other than an exact policy
anniversary — for example, when 3.25 years have elapsed since inception and the
next anniversary falls in 0.75 years.
ts is conceptually different from the t argument of tpx/tqx: t is the
duration of a survival interval, whereas ts shortens the remaining term and
re-anchors survival and discount factors to the valuation date (for d=0, this is
equivalent to calling at attained age \(x + ts\) with remaining term \(n_\text{eff}=\max(n-ts,\,0)\); in general \(n_\text{eff}=\max(n-\max(ts-d,\,0),\,0)\) and \(d_\text{eff}=\max(d-ts,\,0)\) — see Prospective Reserves and the ts Parameter and Last Payment Adjustment).
from lactuca import LifeTable
lt = LifeTable("PER2020_Ind_1o", "m", cohort=1976, interest_rate=0.03)
# Mid-year reserve: 15-year annuity-due at age 50, 6 months into the policy year
a_midyear = lt.äx(50, n=15, ts=0.5)
# Off-anniversary reserve: 3.25 years since inception (next anniversary in 0.75 yr)
a_offann = lt.äx(50, n=10, ts=3.25)
When ts is fractional and a GrowthRate is active (gr= specified), Lactuca
emits a UserWarning as a reminder that benefit revaluation follows whole-year
anniversaries, not fractional ones. Without a GrowthRate, fractional ts is
silently accepted. To reject fractional shifts entirely, set
config.force_integer_ts = True. Full details in Growth Rate Conventions.
Next steps#
Use Lactuca with AI assistants: Working with AI assistants (context files for Cursor, Copilot, ChatGPT)
Explore the API Reference for detailed API documentation
Check Actuarial Formulas for mathematical foundations
See Bundled Actuarial Tables for the complete list of bundled tables
Pricing a portfolio? See Batch Calculations for batch mode and BEL calculations
Inspect per-payment flows? See Inspecting Cash Flows
Review Changelog for version history
Support#
For bug reports, feature requests, and questions, see the Support page.