Cookbook#
Practical, self-contained recipes for common actuarial tasks using Lactuca.
Each snippet below is copy-paste ready: imports, table identifiers, and parameters match the bundled catalogue. Run them in an activated Python session (see Activation Guide).
Tables are loaded by string identifier and sex code ('m', 'f', or 'u').
See Bundled Actuarial Tables for the full catalogue of available tables.
For bulk portfolio work, see Bulk portfolio calculations in Using Actuarial Tables.
Recipes#
# |
Topic |
Pattern |
|---|---|---|
1 |
Term life insurance |
|
2 |
Whole-life annuity-due |
|
3 |
Monthly pension liability |
|
4 |
Joint-life / last-survivor pension |
vectorized |
5 |
Interest-rate sensitivity |
multi-scenario |
6 |
Static vs generational mortality |
|
7 |
Tiered benefit schedule |
|
8 |
Complete expectation of life |
|
9 |
Portfolio liability (plain Python) |
loop + cohort setter |
10 |
Portfolio liability (Polars) |
|
11 |
Polars |
fixed table, expression pipeline |
12 |
Portfolio liability (Pandas) |
|
13 |
Portfolio liability (batch API) |
functional |
14 |
Net annual premium |
|
15 |
Deferred pension |
|
16 |
Prospective reserve |
|
17 |
Group loop with deferred construction |
|
18 |
Heterogeneous batch with |
|
1. Price a term life insurance#
Net single premium for a 20-year term life insurance on a male aged 45,
using the PASEM2020 sector aggregate general life-risk table NoRel (first order) and a 3 % flat rate.
Note
The classical summation above uses end-of-year discounting (\(v^{k+1}\)). Lactuca’s default
config.mortality_placement is "mid" (\(f=0.5\)), so lt.Ax(...) values the benefit at
\(k+f\) unless you set config.mortality_placement = "end". See Commutation Functions.
Pass n=20 to Ax() for a temporary insurance; omit n
for whole-life.
from lactuca import LifeTable
lt = LifeTable('PASEM2020_NoRel_1o', 'm', interest_rate=0.03)
term_pv = lt.Ax(x=45, n=20)
print(f"Term insurance APV (A^1_45:20) = {term_pv:.6f}")
2. Whole-life annuity-due#
Present value of a unit whole-life annuity-due for a female aged 60
(born 1966; valuation year ≈ 2026), using the Spanish longevity table
PER2020_Ind_1o (first order):
from lactuca import LifeTable
lt = LifeTable('PER2020_Ind_1o', 'f', cohort=1966, interest_rate=0.03)
a_due = lt.äx(x=60)
print(f"Whole-life annuity-due (a_dd_60) = {a_due:.6f}")
3. Pension liability (monthly payments, generational mortality)#
Value a pension of €1 000 per month for a male aged 65 born in 1961, using the generational individual table PER2020 (first order):
from lactuca import LifeTable
lt = LifeTable('PER2020_Ind_1o', 'm', cohort=1961, interest_rate=0.02)
monthly_annuity = lt.äx(x=65, m=12) # unit annual benefit, m-thly (IAA ä_x^{(12)})
liability = 1_000 * 12 * monthly_annuity # EUR 1 000/month = EUR 12 000/year
print(f"Pension PV = EUR {liability:,.2f}")
Note
äx(..., m=12) follows standard actuarial notation: it values one currency unit
per year payable in 12 instalments (not one unit per monthly payment).
Scale by the annual pension — here 1_000 * 12 — or equivalently
12_000 * monthly_annuity.
4. Joint-life pension (couple, vectorized instantiation)#
Create male and female tables in a single call. The last-survivor annuity follows from the standard identity:
from lactuca import LifeTable
# Vectorized instantiation: two LifeTable objects in one call
# Ages 65/62 in 2026 → cohorts 1961 / 1964
lt_m, lt_f = LifeTable(
'PER2020_Ind_1o', ('m', 'f'), cohort=[1961, 1964], interest_rate=0.02
)
a_x = lt_m.äx(x=65, m=12)
a_y = lt_f.äx(x=62, m=12)
a_joint = lt_m.äxy(ages=[65, 62], table_y=lt_f, m=12) # both survive
a_ls = a_x + a_y - a_joint # last survivor
print(f"Individual (male 65): {a_x:.4f}")
print(f"Individual (female 62): {a_y:.4f}")
print(f"Joint-life (both alive): {a_joint:.4f}")
print(f"Last-survivor: {a_ls:.4f}")
See Joint-Life Calculations for first-death insurances
(Axy(), Afirst())
and other derivable joint-life formulas.
5. Interest rate sensitivity#
Compare annuity values across named interest rate scenarios using a multiscenario
InterestRate — the natural pattern for sensitivity analyses and Solvency II stress tests.
Pattern A — iterate sub-curves. Extract each simple scenario and pass it as ir=:
from lactuca import LifeTable, InterestRate
ir = InterestRate({
'base': 0.03,
'adverse': 0.01,
'stressed': 0.00,
})
lt = LifeTable('GRMF95', 'm')
for name, scenario in ir.scenarios.items():
value = lt.äx(x=65, ir=scenario)
print(f"{name:<10} i = {scenario.rate:.2%} -> a_dd_65 = {value:.4f}")
Pattern B — switch active_scenario on a shared container. Attach the
multi-scenario object once (to LifeTable or via ir=) and switch scenarios in place:
from lactuca import LifeTable, InterestRate
ir = InterestRate({
'base': 0.03,
'adverse': 0.01,
'stressed': 0.00,
})
lt = LifeTable('GRMF95', 'm', interest_rate=ir)
ir.active_scenario = 'base'
bel_base = lt.äx(65)
ir.active_scenario = 'stressed'
bel_stressed = lt.äx(65)
print(f"base: {bel_base:.4f} stressed: {bel_stressed:.4f}")
Both patterns yield the same PV per scenario. Pattern B avoids allocating separate
InterestRate wrappers in a loop; Pattern A makes each scenario explicit at the call site.
See Scenarios with LifeTable and batch methods for copy() snapshotting and batch semantics.
Note
scenario.rate is defined for constant sub-curves (as in this example).
For piecewise scenarios, use scenario.get_rate(t) or inspect scenario.rates.
6. Static vs. generational mortality#
Compare a static (period) table with a generational longevity table.
A 65-year-old in 2026 was born in 1961. The generational table PER2020_Ind_2o
embeds projection improvements; GRMF95 is a classical static table (no cohort):
from lactuca import LifeTable
lt_static = LifeTable('GRMF95', 'm', interest_rate=0.03)
lt_cohort = LifeTable('PER2020_Ind_2o', 'm', cohort=1961, interest_rate=0.03)
a_static = lt_static.äx(x=65)
a_cohort = lt_cohort.äx(x=65)
print(f"Static annuity: {a_static:.4f}")
print(f"Generational annuity: {a_cohort:.4f}")
print(f"Difference: {a_cohort - a_static:.4f}")
7. Custom benefit schedule#
Value a pension that pays €12 000/year for 10 years, then €8 000/year thereafter,
using ax (annuity-immediate), which supports custom cashflow schedules:
from lactuca import LifeTable, payment_times, tiered_amounts
lt = LifeTable('PER2020_Ind_1o', 'm', cohort=1961, interest_rate=0.03)
times = payment_times(n=40, m=1)
amounts = tiered_amounts(times, breakpoints=[10], values=[12_000.0, 8_000.0])
pv = lt.ax(x=65, cashflow_times=times, cashflow_amounts=amounts)
print(f"Custom benefit PV = EUR {pv:,.2f}")
Note
cashflow_times is supported only on immediate (postpayable) methods such as
ax() and requires
calculation_mode='discrete_precision' (the default). Omit n and keep m=1
when supplying a custom schedule. See Irregular Cashflows.
8. Complete expectation of life#
Compute \(\mathring{e}_x\) at an integer age and at a fractional age.
ex_continuous only accepts fractional ages; pass 65.5, not 65:
from lactuca import LifeTable
lt = LifeTable('PASEM2020_Rel_1o', 'm')
ex_int = lt.ex(65)
ex_frac = lt.ex_continuous(65.5)
print(f"e_65 = {ex_int:.2f} years (integer age, discrete)")
print(f"e_65.5 = {ex_frac:.2f} years (fractional age, continuous)")
9. Portfolio liability valuation#
Compute the present value of a pension portfolio from a plain Python list of
records — no external dependencies beyond Lactuca.
Ages are derived with alb, the cohort setter is updated inside the loop
only when it changes, following the bulk-portfolio approach from
Using Actuarial Tables.
from lactuca import LifeTable, GrowthRate, alb
VALUATION_DATE = '2026-04-09'
# Pension portfolio: birth date, sex, annual pension,
# term in years (n=None -> whole-life), escalation rate.
portfolio = [
{'id': 'P001', 'birth': '1955-03-15', 'sex': 'm', 'pension': 18_000, 'n': 25, 'g': 0.020},
{'id': 'P002', 'birth': '1958-11-22', 'sex': 'f', 'pension': 12_000, 'n': None, 'g': 0.010},
{'id': 'P003', 'birth': '1950-07-04', 'sex': 'm', 'pension': 24_000, 'n': 20, 'g': 0.025},
{'id': 'P004', 'birth': '1962-01-30', 'sex': 'f', 'pension': 9_600, 'n': None, 'g': 0.000},
{'id': 'P005', 'birth': '1957-09-10', 'sex': 'm', 'pension': 15_000, 'n': 28, 'g': 0.015},
]
# Vectorized age and cohort (birth year) derivation
births = [p['birth'] for p in portfolio]
ages_alb = alb(births, VALUATION_DATE) # NDArray[float64]
for p, age in zip(portfolio, ages_alb):
p['age'] = int(age)
p['cohort'] = int(p['birth'][:4]) # birth year from ISO date string
# One LifeTable per sex; cohort updated per policy inside the loop
tables = {
'm': LifeTable('PER2020_Ind_1o', 'm', cohort=1950, interest_rate=0.03),
'f': LifeTable('PER2020_Ind_1o', 'f', cohort=1950, interest_rate=0.03),
}
# Sort by (sex, cohort) to minimise cohort-setter rebuilds
for p in sorted(portfolio, key=lambda r: (r['sex'], r['cohort'])):
lt = tables[p['sex']]
if lt.cohort != p['cohort']:
lt.cohort = p['cohort']
gr = GrowthRate(p['g']) if p['g'] else None
pv = p['pension'] * lt.äx(x=p['age'], n=p['n'], m=12, gr=gr)
p['pv'] = round(pv, 2)
total_liability = sum(p['pv'] for p in portfolio)
print(f"{'ID':<6} {'Sex':>3} {'Age':>4} {'Cohort':>6} {'Pension':>10} {'g':>5} {'PV':>14}")
print('-' * 52)
for p in portfolio:
print(f"{p['id']:<6} {p['sex']:>3} {p['age']:>4} {p['cohort']:>6} "
f"{p['pension']:>10,.0f} {p['g']:>5.1%} {p['pv']:>14,.2f}")
print('-' * 52)
print(f"{'Total liability':>42} {total_liability:>14,.2f}")
10. Portfolio liability valuation with Polars#
Compute the present value of a pension portfolio loaded as a Polars DataFrame
(simulating a read from Excel or any other tabular source).
Ages are derived with alb from the birth date column; the cohort setter
is updated inside the loop only when it changes, following the bulk-portfolio
approach from Using Actuarial Tables.
import polars as pl
from lactuca import LifeTable, GrowthRate, alb
VALUATION_DATE = '2026-04-09'
# In production: df = pl.read_excel("portfolio.xlsx", sheet_name="Policyholders")
df = pl.DataFrame({
'id': ['P001', 'P002', 'P003', 'P004', 'P005'],
'birth': ['1955-03-15', '1958-11-22', '1950-07-04', '1962-01-30', '1957-09-10'],
'sex': ['m', 'f', 'm', 'f', 'm'],
'pension': [18_000.0, 12_000.0, 24_000.0, 9_600.0, 15_000.0],
'n': [25, None, 20, None, 28], # None -> whole-life annuity
'g': [0.020, 0.010, 0.025, 0.000, 0.015],
}).with_columns(
pl.col('birth').str.to_date() # parse ISO 8601 strings -> pl.Date
)
# Add age-at-last-birthday (vectorized) and cohort (birth year) columns
ages_alb = alb(df['birth'].to_list(), VALUATION_DATE) # NDArray[float64]
df = df.with_columns(
pl.Series('age', ages_alb).cast(pl.Int32),
pl.col('birth').dt.year().alias('cohort'),
)
# One LifeTable per sex; cohort will be updated inside the loop only when it changes
tables = {
'm': LifeTable('PER2020_Ind_1o', 'm', cohort=1950, interest_rate=0.03),
'f': LifeTable('PER2020_Ind_1o', 'f', cohort=1950, interest_rate=0.03),
}
# Sort by (sex, cohort) to minimise cohort-setter rebuilds
df_sorted = df.sort(['sex', 'cohort'])
pv_list = []
for row in df_sorted.iter_rows(named=True):
lt = tables[row['sex']]
if lt.cohort != row['cohort']:
lt.cohort = row['cohort']
gr = GrowthRate(row['g']) if row['g'] else None
pv = row['pension'] * lt.äx(x=row['age'], n=row['n'], m=12, gr=gr)
pv_list.append(round(pv, 2))
result = df_sorted.with_columns(pl.Series('pv', pv_list))
print(result.select(['id', 'sex', 'age', 'cohort', 'pension', 'g', 'pv']))
print(f"\nTotal liability: {result['pv'].sum():>14,.2f}")
11. Adding a present-value column with Polars map_elements#
Use Polars’ map_elements on a struct column to add a pv column inline within
an expression pipeline. This pattern is ideal when the table is fixed (single sex,
no cohort updates) and you want to keep the transformation inside a Polars chain.
import polars as pl
from lactuca import LifeTable, GrowthRate
lt = LifeTable('PER2020_Ind_1o', 'f', cohort=1961, interest_rate=0.03)
df = pl.DataFrame({
'id': ['A001', 'A002', 'A003'],
'age': [60, 62, 65],
'pension': [15_000.0, 12_000.0, 20_000.0],
'n': [30, 28, 25],
'g': [0.02, 0.01, 0.00],
})
result = df.with_columns(
pl.struct(['age', 'pension', 'n', 'g']).map_elements(
lambda row: row['pension'] * lt.äx(
x=row['age'],
n=row['n'],
m=12,
gr=GrowthRate(row['g']) if row['g'] else None,
),
return_dtype=pl.Float64,
).alias('pv')
)
print(result)
Note
map_elements applies a Python function to each element of the struct Series —
it is equivalent to a row-wise loop and does not unlock Polars parallelism.
Use it when you need the result as a Polars expression inside a pipeline
(with_columns, select, lazy frames, etc.).
For portfolios with cohort updates or multiple tables, prefer iter_rows(named=True)
as in recipe 10 (or the pure-Python variant in
recipe 9).
12. Portfolio valuation with Pandas#
The Pandas equivalent of recipe 9. Use itertuples (faster than iterrows) after
sorting by (sex, cohort) to minimise cohort-setter rebuilds.
import pandas as pd
from lactuca import LifeTable, GrowthRate, alb
VALUATION_DATE = '2026-04-09'
# In production: df = pd.read_excel("portfolio.xlsx", sheet_name="Policyholders")
df = pd.DataFrame({
'id': ['P001', 'P002', 'P003', 'P004', 'P005'],
'birth': ['1955-03-15', '1958-11-22', '1950-07-04', '1962-01-30', '1957-09-10'],
'sex': ['m', 'f', 'm', 'f', 'm'],
'pension': [18_000.0, 12_000.0, 24_000.0, 9_600.0, 15_000.0],
'n': [25, None, 20, None, 28], # None -> whole-life annuity
'g': [0.020, 0.010, 0.025, 0.000, 0.015],
})
df['birth'] = pd.to_datetime(df['birth'])
# Vectorized age and cohort columns
ages_alb = alb(df['birth'].tolist(), VALUATION_DATE) # NDArray[float64]
df['age'] = ages_alb.astype(int)
df['cohort'] = df['birth'].dt.year
# One LifeTable per sex
tables = {
'm': LifeTable('PER2020_Ind_1o', 'm', cohort=1950, interest_rate=0.03),
'f': LifeTable('PER2020_Ind_1o', 'f', cohort=1950, interest_rate=0.03),
}
# Sort to minimise cohort-setter rebuilds, then iterate with itertuples
df_sorted = df.sort_values(['sex', 'cohort']).reset_index(drop=True)
pv_list = []
for row in df_sorted.itertuples():
lt = tables[row.sex]
if lt.cohort != row.cohort:
lt.cohort = row.cohort
gr = GrowthRate(row.g) if row.g else None
n_val = None if pd.isna(row.n) else row.n
pv = row.pension * lt.äx(x=row.age, n=n_val, m=12, gr=gr)
pv_list.append(round(pv, 2))
df_sorted['pv'] = pv_list
print(df_sorted[['id', 'sex', 'age', 'cohort', 'pension', 'g', 'pv']].to_string(index=False))
print(f"\nTotal liability: {df_sorted['pv'].sum():>14,.2f}")
Note
df['birth'].tolist() on a datetime64 column yields pd.Timestamp objects,
which are a valid date type for alb. Passing the list directly avoids any
intermediate conversion.
itertuples iterates as named tuples (fields accessed as row.sex, row.age, etc.)
and is significantly faster than iterrows for large DataFrames.
13. Portfolio liability valuation (batch API)#
Vectorized alternative to recipes 9–12: one functional call prices the entire portfolio
without a Python loop over policies. Build one LifeTable per unique (sex, cohort) pair,
then pass a per-policy table list to äx() with benefits=.
from lactuca import LifeTable, TableKey, GrowthRate, alb, äx
VALUATION_DATE = '2026-04-09'
portfolio = [
{'id': 'P001', 'birth': '1955-03-15', 'sex': 'm', 'pension': 18_000, 'n': 25, 'g': 0.020},
{'id': 'P002', 'birth': '1958-11-22', 'sex': 'f', 'pension': 12_000, 'n': None, 'g': 0.010},
{'id': 'P003', 'birth': '1950-07-04', 'sex': 'm', 'pension': 24_000, 'n': 20, 'g': 0.025},
{'id': 'P004', 'birth': '1962-01-30', 'sex': 'f', 'pension': 9_600, 'n': None, 'g': 0.000},
{'id': 'P005', 'birth': '1957-09-10', 'sex': 'm', 'pension': 15_000, 'n': 28, 'g': 0.015},
]
births = [p['birth'] for p in portfolio]
ages_alb = alb(births, VALUATION_DATE)
for p, age in zip(portfolio, ages_alb):
p['age'] = int(age)
p['cohort'] = int(p['birth'][:4])
# One LifeTable per unique (sex, cohort) — not one per policy row
unique_pairs = sorted({(p['sex'], p['cohort']) for p in portfolio})
tables_by_key = LifeTable(
'PER2020_Ind_1o',
[s for s, _ in unique_pairs],
cohort=[c for _, c in unique_pairs],
return_dict=True,
interest_rate=0.03,
)
table_list = [
tables_by_key[TableKey('PER2020_Ind_1o', p['sex'], p['cohort'])]
for p in portfolio
]
ages = [p['age'] for p in portfolio]
n_list = [p['n'] for p in portfolio] # None -> whole-life (same as scalar)
gr_list = [GrowthRate(p['g']) if p['g'] else None for p in portfolio]
benefits = [p['pension'] for p in portfolio]
pv_arr = äx(table_list, ages, n=n_list, m=12, gr=gr_list, benefits=benefits)
for p, pv in zip(portfolio, pv_arr):
p['pv'] = round(float(pv), 2)
total_liability = float(pv_arr.sum())
print(f"Total liability (batch): {total_liability:>14,.2f}")
Note
benefits=. In batch mode, benefits= scales each policy’s unit annuity factor by the
annual pension (replacing the manual pension * lt.äx(...) from recipes 9–12). Unit PVs are
rounded per decimals.annuities before scaling.
Row vs column layout. portfolio is a list of row dicts (as in recipe 9) because it matches
typical JSON/CSV records and keeps per-policy enrichment (age, cohort, pv) simple.
The list comprehensions above extract parallel columns for the batch call.
If your data is already column-oriented — {'age': [...], 'n': [...], ...} — pass
portfolio['age'], portfolio['n'], and so on directly; gr still needs a
GrowthRate wrapper per element. For Excel or database sources, prefer recipes
10–12 (Polars or Pandas DataFrames).
Important
In both scalar and batch mode, n=None means whole-life — including
None or missing values in a per-policy n list or DataFrame column (Series).
You may also write np.inf explicitly; both forms are equivalent. Pure endowments
(nEx, …) require a finite term and reject whole-life sentinels.
See Batch Calculations for per-policy parameters, on_error='nan',
and aggregate cash-flow patterns.
15. Deferred pension to retirement age#
Value an annual pension of EUR 24 000 starting at age 65 for a member currently aged 52.
Pass the deferment in years as d= — payments begin at age x + d:
from lactuca import LifeTable
lt = LifeTable('PER2020_Ind_1o', 'm', cohort=1974, interest_rate=0.03)
current_age = 52
retirement_age = 65
defer_years = retirement_age - current_age
unit_annuity = lt.äx(x=current_age, d=defer_years, m=12)
annual_pension = 24_000
liability = annual_pension * unit_annuity
print(f"Unit deferred annuity (m=12) = {unit_annuity:.4f}")
print(f"Deferred pension PV = EUR {liability:,.2f}")
Note
d= is future deferment before the first payment. For valuation after issue,
use ts= instead (see recipe 16 and Deferred Life Contingencies).
Benefit is annual; äx(..., m=12) uses standard IAA notation (one unit per year
in 12 instalments) — scale by the annual amount directly.
16. Prospective reserve at policy anniversaries#
The prospective reserve at elapsed time \(t\) is future benefits minus future net premiums,
both evaluated from attained age \(x + t\). In Lactuca, pass the issue age x, full term n,
and elapsed years ts=t:
from lactuca import LifeTable
lt = LifeTable('PASEM2020_Rel_1o', 'm', interest_rate=0.03)
x, n = 40, 25
# Net level premium at issue (ts = 0)
P = lt.Ax(x, n=n) / lt.äx(x, n=n)
print(f"Net level premium P = {P:.6f}")
# Prospective reserve at selected anniversaries
print(f"\n{'t':>4} {'A(x+t)':>12} {'a_dd(x+t)':>12} {'tV':>12}")
for t in (0, 5, 10, 15, 20, 25):
At = lt.Ax(x, n=n, ts=t)
at = lt.äx(x, n=n, ts=t)
tV = At - P * at
print(f"{t:>4} {At:>12.6f} {at:>12.6f} {tV:>12.6f}")
Note
At ts=0 the reserve is zero by construction (equivalence principle). At ts=n both
benefit and premium APVs are zero, so ${}_n V_x = 0. See {doc}user_guide/prospective_reservefor fractionalts, interaction with d=`, and joint-life products.
17. Group loop with deferred construction#
Process a portfolio grouped by cohort without creating a new LifeTable instance per
group. pending=True loads the base table once; configure(cohort=c) applies the
cohort projection and rebuilds the decrement in one step.
from lactuca import LifeTable, ax
# Portfolio: list of (cohort, age) pairs sorted by cohort
portfolio = [
{"cohort": 1960, "age": 65},
{"cohort": 1960, "age": 62},
{"cohort": 1970, "age": 55},
{"cohort": 1975, "age": 50},
{"cohort": 1975, "age": 48},
]
portfolio.sort(key=lambda p: p["cohort"])
# One shell — base data loaded once, cohort projection rebuilt per group
lt = LifeTable("PER2020_Ind_1o", "m", pending=True)
from itertools import groupby
results = {}
for cohort, group in groupby(portfolio, key=lambda p: p["cohort"]):
group = list(group)
lt.configure(cohort=cohort) # one rebuild per cohort
ages = [p["age"] for p in group]
pvs = ax(lt, ages, ir=0.03) # vectorised batch for this group
for p, pv in zip(group, pvs):
p["ax"] = float(pv)
print([p["ax"] for p in portfolio])
Note
configure() returns self, so you can chain: lt.configure(cohort=c).ax(65, ir=0.03).
If you need both LifeTable.ax (scalar call) and the result immediately, the chain is
the most compact form. For batch calls on a group, store the configure() result in
the loop and call the functional API separately.
18. Heterogeneous batch with TableRegistry#
When different policies require different cohorts at the same time (they cannot be
grouped sequentially), TableRegistry caches one configured instance per TableKey and
passes a stable per-policy list to the functional API.
from lactuca import LifeTable, TableRegistry, ax
reg = TableRegistry(LifeTable)
# Per-policy data: cohort and sex vary per row
policies = [
{"sex": "m", "cohort": 1960, "age": 65, "term": 20},
{"sex": "f", "cohort": 1975, "age": 55, "term": 15},
{"sex": "m", "cohort": 1960, "age": 62, "term": 20},
{"sex": "m", "cohort": 1982, "age": 42, "term": 25},
{"sex": "f", "cohort": 1975, "age": 50, "term": 15},
]
# Build per-policy table list — registry caches by (table_name, sex, cohort)
tables = [reg.get_or_create(None, "PER2020_Ind_1o", p["sex"], cohort=p["cohort"]) for p in policies]
ages = [p["age"] for p in policies]
terms = [p["term"] for p in policies]
pvs = ax(tables, ages, n=terms, ir=0.03)
for p, pv in zip(policies, pvs):
p["ax"] = float(pv)
print([p["ax"] for p in policies])
print(f"Distinct table instances cached: {len(reg)}")
The registry reuses the same instance whenever the same (sex, cohort) key appears —
here the two male-1960 and the two female-1975 policies each share one instance. No
mutation happens between batch calls, so there is no aliasing.
See also#
Using Actuarial Tables — vectorized construction, cohort setter, bulk portfolios
Batch Calculations — vectorized alternative to recipes 9–12: pass an age array to a single call for 50–250× speedup
Building Custom Table Files (.ltk) — create custom
.ltkfiles withTableBuilderInterest Rates —
InterestRateconstruction and scenariosJoint-Life Calculations — joint-life annuities, first-death insurances, and derivable formulas
Deferred Life Contingencies — deferred benefits (
d=) and distinction fromtsProspective Reserves and the ts Parameter — fractional
ts, growth with reserves, joint-lifeIrregular Cashflows — arbitrary cashflow timing