LifeTable#

LifeTable is the core actuarial table class in Lactuca. It inherits from DecrementTable and adds the full set of life annuity (\(\ddot{a}_x\) via äx(), \(a_x\) via ax(), continuous \(\bar{a}_x\) via Calculation Modes), life insurance (\(A_x\)), pure endowment (\({}_{n}E_x\)), commutation functions (\(D_x\), \(N_x\), \(M_x\), …), complete life expectancy (\(\mathring{e}_x\) via ex), and curtate \(e_x\) via ex_curtate.

A life table is identified by a repository table name (bundled with Lactuca or installed as a .ltk file), a sex code ('m', 'f', or 'u'), and optionally a birth-year cohort for generational tables that include mortality improvement factors.

See also

Getting Started — Quick-start guide with common patterns.
Life Annuities — Full annuity calculation reference.
Life Insurances — Insurance and endowment reference.
Commutation Functions — Commutation functions reference.
Calculation Modes — Discrete vs. continuous modes.
Joint-Life Calculations — Multi-life annuities, first-death insurances (Axy, Axyz, Afirst), and derivable last-survivor formulas.
lx Interpolation — Fractional-age survival assumptions (UDD vs. CFM).
Deferred Life Contingencies — Deferred annuities and the d= parameter.
Mortality Improvement (MI) — Generational tables and improvement factors.
Batch Calculations — Batch mode: array ages, per-policy params, portfolio BEL.

class lactuca.LifeTable(table_name: str | list | tuple, sex: Literal['m', 'f', 'u'] | Sequence[Literal['m', 'f', 'u']] | None = None, *, cohort: int | Sequence | None = None, interest_rate: object = None, unisex_blend: object = None, duration: int | str | Sequence | None = None, cartesian: bool = False, return_dict: bool = False, pending: bool = False)#

Bases: DecrementTable

Concrete implementation of DecrementTable for standard mortality life tables.

LifeTable represents actuarial mortality tables with qx (mortality probability), providing comprehensive life insurance and annuity calculations. It implements all standard commutation functions (Dx, Nx, Sx, Cx, Mx, Rx, Lx, Tx, ex) and continuous variants for fractional ages, along with life annuities, life insurances, and pure endowments.

LifeTable supports multiple calculation modes (discrete_precision, discrete_simplified, continuous_precision, continuous_simplified), interest rate integration (constant and variable curves), and actuarial conventions for mortality placement and force of mortality.

Construction accepts scalar arguments for a single instance, or sequences for vectorial creation (families, scenarios, pricing grids). See __new__() for full dispatch details.

Parameters:
  • table_name (str, list, or tuple) – Name of one or more life table files (without .ltk extension). A list or tuple enables multi-table vectorial creation.

  • sex (str or sequence of str) – Single sex ('m', 'f', 'u') or sequence for vectorial creation.

  • cohort (int, sequence of int, or None, optional) – Birth year(s) for generational tables. If None, loads a period table. Broadcast and zip-alignment rules match __new__().

  • interest_rate (float, InterestRate, list, tuple, or None, optional) – Default interest rate for annuity/insurance calculations. A scalar (float, InterestRate, or None) is broadcast to every instance; a list or tuple must have exactly one element per instance in vectorial mode. int values are not accepted — use float (e.g. 0.03) or an InterestRate object. If None, rates must be passed explicitly in calculation methods.

  • unisex_blend (float, list of float, or None, optional) – Male weight for unisex blending in \([0, 1]\) (0.0 = all female, 1.0 = all male). A list is accepted in zip mode (cartesian=False) to assign a distinct blend to each instance; scalar sex='u' broadcasts to the list length. See __new__().

  • duration (int, 'ult', sequence, or None, optional) – Duration slice for select-ultimate tables. Pass an integer >= start_duration for a select column, 'ult' for the ultimate column, or None for non-select tables.

  • cartesian (bool, optional) – If True, create the full Cartesian product of all table_name, sex, cohort, and duration values. All table names must share the same generational structure and the same select structure. With cartesian=True, a unisex_blend sequence is allowed only when every sex value is 'u' (fifth cartesian axis). A scalar unisex_blend is replicated uniformly to all combinations where sex='u'. Default is False (zip/broadcast alignment).

  • return_dict (bool, optional) – If True, return a dict mapping TableKey to each LifeTable instance instead of a plain tuple. Default is False.

Returns:

Single LifeTable when sex is a scalar string, table_name is a bare string (not a list/tuple), cohort and duration are scalar or None, no sequence dimensions are provided, and return_dict=False. A tuple when any parameter is a sequence or cartesian=True and return_dict=False (including length-1 tuples). A dict keyed by TableKey when return_dict=True.

Return type:

LifeTable or tuple[LifeTable, …] or dict

Notes

  • LifeTable uses qx (mortality probability) internally.

  • All probability calculations (px, tpx, tqx) inherit from DecrementTable.

  • Commutation functions are strictly defined for integer ages (annual periods).

  • Continuous variants (*_continuous) support fractional ages via numerical integration.

  • Interest rate can be constant (float) or variable (InterestRate).

  • Joint-life calculations assume independence between lives.

  • ix() and ox() are intentionally blocked; use qx() and the life-contingency methods.

  • Inherits the modification system via modify_qx().

  • Zip mode (default): table_name, sex, cohort, and duration are aligned element-wise (or broadcast when length is 1).

  • Cartesian mode (cartesian=True): every combination is created — suited to sensitivity grids and pricing studies, not sparse portfolio processing.

  • A ResourceWarning is emitted when more than 100 instances are created in a single constructor call.

Raises:
  • FileNotFoundError – If a table file does not exist in the actuarial tables directory.

  • ValueError – If the table is not a life table; if cohort, sex, or duration sequences have incompatible lengths; if unisex_blend is out of \([0, 1]\) or provided when sex is not 'u'; if sex='u' and the table lacks a native unisex column while unisex_blend is None; if cartesian=True and table names mix generational and period tables or mix select and non-select tables; if cartesian=True and unisex_blend is a sequence while sex contains any value other than 'u'; or if an interest_rate sequence length differs from the number of instances created.

  • TypeError – If interest_rate (or any element of its sequence) is not float, InterestRate, or None; or if cohort, duration, sex, or unisex_blend arguments have invalid types.

See also

DecrementTable

Abstract base class providing core decrement logic.

TableKey

Structured lookup key for return_dict=True results.

DisabilityTable

Disability incidence table implementation using ix rates.

ExitTable

Exit/withdrawal table implementation using ox rates.

Examples

>>> from lactuca import LifeTable, InterestRate, TableKey
>>> # Single period table
>>> lt = LifeTable('PASEM2010', 'm')
>>> # Generational table with default interest rate
>>> lt_gen = LifeTable('PER2020_Ind_1o', 'm', cohort=1969, interest_rate=0.03)
>>> # Zip mode: two sexes, different cohorts
>>> lt_m, lt_f = LifeTable('PER2020_Ind_1o', ('m', 'f'), cohort=(1960, 1963))
>>> # Cartesian product: 2 sex × 2 cohorts → 4 instances
>>> tables = LifeTable(
...     'PER2020_Ind_1o', ('m', 'f'), cohort=[1960, 1970], cartesian=True
... )
>>> len(tables)
4
>>> # Dict return keyed by TableKey
>>> d = LifeTable('PER2020_Ind_1o', ('m', 'f'), cohort=1970, return_dict=True)
>>> lt_m = d[TableKey('PER2020_Ind_1o', 'm', 1970)]
>>> # Commutation and products (interest rate required per call if not set)
>>> D65 = lt.Dx(65, ir=InterestRate(0.03))
>>> annuity = lt.ax(65, n=10, m=12, ir=0.03)
Afirst(ages: object, tables_others: Sequence[LifeTable] | Sequence[Sequence[LifeTable]] | None = None, *, ts: object = 0.0, d: object = 0.0, n: object = None, m: Literal[1, 2, 3, 4, 6, 12, 14, 24, 26, 52, 365] | Sequence[int] | NDArray[int64] = 1, gr: GrowthRate | float | None | Sequence[GrowthRate | float | None] = None, ir: object = None, cashflow_times: Sequence[float] | None = None, cashflow_amounts: Sequence[float] | None = None, return_flows: bool = False, t_output: NDArray[float64] | None = None, benefits: Sequence[float] | NDArray[float64] | None = None, on_error: Literal['raise', 'nan'] = 'raise', record_ids: Sequence[Any] | None = None) float | dict[str, NDArray[float64]] | BatchResult#

Compute the present value of a first-death (first-to-die) insurance for multiple independent lives.

Pays one unit of benefits at the first death among the insured lives. Supports shift (ts), deferment (d), term (n), payment frequency (m), growth (gr), custom cash flows (cashflow_times, cashflow_amounts), and all four calculation modes. If n is None the insurance is whole-life; otherwise it is a term insurance (n may be fractional). All actuarial conventions follow international standards.

Parameters:
  • ages (Sequence[float] or list[Sequence[float]]) – Ages of the insured lives (at contract start). In batch mode, pass a list of r N-element arrays: [x0_arr, x1_arr, ..., xr_arr] where each has shape (N,).

  • tables_others (Sequence[LifeTable] or Sequence[Sequence[LifeTable]] or None, optional) –

    Life tables for additional lives (all except the first, which uses self). Three modes in batch:

    • None: uses self for all lives in all policies.

    • Sequence[LifeTable] of length n_lives - 1: shared across all N policies.

    • Sequence[Sequence[LifeTable]] of length N: per-policy; each inner list must have exactly n_lives - 1 elements. Raises ValueError if length != N.

  • ts (float or NDArray[np.float64], optional) – Shift (displacement) in years from origin. In batch mode, may be a scalar or an array of length N. Pandas and Polars Series are accepted; values are converted to float64. Scalar int literals (e.g. n=10) are coerced to float.

  • d (float or NDArray[np.float64], optional) – Deferment period in years. In batch mode, may be a scalar or an array of length N. Pandas and Polars Series are accepted; values are converted to float64. Scalar int literals (e.g. n=10) are coerced to float.

  • n (float or NDArray[np.float64] or None, optional) – Duration of the insurance (years). If None, whole life. In batch mode, may be a scalar or an array of length N. Pandas and Polars Series are accepted; values are converted to float64. Scalar int literals (e.g. n=10) are coerced to float.

  • m (PaymentFrequencyLiteral, sequence of int, NDArray[np.int64], or array-like, optional) – Number of payments per year (1=annual, 12=monthly, etc.). Integer-valued floats (e.g. 12.0) are coerced to int. In batch mode, may be a scalar (broadcast to all N) or an array/list of length N for per-policy payment frequencies; distinct values are processed in sub-groups by the vectorised batch engine. Pandas and Polars Series are accepted; values are converted to int64.

  • gr (GrowthRate, float, or None, optional) – Growth rate model. A scalar float is auto-wrapped as GrowthRate(gr) (geometric growth by default). If None, no growth is applied. In batch mode, may be a scalar or a list of N values. Pandas and Polars Series are accepted without .to_numpy(). A numeric Series is converted to per-policy floats (auto-wrapped as GrowthRate where applicable). An object-dtype Series of GrowthRate instances is treated like a list of length N (including piecewise curves). Mixed numeric and object elements in one array-like sequence raise ValueError.

  • ir (float, InterestRate, list[float or InterestRate], NDArray[np.float64], or None, optional) – Interest rate (constant or curve). If None, uses interest_rate. In batch mode, may be a scalar (broadcast to all N), a list of N float values, or a list of N InterestRate objects (one curve per policy). Pandas and Polars Series are accepted without .to_numpy(). A numeric Series is converted to per-policy floats (auto-wrapped as InterestRate where applicable). An object-dtype Series of InterestRate instances is treated like a list of length N (including piecewise curves). Mixed numeric and object elements in one array-like sequence raise ValueError.

  • cashflow_times (sequence of float, optional) – Times (years since origin) at which payments are made. Requires calculation_mode='discrete_precision'; raises ValueError in other modes. If None, uses standard schedule. Pandas and Polars Series are accepted; converted to NDArray[np.float64] internally.

  • cashflow_amounts (sequence of float, optional) – Capital insured for each payment period. If None, assumes unit capital. Pandas and Polars Series are accepted; converted to NDArray[np.float64] internally.

  • return_flows (bool, optional) –

    If True, returns a cashflow decomposition dict instead of the present value. The dict schema differs between single-policy and batch:

    Single-policy (scalar ages): per-payment engine dict. Same keys as Ax(); see that method’s return_flows parameter for the full key reference.

    Batch (array ages): aggregate portfolio dict {'time_grid', 'expected_cf', 'pv_cf', 'total_pv'}. total_pv == np.sum(pv_cf) is always satisfied. Raises ValueError in discrete_simplified / continuous_simplified modes. Default is False.

  • t_output (NDArray[np.float64] or None, optional) – External bucketing grid for aggregate flow output. When provided, per-policy present-value contributions — already discounted at their exact payment times — are accumulated into the nearest bucket via np.searchsorted without re-discounting. Controls only the shape of expected_cf and pv_cf; total_pv is always the exact sum of all per-payment present values and is identical to the t_output=None result. Typical use: regulatory templates that require a fixed set of time buckets (e.g., annual IFRS 17 disclosure, Solvency II SCR projection). Only effective when return_flows=True; raises ValueError if return_flows=False. Default is None. Pandas and Polars Series are accepted; converted to NDArray[np.float64] before use.

  • benefits (sequence of float or NDArray[np.float64] or None, optional) – Per-policy benefit amounts, shape (N,). Scales each policy’s contribution to the aggregate flows (e.g., sum insured, PVDBO weight). Requires batch mode. Any sequence is accepted, including Pandas and Polars Series; values are converted to float64. Compatible with return_flows=False (returns NDArray[np.float64] with per-policy scaled PVs) and on_error='nan'. Default is None.

  • on_error ({'raise', 'nan'}, optional) – Error handling for invalid ages or parameters. 'raise' (default) raises ValueError; 'nan' marks invalid policies with np.nan and returns BatchResult. Default is 'raise'.

  • record_ids (sequence or None, optional) – Identifiers for each policy in batch mode, used to populate the error report when on_error='nan'. Length must equal N. Any sequence is accepted, including Pandas and Polars Series; elements are always accessed by positional integer index. Default is None.

Returns:

  • float – When ages[0] is scalar and return_flows=False.

  • NDArray[np.float64] – When ages[0] is an array or list and return_flows=False. Shape (N,).

  • dict – When return_flows=True. Single-policy: per-payment engine dict (same keys as Ax()). Batch: aggregate portfolio dict {'time_grid', 'expected_cf', 'pv_cf', 'total_pv'}.

  • BatchResult – When on_error='nan'.

Raises:
  • ValueError – If tables_others length does not match len(ages) - 1. If any age is outside [0, w] for the corresponding table. If n <= 0 or ts < 0 (invalid timing parameters). If no interest rate provided and interest_rate is None. If return_flows=True in batch mode and calculation_mode is 'discrete_simplified' or 'continuous_simplified'. If return_flows=True and m is a vector with heterogeneous values. If t_output is not None and return_flows=False. If t_output is not None and input is scalar (non-batch mode). If on_error is not 'raise' or 'nan'. If on_error='nan' is passed with scalar x (or ages for joint-life methods). Use a list or array of ages for batch mode. If record_ids is not None and input is scalar (non-batch mode).

  • TypeError – If ages contains non-numeric values. If tables_others contains non-LifeTable instances. If ir is not float, InterestRate, or None. If any numeric argument is bool.

Notes

Boolean values for age, duration, shift, deferment, and rate arguments raise TypeError. Accepted types for each parameter are described in Parameters above.

First-death (first-to-die) insurance for \(r\) lives under the independence assumption:

\[A_{x_1 x_2 \cdots x_r} = \sum_{k=0}^{\omega-1} v^{k+\alpha} \prod_{i=1}^{r} {}_{k}p_{x_i} \left(1 - \prod_{i=1}^{r} p_{x_i+k}\right)\]

where \(\alpha\) is set by self.mortality_placement ('beginning' → 0, 'mid' → 0.5, 'end' → 1). For a term insurance of duration \(n\), the sum is restricted to \(k = 0, \ldots, \lfloor nm \rfloor - 1\).

  • Follows international actuarial practice for first-death insurance.

  • Supports any number of lives (n-heads).

  • If cashflow_times is provided, m is ignored and payments are made at the specified times.

  • If cashflow_amounts is provided, disables growth.

  • Optimized for speed and memory using NumPy vectorization.

  • When gr is provided, the growth factor at the j-th payment interval is (1+g)^(j // m) — anniversaries counted from the first payment, not the contract origin. GrowthRate.shifted(ts) consumes int(ts) anniversary years; fractional ts is actuarially correct (revaluations operate on complete policy years). See Growth Rate Conventions for the full reference.

Batch mode#

Batch mode is activated when either of these conditions is met:

  1. ages[0] is a list or NDArray (age-triggered batch): pass ages as a list of r N-element arrays, each shape (N,).

  2. ages[0] is scalar but any of n, ts, d, ir, gr, or m is a list/tuple/NDArray of length N (param-triggered batch): scalar ages broadcast to N copies.

In both cases the method returns NDArray[np.float64] of shape (N,).

  • Per-policy parameters ts, d, n, ir, and gr may each be a scalar or an array of length N.

  • m may be a scalar (broadcast to all N) or an array/list of length N for per-policy payment frequencies; the batch engine groups policies by unique m value and vectorises each group independently.

  • Pandas and Polars Series are accepted for all per-policy parameters (n, ts, d, ir, gr, m, cashflow_times, cashflow_amounts, t_output, and each per-life age array within ages) without calling .to_numpy(); they are converted internally via the array protocol.

  • on_error='nan' marks invalid policies with np.nan.

See also

LifeTable.Ax

Single-life insurance.

LifeTable.Axy

First-death insurance for two lives.

LifeTable.Axyz

First-death insurance for three lives.

Examples

>>> from lactuca import LifeTable
>>> table = LifeTable('PER2020_Ind_1o', 'm', cohort=1960)
>>> table.interest_rate = 0.03
>>> pv = table.Afirst([65, 60, 55, 50], n=20)
>>> table_m, table_f1, table_f2 = LifeTable(
...     'PER2020_Ind_1o', ('m', 'f', 'f'), cohort=1960
... )
>>> table_m.interest_rate = 0.03
>>> pv = table_m.Afirst([65, 62, 58], [table_f1, table_f2], n=15)

Batch mode:

>>> import numpy as np
>>> a0 = np.array([60.0, 65.0])
>>> a1 = np.array([55.0, 60.0])
>>> table.Afirst([a0, a1], n=20, ir=0.03)
Ax(x: object, *, ts: object = 0.0, d: object = 0.0, n: object = None, m: Literal[1, 2, 3, 4, 6, 12, 14, 24, 26, 52, 365] | Sequence[int] | NDArray[int64] = 1, gr: GrowthRate | float | None | Sequence[GrowthRate | float | None] = None, ir: object = None, cashflow_times: Sequence[float] | None = None, cashflow_amounts: Sequence[float] | None = None, return_flows: bool = False, t_output: NDArray[float64] | None = None, benefits: Sequence[float] | NDArray[float64] | None = None, on_error: Literal['raise', 'nan'] = 'raise', record_ids: Sequence[Any] | None = None) float | NDArray[float64] | dict[str, NDArray[float64] | float] | BatchResult#

Compute the present value of a whole-life or term life insurance for a single life.

Pays one unit of benefits at death. Supports shift (ts), deferment (d), term (n), payment frequency (m), growth (gr), custom cash flows (cashflow_times, cashflow_amounts), and all four calculation modes. When cashflow_times is provided, m is ignored for payment scheduling (it is still used for mortality placement).

Parameters:
  • x (float or list of float or NDArray[np.float64]) – Initial age. Pass an array or list of N ages to activate batch mode, which returns an ndarray of shape (N,).

  • ts (float or NDArray[np.float64], optional) – Shift (displacement) in years from origin. In batch mode, may be a scalar (broadcast) or an array of length N. Pandas and Polars Series are accepted; values are converted to float64. Scalar int literals (e.g. n=10) are coerced to float.

  • d (float or NDArray[np.float64], optional) – Deferment period in years. In batch mode, may be a scalar or an array of length N. Pandas and Polars Series are accepted; values are converted to float64. Scalar int literals (e.g. n=10) are coerced to float.

  • n (float or NDArray[np.float64] or None, optional) – Duration of the insurance (years). If None, whole life. In batch mode, may be a scalar or an array of length N. Pandas and Polars Series are accepted; values are converted to float64. Scalar int literals (e.g. n=10) are coerced to float.

  • m (PaymentFrequencyLiteral, sequence of int, NDArray[np.int64], or array-like, optional) – Number of payments per year (1=annual, 12=monthly, etc.). Integer-valued floats (e.g. 12.0) are coerced to int. In batch mode, may be a scalar (broadcast to all N) or an array/list of length N for per-policy payment frequencies; distinct values are processed in sub-groups by the vectorised batch engine. Pandas and Polars Series are accepted; values are converted to int64.

  • gr (GrowthRate, float, or None, optional) – Growth rate model. A scalar float is auto-wrapped as GrowthRate(gr) (geometric growth by default). If None, no growth is applied. In batch mode, may be a scalar or a list of N values. Pandas and Polars Series are accepted without .to_numpy(). A numeric Series is converted to per-policy floats (auto-wrapped as GrowthRate where applicable). An object-dtype Series of GrowthRate instances is treated like a list of length N (including piecewise curves). Mixed numeric and object elements in one array-like sequence raise ValueError.

  • ir (float, InterestRate, list[float or InterestRate], NDArray[np.float64], or None, optional) – Interest rate (constant or curve). If None, uses interest_rate. In batch mode, may be a scalar (broadcast to all N), a list of N float values, or a list of N InterestRate objects (one curve per policy). Pandas and Polars Series are accepted without .to_numpy(). A numeric Series is converted to per-policy floats (auto-wrapped as InterestRate where applicable). An object-dtype Series of InterestRate instances is treated like a list of length N (including piecewise curves). Mixed numeric and object elements in one array-like sequence raise ValueError.

  • cashflow_times (sequence of float, optional) – Times (years since origin) at which payments are made. Requires calculation_mode='discrete_precision'; raises ValueError in other modes. If None, uses standard schedule. Pandas and Polars Series are accepted; converted to NDArray[np.float64] internally.

  • cashflow_amounts (sequence of float, optional) – Capital insured for each payment period. If None, assumes unit capital. Pandas and Polars Series are accepted; converted to NDArray[np.float64] internally.

  • return_flows (bool, optional) –

    If True, returns a cashflow decomposition dict instead of the present value. The dict schema differs between scalar and batch:

    Scalar x, discrete_precision / discrete_simplified mode:

    {
      "payment_index"               : NDArray  # payment period indices
      "time_grid"                   : NDArray  # interval end-points (payment times)
      "discount_time"               : NDArray  # t + mortality_placement offset
      "interest_rate"               : NDArray  # i(t) at discount time
      "discount_factor"             : NDArray  # v^t at discount time
      "death_probability_raw"       : NDArray  # raw interval death probabilities
      "death_probability_adjustment": NDArray  # fractional last-payment factor
      "death_probability"           : NDArray  # adjusted death probabilities
      "growth" or "amount"          : NDArray  # growth factors or custom amounts
      "present_value"               : NDArray  # pv per payment period
    }
    

    sum(result["present_value"]) equals the rounded scalar present value.

    Batch (array x), *_precision modes only:

    {
      "time_grid"   : NDArray  # portfolio time grid
      "expected_cf" : NDArray  # sum of undiscounted contributions across policies
      "pv_cf"       : NDArray  # sum of discounted contributions across policies
      "total_pv"    : float    # sum(pv_cf); equals sum of scalar present values
    }
    

    total_pv == np.sum(pv_cf) is always satisfied. Raises ValueError in discrete_simplified / continuous_simplified modes. Default is False.

  • t_output (NDArray[np.float64] or None, optional) – External bucketing grid for aggregate flow output. When provided, per-policy present-value contributions — already discounted at their exact payment times — are accumulated into the nearest bucket via np.searchsorted without re-discounting. Controls only the shape of expected_cf and pv_cf; total_pv is always the exact sum of all per-payment present values and is identical to the t_output=None result. Typical use: regulatory templates that require a fixed set of time buckets (e.g., annual IFRS 17 disclosure, Solvency II SCR projection). Only effective when return_flows=True; raises ValueError if return_flows=False. Default is None. Pandas and Polars Series are accepted; converted to NDArray[np.float64] before use.

  • benefits (sequence of float or NDArray[np.float64] or None, optional) – Per-policy benefit amounts, shape (N,). Scales each policy’s contribution to the aggregate flows (e.g., sum insured, PVDBO weight). Requires batch mode. Any sequence is accepted, including Pandas and Polars Series; values are converted to float64. Compatible with return_flows=False (returns NDArray[np.float64] with per-policy scaled PVs) and on_error='nan'. Default is None.

  • on_error ({'raise', 'nan'}, optional) – Error handling for invalid ages or parameters. 'raise' (default) raises ValueError; 'nan' marks invalid policies with np.nan and returns BatchResult. Default is 'raise'.

  • record_ids (sequence or None, optional) – Identifiers for each policy in batch mode, used to populate the error report when on_error='nan'. Length must equal N. Any sequence is accepted, including Pandas and Polars Series; elements are always accessed by positional integer index. Default is None.

Returns:

  • float – When x is scalar and return_flows=False.

  • NDArray[np.float64] – When x is an array or list and return_flows=False. Shape (N,).

  • dict – When return_flows=True. Schema depends on scalar vs. batch and on calculation_mode; see the return_flows parameter description above for the full key listing of each variant.

  • BatchResult – When on_error='nan', wraps the result array and an error report.

Raises:
  • ValueError – If x is outside [0, w] for the mortality table. If n <= 0 or ts < 0 (invalid timing parameters). If no interest rate provided and interest_rate is None. If return_flows=True in batch mode and calculation_mode is 'discrete_simplified' or 'continuous_simplified'. If return_flows=True and m is a vector with heterogeneous values; use a scalar m or split the batch by payment frequency. If t_output is not None and return_flows=False. If t_output is not None and input is scalar (non-batch mode). If on_error is not 'raise' or 'nan'. If on_error='nan' is passed with scalar x (or ages for joint-life methods). Use a list or array of ages for batch mode. If record_ids is not None and input is scalar (non-batch mode).

  • TypeError – If x is not a numeric type (including bool). If ir is not float, InterestRate, or None.

Notes

Boolean values for age, duration, shift, deferment, and rate arguments raise TypeError. Accepted types for each parameter are described in Parameters above.

Discrete whole-life insurance \(\Ax{x}\) with mortality placement offset \(\alpha\):

\[\Ax{x} = \sum_{k=0}^{\omega-x-1} v^{k+\alpha} \cdot {}_k p_x \cdot q_{x+k}\]

where \(\alpha\) is set by self.mortality_placement ('beginning': 0, 'mid': 0.5, 'end': 1). For integer ages with annual payments, the commutation identity is \(\Ax{x} = \Mx{x} / \Dx{x}\).

  • n can be fractional; lx interpolation is used as defined in config.

  • If cashflow_times is provided, overrides standard payment schedule and m is ignored.

  • If cashflow_amounts is provided, disables growth.

  • cashflow_times must be non-negative, sorted, and without duplicates.

  • Follows international actuarial practice.

  • When gr is provided, the growth factor at the j-th payment interval is (1+g)^(j // m) — anniversaries counted from the first payment, not the contract origin. GrowthRate.shifted(ts) consumes int(ts) anniversary years; fractional ts is actuarially correct (revaluations operate on complete policy years). See Growth Rate Conventions for the full reference.

Batch mode#

When x is a list, sequence, or ndarray, or when any of n, ts, d, ir, gr, or m is a sequence or array with scalar x, the method operates in batch mode and returns an ndarray of shape (N,):

  • A list of 1 element still returns NDArray shape (1,), never a scalar.

  • Per-policy parameters ts, d, n, ir, and gr may each be a scalar (broadcast to all N policies) or an array of length N.

  • m may be a scalar (broadcast to all N policies) or an array/list of length N for per-policy payment frequencies; the batch engine groups policies by unique m value and vectorises each group independently.

  • Pandas and Polars Series are accepted for all per-policy parameters (x, n, ts, d, ir, gr, m, cashflow_times, cashflow_amounts, t_output) without calling .to_numpy(); they are converted internally via the array protocol.

  • discrete_precision mode uses a vectorised engine (50–250× vs scalar loop).

  • on_error='nan' marks invalid policies with np.nan instead of raising.

See also

LifeTable.Axy

First-death insurance for two lives.

LifeTable.Axyz

First-death insurance for three lives.

LifeTable.Afirst

First-death insurance for any number of lives.

Examples

>>> from lactuca import LifeTable
>>> lt = LifeTable('PASEM2010', 'm')
>>> lt.interest_rate = 0.03
>>> lt.Ax(65, n=20)
>>> lt.Ax(65)  # whole life

Batch mode:

>>> import numpy as np
>>> ages = np.array([55.0, 60.0, 65.0, 70.0])
>>> lt.Ax(ages, n=20, ir=0.03)
>>> lt.Ax(ages, n=np.array([25.0, 20.0, 15.0, 10.0]), ir=0.03)
Axy(ages: object, table_y: LifeTable | Sequence[LifeTable] | None = None, *, ts: object = 0.0, d: object = 0.0, n: object = None, m: Literal[1, 2, 3, 4, 6, 12, 14, 24, 26, 52, 365] | Sequence[int] | NDArray[int64] = 1, gr: GrowthRate | float | None | Sequence[GrowthRate | float | None] = None, ir: object = None, cashflow_times: Sequence[float] | None = None, cashflow_amounts: Sequence[float] | None = None, return_flows: bool = False, t_output: NDArray[float64] | None = None, benefits: Sequence[float] | NDArray[float64] | None = None, on_error: Literal['raise', 'nan'] = 'raise', record_ids: Sequence[Any] | None = None) float | NDArray[float64] | dict[str, NDArray[float64]] | BatchResult#

Compute the present value of a first-death insurance for two lives.

Pays one unit of benefits upon the first death among lives (x, y).

Parameters:
  • ages (Sequence[float] or tuple[Sequence[float], Sequence[float]]) – Ages of the two lives [x, y]. Must have exactly 2 elements. In batch mode, pass a pair of N-element arrays: (np.array([x0, …, xN]), np.array([y0, …, yN])).

  • table_y (LifeTable or Sequence[LifeTable] or None, optional) –

    Life table for the second life (y). Three modes in batch:

    • None: uses self for all policies.

    • LifeTable: single shared table for all N policies.

    • Sequence[LifeTable] of length N: per-policy table; policy i uses element i. Raises ValueError if length != N.

  • ts (float or NDArray[np.float64], optional) – Shift (displacement) in years from origin (default 0.0). In batch mode, may be a scalar (broadcast) or an array of length N. Pandas and Polars Series are accepted; values are converted to float64. Scalar int literals (e.g. n=10) are coerced to float.

  • d (float or NDArray[np.float64], optional) – Deferment period in years (default 0.0). In batch mode, may be a scalar or an array of length N. Pandas and Polars Series are accepted; values are converted to float64. Scalar int literals (e.g. n=10) are coerced to float.

  • n (float or NDArray[np.float64] or None, optional) – Duration of the insurance (years). If None, whole life. In batch mode, may be a scalar or an array of length N. Pandas and Polars Series are accepted; values are converted to float64. Scalar int literals (e.g. n=10) are coerced to float.

  • m (PaymentFrequencyLiteral, sequence of int, NDArray[np.int64], or array-like, optional) – Number of payments per year (1=annual, 12=monthly, etc., default 1). Integer-valued floats (e.g. 12.0) are coerced to int. In batch mode, may be a scalar (broadcast to all N) or an array/list of length N for per-policy payment frequencies; distinct values are processed in sub-groups by the vectorised batch engine. Pandas and Polars Series are accepted; values are converted to int64.

  • gr (GrowthRate, float, or None, optional) – Growth rate model (default None). A scalar float is auto-wrapped as GrowthRate(gr) (geometric growth by default). If None, no growth is applied. In batch mode, may be a scalar or a list of N values. Pandas and Polars Series are accepted without .to_numpy(). A numeric Series is converted to per-policy floats (auto-wrapped as GrowthRate where applicable). An object-dtype Series of GrowthRate instances is treated like a list of length N (including piecewise curves). Mixed numeric and object elements in one array-like sequence raise ValueError.

  • ir (float, InterestRate, list[float or InterestRate], NDArray[np.float64], or None, optional) – Interest rate (constant or curve). If None, uses interest_rate. In batch mode, may be a scalar (broadcast to all N), a list of N float values, or a list of N InterestRate objects (one curve per policy). Pandas and Polars Series are accepted without .to_numpy(). A numeric Series is converted to per-policy floats (auto-wrapped as InterestRate where applicable). An object-dtype Series of InterestRate instances is treated like a list of length N (including piecewise curves). Mixed numeric and object elements in one array-like sequence raise ValueError.

  • cashflow_times (Sequence[float], optional) – Times (years since origin) at which payments are made. Requires calculation_mode='discrete_precision'; raises ValueError in other modes. If None, uses standard schedule. Pandas and Polars Series are accepted; converted to NDArray[np.float64] internally.

  • cashflow_amounts (Sequence[float], optional) – Capital insured for each payment period. If None, assumes unit capital. Pandas and Polars Series are accepted; converted to NDArray[np.float64] internally.

  • return_flows (bool, optional) –

    If True, returns a cashflow decomposition dict instead of the present value. The dict schema differs between single-policy and batch:

    Single-policy (scalar ages): per-payment engine dict. Same keys as Ax(); see that method’s return_flows parameter for the full key reference.

    Batch (array ages): aggregate portfolio dict {'time_grid', 'expected_cf', 'pv_cf', 'total_pv'}. total_pv == np.sum(pv_cf) is always satisfied. Raises ValueError in discrete_simplified / continuous_simplified modes. Default is False.

  • t_output (NDArray[np.float64] or None, optional) – External bucketing grid for aggregate flow output. When provided, per-policy present-value contributions — already discounted at their exact payment times — are accumulated into the nearest bucket via np.searchsorted without re-discounting. Controls only the shape of expected_cf and pv_cf; total_pv is always the exact sum of all per-payment present values and is identical to the t_output=None result. Typical use: regulatory templates that require a fixed set of time buckets (e.g., annual IFRS 17 disclosure, Solvency II SCR projection). Only effective when return_flows=True; raises ValueError if return_flows=False. Default is None. Pandas and Polars Series are accepted; converted to NDArray[np.float64] before use.

  • benefits (sequence of float or NDArray[np.float64] or None, optional) – Per-policy benefit amounts, shape (N,). Scales each policy’s contribution to the aggregate flows (e.g., sum insured, PVDBO weight). Requires batch mode. Any sequence is accepted, including Pandas and Polars Series; values are converted to float64. Compatible with return_flows=False (returns NDArray[np.float64] with per-policy scaled PVs) and on_error='nan'. Default is None.

  • on_error ({'raise', 'nan'}, optional) – Error handling for invalid ages or parameters. 'raise' (default) raises ValueError; 'nan' marks invalid policies with np.nan and returns BatchResult. Default is 'raise'.

  • record_ids (sequence or None, optional) – Identifiers for each policy in batch mode, used to populate the error report when on_error='nan'. Length must equal N. Any sequence is accepted, including Pandas and Polars Series; elements are always accessed by positional integer index. Default is None.

Returns:

  • float – When ages[0] is scalar and return_flows=False.

  • NDArray[np.float64] – When ages[0] is an array or list and return_flows=False. Shape (N,).

  • dict – When return_flows=True. Single-policy: per-payment engine dict (same keys as Ax()). Batch: aggregate portfolio dict {'time_grid', 'expected_cf', 'pv_cf', 'total_pv'}.

  • BatchResult – When on_error='nan'.

Raises:
  • ValueError – If ages does not contain exactly 2 elements. If any age is outside [0, w] for corresponding table. If return_flows=True in batch mode and calculation_mode is 'discrete_simplified' or 'continuous_simplified'. If return_flows=True and m is a vector with heterogeneous values. If t_output is not None and return_flows=False. If t_output is not None and input is scalar (non-batch mode). If on_error is not 'raise' or 'nan'. If on_error='nan' is passed with scalar x (or ages for joint-life methods). Use a list or array of ages for batch mode. If record_ids is not None and input is scalar (non-batch mode).

  • TypeError – If ages contains non-numeric values. If table_y is not a LifeTable instance or None. If any numeric argument is bool.

Notes

Boolean values for age, duration, shift, deferment, and rate arguments raise TypeError. Accepted types for each parameter are described in Parameters above.

First-death (first-to-die) insurance for two lives under the independence assumption:

\[A_{xy} = \sum_{k=0}^{\omega-1} v^{k+\alpha} \cdot {}_{k}p_x \cdot {}_{k}p_y \cdot \left(1 - p_{x+k} \cdot p_{y+k}\right)\]

where \(\alpha\) is set by self.mortality_placement ('beginning': 0, 'mid': 0.5, 'end': 1). For a term insurance of duration \(n\), the sum is restricted to \(k = 0, \ldots, \lfloor nm \rfloor - 1\).

  • Benefit is paid upon the first death among the two lives.

  • Uses independence assumption for joint survival probabilities.

  • When gr is provided, the growth factor at the j-th payment interval is (1+g)^(j // m) — anniversaries counted from the first payment, not the contract origin. GrowthRate.shifted(ts) consumes int(ts) anniversary years; fractional ts is actuarially correct (revaluations operate on complete policy years). See Growth Rate Conventions for the full reference.

Batch mode#

Batch mode is activated when either of these conditions is met:

  1. ages[0] is a list or NDArray (age-triggered batch): pass ages as a pair of N-element arrays (x_arr, y_arr), each shape (N,).

  2. ages[0] is scalar but any of n, ts, d, ir, gr, or m is a list/tuple/NDArray of length N (param-triggered batch): scalar ages broadcast to N copies.

In both cases the method returns NDArray[np.float64] of shape (N,).

  • Per-policy parameters ts, d, n, ir, and gr may each be a scalar (broadcast to all N) or an array of length N.

  • m may be a scalar (broadcast to all N policies) or an array/list of length N for per-policy payment frequencies; the batch engine groups policies by unique m value and vectorises each group independently.

  • discrete_precision mode uses a vectorised matrix engine.

  • Pandas and Polars Series are accepted for all per-policy parameters (n, ts, d, ir, gr, m, cashflow_times, cashflow_amounts, t_output, and each per-life age array within ages) without calling .to_numpy(); they are converted internally via the array protocol.

  • on_error='nan' marks invalid policies with np.nan.

See also

LifeTable.Afirst

First-death insurance for any number of lives.

LifeTable.Axyz

First-death insurance for three lives.

LifeTable.Ax

Single-life insurance.

Examples

>>> from lactuca import LifeTable
>>> table = LifeTable('PER2020_Ind_1o', 'm', cohort=1960)
>>> table.interest_rate = 0.03
>>> pv = table.Axy([65, 60], n=20)

Batch mode:

>>> import numpy as np
>>> x_ages = np.array([60.0, 65.0, 70.0])
>>> y_ages = np.array([55.0, 60.0, 65.0])
>>> table.Axy((x_ages, y_ages), n=20, ir=0.03)
>>> table_m, table_f = LifeTable('PER2020_Ind_1o', ('m', 'f'), cohort=1960)
>>> table_m.interest_rate = 0.03
>>> pv = table_m.Axy([65, 62], table_f, n=15)
Axyz(ages: object, tables_yz: Sequence[LifeTable] | Sequence[Sequence[LifeTable]] | None = None, *, ts: object = 0.0, d: object = 0.0, n: object = None, m: Literal[1, 2, 3, 4, 6, 12, 14, 24, 26, 52, 365] | Sequence[int] | NDArray[int64] = 1, gr: GrowthRate | float | None | Sequence[GrowthRate | float | None] = None, ir: object = None, cashflow_times: Sequence[float] | None = None, cashflow_amounts: Sequence[float] | None = None, return_flows: bool = False, t_output: NDArray[float64] | None = None, benefits: Sequence[float] | NDArray[float64] | None = None, on_error: Literal['raise', 'nan'] = 'raise', record_ids: Sequence[Any] | None = None) float | dict[str, NDArray[float64]] | BatchResult#

Compute the present value of a first-death insurance for three lives.

Pays one unit of benefits upon the first death among lives (x, y, z).

Parameters:
  • ages (Sequence[float] or tuple[Sequence[float], Sequence[float], Sequence[float]]) – Ages of the three lives [x, y, z]. Must have exactly 3 elements. In batch mode, pass a triple of N-element arrays: (np.array([x0, …]), np.array([y0, …]), np.array([z0, …])).

  • tables_yz (Sequence[LifeTable] or Sequence[Sequence[LifeTable]] or None, optional) –

    Life tables for the second and third lives [y, z]. Three modes in batch:

    • None: uses self for all three lives in all policies.

    • [LifeTable, LifeTable]: 2 shared tables (y and z) for all N policies.

    • [[t_y0, t_z0], …] of length N: per-policy; each inner list must have exactly 2 elements. Raises ValueError if outer length != N.

  • ts (float or NDArray[np.float64], optional) – Shift (displacement) in years from origin (default 0.0). In batch mode, may be a scalar or an array of length N. Pandas and Polars Series are accepted; values are converted to float64. Scalar int literals (e.g. n=10) are coerced to float.

  • d (float or NDArray[np.float64], optional) – Deferment period in years (default 0.0). In batch mode, may be a scalar or an array of length N. Pandas and Polars Series are accepted; values are converted to float64. Scalar int literals (e.g. n=10) are coerced to float.

  • n (float or NDArray[np.float64] or None, optional) – Duration of the insurance (years). If None, whole life. In batch mode, may be a scalar or an array of length N. Pandas and Polars Series are accepted; values are converted to float64. Scalar int literals (e.g. n=10) are coerced to float.

  • m (PaymentFrequencyLiteral, sequence of int, NDArray[np.int64], or array-like, optional) – Number of payments per year (1=annual, 12=monthly, etc., default 1). Integer-valued floats (e.g. 12.0) are coerced to int. In batch mode, may be a scalar (broadcast to all N) or an array/list of length N for per-policy payment frequencies; distinct values are processed in sub-groups by the vectorised batch engine. Pandas and Polars Series are accepted; values are converted to int64.

  • gr (GrowthRate, float, or None, optional) – Growth rate model (default None). A scalar float is auto-wrapped as GrowthRate(gr) (geometric growth by default). If None, no growth is applied. In batch mode, may be a scalar or a list of N values. Pandas and Polars Series are accepted without .to_numpy(). A numeric Series is converted to per-policy floats (auto-wrapped as GrowthRate where applicable). An object-dtype Series of GrowthRate instances is treated like a list of length N (including piecewise curves). Mixed numeric and object elements in one array-like sequence raise ValueError.

  • ir (float, InterestRate, list[float or InterestRate], NDArray[np.float64], or None, optional) – Interest rate (constant or curve). If None, uses interest_rate. In batch mode, may be a scalar (broadcast to all N), a list of N float values, or a list of N InterestRate objects (one curve per policy). Pandas and Polars Series are accepted without .to_numpy(). A numeric Series is converted to per-policy floats (auto-wrapped as InterestRate where applicable). An object-dtype Series of InterestRate instances is treated like a list of length N (including piecewise curves). Mixed numeric and object elements in one array-like sequence raise ValueError.

  • cashflow_times (Sequence[float], optional) – Times (years since origin) at which payments are made. Requires calculation_mode='discrete_precision'; raises ValueError in other modes. If None, uses standard schedule. Pandas and Polars Series are accepted; converted to NDArray[np.float64] internally.

  • cashflow_amounts (Sequence[float], optional) – Capital insured for each payment period. If None, assumes unit capital. Pandas and Polars Series are accepted; converted to NDArray[np.float64] internally.

  • return_flows (bool, optional) –

    If True, returns a cashflow decomposition dict instead of the present value. The dict schema differs between single-policy and batch:

    Single-policy (scalar ages): per-payment engine dict. Same keys as Ax(); see that method’s return_flows parameter for the full key reference.

    Batch (array ages): aggregate portfolio dict {'time_grid', 'expected_cf', 'pv_cf', 'total_pv'}. total_pv == np.sum(pv_cf) is always satisfied. Raises ValueError in discrete_simplified / continuous_simplified modes. Default is False.

  • t_output (NDArray[np.float64] or None, optional) – External bucketing grid for aggregate flow output. When provided, per-policy present-value contributions — already discounted at their exact payment times — are accumulated into the nearest bucket via np.searchsorted without re-discounting. Controls only the shape of expected_cf and pv_cf; total_pv is always the exact sum of all per-payment present values and is identical to the t_output=None result. Typical use: regulatory templates that require a fixed set of time buckets (e.g., annual IFRS 17 disclosure, Solvency II SCR projection). Only effective when return_flows=True; raises ValueError if return_flows=False. Default is None. Pandas and Polars Series are accepted; converted to NDArray[np.float64] before use.

Returns:

  • float – When ages[0] is scalar and return_flows=False.

  • NDArray[np.float64] – When ages[0] is an array or list and return_flows=False. Shape (N,).

  • dict – When return_flows=True. Single-policy: per-payment engine dict (same keys as Ax()). Batch: aggregate portfolio dict {'time_grid', 'expected_cf', 'pv_cf', 'total_pv'}.

  • BatchResult – When on_error='nan'.

Raises:
  • ValueError – If ages does not contain exactly 3 elements. If tables_yz is provided but does not contain exactly 2 LifeTable instances. If any age is outside [0, w] for corresponding table. If return_flows=True and m is a vector with heterogeneous values. If t_output is not None and return_flows=False. If t_output is not None and input is scalar (non-batch mode). If on_error is not 'raise' or 'nan'. If on_error='nan' is passed with scalar x (or ages for joint-life methods). Use a list or array of ages for batch mode. If record_ids is not None and input is scalar (non-batch mode).

  • TypeError – If ages contains non-numeric values. If tables_yz contains non-LifeTable instances. If any numeric argument is bool.

Notes

Boolean values for age, duration, shift, deferment, and rate arguments raise TypeError. Accepted types for each parameter are described in Parameters above.

First-death (first-to-die) insurance for three lives under the independence assumption:

\[A_{xyz} = \sum_{k=0}^{\omega-1} v^{k+\alpha} \cdot {}_{k}p_x \cdot {}_{k}p_y \cdot {}_{k}p_z \cdot \left(1 - p_{x+k} \cdot p_{y+k} \cdot p_{z+k}\right)\]

where \(\alpha\) is set by self.mortality_placement ('beginning': 0, 'mid': 0.5, 'end': 1). For a term insurance of duration \(n\), the sum is restricted to \(k = 0, \ldots, \lfloor nm \rfloor - 1\).

  • Benefit is paid upon the first death among the three lives.

  • Uses independence assumption for joint survival probabilities.

  • When gr is provided, the growth factor at the j-th payment interval is (1+g)^(j // m) — anniversaries counted from the first payment, not the contract origin. GrowthRate.shifted(ts) consumes int(ts) anniversary years; fractional ts is actuarially correct (revaluations operate on complete policy years). See Growth Rate Conventions for the full reference.

Batch mode#

Batch mode is activated when either of these conditions is met:

  1. ages[0] is a list or NDArray (age-triggered batch): pass ages as a triple of N-element arrays (x_arr, y_arr, z_arr), each (N,).

  2. ages[0] is scalar but any of n, ts, d, ir, gr, or m is a list/tuple/NDArray of length N (param-triggered batch): scalar ages broadcast to N copies.

In both cases the method returns NDArray[np.float64] of shape (N,).

  • Per-policy parameters ts, d, n, ir, and gr may each be a scalar or an array of length N.

  • m may be a scalar (broadcast to all N) or an array/list of length N for per-policy payment frequencies; the batch engine groups policies by unique m value and vectorises each group independently.

  • Pandas and Polars Series are accepted for all per-policy parameters (n, ts, d, ir, gr, m, cashflow_times, cashflow_amounts, t_output, and each per-life age array within ages) without calling .to_numpy(); they are converted internally via the array protocol.

  • on_error='nan' marks invalid policies with np.nan.

See also

LifeTable.Afirst

First-death insurance for any number of lives.

LifeTable.Axy

First-death insurance for two lives.

LifeTable.Ax

Single-life insurance.

Examples

>>> from lactuca import LifeTable
>>> table = LifeTable('PER2020_Ind_1o', 'm', cohort=1960)
>>> table.interest_rate = 0.03
>>> pv = table.Axyz([65, 60, 55], n=20)
>>> table_m, table_f1, table_f2 = LifeTable('PER2020_Ind_1o', ('m', 'f', 'f'), cohort=1960)
>>> table_m.interest_rate = 0.03
>>> pv = table_m.Axyz([65, 62, 58], [table_f1, table_f2], n=15)

Batch mode:

>>> import numpy as np
>>> x_arr = np.array([60.0, 65.0])
>>> y_arr = np.array([55.0, 60.0])
>>> z_arr = np.array([50.0, 55.0])
>>> table.Axyz((x_arr, y_arr, z_arr), n=20, ir=0.03)
Cx(x: object = None, *, ir: object = None, x0: object = 0) object#

Commutation function \(C_x = v^{x+\alpha} \cdot d_x\) (integer ages only).

Following standard actuarial practice, commutation functions are defined for integer ages and annual payment periods only.

Parameters:
  • x (int, sequence of int, NDArray[np.float64], or None) – Integer age(s). Pandas and Polars Series of int are accepted. Pass None to compute the full table (all ages 0..ω).

  • ir (InterestRate or None, optional) – Interest rate curve. If None, uses interest_rate.

  • x0 (int, optional) – Reference age for discounting, consistent with Dx() (default 0).

Returns:

\(C_x\) value(s), rounded to decimals.Cx. Full-table output (x=None) is unrounded (used internally by Mx() and Rx()).

Return type:

float or np.ndarray

Raises:

ValueError – If x contains non-integer ages. If no interest rate is set and ir is None.

Notes

  • \(d_x = l_x - l_{x+1}\) is the number of deaths between ages \(x\) and \(x+1\).

  • The mortality placement offset \(\alpha\) is set by self.mortality_placement: 'beginning': \(\alpha = 0\), 'mid': \(\alpha = 0.5\), and 'end': \(\alpha = 1\).

  • Insurance identity: \(A_x = M_x / D_x\) (whole-life, end-of-year payments).

See also

LifeTable.Dx

Commutation function \(D_x = v^x \cdot l_x\).

LifeTable.Mx

Commutation function \(M_x = \sum_{k \geq x} C_k\).

LifeTable.Rx

Commutation function \(R_x = \sum_{k \geq x} M_k\).

Examples

>>> from lactuca import LifeTable
>>> from lactuca import InterestRate
>>> lt = LifeTable('PASEM2010', 'm')
>>> lt.Cx(65, ir=InterestRate(0.03))
>>> lt.Cx([60, 65, 70], ir=InterestRate(0.03))  # vectorized
Dx(x: object = None, *, ir: object = None, x0: object = 0) object#

Commutation function \(D_x = v^x \cdot l_x\) (integer ages only).

Following standard actuarial practice, commutation functions are defined for integer ages and annual payment periods only.

Parameters:
  • x (int, sequence of int, NDArray[np.float64], or None) – Integer age(s). Pandas and Polars Series of int are accepted. Pass None to compute the full table (all ages 0..ω).

  • ir (InterestRate or None, optional) – Interest rate curve. If None, uses interest_rate.

  • x0 (int, optional) – Reference age for discounting; discount factors are computed as \(v^{x - x_0}\) relative to age x0 (default 0). Used internally by Nx() and Sx() to ensure \(a_x = N_{x+1} / D_x\) holds for variable interest curves.

Returns:

\(D_x\) value(s), rounded to decimals.Dx for single/array input. Full-table output (x=None) is unrounded (used internally by Nx() and Sx()).

Return type:

float or np.ndarray

Raises:

ValueError – If x contains non-integer ages. If no interest rate is set and ir is None.

Notes

  • Commutation identities such as \(N_x = \\sum_{t \\geq x} D_t\) and \(a_x = N_{x+1}/D_x\) hold exactly when ir is a flat annual rate. With a variable InterestRate curve, use Nx() / ax() for product valuations rather than assuming textbook flat-rate commutation algebra.

  • For constant interest \(i\), the discount factor is \(v^{x-x_0} = (1+i)^{-(x-x_0)}\).

  • For variable interest, the factor is the product of annual discount factors from age x0 to age x.

See also

LifeTable.Nx

Commutation function \(N_x = \sum_{k \geq x} D_k\).

LifeTable.Sx

Commutation function \(S_x = \sum_{k \geq x} N_k\).

LifeTable.Cx

Commutation function \(C_x = v^{x+\alpha} \cdot d_x\).

Examples

>>> from lactuca import LifeTable
>>> from lactuca import InterestRate
>>> lt = LifeTable('PASEM2010', 'm')
>>> lt.Dx(65, ir=InterestRate(0.03))
>>> lt.Dx([60, 65, 70], ir=InterestRate(0.03))  # vectorized
Lx(x: object) object#

Person-years lived in [x, x+1) under UDD (integer ages only).

Following standard actuarial practice, commutation functions are defined for integer ages only. For fractional ages, use Lx_continuous().

Parameters:

x (int, sequence of int, or NDArray[np.float64]) – Integer age(s). Pandas and Polars Series of int are accepted. Non-integer ages will raise ValueError.

Returns:

Person-years lived at age interval [x, x+1).

Return type:

float or np.ndarray

Raises:
  • ValueError – If any x is outside [0, w] or contains non-integer ages.

  • TypeError – If x is not numeric or a sequence of numerics.

Notes

  • Lx is a commutation function strictly defined for integer ages.

  • Uses UDD (Uniform Distribution of Deaths) formula: \(L_x = l_x \left(1 - \frac{q_x}{2}\right)\).

  • \(L_{\omega} = 0\) by convention (no person-years beyond terminal age).

  • All results are rounded to the configured number of decimals (decimals.Lx).

  • Fully vectorized and optimized for speed and memory.

  • Follows international actuarial practice (UDD assumption).

See also

LifeTable.Tx

Total person-years lived from age x onwards (integer ages only).

LifeTable.Lx_continuous

Continuous version of Lx for fractional ages.

LifeTable.ex

Complete expectation of life (integer ages only).

Examples

>>> from lactuca import LifeTable
>>> tbl = LifeTable('PASEM2010', 'm')
>>> tbl.Lx(65)
>>> tbl.Lx([60, 65, 70])  # vectorized
Lx_continuous(x: object, *, m: object = 12) object#

Compute Lx at fractional ages using numerical integration.

Accepts only fractional ages; for integer ages use Lx() instead. The integral \(L_x(x) = \int_x^{x+1} l_t \, dt\) is evaluated via the trapezoidal rule with m+1 equally-spaced nodes.

Parameters:
  • x (float, list, or np.ndarray) – Fractional age(s) to evaluate. Integer ages will raise ValueError. Use Lx() for integer ages.

  • m (object, optional) – Trapezoidal integration density (default 12). Interpreted as the number of subintervals per unit age span; must be a value from VALID_PAYMENT_FREQUENCIES: 1, 2, 3, 4, 6, 12, 14, 24, 26, 52, 365. Higher m increases precision at greater computational cost. bool is rejected.

Returns:

Person-years lived at age interval [x, x+1).

Return type:

float or np.ndarray

Raises:
  • ValueError – If any x is an integer age, outside [0, w], or m is not a valid payment frequency. For integer ages, use Lx() instead.

  • TypeError – If x is not numeric or a sequence of numerics, or if m is a bool.

Notes

  • Computes \(L_x(x) = \int_x^{x+1} l_t \, dt\) via the trapezoidal rule.

  • Strictly for fractional (non-integer) ages; use Lx() for integer ages.

  • Each \(l_t\) evaluation uses the configured interpolation method.

  • \(L_{\omega} = 0\) by convention (no person-years beyond terminal age).

  • All results are rounded to the configured number of decimals (decimals.Lx).

  • Fully vectorized and optimized for speed and memory.

  • Follows international actuarial practice for continuous age calculations.

See also

LifeTable.Lx

Standard commutation function for integer ages only (required for integer ages).

LifeTable.Tx_continuous

Continuous version of Tx.

LifeTable.ex_continuous

Continuous version of ex.

Examples

>>> from lactuca import LifeTable
>>> tbl = LifeTable("PASEM2010", "m")
>>> tbl.Lx_continuous(30.5)
>>> tbl.Lx_continuous(30)  # integer age — raises ValueError
Mx(x: object = None, *, ir: object = None, x0: object = 0) object#

Commutation function \(M_x = \sum_{k \geq x} C_k\) (suffix sums of Cx).

Following standard actuarial practice, commutation functions are defined for integer ages and annual payment periods only.

Parameters:
  • x (int, sequence of int, NDArray[np.float64], or None) – Integer age(s). Pandas and Polars Series of int are accepted. Pass None to compute the full table (all ages 0..ω).

  • ir (InterestRate or None, optional) – Interest rate curve. If None, uses interest_rate.

  • x0 (int, optional) – Reference age for discounting, consistent with Dx() (default 0).

Returns:

\(M_x\) value(s), rounded to decimals.Mx. Full-table output (x=None) is unrounded (used internally by Rx() and insurance calculations).

Return type:

float or np.ndarray

Raises:

ValueError – If x contains non-integer ages. If no interest rate is set and ir is None.

Notes

  • Built via reversed cumsum of \(C_k\) in O(ω) time.

  • Insurance identity: \(A_x = M_x / D_x\) (whole-life, end-of-year payments).

See also

LifeTable.Cx

Commutation function \(C_x = v^{x+\alpha} \cdot d_x\).

LifeTable.Rx

Commutation function \(R_x = \sum_{k \geq x} M_k\).

LifeTable.Nx

Commutation function \(N_x\) (annuity analog of \(M_x\)).

Examples

>>> from lactuca import LifeTable
>>> from lactuca import InterestRate
>>> lt = LifeTable('PASEM2010', 'm')
>>> lt.Mx(65, ir=InterestRate(0.03))
>>> lt.Mx([60, 65, 70], ir=InterestRate(0.03))  # vectorized
Nx(x: object = None, *, ir: object = None, x0: object = 0) object#

Commutation function \(N_x = \sum_{k \geq x} D_k\) (suffix sums of Dx).

Following standard actuarial practice, commutation functions are defined for integer ages and annual payment periods only.

Parameters:
  • x (int, sequence of int, NDArray[np.float64], or None) – Integer age(s). Pandas and Polars Series of int are accepted. Pass None to compute the full table (all ages 0..ω).

  • ir (InterestRate or None, optional) – Interest rate curve. If None, uses interest_rate.

  • x0 (int, optional) – Reference age for discounting, consistent with Dx() (default 0).

Returns:

\(N_x\) value(s), rounded to decimals.Nx. Full-table output (x=None) is unrounded (used internally by Sx() and annuity calculations).

Return type:

float or np.ndarray

Raises:

ValueError – If x contains non-integer ages. If no interest rate is set and ir is None.

Notes

  • Built via reversed cumsum of \(D_k\) in O(ω) time.

  • Annuity-due identity: \(\ddot{a}_x = N_{x+1} / D_x\).

See also

LifeTable.Dx

Commutation function \(D_x = v^x \cdot l_x\).

LifeTable.Sx

Commutation function \(S_x = \sum_{k \geq x} N_k\).

LifeTable.Mx

Commutation function \(M_x\) (insurance analog of \(N_x\)).

Examples

>>> from lactuca import LifeTable
>>> from lactuca import InterestRate
>>> lt = LifeTable('PASEM2010', 'm')
>>> lt.Nx(65, ir=InterestRate(0.03))
>>> lt.Nx([60, 65, 70], ir=InterestRate(0.03))  # vectorized
Rx(x: object = None, *, ir: object = None, x0: object = 0) object#

Commutation function \(R_x = \sum_{k \geq x} M_k\) (suffix sums of Mx).

Following standard actuarial practice, commutation functions are defined for integer ages and annual payment periods only.

Parameters:
  • x (int, sequence of int, NDArray[np.float64], or None) – Integer age(s). Pandas and Polars Series of int are accepted. Pass None to compute the full table (all ages 0..ω).

  • ir (InterestRate or None, optional) – Interest rate curve. If None, uses interest_rate.

  • x0 (int, optional) – Reference age for discounting, consistent with Dx() (default 0).

Returns:

\(R_x\) value(s), rounded to decimals.Rx. Full-table output (x=None) is unrounded (used internally).

Return type:

float or np.ndarray

Raises:

ValueError – If x contains non-integer ages. If no interest rate is set and ir is None.

Notes

  • Second-order suffix sum: built via reversed cumsum of \(M_k\) in O(ω) time.

  • Related identity for increasing insurance (when mortality_placement='end', i.e. \(\alpha = 1\)): \((IA)_x = R_x / D_x\).

See also

LifeTable.Mx

Commutation function \(M_x = \sum_{k \geq x} C_k\).

LifeTable.Cx

Commutation function \(C_x = v^{x+\alpha} \cdot d_x\).

LifeTable.Sx

Commutation function \(S_x\) (annuity analog of \(R_x\)).

Examples

>>> from lactuca import LifeTable
>>> from lactuca import InterestRate
>>> lt = LifeTable('PASEM2010', 'm')
>>> lt.Rx(65, ir=InterestRate(0.03))
>>> lt.Rx([60, 65, 70], ir=InterestRate(0.03))  # vectorized
Sx(x: object = None, *, ir: object = None, x0: object = 0) object#

Commutation function \(S_x = \sum_{k \geq x} N_k\) (suffix sums of Nx).

Following standard actuarial practice, commutation functions are defined for integer ages and annual payment periods only.

Parameters:
  • x (int, sequence of int, NDArray[np.float64], or None) – Integer age(s). Pandas and Polars Series of int are accepted. Pass None to compute the full table (all ages 0..ω).

  • ir (InterestRate or None, optional) – Interest rate curve. If None, uses interest_rate.

  • x0 (int, optional) – Reference age for discounting, consistent with Dx() (default 0).

Returns:

\(S_x\) value(s), rounded to decimals.Sx. Full-table output (x=None) is unrounded (used internally).

Return type:

float or np.ndarray

Raises:

ValueError – If x contains non-integer ages. If no interest rate is set and ir is None.

Notes

  • Second-order suffix sum: built via reversed cumsum of \(N_k\) in O(ω) time.

  • The terminal age is included in the suffix, so \(S_{\omega} = N_{\omega}\).

  • For x=None, the full suffix vector is returned at float64 precision (no rounding), which is the internal path used by dependent commutation/annuity routines.

  • Related annuity identity: \((I\ddot{a})_x = S_{x+1} / D_x\).

See also

LifeTable.Nx

Commutation function \(N_x = \sum_{k \geq x} D_k\).

LifeTable.Dx

Commutation function \(D_x = v^x \cdot l_x\).

LifeTable.Rx

Commutation function \(R_x\) (insurance analog of \(S_x\)).

Examples

>>> from lactuca import LifeTable
>>> from lactuca import InterestRate
>>> lt = LifeTable('PASEM2010', 'm')
>>> lt.Sx(65, ir=InterestRate(0.03))
>>> lt.Sx([60, 65, 70], ir=InterestRate(0.03))  # vectorized
Tx(x: object) object#

Return total person-years \(T_x\) from age x onwards (integer ages only).

Defined as \(T_x = \sum_{k=x}^{\omega-1} L_k\), i.e. the sum of Lx from age x to omega - 1 (inclusive). Satisfies the recurrence \(T_x = L_x + T_{x+1}\).

Following standard actuarial practice, commutation functions are defined for integer ages only. For fractional ages, use Tx_continuous().

Parameters:

x (int, sequence of int, or NDArray[np.float64]) – Integer age(s). Pandas and Polars Series of int are accepted. Non-integer ages will raise ValueError.

Returns:

Tx value(s) at integer age(s) x.

Return type:

float or np.ndarray

Raises:
  • ValueError – If any x is outside [0, w] or contains non-integer ages.

  • TypeError – If x is not numeric or a sequence of numerics.

Notes

  • Tx is a commutation function strictly defined for integer ages.

  • Recurrence: \(T_x = L_x + T_{x+1}\); \(T_{\omega} = 0\).

  • All results are rounded to the configured number of decimals (decimals.Tx).

  • Fully vectorized and optimized for speed and memory.

  • Follows international actuarial practice.

See also

LifeTable.Lx

Person-years lived in [x, x+1) under UDD (integer ages only).

LifeTable.Tx_continuous

Continuous version of Tx for fractional ages.

LifeTable.ex

Complete expectation of life (integer ages only).

Examples

>>> from lactuca import LifeTable
>>> tbl = LifeTable('PASEM2010', 'm')
>>> tbl.Tx(65)
>>> tbl.Tx([60, 65, 70])  # vectorized
Tx_continuous(x: object, *, m: object = 12) object#

Compute Tx at fractional ages using numerical integration.

Accepts only fractional ages; for integer ages use Tx() instead.

Parameters:
  • x (float, list, or np.ndarray) – Fractional age(s) to evaluate. Integer ages will raise ValueError. Use Tx() for integer ages.

  • m (object, optional) – Trapezoidal integration density (default 12). Interpreted as the number of subintervals per unit age span; must be a value from VALID_PAYMENT_FREQUENCIES: 1, 2, 3, 4, 6, 12, 14, 24, 26, 52, 365. Higher m increases precision at greater computational cost. bool is rejected.

Returns:

Present value of future years lived at age(s) x.

Return type:

float or np.ndarray

Raises:
  • ValueError – If any x is an integer age, outside [0, w], or m is not a valid payment frequency. For integer ages, use Tx() instead.

  • TypeError – If x is not numeric or a sequence of numerics, or if m is a bool.

Notes

  • Computes \(T_x = \int_x^{\infty} l_t \, dt\) via the trapezoidal rule.

  • Strictly for fractional (non-integer) ages; use Tx() for integer ages.

  • Each \(l_t\) evaluation uses the configured interpolation method.

  • All results are rounded to the configured number of decimals (decimals.Tx).

  • Fully vectorized and optimized for speed and memory.

  • Follows international actuarial practice for continuous ages.

See also

LifeTable.Tx

Standard commutation function for integer ages only (required for integer ages).

LifeTable.Lx_continuous

Continuous version of Lx.

LifeTable.ex_continuous

Continuous version of ex.

Examples

>>> from lactuca import LifeTable
>>> tbl = LifeTable("PASEM2010", "m")
>>> tbl.Tx_continuous(30.5)
>>> tbl.Tx_continuous(30.5, m=365)  # higher precision
>>> tbl.Tx_continuous(30)  # integer age — raises ValueError
ajoint(ages: object, tables_others: Sequence[LifeTable] | Sequence[Sequence[LifeTable]] | None = None, *, ts: object = 0.0, d: object = 0.0, n: object = None, m: Literal[1, 2, 3, 4, 6, 12, 14, 24, 26, 52, 365] | Sequence[int] | NDArray[int64] = 1, gr: GrowthRate | float | None | Sequence[GrowthRate | float | None] = None, cashflow_times: Sequence[float] | None = None, cashflow_amounts: Sequence[float] | None = None, ir: object = None, return_flows: bool = False, t_output: NDArray[float64] | None = None, benefits: Sequence[float] | NDArray[float64] | None = None, on_error: Literal['raise', 'nan'] = 'raise', record_ids: Sequence[Any] | None = None) float | dict[str, NDArray[float64]] | BatchResult#

Compute the present value of a joint-life annuity-immediate (postpayable) for multiple lives.

Parameters:
  • ages (Sequence[float] or list[Sequence[float]]) – Ages of all insured lives. Must have at least 2 elements. In batch mode, pass a list of r N-element arrays: [x0_arr, x1_arr, ..., xr_arr] where each has shape (N,).

  • tables_others (Sequence[LifeTable] or Sequence[Sequence[LifeTable]] or None, optional) –

    Life tables for additional lives (all except the first, which uses self). Three modes in batch:

    • None: uses self for all lives in all policies.

    • Sequence[LifeTable] of length n_lives - 1: shared across all N policies.

    • Sequence[Sequence[LifeTable]] of length N: per-policy; each inner list must have exactly n_lives - 1 elements. Raises ValueError if length != N.

  • ts (float or NDArray[np.float64], optional) – Shift (displacement) in years from origin (default 0.0). In batch mode, may be a scalar or an array of length N. Pandas and Polars Series are accepted; values are converted to float64. Scalar int literals (e.g. n=10) are coerced to float.

  • d (float or NDArray[np.float64], optional) – Deferment period in years (default 0.0). In batch mode, may be a scalar or an array of length N. Pandas and Polars Series are accepted; values are converted to float64. Scalar int literals (e.g. n=10) are coerced to float.

  • n (float or NDArray[np.float64] or None, optional) – Total duration of the annuity (in years) from contract start. If None, perpetual annuity up to limiting age. In batch mode, may be a scalar or an array of length N. Pandas and Polars Series are accepted; values are converted to float64. Scalar int literals (e.g. n=10) are coerced to float.

  • m (PaymentFrequencyLiteral, sequence of int, NDArray[np.int64], or array-like, optional) – Number of payments per year (1=annual, 12=monthly, etc., default 1). Integer-valued floats (e.g. 12.0) are coerced to int. In batch mode, may be a scalar (broadcast to all N) or an array/list of length N for per-policy payment frequencies; distinct values are processed in sub-groups by the vectorised batch engine. Pandas and Polars Series are accepted; values are converted to int64.

  • gr (GrowthRate, float, or None, optional) – Growth rate model (default None). A scalar float is auto-wrapped as GrowthRate(gr) (geometric growth by default). If None, no growth is applied. In batch mode, may be a scalar or a list of N values. Pandas and Polars Series are accepted without .to_numpy(). A numeric Series is converted to per-policy floats (auto-wrapped as GrowthRate where applicable). An object-dtype Series of GrowthRate instances is treated like a list of length N (including piecewise curves). Mixed numeric and object elements in one array-like sequence raise ValueError.

  • cashflow_times (Sequence[float], optional) – Custom payment times (years since origin). Overrides m when provided. Pandas and Polars Series are accepted; converted to NDArray[np.float64] internally.

  • cashflow_amounts (Sequence[float], optional) – Custom payment amounts. Disables growth when provided. Pandas and Polars Series are accepted; converted to NDArray[np.float64] internally.

  • ir (float, InterestRate, list[float or InterestRate], NDArray[np.float64], or None, optional) – Interest rate (constant or curve). If None, uses interest_rate. In batch mode, may be a scalar (broadcast to all N), a list of N float values, or a list of N InterestRate objects (one curve per policy). Pandas and Polars Series are accepted without .to_numpy(). A numeric Series is converted to per-policy floats (auto-wrapped as InterestRate where applicable). An object-dtype Series of InterestRate instances is treated like a list of length N (including piecewise curves). Mixed numeric and object elements in one array-like sequence raise ValueError.

  • return_flows (bool, optional) –

    If True, returns a cashflow decomposition dict instead of the present value. The dict schema differs between single-policy and batch:

    Single-policy (scalar ages): per-payment engine dict. Same keys as ax(); see that method’s return_flows parameter for the full key reference.

    Batch (array ages): aggregate portfolio dict {'time_grid', 'expected_cf', 'pv_cf', 'total_pv'}. total_pv == np.sum(pv_cf) is always satisfied. Raises ValueError in discrete_simplified / continuous_simplified modes. Default is False.

  • t_output (NDArray[np.float64] or None, optional) – External bucketing grid for aggregate flow output. When provided, per-policy present-value contributions — already discounted at their exact payment times — are accumulated into the nearest bucket via np.searchsorted without re-discounting. Controls only the shape of expected_cf and pv_cf; total_pv is always the exact sum of all per-payment present values and is identical to the t_output=None result. Typical use: regulatory templates that require a fixed set of time buckets (e.g., annual IFRS 17 disclosure, Solvency II SCR projection). Only effective when return_flows=True; raises ValueError if return_flows=False. Default is None. Pandas and Polars Series are accepted; converted to NDArray[np.float64] before use.

  • benefits (sequence of float or NDArray[np.float64] or None, optional) – Per-policy benefit amounts, shape (N,). Scales each policy’s contribution to the aggregate flows (e.g., sum insured, PVDBO weight). Requires batch mode. Any sequence is accepted, including Pandas and Polars Series; values are converted to float64. Compatible with return_flows=False (returns NDArray[np.float64] with per-policy scaled PVs) and on_error='nan'. Default is None.

  • on_error ({'raise', 'nan'}, optional) – Error handling for invalid ages or parameters. 'raise' (default) raises ValueError; 'nan' marks invalid policies with np.nan and returns BatchResult. Default is 'raise'.

  • record_ids (sequence or None, optional) – Identifiers for each policy in batch mode, used to populate the error report when on_error='nan'. Length must equal N. Any sequence is accepted, including Pandas and Polars Series; elements are always accessed by positional integer index. Default is None.

Returns:

  • float – When ages[0] is scalar and return_flows=False.

  • NDArray[np.float64] – When ages[0] is an array or list and return_flows=False. Shape (N,).

  • dict – When return_flows=True. Single-policy: per-payment engine dict (same keys as ax()). Batch: aggregate portfolio dict {'time_grid', 'expected_cf', 'pv_cf', 'total_pv'}.

  • BatchResult – When on_error='nan'.

Raises:
  • ValueError – If ages has fewer than 2 elements. If any age is outside [0, w] for corresponding table. If return_flows=True in batch mode and calculation_mode is 'discrete_simplified' or 'continuous_simplified'. If return_flows=True and m is a vector with heterogeneous values. If t_output is not None and return_flows=False. If t_output is not None and input is scalar (non-batch mode). If on_error is not 'raise' or 'nan'. If on_error='nan' is passed with scalar x (or ages for joint-life methods). Use a list or array of ages for batch mode. If record_ids is not None and input is scalar (non-batch mode).

  • TypeError – If ages or tables_others contain invalid types (including bool for numeric kwargs).

Notes

Boolean values for age, duration, shift, deferment, and rate arguments raise TypeError. Accepted types for each parameter are described in Parameters above.

Joint-life annuity-immediate for r lives, under the independence assumption:

\[a_{x_1 x_2 \cdots x_r}^{(m)} = \frac{1}{m} \sum_{k=1}^{\infty} v^{k/m} \prod_{i=1}^{r} {}_{k/m}p_{x_i}\]
  • Payments occur at the END of each period (immediate/postpayable).

  • Annuity continues while ALL lives survive (joint-life status).

  • Uses independence assumption for joint survival probabilities.

  • When gr is provided, the growth factor at the j-th payment interval is (1+g)^(j // m) — anniversaries counted from the first payment, not the contract origin. GrowthRate.shifted(ts) consumes int(ts) anniversary years; fractional ts is actuarially correct (revaluations operate on complete policy years). See Growth Rate Conventions for the full reference.

Batch mode#

Batch mode is activated when either of these conditions is met:

  1. ages[0] is a list or NDArray (age-triggered batch): pass ages as a list of r N-element arrays, each shape (N,).

  2. ages[0] is scalar but any of n, ts, d, ir, gr, or m is a list/tuple/NDArray of length N (param-triggered batch): scalar ages broadcast to N copies.

In both cases the method returns NDArray[np.float64] of shape (N,).

  • Per-policy parameters ts, d, n, ir, and gr may each be a scalar or an array of length N.

  • m may be a scalar (broadcast to all N) or an array/list of length N for per-policy payment frequencies; the batch engine groups policies by unique m value and vectorises each group independently.

  • Pandas and Polars Series are accepted for all per-policy parameters (n, ts, d, ir, gr, m, cashflow_times, cashflow_amounts, t_output, and each per-life age array within ages) without calling .to_numpy(); they are converted internally via the array protocol.

  • on_error='nan' marks invalid policies with np.nan.

  • In discrete_precision, irregular cashflow_times schedules are vectorised in batch (same grid shared across all policies); see Irregular Cashflows.

See also

LifeTable.äjoint

Joint-life annuity-due (prepayable) for multiple lives.

LifeTable.axy

Joint-life annuity-immediate for two lives.

LifeTable.axyz

Joint-life annuity-immediate for three lives.

LifeTable.ax

Single-life annuity-immediate.

Examples

>>> from lactuca import LifeTable
>>> # Four lives with same table
>>> table = LifeTable('PER2020_Ind_1o', 'm', cohort=1960)
>>> table.interest_rate = 0.03
>>> pv = table.ajoint([65, 60, 55, 50], n=20, m=12)
>>> # Multiple lives with different tables
>>> table_m, table_f1, table_f2 = LifeTable('PER2020_Ind_1o', ('m', 'f', 'f'), cohort=1960)
>>> table_m.interest_rate = 0.03
>>> pv = table_m.ajoint([65, 62, 58], [table_f1, table_f2], n=15, m=12)

Batch mode:

>>> import numpy as np
>>> a0 = np.array([60.0, 65.0])
>>> a1 = np.array([55.0, 60.0])
>>> table.ajoint([a0, a1], n=20, ir=0.03)
ax(x: object, *, ts: object = 0.0, d: object = 0.0, n: object = None, m: Literal[1, 2, 3, 4, 6, 12, 14, 24, 26, 52, 365] | Sequence[int] | NDArray[int64] = 1, gr: GrowthRate | float | None | Sequence[GrowthRate | float | None] = None, cashflow_times: Sequence[float] | None = None, cashflow_amounts: Sequence[float] | None = None, ir: object = None, return_flows: bool = False, t_output: NDArray[float64] | None = None, benefits: Sequence[float] | NDArray[float64] | None = None, on_error: Literal['raise', 'nan'] = 'raise', record_ids: Sequence[Any] | None = None) float | NDArray[float64] | dict[str, NDArray[float64] | float] | BatchResult#

Compute the present value of an annuity-immediate (postpayable) for a single life.

Parameters:
  • x (float or list of float or NDArray[np.float64]) – Initial age. Pass an array or list of N ages to activate batch mode, which returns an ndarray of shape (N,).

  • ts (float or NDArray[np.float64], optional) – Shift (displacement) in years from origin. The calculation is performed as if ts years have already elapsed since the contract start. Default is 0.0. In batch mode, may be a scalar (broadcast to all N) or an array of length N. Pandas and Polars Series are accepted; values are converted to float64. Scalar int literals (e.g. n=10) are coerced to float.

  • d (float or NDArray[np.float64], optional) – Deferment period in years. The first payment is made after d years (plus any shift ts). Default is 0.0. In batch mode, may be a scalar (broadcast to all N) or an array of length N. Pandas and Polars Series are accepted; values are converted to float64. Scalar int literals (e.g. n=10) are coerced to float.

  • n (float or NDArray[np.float64] or None, optional) – Total duration of the annuity (in years) from the contract start. In batch mode, may be a scalar or an array of length N. Pandas and Polars Series are accepted; values are converted to float64. Scalar int literals (e.g. n=10) are coerced to float.

  • m (PaymentFrequencyLiteral, sequence of int, NDArray[np.int64], or array-like, optional) – Number of payments per year (1=annual, 12=monthly, etc.). Integer-valued floats (e.g. 12.0) are coerced to int. In batch mode, may be a scalar (broadcast to all N) or an array/list of length N for per-policy payment frequencies; distinct values are processed in sub-groups by the vectorised batch engine. Pandas and Polars Series are accepted; values are converted to int64.

  • gr (GrowthRate, float, or None, optional) – Growth rate model. A scalar float is auto-wrapped as GrowthRate(gr) (geometric growth by default). If None, no growth is applied. In batch mode, may be a scalar or a list of N values. Pandas and Polars Series are accepted without .to_numpy(). A numeric Series is converted to per-policy floats (auto-wrapped as GrowthRate where applicable). An object-dtype Series of GrowthRate instances is treated like a list of length N (including piecewise curves). Mixed numeric and object elements in one array-like sequence raise ValueError.

  • cashflow_times (sequence of float, optional) – Custom payment times (years since contract origin). When provided, overrides the regular payment grid generated by m. Pandas and Polars Series are accepted; converted to NDArray[np.float64] internally.

  • cashflow_amounts (sequence of float, optional) – Custom payment amounts on the payment grid. When cashflow_times is None, length must equal the regular-grid payment count (\lfloor n_\text{eff} \cdot m \rfloor, plus one when n_\text{eff} \cdot m is not an integer — see Last Payment Adjustment); when cashflow_times is provided, length must equal len(cashflow_times). Disables growth (gr is ignored) when provided. Pandas and Polars Series are accepted; converted to NDArray[np.float64] internally.

  • ir (float, InterestRate, list[float or InterestRate], NDArray[np.float64], or None, optional) – Interest rate (constant or curve). If None, uses interest_rate. In batch mode, may be a scalar (broadcast to all N), a list of N float values, or a list of N InterestRate objects (one curve per policy). Pandas and Polars Series are accepted without .to_numpy(). A numeric Series is converted to per-policy floats (auto-wrapped as InterestRate where applicable). An object-dtype Series of InterestRate instances is treated like a list of length N (including piecewise curves). Mixed numeric and object elements in one array-like sequence raise ValueError.

  • return_flows (bool, optional) –

    If True, returns a cashflow decomposition dict instead of the present value. The dict schema differs between scalar and batch:

    Scalar x, discrete_precision / discrete_simplified mode:

    {
      "time_grid"            : NDArray  # payment times
      "interest_rate"        : NDArray  # i(t) at each payment
      "discount_factor"      : NDArray  # v^t at each payment
      "survival_probability" : NDArray  # tpx at each payment
      "growth" or "amount"   : NDArray  # growth factors or custom amounts
      "payment_adjustment"   : NDArray  # fractional last-payment factor
      "present_value_raw"    : NDArray  # pv before /m scaling
      "present_value"        : NDArray  # final pv per payment (/m applied)
    }
    

    sum(result["present_value"]) equals the rounded scalar present value.

    Scalar x, continuous_precision mode:

    {
      "time_grid"   : NDArray  # integration points
      "expected_cf" : NDArray  # undiscounted density × dt
      "pv_cf"       : NDArray  # discounted density × dt
    }
    

    np.sum(result["pv_cf"]) approximates the present value.

    Batch (array x), *_precision modes only:

    {
      "time_grid"   : NDArray  # portfolio time grid
      "expected_cf" : NDArray  # sum of undiscounted contributions across policies
      "pv_cf"       : NDArray  # sum of discounted contributions across policies
      "total_pv"    : float    # sum(pv_cf); equals sum of scalar present values
    }
    

    total_pv == np.sum(pv_cf) is always satisfied. Raises ValueError in discrete_simplified / continuous_simplified modes. Default is False.

  • t_output (NDArray[np.float64] or None, optional) – External bucketing grid for aggregate flow output. When provided, per-policy present-value contributions — already discounted at their exact payment times — are accumulated into the nearest bucket via np.searchsorted without re-discounting. Controls only the shape of expected_cf and pv_cf; total_pv is always the exact sum of all per-payment present values and is identical to the t_output=None result. Typical use: regulatory templates that require a fixed set of time buckets (e.g., annual IFRS 17 disclosure, Solvency II SCR projection). Only effective when return_flows=True; raises ValueError if return_flows=False. Default is None. Pandas and Polars Series are accepted; converted to NDArray[np.float64] before use.

  • benefits (sequence of float or NDArray[np.float64] or None, optional) – Per-policy benefit amounts, shape (N,). Scales each policy’s contribution to the aggregate flows (e.g., sum insured, PVDBO weight). Requires batch mode. Any sequence is accepted, including Pandas and Polars Series; values are converted to float64. Compatible with return_flows=False (returns NDArray[np.float64] with per-policy scaled PVs) and on_error='nan'. Default is None.

  • on_error ({'raise', 'nan'}, optional) – Error handling for invalid ages or parameters. 'raise' (default) raises ValueError; 'nan' marks invalid policies with np.nan and returns BatchResult. Default is 'raise'.

  • record_ids (sequence or None, optional) – Identifiers for each policy in batch mode, used to populate the error report when on_error='nan'. Length must equal N. Any sequence is accepted, including Pandas and Polars Series; elements are always accessed by positional integer index. Default is None.

Returns:

  • float – When x is scalar and return_flows=False.

  • NDArray[np.float64] – When x is an array or list and return_flows=False. Shape (N,). A one-element list returns shape (1,), never float.

  • dict – When return_flows=True. Schema depends on scalar vs. batch and on calculation_mode; see the return_flows parameter description above for the full key listing of each variant.

  • BatchResult – When on_error='nan', wraps the result array and an error report.

Raises:
  • ValueError – If no interest rate is provided and interest_rate is None. If ts, d, or n fail timing validation. If return_flows=True in batch mode and calculation_mode is 'discrete_simplified' or 'continuous_simplified'. If return_flows=True and m is a vector with heterogeneous values; use a scalar m or split the batch by payment frequency. If t_output is not None and return_flows=False. If t_output is not None and input is scalar (non-batch mode). If on_error is not 'raise' or 'nan'. If on_error='nan' is passed with scalar x (or ages for joint-life methods). Use a list or array of ages for batch mode. If record_ids is not None and input is scalar (non-batch mode).

  • TypeError – If any argument has an incompatible type (including bool for numeric kwargs).

Notes

Boolean values for age, duration, shift, deferment, and rate arguments raise TypeError. Accepted types for each parameter are described in Parameters above.

Discrete whole-life annuity-immediate (\(m\) payments per year):

\[a_x^{(m)} = \frac{1}{m} \sum_{k=1}^{\infty} v^{k/m} \cdot {}_{k/m}p_x\]

For a temporary n-year annuity with optional d-year deferment:

\[{}_{d|}a_{x:\overline{n}|}^{(m)} = \frac{1}{m} \sum_{k=dm+1}^{(d+n)m} v^{k/m} \cdot {}_{k/m}p_x\]
  • The calculation is performed at age x + ts, with effective deferment max(d - ts, 0) and effective duration max(n - max(ts - d, 0), 0).

  • If ts >= d + n (shift absorbs the entire deferred annuity), the value is zero.

  • When gr is provided, the growth factor at the j-th payment is (1+g)^(j // m) — anniversaries are counted from the first payment of the call, not from the contract origin. Deferment d shifts payment times but does not affect this anniversary index. Fractional ts values are accepted; GrowthRate.shifted(ts) discards the first int(ts) anniversary years of the schedule, which is actuarially correct because revaluations operate on complete policy years. See Growth Rate Conventions for the full reference.

Batch mode#

When x is a list, sequence, or ndarray, or when any of n, ts, d, ir, gr, or m is a sequence or array with scalar x, the method operates in batch mode and returns an ndarray of shape (N,):

  • A list of 1 element still returns NDArray shape (1,), never a scalar.

  • Per-policy parameters ts, d, n, ir, and gr may each be a scalar (broadcast to all N policies) or an array of length N.

  • m may be a scalar (broadcast to all N policies) or an array/list of length N for per-policy payment frequencies; the batch engine groups policies by unique m value and vectorises each group independently.

  • Pandas and Polars Series are accepted for all per-policy parameters (x, n, ts, d, ir, gr, m, cashflow_times, cashflow_amounts, t_output) without calling .to_numpy(); they are converted internally via the array protocol.

  • discrete_precision mode dispatches to a vectorised matrix engine (50–250× faster than a Python scalar loop for N ≥ 100).

  • continuous_* modes use a serial fallback (~1.5× vs loop).

  • on_error='nan' marks invalid policies with np.nan instead of raising; combine with record_ids to trace failures.

See also

LifeTable.äx

Annuity-due (prepayable) equivalent.

LifeTable.axy

Joint-life annuity-immediate for two lives.

LifeTable.ajoint

Joint-life annuity-immediate for multiple lives.

Examples

>>> from lactuca import LifeTable
>>> lt = LifeTable('PASEM2010', 'm')
>>> lt.interest_rate = 0.03
>>> lt.ax(65, n=10, m=12)
>>> lt.ax(65)  # whole life

Batch mode:

>>> import numpy as np
>>> ages = np.array([55.0, 60.0, 65.0, 70.0])
>>> lt.ax(ages, n=20, ir=0.03)
>>> lt.ax([65], n=20, ir=0.03)          # NDArray shape (1,)
>>> lt.ax(ages, n=np.array([25.0, 20.0, 15.0, 10.0]), ir=0.03)
>>> Ax = lt.Ax(ages, n=20, ir=0.03)
>>> ax_ = lt.ax(ages, n=20, ir=0.03)
>>> P = Ax / ax_           # batch net premium, NDArray (4,)
axy(ages: object, table_y: LifeTable | Sequence[LifeTable] | None = None, *, ts: object = 0.0, d: object = 0.0, n: object = None, m: Literal[1, 2, 3, 4, 6, 12, 14, 24, 26, 52, 365] | Sequence[int] | NDArray[int64] = 1, gr: GrowthRate | float | None | Sequence[GrowthRate | float | None] = None, cashflow_times: Sequence[float] | None = None, cashflow_amounts: Sequence[float] | None = None, ir: object = None, return_flows: bool = False, t_output: NDArray[float64] | None = None, benefits: Sequence[float] | NDArray[float64] | None = None, on_error: Literal['raise', 'nan'] = 'raise', record_ids: Sequence[Any] | None = None) float | NDArray[float64] | dict[str, NDArray[float64]] | BatchResult#

Compute the present value of a joint-life annuity-immediate (postpayable) for two lives.

Parameters:
  • ages (Sequence[float] or tuple[Sequence[float], Sequence[float]]) – Ages of the two lives [x, y]. Must have exactly 2 elements. In batch mode, pass a pair of N-element arrays: (np.array([x0, …, xN]), np.array([y0, …, yN])).

  • table_y (LifeTable or Sequence[LifeTable] or None, optional) –

    Life table for the second life (y). Three modes in batch:

    • None: uses self for all policies.

    • LifeTable: single shared table for all N policies.

    • Sequence[LifeTable] of length N: per-policy table; policy i uses element i. Raises ValueError if length != N.

  • ts (float or NDArray[np.float64], optional) – Shift (displacement) in years from origin (default 0.0). In batch mode, may be a scalar (broadcast) or an array of length N. Pandas and Polars Series are accepted; values are converted to float64. Scalar int literals (e.g. n=10) are coerced to float.

  • d (float or NDArray[np.float64], optional) – Deferment period in years (default 0.0). In batch mode, may be a scalar or an array of length N. Pandas and Polars Series are accepted; values are converted to float64. Scalar int literals (e.g. n=10) are coerced to float.

  • n (float or NDArray[np.float64] or None, optional) – Total duration of the annuity (in years) from contract start. If None, perpetual annuity up to limiting age. In batch mode, may be a scalar or an array of length N. Pandas and Polars Series are accepted; values are converted to float64. Scalar int literals (e.g. n=10) are coerced to float.

  • m (PaymentFrequencyLiteral, sequence of int, NDArray[np.int64], or array-like, optional) – Number of payments per year (1=annual, 12=monthly, etc., default 1). Integer-valued floats (e.g. 12.0) are coerced to int. In batch mode, may be a scalar (broadcast to all N) or an array/list of length N for per-policy payment frequencies; distinct values are processed in sub-groups by the vectorised batch engine. Pandas and Polars Series are accepted; values are converted to int64.

  • gr (GrowthRate, float, or None, optional) – Growth rate model (default None). A scalar float is auto-wrapped as GrowthRate(gr) (geometric growth by default). If None, no growth is applied. In batch mode, may be a scalar or a list of N values. Pandas and Polars Series are accepted without .to_numpy(). A numeric Series is converted to per-policy floats (auto-wrapped as GrowthRate where applicable). An object-dtype Series of GrowthRate instances is treated like a list of length N (including piecewise curves). Mixed numeric and object elements in one array-like sequence raise ValueError.

  • cashflow_times (Sequence[float], optional) – Custom payment times (years since origin). Overrides m when provided. Pandas and Polars Series are accepted; converted to NDArray[np.float64] internally.

  • cashflow_amounts (Sequence[float], optional) – Custom payment amounts. Disables growth when provided. Pandas and Polars Series are accepted; converted to NDArray[np.float64] internally.

  • ir (float, InterestRate, list[float or InterestRate], NDArray[np.float64], or None, optional) – Interest rate (constant or curve). If None, uses interest_rate. In batch mode, may be a scalar (broadcast to all N), a list of N float values, or a list of N InterestRate objects (one curve per policy). Pandas and Polars Series are accepted without .to_numpy(). A numeric Series is converted to per-policy floats (auto-wrapped as InterestRate where applicable). An object-dtype Series of InterestRate instances is treated like a list of length N (including piecewise curves). Mixed numeric and object elements in one array-like sequence raise ValueError.

  • return_flows (bool, optional) –

    If True, returns a cashflow decomposition dict instead of the present value. The dict schema differs between single-policy and batch:

    Single-policy (scalar ages): per-payment engine dict. Same keys as ax(); see that method’s return_flows parameter for the full key reference.

    Batch (array ages): aggregate portfolio dict {'time_grid', 'expected_cf', 'pv_cf', 'total_pv'}. total_pv == np.sum(pv_cf) is always satisfied. Raises ValueError in discrete_simplified / continuous_simplified modes. Default is False.

  • t_output (NDArray[np.float64] or None, optional) – External bucketing grid for aggregate flow output. When provided, per-policy present-value contributions — already discounted at their exact payment times — are accumulated into the nearest bucket via np.searchsorted without re-discounting. Controls only the shape of expected_cf and pv_cf; total_pv is always the exact sum of all per-payment present values and is identical to the t_output=None result. Typical use: regulatory templates that require a fixed set of time buckets (e.g., annual IFRS 17 disclosure, Solvency II SCR projection). Only effective when return_flows=True; raises ValueError if return_flows=False. Default is None. Pandas and Polars Series are accepted; converted to NDArray[np.float64] before use.

  • benefits (sequence of float or NDArray[np.float64] or None, optional) – Per-policy benefit amounts, shape (N,). Scales each policy’s contribution to the aggregate flows (e.g., sum insured, PVDBO weight). Requires batch mode. Any sequence is accepted, including Pandas and Polars Series; values are converted to float64. Compatible with return_flows=False (returns NDArray[np.float64] with per-policy scaled PVs) and on_error='nan'. Default is None.

  • on_error ({'raise', 'nan'}, optional) – Error handling for invalid ages or parameters. 'raise' (default) raises ValueError; 'nan' marks invalid policies with np.nan and returns BatchResult. Default is 'raise'.

  • record_ids (sequence or None, optional) – Identifiers for each policy in batch mode, used to populate the error report when on_error='nan'. Length must equal N. Any sequence is accepted, including Pandas and Polars Series; elements are always accessed by positional integer index. Default is None.

Returns:

  • float – When ages[0] is scalar and return_flows=False.

  • NDArray[np.float64] – When ages[0] is an array or list and return_flows=False. Shape (N,).

  • dict – When return_flows=True. Single-policy: per-payment engine dict (same keys as ax()). Batch: aggregate portfolio dict {'time_grid', 'expected_cf', 'pv_cf', 'total_pv'}.

  • BatchResult – When on_error='nan'.

Raises:
  • ValueError – If ages does not contain exactly 2 elements. If any age is outside [0, w] for corresponding table. If return_flows=True in batch mode and calculation_mode is 'discrete_simplified' or 'continuous_simplified'. If return_flows=True and m is a vector with heterogeneous values. If t_output is not None and return_flows=False. If t_output is not None and input is scalar (non-batch mode). If on_error is not 'raise' or 'nan'. If on_error='nan' is passed with scalar x (or ages for joint-life methods). Use a list or array of ages for batch mode. If record_ids is not None and input is scalar (non-batch mode).

  • TypeError – If ages contains non-numeric values. If table_y is not a LifeTable instance or None. If any numeric argument is bool.

Notes

Boolean values for age, duration, shift, deferment, and rate arguments raise TypeError. Accepted types for each parameter are described in Parameters above.

Joint-life annuity-immediate for two lives, under the independence assumption:

\[a_{xy}^{(m)} = \frac{1}{m} \sum_{k=1}^{\infty} v^{k/m} \cdot {}_{k/m}p_x \cdot {}_{k/m}p_y\]
  • Payments occur at the END of each period (immediate/postpayable).

  • Annuity continues while BOTH lives survive (joint-life status).

  • Uses independence assumption for joint survival probabilities.

  • When gr is provided, the growth factor at the j-th payment interval is (1+g)^(j // m) — anniversaries counted from the first payment, not the contract origin. GrowthRate.shifted(ts) consumes int(ts) anniversary years; fractional ts is actuarially correct (revaluations operate on complete policy years). See Growth Rate Conventions for the full reference.

Batch mode#

Batch mode is activated when either of these conditions is met:

  1. ages[0] is a list or NDArray (age-triggered batch): pass ages as a pair of N-element arrays (x_arr, y_arr), each shape (N,).

  2. ages[0] is scalar but any of n, ts, d, ir, gr, or m is a list/tuple/NDArray of length N (param-triggered batch): scalar ages broadcast to N copies.

In both cases the method returns NDArray[np.float64] of shape (N,).

  • Per-policy parameters ts, d, n, ir, and gr may each be a scalar (broadcast to all N) or an array of length N.

  • m may be a scalar (broadcast to all N policies) or an array/list of length N for per-policy payment frequencies; the batch engine groups policies by unique m value and vectorises each group independently.

  • discrete_precision mode uses a vectorised matrix engine (50–250× faster than a Python scalar loop for N ≥ 100).

  • continuous_* modes use a serial fallback (~1.5× vs loop).

  • return_flows=True with batch ages returns aggregate portfolio flows.

  • Pandas and Polars Series are accepted for all per-policy parameters (n, ts, d, ir, gr, m, cashflow_times, cashflow_amounts, t_output, and each per-life age array within ages) without calling .to_numpy(); they are converted internally via the array protocol.

  • on_error='nan' marks invalid policies with np.nan.

See also

LifeTable.äxy

Joint-life annuity-due for two lives.

LifeTable.ajoint

Joint-life annuity-immediate for multiple lives.

LifeTable.axyz

Joint-life annuity-immediate for three lives.

LifeTable.ax

Single-life annuity-immediate.

Examples

>>> from lactuca import LifeTable
>>> # Two lives with same table
>>> table = LifeTable('PER2020_Ind_1o', 'm', cohort=1960)
>>> table.interest_rate = 0.03
>>> pv = table.axy([65, 60], n=20, m=12)
>>> # Two lives with different tables
>>> table_m, table_f = LifeTable('PER2020_Ind_1o', ('m', 'f'), cohort=1960)
>>> table_m.interest_rate = 0.03
>>> pv = table_m.axy([65, 62], table_f, n=15, m=12)

Batch mode:

>>> import numpy as np
>>> x_ages = np.array([60.0, 65.0, 70.0])
>>> y_ages = np.array([55.0, 60.0, 65.0])
>>> table.axy((x_ages, y_ages), n=20, ir=0.03)
>>> table.axy((x_ages, y_ages), n=np.array([25.0, 20.0, 15.0]), ir=0.03)
axyz(ages: object, tables_yz: Sequence[LifeTable] | Sequence[Sequence[LifeTable]] | None = None, *, ts: object = 0.0, d: object = 0.0, n: object = None, m: Literal[1, 2, 3, 4, 6, 12, 14, 24, 26, 52, 365] | Sequence[int] | NDArray[int64] = 1, gr: GrowthRate | float | None | Sequence[GrowthRate | float | None] = None, cashflow_times: Sequence[float] | None = None, cashflow_amounts: Sequence[float] | None = None, ir: object = None, return_flows: bool = False, t_output: NDArray[float64] | None = None, benefits: Sequence[float] | NDArray[float64] | None = None, on_error: Literal['raise', 'nan'] = 'raise', record_ids: Sequence[Any] | None = None) float | dict[str, NDArray[float64]] | BatchResult#

Compute the present value of a joint-life annuity-immediate (postpayable) for three lives.

Parameters:
  • ages (Sequence[float] or tuple[Sequence[float], Sequence[float], Sequence[float]]) – Ages of the three lives [x, y, z]. Must have exactly 3 elements. In batch mode, pass a triple of N-element arrays: (np.array([x0, …]), np.array([y0, …]), np.array([z0, …])).

  • tables_yz (Sequence[LifeTable] or Sequence[Sequence[LifeTable]] or None, optional) –

    Life tables for the second and third lives [y, z]. Three modes in batch:

    • None: uses self for all three lives in all policies.

    • [LifeTable, LifeTable]: 2 shared tables (y and z) for all N policies.

    • [[t_y0, t_z0], …] of length N: per-policy; each inner list must have exactly 2 elements. Raises ValueError if outer length != N.

  • ts (float or NDArray[np.float64], optional) – Shift (displacement) in years from origin (default 0.0). In batch mode, may be a scalar or an array of length N. Pandas and Polars Series are accepted; values are converted to float64. Scalar int literals (e.g. n=10) are coerced to float.

  • d (float or NDArray[np.float64], optional) – Deferment period in years (default 0.0). In batch mode, may be a scalar or an array of length N. Pandas and Polars Series are accepted; values are converted to float64. Scalar int literals (e.g. n=10) are coerced to float.

  • n (float or NDArray[np.float64] or None, optional) – Total duration of the annuity (in years) from contract start. If None, perpetual annuity up to limiting age. In batch mode, may be a scalar or an array of length N. Pandas and Polars Series are accepted; values are converted to float64. Scalar int literals (e.g. n=10) are coerced to float.

  • m (PaymentFrequencyLiteral, sequence of int, NDArray[np.int64], or array-like, optional) – Number of payments per year (1=annual, 12=monthly, etc., default 1). Integer-valued floats (e.g. 12.0) are coerced to int. In batch mode, may be a scalar (broadcast to all N) or an array/list of length N for per-policy payment frequencies; distinct values are processed in sub-groups by the vectorised batch engine. Pandas and Polars Series are accepted; values are converted to int64.

  • gr (GrowthRate, float, or None, optional) – Growth rate model (default None). A scalar float is auto-wrapped as GrowthRate(gr) (geometric growth by default). If None, no growth is applied. In batch mode, may be a scalar or a list of N values. Pandas and Polars Series are accepted without .to_numpy(). A numeric Series is converted to per-policy floats (auto-wrapped as GrowthRate where applicable). An object-dtype Series of GrowthRate instances is treated like a list of length N (including piecewise curves). Mixed numeric and object elements in one array-like sequence raise ValueError.

  • cashflow_times (Sequence[float], optional) – Custom payment times (years since origin). Overrides m when provided. Pandas and Polars Series are accepted; converted to NDArray[np.float64] internally.

  • cashflow_amounts (Sequence[float], optional) – Custom payment amounts. Disables growth when provided. Pandas and Polars Series are accepted; converted to NDArray[np.float64] internally.

  • ir (float, InterestRate, list[float or InterestRate], NDArray[np.float64], or None, optional) – Interest rate (constant or curve). If None, uses interest_rate. In batch mode, may be a scalar (broadcast to all N), a list of N float values, or a list of N InterestRate objects (one curve per policy). Pandas and Polars Series are accepted without .to_numpy(). A numeric Series is converted to per-policy floats (auto-wrapped as InterestRate where applicable). An object-dtype Series of InterestRate instances is treated like a list of length N (including piecewise curves). Mixed numeric and object elements in one array-like sequence raise ValueError.

  • return_flows (bool, optional) –

    If True, returns a cashflow decomposition dict instead of the present value. The dict schema differs between single-policy and batch:

    Single-policy (scalar ages): per-payment engine dict. Same keys as ax(); see that method’s return_flows parameter for the full key reference.

    Batch (array ages): aggregate portfolio dict {'time_grid', 'expected_cf', 'pv_cf', 'total_pv'}. total_pv == np.sum(pv_cf) is always satisfied. Raises ValueError in discrete_simplified / continuous_simplified modes. Default is False.

  • t_output (NDArray[np.float64] or None, optional) – External bucketing grid for aggregate flow output. When provided, per-policy present-value contributions — already discounted at their exact payment times — are accumulated into the nearest bucket via np.searchsorted without re-discounting. Controls only the shape of expected_cf and pv_cf; total_pv is always the exact sum of all per-payment present values and is identical to the t_output=None result. Typical use: regulatory templates that require a fixed set of time buckets (e.g., annual IFRS 17 disclosure, Solvency II SCR projection). Only effective when return_flows=True; raises ValueError if return_flows=False. Default is None. Pandas and Polars Series are accepted; converted to NDArray[np.float64] before use.

Returns:

  • float – When ages[0] is scalar and return_flows=False.

  • NDArray[np.float64] – When ages[0] is an array or list and return_flows=False. Shape (N,).

  • dict – When return_flows=True. Single-policy: per-payment engine dict (same keys as ax()). Batch: aggregate portfolio dict {'time_grid', 'expected_cf', 'pv_cf', 'total_pv'}.

  • BatchResult – When on_error='nan'.

Raises:
  • ValueError – If ages does not contain exactly 3 elements. If tables_yz is provided but does not contain exactly 2 LifeTable instances. If any age is outside [0, w] for corresponding table. If return_flows=True and m is a vector with heterogeneous values. If t_output is not None and return_flows=False. If t_output is not None and input is scalar (non-batch mode). If on_error is not 'raise' or 'nan'. If on_error='nan' is passed with scalar x (or ages for joint-life methods). Use a list or array of ages for batch mode. If record_ids is not None and input is scalar (non-batch mode).

  • TypeError – If ages contains non-numeric values. If tables_yz contains non-LifeTable instances. If any numeric argument is bool.

Notes

Boolean values for age, duration, shift, deferment, and rate arguments raise TypeError. Accepted types for each parameter are described in Parameters above.

Joint-life annuity-immediate for three lives, under the independence assumption:

\[a_{xyz}^{(m)} = \frac{1}{m} \sum_{k=1}^{\infty} v^{k/m} \cdot {}_{k/m}p_x \cdot {}_{k/m}p_y \cdot {}_{k/m}p_z\]
  • Payments occur at the END of each period (immediate/postpayable).

  • Annuity continues while ALL THREE lives survive (joint-life status).

  • Uses independence assumption for joint survival probabilities.

  • When gr is provided, the growth factor at the j-th payment interval is (1+g)^(j // m) — anniversaries counted from the first payment, not the contract origin. GrowthRate.shifted(ts) consumes int(ts) anniversary years; fractional ts is actuarially correct (revaluations operate on complete policy years). See Growth Rate Conventions for the full reference.

Batch mode#

Batch mode is activated when either of these conditions is met:

  1. ages[0] is a list or NDArray (age-triggered batch): pass ages as a triple of N-element arrays (x_arr, y_arr, z_arr), each (N,).

  2. ages[0] is scalar but any of n, ts, d, ir, gr, or m is a list/tuple/NDArray of length N (param-triggered batch): scalar ages broadcast to N copies.

In both cases the method returns NDArray[np.float64] of shape (N,).

  • Per-policy parameters ts, d, n, ir, and gr may each be a scalar or an array of length N.

  • m may be a scalar (broadcast to all N) or an array/list of length N for per-policy payment frequencies; the batch engine groups policies by unique m value and vectorises each group independently.

  • Pandas and Polars Series are accepted for all per-policy parameters (n, ts, d, ir, gr, m, cashflow_times, cashflow_amounts, t_output, and each per-life age array within ages) without calling .to_numpy(); they are converted internally via the array protocol.

  • on_error='nan' marks invalid policies with np.nan.

See also

LifeTable.äxyz

Joint-life annuity-due for three lives.

LifeTable.ajoint

Joint-life annuity-immediate for multiple lives.

LifeTable.axy

Joint-life annuity-immediate for two lives.

LifeTable.ax

Single-life annuity-immediate.

Examples

>>> from lactuca import LifeTable
>>> # Three lives with same table
>>> table = LifeTable('PER2020_Ind_1o', 'm', cohort=1960)
>>> table.interest_rate = 0.03
>>> pv = table.axyz([65, 60, 55], n=20, m=12)
>>> # Three lives with different tables
>>> table_m, table_f1, table_f2 = LifeTable('PER2020_Ind_1o', ('m', 'f', 'f'), cohort=1960)
>>> table_m.interest_rate = 0.03
>>> pv = table_m.axyz([65, 62, 58], [table_f1, table_f2], n=15, m=12)

Batch mode:

>>> import numpy as np
>>> x_arr = np.array([60.0, 65.0])
>>> y_arr = np.array([55.0, 60.0])
>>> z_arr = np.array([50.0, 55.0])
>>> table.axyz((x_arr, y_arr, z_arr), n=20, ir=0.03)
configure(sex: ~typing.Literal['m', 'f', 'u'] | None = None, cohort: int | None = None, duration: object = None, unisex_blend: object = None, interest_rate: object = <object object>, **_extra) LifeTable#

Configure table metadata and optional interest rate atomically.

Extends configure() with interest_rate support. The interest rate is applied after the structural rebuild so it never triggers an additional rebuild.

Parameters:
  • sex ({'m', 'f', 'u'} or None, optional) – Sex for calculations. None means “keep current value”.

  • cohort (int or None, optional) – Birth cohort year for generational tables. None means “keep current value”.

  • duration (int, 'ult', or None, optional) – Select-table duration. None means “keep current value”.

  • unisex_blend (float or None, optional) – Male weight for unisex blending in [0.0, 1.0]. None means “keep current value”.

  • interest_rate (float, InterestRate, or None, optional) – Default interest rate for annuity/insurance calculations. Omitting this argument leaves the current interest rate unchanged. Pass None explicitly to clear the interest rate.

Returns:

self, enabling method chaining.

Return type:

LifeTable

Raises:
  • ValueError – If called with no arguments, or if a supplied value is invalid.

  • TypeError – If a supplied value has an incorrect type.

Notes

interest_rate is applied after the structural rebuild (sex, cohort, duration, unisex_blend) so it never triggers an additional recomputation. Passing only interest_rate is valid and does not perform any structural rebuild.

Omitting interest_rate entirely (the default) leaves the current rate unchanged. Pass interest_rate=None explicitly to clear the rate so that subsequent calculations require an explicit ir= argument.

See also

configure()

Base implementation (without interest_rate).

batch_update()

Context manager for imperative setter-based configuration.

LifeTable.interest_rate

Property setter for updating the rate without a full rebuild.

Examples

>>> from lactuca import LifeTable
>>> lt = LifeTable('PER2020_Ind_1o', pending=True)
>>> lt.configure(sex='m', cohort=1969, interest_rate=0.03).ax(65)
ex(x: object) object#

Compute the complete expectation of life ex at integer ages.

For fractional ages use ex_continuous(). Defined for integer ages only.

Parameters:

x (int, sequence of int, or NDArray[np.float64]) – Integer age(s). Pandas and Polars Series of int are accepted. Non-integer ages will raise ValueError.

Returns:

Complete expectation of life at integer age(s) x.

Return type:

float or np.ndarray

Raises:
  • ValueError – If any x is outside [0, w] or contains non-integer ages.

  • TypeError – If x is not numeric or a sequence of numerics.

Notes

  • The method name ex is a short API alias; the actuarial symbol is \(\mathring{e}_x\) (complete expectation of life), not curtate \(e_x = \sum_{k=1}^{\omega-x} {}_kp_x\).

  • Defined as \(\mathring{e}_x = T_x / l_x\), the complete expectation of life.

  • \(e_{\omega} = 0\) if all decrements occur at age \(\omega\).

  • All results are rounded to the configured number of decimals (decimals.ex).

  • Fully vectorized and optimized for speed and memory.

  • Follows international actuarial practice.

See also

LifeTable.Tx

Total person-years lived from age x (integer ages only).

LifeTable.ex_curtate

Curtate expectation \(e_x\) at integer ages.

LifeTable.ex_continuous

Continuous version of ex for fractional ages.

LifeTable.Lx

Person-years lived in [x, x+1) under UDD (integer ages only).

Examples

>>> from lactuca import LifeTable
>>> tbl = LifeTable('PASEM2010', 'm')
>>> tbl.ex(65)
>>> tbl.ex([60, 65, 70])  # vectorized
ex_continuous(x: object, *, m: object = 12) object#

Compute ex at fractional ages using numerical integration.

Accepts only fractional ages; for integer ages use ex() instead. Computes \(\mathring{e}_x = T_x / l_x\) where \(T_x\) is evaluated via numerical integration.

Parameters:
  • x (float, list, or np.ndarray) – Fractional age(s) to evaluate. Integer ages will raise ValueError. Use ex() for integer ages.

  • m (object, optional) – Trapezoidal integration density (default 12). Interpreted as the number of subintervals per unit age span; must be a value from VALID_PAYMENT_FREQUENCIES: 1, 2, 3, 4, 6, 12, 14, 24, 26, 52, 365. Higher m increases precision at greater computational cost. bool is rejected.

Returns:

Complete expectation of life at age(s) x.

Return type:

float or np.ndarray

Raises:
  • ValueError – If any x is an integer age, outside [0, w], or m is not a valid payment frequency. For integer ages, use ex() instead.

  • TypeError – If x is not numeric or a sequence of numerics, or if m is a bool.

Notes

  • Computes \(\mathring{e}_x = T_x / l_x\) using numerical integration for \(T_x = \int_x^{\infty} l_t \, dt\).

  • Strictly for fractional (non-integer) ages; use ex() for integer ages.

  • Each \(l_t\) evaluation uses the configured interpolation method.

  • This is the complete (entire) expectation of life, not the curtate expectation.

  • \(e_{\omega} = 0\) if all decrements occur at age \(\omega\).

  • All results are rounded to the configured number of decimals (decimals.ex).

  • Fully vectorized and optimized for speed and memory.

  • Follows international actuarial practice for continuous ages.

See also

LifeTable.ex

Standard commutation function for integer ages only (required for integer ages).

LifeTable.ex_curtate

Curtate expectation \(e_x\) at integer ages.

LifeTable.Tx_continuous

Continuous version of Tx.

LifeTable.Lx_continuous

Continuous version of Lx.

Examples

>>> from lactuca import LifeTable
>>> tbl = LifeTable("PASEM2010", "m")
>>> tbl.ex_continuous(30.5)
>>> tbl.ex_continuous(30.5, m=365)  # higher precision
>>> tbl.ex_continuous(30)  # integer age — raises ValueError
ex_curtate(x: object) object#

Compute the curtate expectation of life \(e_x\) at integer ages.

For fractional ages use ex_continuous() (complete only). Defined for integer ages only.

Parameters:

x (int, sequence of int, or NDArray[np.float64]) – Integer age(s). Pandas and Polars Series of int are accepted. Non-integer ages will raise ValueError.

Returns:

Curtate expectation of life at integer age(s) x.

Return type:

float or np.ndarray

Raises:
  • ValueError – If any x is outside [0, w] or contains non-integer ages.

  • TypeError – If x is not numeric or a sequence of numerics.

Notes

  • Computes \(e_x = \sum_{k=1}^{\omega-x} {}_k p_x\) as the sum of tpx() values (each term rounded per decimals.tpx).

  • Under UDD, the complete expectation from ex() satisfies \(\mathring{e}_x \approx e_x + \tfrac{1}{2}\) (approximation after rounding; not an exact identity).

  • \(e_{\omega} = 0\) if all decrements occur at age \(\omega\).

  • If \(l_x = 0\), returns 0 (consistent with ex()).

  • All results are rounded to decimals.ex.

  • Fully vectorized and optimized for speed and memory.

See also

LifeTable.ex

Complete expectation \(\mathring{e}_x = T_x / l_x\).

LifeTable.ex_continuous

Complete expectation at fractional ages.

LifeTable.tpx

Survival probabilities used in the sum.

Examples

>>> from lactuca import LifeTable
>>> tbl = LifeTable('PASEM2010', 'm')
>>> tbl.ex_curtate(65)
>>> tbl.ex_curtate([60, 65, 70])  # vectorized
ix(x=None, m: object = 1) None#

Disability incidence rate (blocked for LifeTable).

Parameters:
  • x (optional) – Ignored placeholder maintained for API compatibility with other decrement-table subclasses.

  • m (object, optional) – Ignored payment-frequency placeholder maintained for API compatibility. Must be a valid payment frequency if provided; bool is rejected.

Raises:
  • TypeError – If m is a bool.

  • NotImplementedError – Always raised. LifeTable uses qx (mortality) instead of ix (disability incidence).

Notes

  • ix is only available in DisabilityTable.

  • For mortality tables, use qx() instead.

See also

DisabilityTable

Table type that provides ix rates.

Examples

>>> from lactuca import LifeTable
>>> lt = LifeTable('PASEM2010', 'm')
>>> lt.ix(65)
NotImplementedError: ix is not available for LifeTable. Use qx() instead.
nEjoint(ages: object, tables_others: Sequence[LifeTable] | Sequence[Sequence[LifeTable]] | None = None, *, n: object, ts: object = 0.0, ir: object = None, return_flows: bool = False, t_output: NDArray[float64] | None = None, benefits: Sequence[float] | NDArray[float64] | None = None, on_error: Literal['raise', 'nan'] = 'raise', record_ids: Sequence[Any] | None = None) float | dict[str, NDArray[float64]] | BatchResult#

Compute the present value of a joint-life pure endowment for multiple lives.

Pays one unit of benefits conditional on survival of ALL lives to time n, discounted to the contract start. Joint survival is computed under the independence assumption.

Parameters:
  • ages (Sequence[float] or list[Sequence[float]]) – Ages of all lives at contract start. Must contain at least 2 elements. In batch mode, pass a list of r N-element arrays: [x0_arr, x1_arr, ..., xr_arr] where each has shape (N,).

  • tables_others (Sequence[LifeTable] or Sequence[Sequence[LifeTable]] or None, optional) –

    Life tables for additional lives (all except the first, which uses self). Three modes in batch:

    • None: uses self for all lives in all policies.

    • Sequence[LifeTable] of length n_lives - 1: shared across all N policies.

    • Sequence[Sequence[LifeTable]] of length N: per-policy; each inner list must have exactly n_lives - 1 elements. Raises ValueError if length != N.

  • n (float or NDArray[np.float64]) – Duration of the endowment in years. Must be positive. Can be fractional (e.g., 10.5 years). In batch mode, may be a scalar or an array of length N. Scalar int literals (e.g. n=10) are coerced to float.

  • ts (float or NDArray[np.float64], optional) – Time shift in years from contract origin. Default is 0.0. Used for reserve calculations. Adjusts all ages (ages + ts) and effective duration (n - ts). Must be non-negative. In batch mode, may be a scalar or an array of length N.

  • ir (float, InterestRate, NDArray[np.float64], or None, optional) – Interest rate for discounting. If None, uses interest_rate. In batch mode, may be a scalar or a list of N values. Pandas and Polars Series are accepted; values are converted to float64. Scalar int literals (e.g. n=10) are coerced to float.

  • return_flows (bool, optional) –

    If True, returns a cashflow decomposition dict instead of the present value. The dict schema differs between single-policy and batch:

    Single-policy (scalar ages): per-payment engine dict. Same keys as nEx(); see that method’s return_flows parameter for the full key reference.

    Batch (array ages): aggregate portfolio dict {'time_grid', 'expected_cf', 'pv_cf', 'total_pv'}. total_pv == np.sum(pv_cf) is always satisfied. Raises ValueError in discrete_simplified / continuous_simplified modes. Default is False.

  • t_output (NDArray[np.float64] or None, optional) – External bucketing grid for aggregate flow output. When provided, per-policy present-value contributions — already discounted at their exact payment times — are accumulated into the nearest bucket via np.searchsorted without re-discounting. Controls only the shape of expected_cf and pv_cf; total_pv is always the exact sum of all per-payment present values and is identical to the t_output=None result. Typical use: regulatory templates that require a fixed set of time buckets (e.g., annual IFRS 17 disclosure, Solvency II SCR projection). Only effective when return_flows=True; raises ValueError if return_flows=False. Default is None. Pandas and Polars Series are accepted; converted to NDArray[np.float64] before use.

  • benefits (sequence of float or NDArray[np.float64] or None, optional) – Per-policy benefit amounts, shape (N,). Scales each policy’s contribution to the aggregate flows (e.g., sum insured, PVDBO weight). Requires batch mode. Any sequence is accepted, including Pandas and Polars Series; values are converted to float64. Compatible with return_flows=False (returns NDArray[np.float64] with per-policy scaled PVs) and on_error='nan'. Default is None.

  • on_error ({'raise', 'nan'}, optional) – Error handling for invalid ages or parameters. 'raise' (default) raises ValueError; 'nan' marks invalid policies with np.nan and returns BatchResult. Default is 'raise'.

  • record_ids (sequence or None, optional) – Identifiers for each policy in batch mode, used to populate the error report when on_error='nan'. Length must equal N. Any sequence is accepted, including Pandas and Polars Series; elements are always accessed by positional integer index. Default is None.

Returns:

  • float – When ages[0] is scalar and return_flows=False.

  • NDArray[np.float64] – When ages[0] is an array or list and return_flows=False. Shape (N,).

  • dict – When return_flows=True. Single-policy: per-payment engine dict (same keys as nEx()). Batch: aggregate portfolio dict {'time_grid', 'expected_cf', 'pv_cf', 'total_pv'}.

  • BatchResult – When on_error='nan'.

Raises:
  • ValueError – If ages contains fewer than 2 elements. If tables_others is provided but length does not match len(ages) - 1. If any age is outside [0, w] for the corresponding mortality table. If n <= 0. If ts < 0. If no interest rate is provided and interest_rate is None. If any age + n exceeds the table limit w. If return_flows=True in batch mode and calculation_mode is 'discrete_simplified' or 'continuous_simplified'. If t_output is not None and return_flows=False. If t_output is not None and input is scalar (non-batch mode). If on_error is not 'raise' or 'nan'. If on_error='nan' is passed with scalar x (or ages for joint-life methods). Use a list or array of ages for batch mode. If record_ids is not None and input is scalar (non-batch mode).

  • TypeError – If ages contains non-numeric values. If tables_others contains non-LifeTable instances. If ir is not float, InterestRate, or None. If any numeric argument is bool.

Notes

Boolean values for age, duration, shift, and rate arguments raise TypeError. Accepted types for each parameter are described in Parameters above.

  • Joint pure endowment formula for \(k\) lives: \({}_{n}E_{x_1 \cdots x_k} = \vx{n} \cdot \prod_{i=1}^{k} \px{n}{x_i}\).

  • Joint survival computed under the independence assumption: \(\prod_{i=1}^{k} {}_{n}p_{x_i}\).

  • Uses self.calculation_mode from Config to select the method: discrete_precision, discrete_simplified, continuous_precision, or continuous_simplified.

Batch mode#

Batch mode is activated when either of these conditions is met:

  1. ages[0] is a list or NDArray (age-triggered batch): pass ages as a list of r N-element arrays, each shape (N,).

  2. ages[0] is scalar but any of n, ts, or ir is a list/tuple/NDArray of length N (param-triggered batch): scalar ages broadcast to N copies.

In both cases the method returns NDArray[np.float64] of shape (N,).

  • Per-policy parameters n, ts, and ir may each be a scalar or an array of length N.

  • Pandas and Polars Series are accepted for n, ts, and ir without calling .to_numpy(); they are converted internally via the array protocol.

  • Each per-life age array within ages may also be a Pandas or Polars Series; it is converted internally via the array protocol.

  • on_error='nan' marks invalid policies with np.nan.

Examples

>>> from lactuca import LifeTable
>>> table = LifeTable('PER2020_Ind_1o', 'm', cohort=1960)
>>> table.interest_rate = 0.03
>>> pv = table.nEjoint([65, 60, 55, 50], n=10)

Batch mode:

>>> import numpy as np
>>> a0 = np.array([60.0, 65.0])
>>> a1 = np.array([55.0, 60.0])
>>> a2 = np.array([50.0, 55.0])
>>> table.nEjoint([a0, a1, a2], n=10, ir=0.03)

See also

LifeTable.nEx

Single-life pure endowment.

LifeTable.nExy

Joint-life pure endowment for two lives.

LifeTable.nExyz

Joint-life pure endowment for three lives.

nEx(x: object, *, n: object, ts: object = 0.0, ir: object = None, return_flows: bool = False, t_output: NDArray[float64] | None = None, benefits: Sequence[float] | NDArray[float64] | None = None, on_error: Literal['raise', 'nan'] = 'raise', record_ids: Sequence[Any] | None = None) float | NDArray[float64] | dict[str, NDArray[float64] | float] | BatchResult#

Compute the present value of a pure endowment for a single life.

Pays one unit of benefits conditional on the insured’s survival to time n, discounted to the contract start.

Parameters:
  • x (float or list of float or NDArray[np.float64]) – Initial age at contract start. Pass an array or list of N ages to activate batch mode, which returns an ndarray of shape (N,).

  • n (float or NDArray[np.float64]) – Duration of the endowment in years. Must be positive. Can be fractional (e.g., 10.5 years) with appropriate interpolation. In batch mode, may be a scalar (broadcast to all N) or an array of length N. Pandas and Polars Series are accepted; values are converted to float64. Scalar int literals (e.g. n=10) are coerced to float.

  • ts (float or NDArray[np.float64], optional) – Time shift (displacement) in years from contract origin. Default is 0.0. Used for reserve calculations where some time has elapsed since contract start. In batch mode, may be a scalar or an array of length N. Adjusts both age (x + ts) and effective duration (n - ts). Must be non-negative. Pandas and Polars Series are accepted; values are converted to float64. Scalar int literals (e.g. n=10) are coerced to float.

  • ir (float, InterestRate, or None, optional) – Interest rate for discounting. If None, uses interest_rate. Can be a constant rate (float) or an interest rate curve (InterestRate). In batch mode, may be a scalar or a list of N values. Pandas and Polars Series are accepted; values are converted to float64. Scalar int literals (e.g. n=10) are coerced to float.

  • return_flows (bool, optional) –

    If True, returns a cashflow decomposition dict instead of the present value. The dict schema differs between scalar and batch:

    Scalar x (all modes):

    {
      "time_grid"                   : NDArray  # [n] — single payment at maturity
      "joint_survival_probability"  : float    # tpx(n)
      "discount_factor"             : float    # v^n
      "expected_cf"                 : NDArray  # [tpx(n)] — undiscounted unit cf
      "pv_cf"                       : NDArray  # [tpx(n) * v^n] — discounted cf
      "total_pv"                    : float    # = pv_cf[0]
    }
    

    result["total_pv"] equals the rounded scalar present value.

    Batch (array x), *_precision modes only:

    {
      "time_grid"   : NDArray  # portfolio time grid
      "expected_cf" : NDArray  # sum of undiscounted contributions across policies
      "pv_cf"       : NDArray  # sum of discounted contributions across policies
      "total_pv"    : float    # sum(pv_cf); equals sum of scalar present values
    }
    

    total_pv == np.sum(pv_cf) is always satisfied. Raises ValueError in discrete_simplified / continuous_simplified modes. Default is False.

  • t_output (NDArray[np.float64] or None, optional) – External bucketing grid for aggregate flow output. When provided, per-policy present-value contributions — already discounted at their exact payment times — are accumulated into the nearest bucket via np.searchsorted without re-discounting. Controls only the shape of expected_cf and pv_cf; total_pv is always the exact sum of all per-payment present values and is identical to the t_output=None result. Typical use: regulatory templates that require a fixed set of time buckets (e.g., annual IFRS 17 disclosure, Solvency II SCR projection). Only effective when return_flows=True; raises ValueError if return_flows=False. Default is None. Pandas and Polars Series are accepted; converted to NDArray[np.float64] before use.

  • benefits (sequence of float or NDArray[np.float64] or None, optional) – Per-policy benefit amounts, shape (N,). Scales each policy’s contribution to the aggregate flows (e.g., sum insured, PVDBO weight). Requires batch mode. Any sequence is accepted, including Pandas and Polars Series; values are converted to float64. Compatible with return_flows=False (returns NDArray[np.float64] with per-policy scaled PVs) and on_error='nan'. Default is None.

  • on_error ({'raise', 'nan'}, optional) – Error handling for invalid ages or parameters. 'raise' (default) raises ValueError; 'nan' marks invalid policies with np.nan and returns BatchResult. Default is 'raise'.

  • record_ids (sequence or None, optional) – Identifiers for each policy in batch mode, used to populate the error report when on_error='nan'. Length must equal N. Any sequence is accepted, including Pandas and Polars Series; elements are always accessed by positional integer index. Default is None.

Returns:

  • float – When x is scalar and return_flows=False.

  • NDArray[np.float64] – When x is an array or list and return_flows=False. Shape (N,).

  • dict – When return_flows=True. Schema depends on scalar vs. batch; see the return_flows parameter description above for the full key listing. Scalar returns a single-element time grid (one payment at maturity n). Batch returns a portfolio aggregate over the shared time grid.

  • BatchResult – When on_error='nan', wraps the result array and an error report.

Raises:
  • ValueError – If x is outside [0, w] for the mortality table. If n <= 0. If ts < 0. If no interest rate is provided and interest_rate is None. If x + n exceeds the table limit w. If return_flows=True in batch mode and calculation_mode is 'discrete_simplified' or 'continuous_simplified'. If t_output is not None and return_flows=False. If t_output is not None and input is scalar (non-batch mode). If on_error is not 'raise' or 'nan'. If on_error='nan' is passed with scalar x (or ages for joint-life methods). Use a list or array of ages for batch mode. If record_ids is not None and input is scalar (non-batch mode).

  • TypeError – If x or n are not numeric. If ir is not float, InterestRate, or None. If any numeric argument is bool.

Notes

Boolean values for age, duration, shift, and rate arguments raise TypeError. Accepted types for each parameter are described in Parameters above.

  • Pure endowment formula: \({}_{n}E_x = \vx{n} \cdot \px{n}{x}\).

  • nEx does not accept a gr parameter (pure endowments are not revalued).

  • Uses self.calculation_mode from Config to select the method: discrete_precision, discrete_simplified, continuous_precision, or continuous_simplified.

  • Fractional ts is accepted; set Config.force_integer_ts = True to reject non-integer shifts.

Batch mode#

When x is a list, sequence, or ndarray, or when any of n, ts, or ir is a sequence or array with scalar x, the method operates in batch mode and returns an ndarray of shape (N,):

  • A list of 1 element still returns NDArray shape (1,), never a scalar.

  • Per-policy parameters n, ts, and ir may each be a scalar (broadcast to all N policies) or an array of length N.

  • discrete_precision mode uses a vectorised engine (50–250× vs scalar loop).

  • continuous_* modes use a serial fallback (~1.5× vs loop).

  • Pandas and Polars Series are accepted for n, ts, and ir without calling .to_numpy(); they are converted internally via the array protocol.

  • on_error='nan' marks invalid policies with np.nan instead of raising.

Examples

>>> from lactuca import LifeTable
>>> table = LifeTable('PER2020_Ind_1o', 'm', cohort=1960)
>>> table.interest_rate = 0.03
>>> pv = table.nEx(65, n=10)
>>> # With time shift (reserve after 5 years have elapsed)
>>> pv_t5 = table.nEx(65, n=10, ts=5)

Batch mode:

>>> import numpy as np
>>> ages = np.array([55.0, 60.0, 65.0, 70.0])
>>> table.nEx(ages, n=10, ir=0.03)
>>> table.nEx(ages, n=np.array([15.0, 12.0, 10.0, 8.0]), ir=0.03)

See also

LifeTable.nExy

Joint-life pure endowment for two lives.

LifeTable.nExyz

Joint-life pure endowment for three lives.

LifeTable.nEjoint

Joint-life pure endowment for multiple lives.

nExy(ages: object, table_y: LifeTable | Sequence[LifeTable] | None = None, *, n: object, ts: object = 0.0, ir: object = None, return_flows: bool = False, t_output: NDArray[float64] | None = None, benefits: Sequence[float] | NDArray[float64] | None = None, on_error: Literal['raise', 'nan'] = 'raise', record_ids: Sequence[Any] | None = None) float | NDArray[float64] | dict[str, NDArray[float64]] | BatchResult#

Compute the present value of a joint-life pure endowment for two lives.

Pays one unit of benefits conditional on survival of BOTH lives to time n, discounted to the contract start. Joint survival is computed under the independence assumption.

Parameters:
  • ages (Sequence[float] or tuple[Sequence[float], Sequence[float]]) – Ages [x, y] at contract start. Must have exactly 2 elements. In batch mode, pass a pair of N-element arrays: (np.array([x0, …, xN]), np.array([y0, …, yN])).

  • table_y (LifeTable or Sequence[LifeTable] or None, optional) –

    Life table for the second life (y). Three modes in batch:

    • None: uses self for all policies.

    • LifeTable: single shared table for all N policies.

    • Sequence[LifeTable] of length N: per-policy table; policy i uses element i. Raises ValueError if length != N.

  • n (float or NDArray[np.float64]) – Duration of the endowment in years. Must be positive. Can be fractional (e.g., 10.5 years). In batch mode, may be a scalar or an array of length N. Pandas and Polars Series are accepted; values are converted to float64. Scalar int literals (e.g. n=10) are coerced to float.

  • ts (float or NDArray[np.float64], optional) – Time shift in years from contract origin. Default is 0.0. Used for reserve calculations. Adjusts both ages (ages + ts) and effective duration (n - ts). Must be non-negative. In batch mode, may be a scalar or an array of length N. Pandas and Polars Series are accepted; values are converted to float64. Scalar int literals (e.g. n=10) are coerced to float.

  • ir (float, InterestRate, NDArray[np.float64], or None, optional) – Interest rate for discounting. If None, uses interest_rate. In batch mode, may be a scalar or a list of N values. Pandas and Polars Series are accepted; values are converted to float64. Scalar int literals (e.g. n=10) are coerced to float.

  • return_flows (bool, optional) –

    If True, returns a cashflow decomposition dict instead of the present value. The dict schema differs between single-policy and batch:

    Single-policy (scalar ages): per-payment engine dict. Same keys as nEx(); see that method’s return_flows parameter for the full key reference.

    Batch (array ages): aggregate portfolio dict {'time_grid', 'expected_cf', 'pv_cf', 'total_pv'}. total_pv == np.sum(pv_cf) is always satisfied. Raises ValueError in discrete_simplified / continuous_simplified modes. Default is False.

  • t_output (NDArray[np.float64] or None, optional) – External bucketing grid for aggregate flow output. When provided, per-policy present-value contributions — already discounted at their exact payment times — are accumulated into the nearest bucket via np.searchsorted without re-discounting. Controls only the shape of expected_cf and pv_cf; total_pv is always the exact sum of all per-payment present values and is identical to the t_output=None result. Typical use: regulatory templates that require a fixed set of time buckets (e.g., annual IFRS 17 disclosure, Solvency II SCR projection). Only effective when return_flows=True; raises ValueError if return_flows=False. Default is None. Pandas and Polars Series are accepted; converted to NDArray[np.float64] before use.

  • benefits (sequence of float or NDArray[np.float64] or None, optional) – Per-policy benefit amounts, shape (N,). Scales each policy’s contribution to the aggregate flows (e.g., sum insured, PVDBO weight). Requires batch mode. Any sequence is accepted, including Pandas and Polars Series; values are converted to float64. Compatible with return_flows=False (returns NDArray[np.float64] with per-policy scaled PVs) and on_error='nan'. Default is None.

  • on_error ({'raise', 'nan'}, optional) – Error handling for invalid ages or parameters. 'raise' (default) raises ValueError; 'nan' marks invalid policies with np.nan and returns BatchResult. Default is 'raise'.

  • record_ids (sequence or None, optional) – Identifiers for each policy in batch mode, used to populate the error report when on_error='nan'. Length must equal N. Any sequence is accepted, including Pandas and Polars Series; elements are always accessed by positional integer index. Default is None.

Returns:

  • float – When ages[0] is scalar and return_flows=False.

  • NDArray[np.float64] – When ages[0] is an array or list and return_flows=False. Shape (N,).

  • dict – When return_flows=True. Single-policy: per-payment engine dict (same keys as nEx()). Batch: aggregate portfolio dict {'time_grid', 'expected_cf', 'pv_cf', 'total_pv'}.

  • BatchResult – When on_error='nan'.

Raises:
  • ValueError – If ages does not contain exactly 2 elements. If any age is outside [0, w] for the corresponding mortality table. If n <= 0. If ts < 0. If no interest rate is provided and interest_rate is None. If any age + n exceeds the table limit w. If return_flows=True in batch mode and calculation_mode is 'discrete_simplified' or 'continuous_simplified'. If t_output is not None and return_flows=False. If t_output is not None and input is scalar (non-batch mode). If on_error is not 'raise' or 'nan'. If on_error='nan' is passed with scalar x (or ages for joint-life methods). Use a list or array of ages for batch mode. If record_ids is not None and input is scalar (non-batch mode).

  • TypeError – If ages contains non-numeric values. If table_y is not a LifeTable instance or None. If ir is not float, InterestRate, or None. If any numeric argument is bool.

Notes

Boolean values for age, duration, shift, and rate arguments raise TypeError. Accepted types for each parameter are described in Parameters above.

  • Joint pure endowment formula: \({}_{n}E_{xy} = \vx{n} \cdot \px{n}{x} \cdot \px{n}{y}\).

  • Joint survival computed under the independence assumption: \({}_{n}p_x \cdot {}_{n}p_y\).

  • Uses self.calculation_mode from Config to select the method: discrete_precision, discrete_simplified, continuous_precision, or continuous_simplified.

Batch mode#

Batch mode is activated when either of these conditions is met:

  1. ages[0] is a list or NDArray (age-triggered batch): pass ages as a pair of N-element arrays, each shape (N,).

  2. ages[0] is scalar but any of n, ts, or ir is a list/tuple/NDArray of length N (param-triggered batch): scalar ages broadcast to N copies.

In both cases the method returns NDArray[np.float64] of shape (N,).

  • Per-policy parameters n, ts, and ir may each be a scalar (broadcast to all N) or an array of length N.

  • Pandas and Polars Series are accepted for n, ts, ir, and each per-life age array within ages without calling .to_numpy(); they are converted internally via the array protocol.

  • on_error='nan' marks invalid policies with np.nan.

Examples

>>> from lactuca import LifeTable
>>> table = LifeTable('PER2020_Ind_1o', 'm', cohort=1960)
>>> table.interest_rate = 0.03
>>> pv = table.nExy([65, 60], n=10)
>>> # With time shift
>>> pv_t5 = table.nExy([65, 60], n=10, ts=5)

Batch mode:

>>> import numpy as np
>>> x_ages = np.array([60.0, 65.0, 70.0])
>>> y_ages = np.array([55.0, 60.0, 65.0])
>>> table.nExy((x_ages, y_ages), n=10, ir=0.03)

See also

LifeTable.nEx

Single-life pure endowment.

LifeTable.nExyz

Joint-life pure endowment for three lives.

LifeTable.nEjoint

Joint-life pure endowment for multiple lives.

nExyz(ages: object, tables_yz: Sequence[LifeTable] | Sequence[Sequence[LifeTable]] | None = None, *, n: object, ts: object = 0.0, ir: object = None, return_flows: bool = False, t_output: NDArray[float64] | None = None, benefits: Sequence[float] | NDArray[float64] | None = None, on_error: Literal['raise', 'nan'] = 'raise', record_ids: Sequence[Any] | None = None) float | dict[str, NDArray[float64]] | BatchResult#

Compute the present value of a joint-life pure endowment for three lives.

Pays one unit of benefits conditional on survival of ALL THREE lives to time n, discounted to the contract start. Joint survival is computed under the independence assumption.

Parameters:
  • ages (sequence of float) – Ages [x, y, z] at contract start. Must have exactly 3 elements. Each age must be within [0, w] for the corresponding table.

  • tables_yz (Sequence[LifeTable] or Sequence[Sequence[LifeTable]] or None, optional) –

    Life tables for the second and third lives [y, z]. Three modes in batch:

    • None: uses self for all three lives in all policies.

    • [LifeTable, LifeTable]: 2 shared tables (y and z) for all N policies.

    • [[t_y0, t_z0], …] of length N: per-policy; each inner list must have exactly 2 elements. Raises ValueError if outer length != N.

  • n (float) – Duration of the endowment in years. Must be positive. Can be fractional (e.g., 10.5 years). In batch mode, may be a scalar or an array of length N. Pandas and Polars Series are accepted; values are converted to float64. Scalar int literals (e.g. n=10) are coerced to float.

  • ts (float or NDArray[np.float64], optional) – Time shift in years from contract origin. Default is 0.0. Used for reserve calculations. Adjusts both ages (ages + ts) and effective duration (n - ts). Must be non-negative. In batch mode, may be a scalar or an array of length N. Pandas and Polars Series are accepted; values are converted to float64. Scalar int literals (e.g. n=10) are coerced to float.

  • ir (float, InterestRate, NDArray[np.float64], or None, optional) – Interest rate for discounting. If None, uses interest_rate. In batch mode, may be a scalar or a list of N values. Pandas and Polars Series are accepted; values are converted to float64. Scalar int literals (e.g. n=10) are coerced to float.

  • return_flows (bool, optional) –

    If True, returns a cashflow decomposition dict instead of the present value. The dict schema differs between single-policy and batch:

    Single-policy (scalar ages): per-payment engine dict. Same keys as nEx(); see that method’s return_flows parameter for the full key reference.

    Batch (array ages): aggregate portfolio dict {'time_grid', 'expected_cf', 'pv_cf', 'total_pv'}. total_pv == np.sum(pv_cf) is always satisfied. Raises ValueError in discrete_simplified / continuous_simplified modes. Default is False.

  • t_output (NDArray[np.float64] or None, optional) – External bucketing grid for aggregate flow output. When provided, per-policy present-value contributions — already discounted at their exact payment times — are accumulated into the nearest bucket via np.searchsorted without re-discounting. Controls only the shape of expected_cf and pv_cf; total_pv is always the exact sum of all per-payment present values and is identical to the t_output=None result. Typical use: regulatory templates that require a fixed set of time buckets (e.g., annual IFRS 17 disclosure, Solvency II SCR projection). Only effective when return_flows=True; raises ValueError if return_flows=False. Default is None. Pandas and Polars Series are accepted; converted to NDArray[np.float64] before use.

Returns:

  • float – When ages[0] is scalar and return_flows=False.

  • NDArray[np.float64] – When ages[0] is an array or list and return_flows=False. Shape (N,).

  • dict – When return_flows=True. Single-policy: per-payment engine dict (same keys as nEx()). Batch: aggregate portfolio dict {'time_grid', 'expected_cf', 'pv_cf', 'total_pv'}.

  • BatchResult – When on_error='nan'.

Raises:
  • ValueError – If ages does not contain exactly 3 elements. If tables_yz is provided but does not contain exactly 2 LifeTable instances. If any age is outside [0, w] for the corresponding mortality table. If n <= 0. If ts < 0. If no interest rate is provided and interest_rate is None. If any age + n exceeds the table limit w. If t_output is not None and return_flows=False. If t_output is not None and input is scalar (non-batch mode). If on_error is not 'raise' or 'nan'. If on_error='nan' is passed with scalar x (or ages for joint-life methods). Use a list or array of ages for batch mode. If record_ids is not None and input is scalar (non-batch mode).

  • TypeError – If ages contains non-numeric values. If tables_yz contains non-LifeTable instances. If ir is not float, InterestRate, or None. If any numeric argument is bool.

Notes

Boolean values for age, duration, shift, and rate arguments raise TypeError. Accepted types for each parameter are described in Parameters above.

  • Joint pure endowment formula: \({}_{n}E_{xyz} = \vx{n} \cdot \px{n}{x} \cdot \px{n}{y} \cdot \px{n}{z}\).

  • Joint survival computed under the independence assumption: \({}_{n}p_x \cdot {}_{n}p_y \cdot {}_{n}p_z\).

  • Uses self.calculation_mode from Config to select the method: discrete_precision, discrete_simplified, continuous_precision, or continuous_simplified.

Batch mode#

Batch mode is activated when either of these conditions is met:

  1. ages[0] is a list or NDArray (age-triggered batch): pass ages as a triple of N-element arrays, each shape (N,).

  2. ages[0] is scalar but any of n, ts, or ir is a list/tuple/NDArray of length N (param-triggered batch): scalar ages broadcast to N copies.

In both cases the method returns NDArray[np.float64] of shape (N,).

  • Per-policy parameters n, ts, and ir may each be a scalar or an array of length N.

  • Pandas and Polars Series are accepted for n, ts, ir, and each per-life age array within ages without calling .to_numpy(); they are converted internally via the array protocol.

  • on_error='nan' marks invalid policies with np.nan.

Examples

>>> from lactuca import LifeTable
>>> table = LifeTable('PER2020_Ind_1o', 'm', cohort=1960)
>>> table.interest_rate = 0.03
>>> pv = table.nExyz([65, 60, 55], n=10)
>>> # With time shift
>>> pv_t5 = table.nExyz([65, 60, 55], n=10, ts=5)

Batch mode:

>>> import numpy as np
>>> x_arr = np.array([60.0, 65.0])
>>> y_arr = np.array([55.0, 60.0])
>>> z_arr = np.array([50.0, 55.0])
>>> table.nExyz((x_arr, y_arr, z_arr), n=10, ir=0.03)

See also

LifeTable.nEx

Single-life pure endowment.

LifeTable.nExy

Joint-life pure endowment for two lives.

LifeTable.nEjoint

Joint-life pure endowment for multiple lives.

ox(x=None, m: object = 1) None#

Exit/turnover rate (blocked for LifeTable).

Parameters:
  • x (optional) – Ignored placeholder maintained for API compatibility with other decrement-table subclasses.

  • m (object, optional) – Ignored payment-frequency placeholder maintained for API compatibility. Must be a valid payment frequency if provided; bool is rejected.

Raises:
  • TypeError – If m is a bool.

  • NotImplementedError – Always raised. LifeTable uses qx (mortality) instead of ox (exit probability).

Notes

  • ox is only available in ExitTable.

  • For mortality tables, use qx() instead.

See also

ExitTable

Table type that provides ox rates.

Examples

>>> from lactuca import LifeTable
>>> lt = LifeTable('PASEM2010', 'm')
>>> lt.ox(65)
NotImplementedError: ox is not available for LifeTable. Use qx() instead.
summary() str#

Return a formatted summary of the life table state and sample qx values.

Calls summary() for the common header block, then appends:

  • Decimals: qx=<n>

  • Sample qx values for the first and last 5 ages. When a modification is active, each line shows both the current and the original base value:

    qx(0) = 0.00012642 (original: 0.0020038)
    

    Without a modification the parenthetical is omitted:

    qx(0) = 0.0020038
    
Returns:

Multi-line summary string.

Return type:

str

Notes

  • Use modify_qx() to apply modifications before calling this method.

  • Use reset_modifications() to restore base rates and clear modification state.

  • When metadata_pending=True, this method delegates entirely to summary() (no sample qx values are computed or shown).

Examples

>>> from lactuca import LifeTable
>>> lt = LifeTable("PASEM2020_Rel_1o", sex="m")
>>> lt.modify_qx({"age_shift": 2, "decrement_multiplier": 1.05})
>>> print(lt.summary())
LifeTable: PASEM2020_Rel_1er.orden
Sex: m
  ...
w (current): 107
Modified: True
Modifications applied: age_shift=2, decrement_multiplier=1.05
Decimals: qx=15
Sample qx values (first 5):
  qx(0) = 0.00012642 (original: 0.0020038)
  ...
äjoint(ages: object, tables_others: Sequence[LifeTable] | Sequence[Sequence[LifeTable]] | None = None, *, ts: object = 0.0, d: object = 0.0, n: object = None, m: Literal[1, 2, 3, 4, 6, 12, 14, 24, 26, 52, 365] | Sequence[int] | NDArray[int64] = 1, gr: GrowthRate | float | None | Sequence[GrowthRate | float | None] = None, cashflow_amounts: Sequence[float] | None = None, ir: object = None, return_flows: bool = False, t_output: NDArray[float64] | None = None, benefits: Sequence[float] | NDArray[float64] | None = None, on_error: Literal['raise', 'nan'] = 'raise', record_ids: Sequence[Any] | None = None) float | dict[str, NDArray[float64]] | BatchResult#

Compute the present value of a joint-life annuity-due (prepayable) for multiple lives.

Parameters:
  • ages (Sequence[float] or list[Sequence[float]]) – Ages of all insured lives. Must have at least 2 elements. In batch mode, pass a list of r N-element arrays: [x0_arr, x1_arr, ..., xr_arr] where each has shape (N,).

  • tables_others (Sequence[LifeTable] or Sequence[Sequence[LifeTable]] or None, optional) –

    Life tables for additional lives (all except the first, which uses self). Three modes in batch:

    • None: uses self for all lives in all policies.

    • Sequence[LifeTable] of length n_lives - 1: shared across all N policies.

    • Sequence[Sequence[LifeTable]] of length N: per-policy; each inner list must have exactly n_lives - 1 elements. Raises ValueError if length != N.

  • ts (float or NDArray[np.float64], optional) – Shift (displacement) in years from origin (default 0.0). In batch mode, may be a scalar or an array of length N. Pandas and Polars Series are accepted; values are converted to float64. Scalar int literals (e.g. n=10) are coerced to float.

  • d (float or NDArray[np.float64], optional) – Deferment period in years (default 0.0). In batch mode, may be a scalar or an array of length N. Pandas and Polars Series are accepted; values are converted to float64. Scalar int literals (e.g. n=10) are coerced to float.

  • n (float or NDArray[np.float64] or None, optional) – Total duration of the annuity (in years) from contract start. If None, perpetual annuity up to limiting age. In batch mode, may be a scalar or an array of length N. Pandas and Polars Series are accepted; values are converted to float64. Scalar int literals (e.g. n=10) are coerced to float.

  • m (PaymentFrequencyLiteral, sequence of int, NDArray[np.int64], or array-like, optional) – Number of payments per year (1=annual, 12=monthly, etc., default 1). Integer-valued floats (e.g. 12.0) are coerced to int. In batch mode, may be a scalar (broadcast to all N) or an array/list of length N for per-policy payment frequencies; distinct values are processed in sub-groups by the vectorised batch engine. Pandas and Polars Series are accepted; values are converted to int64.

  • gr (GrowthRate, float, or None, optional) – Growth rate model (default None). A scalar float is auto-wrapped as GrowthRate(gr) (geometric growth by default). If None, no growth is applied. In batch mode, may be a scalar or a list of N values. Pandas and Polars Series are accepted without .to_numpy(). A numeric Series is converted to per-policy floats (auto-wrapped as GrowthRate where applicable). An object-dtype Series of GrowthRate instances is treated like a list of length N (including piecewise curves). Mixed numeric and object elements in one array-like sequence raise ValueError.

  • cashflow_amounts (Sequence[float], optional) – Custom payment amounts (one per payment period). When provided, disables growth (gr is ignored). Note: due-annuity methods do not accept cashflow_times; for custom payment timing use the postpayable counterpart (ajoint()). Pandas and Polars Series are accepted; converted to NDArray[np.float64] internally.

  • ir (float, InterestRate, list[float or InterestRate], NDArray[np.float64], or None, optional) – Interest rate (constant or curve). If None, uses interest_rate. In batch mode, may be a scalar (broadcast to all N), a list of N float values, or a list of N InterestRate objects (one curve per policy). Pandas and Polars Series are accepted without .to_numpy(). A numeric Series is converted to per-policy floats (auto-wrapped as InterestRate where applicable). An object-dtype Series of InterestRate instances is treated like a list of length N (including piecewise curves). Mixed numeric and object elements in one array-like sequence raise ValueError.

  • return_flows (bool, optional) –

    If True, returns a cashflow decomposition dict instead of the present value. The dict schema differs between single-policy and batch:

    Single-policy (scalar ages): per-payment engine dict. Same keys as ax(); see that method’s return_flows parameter for the full key reference.

    Batch (array ages): aggregate portfolio dict {'time_grid', 'expected_cf', 'pv_cf', 'total_pv'}. total_pv == np.sum(pv_cf) is always satisfied. Raises ValueError in discrete_simplified / continuous_simplified modes. Default is False.

  • t_output (NDArray[np.float64] or None, optional) – External bucketing grid for aggregate flow output. When provided, per-policy present-value contributions — already discounted at their exact payment times — are accumulated into the nearest bucket via np.searchsorted without re-discounting. Controls only the shape of expected_cf and pv_cf; total_pv is always the exact sum of all per-payment present values and is identical to the t_output=None result. Typical use: regulatory templates that require a fixed set of time buckets (e.g., annual IFRS 17 disclosure, Solvency II SCR projection). Only effective when return_flows=True; raises ValueError if return_flows=False. Default is None. Pandas and Polars Series are accepted; converted to NDArray[np.float64] before use.

  • benefits (sequence of float or NDArray[np.float64] or None, optional) – Per-policy benefit amounts, shape (N,). Scales each policy’s contribution to the aggregate flows (e.g., sum insured, PVDBO weight). Requires batch mode. Any sequence is accepted, including Pandas and Polars Series; values are converted to float64. Compatible with return_flows=False (returns NDArray[np.float64] with per-policy scaled PVs) and on_error='nan'. Default is None.

  • on_error ({'raise', 'nan'}, optional) – Error handling for invalid ages or parameters. 'raise' (default) raises ValueError; 'nan' marks invalid policies with np.nan and returns BatchResult. Default is 'raise'.

  • record_ids (sequence or None, optional) – Identifiers for each policy in batch mode, used to populate the error report when on_error='nan'. Length must equal N. Any sequence is accepted, including Pandas and Polars Series; elements are always accessed by positional integer index. Default is None.

Returns:

  • float – When ages[0] is scalar and return_flows=False.

  • NDArray[np.float64] – When ages[0] is an array or list and return_flows=False. Shape (N,).

  • dict – When return_flows=True. Single-policy: per-payment engine dict (same keys as ax()). Batch: aggregate portfolio dict {'time_grid', 'expected_cf', 'pv_cf', 'total_pv'}.

  • BatchResult – When on_error='nan'.

Raises:
  • ValueError – If ages has fewer than 2 elements. If tables_others length does not match len(ages)-1. If any age is outside [0, w] for corresponding table. If return_flows=True in batch mode and calculation_mode is 'discrete_simplified' or 'continuous_simplified'. If return_flows=True and m is a vector with heterogeneous values. If t_output is not None and return_flows=False. If t_output is not None and input is scalar (non-batch mode). If on_error is not 'raise' or 'nan'. If on_error='nan' is passed with scalar x (or ages for joint-life methods). Use a list or array of ages for batch mode. If record_ids is not None and input is scalar (non-batch mode).

  • TypeError – If ages or tables_others contain invalid types (including bool for numeric kwargs).

Notes

Boolean values for age, duration, shift, deferment, and rate arguments raise TypeError. Accepted types for each parameter are described in Parameters above.

Joint-life annuity-due for r lives, under the independence assumption:

\[\ddot{a}_{x_1 x_2 \cdots x_r}^{(m)} = \frac{1}{m} \sum_{k=0}^{\infty} v^{k/m} \prod_{i=1}^{r} {}_{k/m}p_{x_i}\]
  • Payments occur at the BEGINNING of each period (due/prepayable).

  • Annuity continues while ALL lives survive (joint-life status).

  • Uses independence assumption for joint survival probabilities.

  • cashflow_times is not supported for annuities-due; cashflow_amounts scales benefits on the standard due payment grid. Use ajoint() for custom payment times.

  • When gr is provided, the growth factor at the j-th payment interval is (1+g)^(j // m) — anniversaries counted from the first payment, not the contract origin. GrowthRate.shifted(ts) consumes int(ts) anniversary years; fractional ts is actuarially correct (revaluations operate on complete policy years). See Growth Rate Conventions for the full reference.

Batch mode#

Batch mode is activated when either of these conditions is met:

  1. ages[0] is a list or NDArray (age-triggered batch): pass ages as a list of r N-element arrays, each shape (N,).

  2. ages[0] is scalar but any of n, ts, d, ir, gr, or m is a list/tuple/NDArray of length N (param-triggered batch): scalar ages broadcast to N copies.

In both cases the method returns NDArray[np.float64] of shape (N,).

  • Per-policy parameters ts, d, n, ir, and gr may each be a scalar or an array of length N.

  • m may be a scalar (broadcast to all N) or an array/list of length N for per-policy payment frequencies; the batch engine groups policies by unique m value and vectorises each group independently.

  • Pandas and Polars Series are accepted for all per-policy parameters (n, ts, d, ir, gr, m, cashflow_amounts, t_output, and each per-life age array within ages) without calling .to_numpy(); they are converted internally via the array protocol.

  • on_error='nan' marks invalid policies with np.nan.

See also

LifeTable.ajoint

Joint-life annuity-immediate (postpayable) for multiple lives.

LifeTable.äxy

Joint-life annuity-due for two lives.

LifeTable.äxyz

Joint-life annuity-due for three lives.

LifeTable.äx

Single-life annuity-due.

Examples

>>> from lactuca import LifeTable
>>> # Four lives with same table
>>> table = LifeTable('PER2020_Ind_1o', 'm', cohort=1960)
>>> table.interest_rate = 0.03
>>> pv = table.äjoint([65, 60, 55, 50], n=20, m=12)
>>> # Multiple lives with different tables
>>> table_m, table_f1, table_f2 = LifeTable('PER2020_Ind_1o', ('m', 'f', 'f'), cohort=1960)
>>> table_m.interest_rate = 0.03
>>> pv = table_m.äjoint([65, 62, 58], [table_f1, table_f2], n=15, m=12)

Batch mode:

>>> import numpy as np
>>> a0 = np.array([60.0, 65.0])
>>> a1 = np.array([55.0, 60.0])
>>> table.äjoint([a0, a1], n=20, ir=0.03)
äx(x: object, *, ts: object = 0.0, d: object = 0.0, n: object = None, m: Literal[1, 2, 3, 4, 6, 12, 14, 24, 26, 52, 365] | Sequence[int] | NDArray[int64] = 1, gr: GrowthRate | float | None | Sequence[GrowthRate | float | None] = None, cashflow_amounts: Sequence[float] | None = None, ir: object = None, return_flows: bool = False, t_output: NDArray[float64] | None = None, benefits: Sequence[float] | NDArray[float64] | None = None, on_error: Literal['raise', 'nan'] = 'raise', record_ids: Sequence[Any] | None = None) float | NDArray[float64] | dict[str, NDArray[float64] | float] | BatchResult#

Compute the present value of an annuity-due (prepayable) for a single life.

Parameters:
  • x (float or list of float or NDArray[np.float64]) – Initial age. Pass an array or list of N ages to activate batch mode, which returns an ndarray of shape (N,).

  • ts (float or NDArray[np.float64], optional) – Shift (displacement) in years from origin. Default is 0.0. In batch mode, may be a scalar (broadcast) or an array of length N. Pandas and Polars Series are accepted; values are converted to float64. Scalar int literals (e.g. n=10) are coerced to float.

  • d (float or NDArray[np.float64], optional) – Deferment period in years. Default is 0.0. In batch mode, may be a scalar or an array of length N. Pandas and Polars Series are accepted; values are converted to float64. Scalar int literals (e.g. n=10) are coerced to float.

  • n (float or NDArray[np.float64] or None, optional) – Total duration of the annuity (in years) from the contract start. In batch mode, may be a scalar or an array of length N. Pandas and Polars Series are accepted; values are converted to float64. Scalar int literals (e.g. n=10) are coerced to float.

  • m (PaymentFrequencyLiteral, sequence of int, NDArray[np.int64], or array-like, optional) – Number of payments per year (1=annual, 12=monthly, etc.). Integer-valued floats (e.g. 12.0) are coerced to int. In batch mode, may be a scalar (broadcast to all N) or an array/list of length N for per-policy payment frequencies; distinct values are processed in sub-groups by the vectorised batch engine. Pandas and Polars Series are accepted; values are converted to int64.

  • gr (GrowthRate, float, or None, optional) – Growth rate model. A scalar float is auto-wrapped as GrowthRate(gr) (geometric growth by default). If None, no growth is applied. In batch mode, may be a scalar or a list of N values. Pandas and Polars Series are accepted without .to_numpy(). A numeric Series is converted to per-policy floats (auto-wrapped as GrowthRate where applicable). An object-dtype Series of GrowthRate instances is treated like a list of length N (including piecewise curves). Mixed numeric and object elements in one array-like sequence raise ValueError.

  • cashflow_amounts (Sequence[float], optional) – Custom payment amounts (one per payment period). When provided, disables growth (gr is ignored). Length must match the due-annuity payment count on the regular grid (\lfloor n_\text{eff} \cdot m \rfloor, plus one when n_\text{eff} \cdot m is fractional, and plus one for whole-life due grids — see Last Payment Adjustment). Note: due-annuity methods do not accept cashflow_times; for custom payment timing use the postpayable counterpart (ax()). Pandas and Polars Series are accepted; converted to NDArray[np.float64] internally.

  • ir (float, InterestRate, list[float or InterestRate], NDArray[np.float64], or None, optional) – Interest rate (constant or curve). If None, uses interest_rate. In batch mode, may be a scalar (broadcast to all N), a list of N float values, or a list of N InterestRate objects (one curve per policy). Pandas and Polars Series are accepted without .to_numpy(). A numeric Series is converted to per-policy floats (auto-wrapped as InterestRate where applicable). An object-dtype Series of InterestRate instances is treated like a list of length N (including piecewise curves). Mixed numeric and object elements in one array-like sequence raise ValueError.

  • return_flows (bool, optional) –

    If True, returns a cashflow decomposition dict instead of the present value. The dict schema differs between scalar and batch:

    Scalar x, discrete_precision / discrete_simplified mode:

    {
      "time_grid"            : NDArray  # payment times
      "interest_rate"        : NDArray  # i(t) at each payment
      "discount_factor"      : NDArray  # v^t at each payment
      "survival_probability" : NDArray  # tpx at each payment
      "growth" or "amount"   : NDArray  # growth factors or custom amounts
      "payment_adjustment"   : NDArray  # fractional last-payment factor
      "present_value_raw"    : NDArray  # pv before /m scaling
      "present_value"        : NDArray  # final pv per payment (/m applied)
    }
    

    sum(result["present_value"]) equals the rounded scalar present value.

    Scalar x, continuous_precision mode:

    {
      "time_grid"   : NDArray  # integration points
      "expected_cf" : NDArray  # undiscounted density × dt
      "pv_cf"       : NDArray  # discounted density × dt
    }
    

    np.sum(result["pv_cf"]) approximates the present value.

    Batch (array x), *_precision modes only:

    {
      "time_grid"   : NDArray  # portfolio time grid
      "expected_cf" : NDArray  # sum of undiscounted contributions across policies
      "pv_cf"       : NDArray  # sum of discounted contributions across policies
      "total_pv"    : float    # sum(pv_cf); equals sum of scalar present values
    }
    

    total_pv == np.sum(pv_cf) is always satisfied. Raises ValueError in discrete_simplified / continuous_simplified modes. Default is False.

  • t_output (NDArray[np.float64] or None, optional) – External bucketing grid for aggregate flow output. When provided, per-policy present-value contributions — already discounted at their exact payment times — are accumulated into the nearest bucket via np.searchsorted without re-discounting. Controls only the shape of expected_cf and pv_cf; total_pv is always the exact sum of all per-payment present values and is identical to the t_output=None result. Typical use: regulatory templates that require a fixed set of time buckets (e.g., annual IFRS 17 disclosure, Solvency II SCR projection). Only effective when return_flows=True; raises ValueError if return_flows=False. Default is None. Pandas and Polars Series are accepted; converted to NDArray[np.float64] before use.

  • benefits (sequence of float or NDArray[np.float64] or None, optional) – Per-policy benefit amounts, shape (N,). Scales each policy’s contribution to the aggregate flows (e.g., sum insured, PVDBO weight). Requires batch mode. Any sequence is accepted, including Pandas and Polars Series; values are converted to float64. Compatible with return_flows=False (returns NDArray[np.float64] with per-policy scaled PVs) and on_error='nan'. Default is None.

  • on_error ({'raise', 'nan'}, optional) – Error handling for invalid ages or parameters. 'raise' (default) raises ValueError; 'nan' marks invalid policies with np.nan and returns BatchResult. Default is 'raise'.

  • record_ids (sequence or None, optional) – Identifiers for each policy in batch mode, used to populate the error report when on_error='nan'. Length must equal N. Any sequence is accepted, including Pandas and Polars Series; elements are always accessed by positional integer index. Default is None.

Returns:

  • float – When x is scalar and return_flows=False.

  • NDArray[np.float64] – When x is an array or list and return_flows=False. Shape (N,).

  • dict – When return_flows=True. Schema depends on scalar vs. batch and on calculation_mode; see the return_flows parameter description above for the full key listing of each variant.

  • BatchResult – When on_error='nan', wraps the result array and an error report.

Raises:
  • ValueError – If no interest rate is provided and interest_rate is None. If ts, d, or n fail timing validation. If return_flows=True in batch mode and calculation_mode is 'discrete_simplified' or 'continuous_simplified'. If return_flows=True and m is a vector with heterogeneous values; use a scalar m or split the batch by payment frequency. If t_output is not None and return_flows=False. If t_output is not None and input is scalar (non-batch mode). If on_error is not 'raise' or 'nan'. If on_error='nan' is passed with scalar x (or ages for joint-life methods). Use a list or array of ages for batch mode. If record_ids is not None and input is scalar (non-batch mode).

  • TypeError – If any argument has an incompatible type (including bool for numeric kwargs).

Notes

Boolean values for age, duration, shift, deferment, and rate arguments raise TypeError. Accepted types for each parameter are described in Parameters above.

Discrete whole-life annuity-due (\(m\) payments per year):

\[\ddot{a}_x^{(m)} = \frac{1}{m} \sum_{k=0}^{\infty} v^{k/m} \cdot {}_{k/m}p_x\]

For a temporary n-year annuity with optional d-year deferment:

\[{}_{d|}\ddot{a}_{x:\overline{n}|}^{(m)} = \frac{1}{m} \sum_{k=dm}^{(d+n)m-1} v^{k/m} \cdot {}_{k/m}p_x\]
  • The calculation is performed at age x + ts, with effective deferment max(d - ts, 0) and effective duration max(n - max(ts - d, 0), 0).

  • If ts >= d + n (shift absorbs the entire deferred annuity), the value is zero.

  • cashflow_times is not supported for annuities-due; cashflow_amounts scales benefits on the standard due payment grid (payment_timing=0). Use ax() for custom payment times.

  • When gr is provided, the growth factor at the j-th payment is (1+g)^(j // m) — anniversaries are counted from the first payment of the call, not from the contract origin. Deferment d shifts payment times but does not affect this anniversary index. Fractional ts values are accepted; GrowthRate.shifted(ts) discards the first int(ts) anniversary years of the schedule, which is actuarially correct because revaluations operate on complete policy years. See Growth Rate Conventions for the full reference.

Batch mode#

When x is a list, sequence, or ndarray, or when any of n, ts, d, ir, gr, or m is a sequence or array with scalar x, the method operates in batch mode and returns an ndarray of shape (N,):

  • A list of 1 element still returns NDArray shape (1,), never a scalar.

  • Per-policy parameters ts, d, n, ir, and gr may each be a scalar (broadcast to all N policies) or an array of length N.

  • m may be a scalar (broadcast to all N policies) or an array/list of length N for per-policy payment frequencies; the batch engine groups policies by unique m value and vectorises each group independently.

  • Pandas and Polars Series are accepted for all per-policy parameters (x, n, ts, d, ir, gr, m, cashflow_amounts, t_output) without calling .to_numpy(); they are converted internally via the array protocol.

  • discrete_precision mode uses a vectorised engine (50–250× vs scalar loop).

  • continuous_* modes use a serial fallback (~1.5× vs loop).

  • on_error='nan' marks invalid policies with np.nan instead of raising.

See also

LifeTable.ax

Annuity-immediate (postpayable) equivalent.

LifeTable.äxy

Joint-life annuity-due for two lives.

LifeTable.äjoint

Joint-life annuity-due for multiple lives.

Examples

>>> from lactuca import LifeTable
>>> lt = LifeTable('PASEM2010', 'm')
>>> lt.interest_rate = 0.03
>>> lt.äx(65, n=10, m=12)
>>> lt.äx(65)  # whole life

Batch mode:

>>> import numpy as np
>>> ages = np.array([55.0, 60.0, 65.0, 70.0])
>>> lt.äx(ages, n=10, ir=0.03)
>>> lt.äx(ages, n=np.array([20.0, 15.0, 10.0, 8.0]), ir=0.03)
äxy(ages: object, table_y: LifeTable | Sequence[LifeTable] | None = None, *, ts: object = 0.0, d: object = 0.0, n: object = None, m: Literal[1, 2, 3, 4, 6, 12, 14, 24, 26, 52, 365] | Sequence[int] | NDArray[int64] = 1, gr: GrowthRate | float | None | Sequence[GrowthRate | float | None] = None, cashflow_amounts: Sequence[float] | None = None, ir: object = None, return_flows: bool = False, t_output: NDArray[float64] | None = None, benefits: Sequence[float] | NDArray[float64] | None = None, on_error: Literal['raise', 'nan'] = 'raise', record_ids: Sequence[Any] | None = None) float | NDArray[float64] | dict[str, NDArray[float64]] | BatchResult#

Compute the present value of a joint-life annuity-due (prepayable) for two lives.

Parameters:
  • ages (Sequence[float] or tuple[Sequence[float], Sequence[float]]) – Ages of the two lives [x, y]. Must have exactly 2 elements. In batch mode, pass a pair of N-element arrays: (np.array([x0, …, xN]), np.array([y0, …, yN])).

  • table_y (LifeTable or Sequence[LifeTable] or None, optional) –

    Life table for the second life (y). Three modes in batch:

    • None: uses self for all policies.

    • LifeTable: single shared table for all N policies.

    • Sequence[LifeTable] of length N: per-policy table; policy i uses element i. Raises ValueError if length != N.

  • ts (float or NDArray[np.float64], optional) – Shift (displacement) in years from origin (default 0.0). In batch mode, may be a scalar (broadcast) or an array of length N. Pandas and Polars Series are accepted; values are converted to float64. Scalar int literals (e.g. n=10) are coerced to float.

  • d (float or NDArray[np.float64], optional) – Deferment period in years (default 0.0). In batch mode, may be a scalar or an array of length N. Pandas and Polars Series are accepted; values are converted to float64. Scalar int literals (e.g. n=10) are coerced to float.

  • n (float or NDArray[np.float64] or None, optional) – Total duration of the annuity (in years) from contract start. If None, perpetual annuity up to limiting age. In batch mode, may be a scalar or an array of length N. Pandas and Polars Series are accepted; values are converted to float64. Scalar int literals (e.g. n=10) are coerced to float.

  • m (PaymentFrequencyLiteral, sequence of int, NDArray[np.int64], or array-like, optional) – Number of payments per year (1=annual, 12=monthly, etc., default 1). Integer-valued floats (e.g. 12.0) are coerced to int. In batch mode, may be a scalar (broadcast to all N) or an array/list of length N for per-policy payment frequencies; distinct values are processed in sub-groups by the vectorised batch engine. Pandas and Polars Series are accepted; values are converted to int64.

  • gr (GrowthRate, float, or None, optional) – Growth rate model (default None). A scalar float is auto-wrapped as GrowthRate(gr) (geometric growth by default). If None, no growth is applied. In batch mode, may be a scalar or a list of N values. Pandas and Polars Series are accepted without .to_numpy(). A numeric Series is converted to per-policy floats (auto-wrapped as GrowthRate where applicable). An object-dtype Series of GrowthRate instances is treated like a list of length N (including piecewise curves). Mixed numeric and object elements in one array-like sequence raise ValueError.

  • cashflow_amounts (Sequence[float], optional) – Custom payment amounts (one per payment period). When provided, disables growth (gr is ignored). Note: due-annuity methods do not accept cashflow_times; for custom payment timing use the postpayable counterpart (axy()). Pandas and Polars Series are accepted; converted to NDArray[np.float64] internally.

  • ir (float, InterestRate, list[float or InterestRate], NDArray[np.float64], or None, optional) – Interest rate (constant or curve). If None, uses interest_rate. In batch mode, may be a scalar (broadcast to all N), a list of N float values, or a list of N InterestRate objects (one curve per policy). Pandas and Polars Series are accepted without .to_numpy(). A numeric Series is converted to per-policy floats (auto-wrapped as InterestRate where applicable). An object-dtype Series of InterestRate instances is treated like a list of length N (including piecewise curves). Mixed numeric and object elements in one array-like sequence raise ValueError.

  • return_flows (bool, optional) –

    If True, returns a cashflow decomposition dict instead of the present value. The dict schema differs between single-policy and batch:

    Single-policy (scalar ages): per-payment engine dict. Same keys as ax(); see that method’s return_flows parameter for the full key reference.

    Batch (array ages): aggregate portfolio dict {'time_grid', 'expected_cf', 'pv_cf', 'total_pv'}. total_pv == np.sum(pv_cf) is always satisfied. Raises ValueError in discrete_simplified / continuous_simplified modes. Default is False.

  • t_output (NDArray[np.float64] or None, optional) – External bucketing grid for aggregate flow output. When provided, per-policy present-value contributions — already discounted at their exact payment times — are accumulated into the nearest bucket via np.searchsorted without re-discounting. Controls only the shape of expected_cf and pv_cf; total_pv is always the exact sum of all per-payment present values and is identical to the t_output=None result. Typical use: regulatory templates that require a fixed set of time buckets (e.g., annual IFRS 17 disclosure, Solvency II SCR projection). Only effective when return_flows=True; raises ValueError if return_flows=False. Default is None. Pandas and Polars Series are accepted; converted to NDArray[np.float64] before use.

  • benefits (sequence of float or NDArray[np.float64] or None, optional) – Per-policy benefit amounts, shape (N,). Scales each policy’s contribution to the aggregate flows (e.g., sum insured, PVDBO weight). Requires batch mode. Any sequence is accepted, including Pandas and Polars Series; values are converted to float64. Compatible with return_flows=False (returns NDArray[np.float64] with per-policy scaled PVs) and on_error='nan'. Default is None.

  • on_error ({'raise', 'nan'}, optional) – Error handling for invalid ages or parameters. 'raise' (default) raises ValueError; 'nan' marks invalid policies with np.nan and returns BatchResult. Default is 'raise'.

  • record_ids (sequence or None, optional) – Identifiers for each policy in batch mode, used to populate the error report when on_error='nan'. Length must equal N. Any sequence is accepted, including Pandas and Polars Series; elements are always accessed by positional integer index. Default is None.

Returns:

  • float – When ages[0] is scalar and return_flows=False.

  • NDArray[np.float64] – When ages[0] is an array or list and return_flows=False. Shape (N,).

  • dict – When return_flows=True. Single-policy: per-payment engine dict (same keys as ax()). Batch: aggregate portfolio dict {'time_grid', 'expected_cf', 'pv_cf', 'total_pv'}.

  • BatchResult – When on_error='nan'.

Raises:
  • ValueError – If ages does not contain exactly 2 elements. If any age is outside [0, w] for corresponding table. If return_flows=True in batch mode and calculation_mode is 'discrete_simplified' or 'continuous_simplified'. If return_flows=True and m is a vector with heterogeneous values. If t_output is not None and return_flows=False. If t_output is not None and input is scalar (non-batch mode). If on_error is not 'raise' or 'nan'. If on_error='nan' is passed with scalar x (or ages for joint-life methods). Use a list or array of ages for batch mode. If record_ids is not None and input is scalar (non-batch mode).

  • TypeError – If ages contains non-numeric values. If table_y is not a LifeTable instance or None. If any numeric argument is bool.

Notes

Boolean values for age, duration, shift, deferment, and rate arguments raise TypeError. Accepted types for each parameter are described in Parameters above.

Joint-life annuity-due for two lives, under the independence assumption:

\[\ddot{a}_{xy}^{(m)} = \frac{1}{m} \sum_{k=0}^{\infty} v^{k/m} \cdot {}_{k/m}p_x \cdot {}_{k/m}p_y\]
  • Payments occur at the BEGINNING of each period (due/prepayable).

  • Annuity continues while BOTH lives survive (joint-life status).

  • Uses independence assumption for joint survival probabilities.

  • cashflow_times is not supported for annuities-due; cashflow_amounts scales benefits on the standard due payment grid. Use axy() for custom payment times.

  • When gr is provided, the growth factor at the j-th payment interval is (1+g)^(j // m) — anniversaries counted from the first payment, not the contract origin. GrowthRate.shifted(ts) consumes int(ts) anniversary years; fractional ts is actuarially correct (revaluations operate on complete policy years). See Growth Rate Conventions for the full reference.

Batch mode#

Batch mode is activated when either of these conditions is met:

  1. ages[0] is a list or NDArray (age-triggered batch): pass ages as a pair of N-element arrays (x_arr, y_arr), each shape (N,).

  2. ages[0] is scalar but any of n, ts, d, ir, gr, or m is a list/tuple/NDArray of length N (param-triggered batch): scalar ages broadcast to N copies.

In both cases the method returns NDArray[np.float64] of shape (N,).

  • Per-policy parameters ts, d, n, ir, and gr may each be a scalar (broadcast to all N) or an array of length N.

  • m may be a scalar (broadcast to all N policies) or an array/list of length N for per-policy payment frequencies; the batch engine groups policies by unique m value and vectorises each group independently.

  • discrete_precision mode uses a vectorised matrix engine.

  • Pandas and Polars Series are accepted for all per-policy parameters (n, ts, d, ir, gr, m, cashflow_amounts, t_output, and each per-life age array within ages) without calling .to_numpy(); they are converted internally via the array protocol.

  • on_error='nan' marks invalid policies with np.nan.

See also

LifeTable.axy

Joint-life annuity-immediate for two lives.

LifeTable.äjoint

Joint-life annuity-due for multiple lives.

LifeTable.äxyz

Joint-life annuity-due for three lives.

LifeTable.äx

Single-life annuity-due.

Examples

>>> from lactuca import LifeTable
>>> # Two lives with same table
>>> table = LifeTable('PER2020_Ind_1o', 'm', cohort=1960)
>>> table.interest_rate = 0.03
>>> pv = table.äxy([65, 60], n=20, m=12)
>>> # Two lives with different tables
>>> table_m, table_f = LifeTable('PER2020_Ind_1o', ('m', 'f'), cohort=1960)
>>> table_m.interest_rate = 0.03
>>> pv = table_m.äxy([65, 62], table_f, n=15, m=12)

Batch mode:

>>> import numpy as np
>>> x_ages = np.array([60.0, 65.0, 70.0])
>>> y_ages = np.array([55.0, 60.0, 65.0])
>>> table.äxy((x_ages, y_ages), n=20, ir=0.03)
äxyz(ages: object, tables_yz: Sequence[LifeTable] | Sequence[Sequence[LifeTable]] | None = None, *, ts: object = 0.0, d: object = 0.0, n: object = None, m: Literal[1, 2, 3, 4, 6, 12, 14, 24, 26, 52, 365] | Sequence[int] | NDArray[int64] = 1, gr: GrowthRate | float | None | Sequence[GrowthRate | float | None] = None, cashflow_amounts: Sequence[float] | None = None, ir: object = None, return_flows: bool = False, t_output: NDArray[float64] | None = None, benefits: Sequence[float] | NDArray[float64] | None = None, on_error: Literal['raise', 'nan'] = 'raise', record_ids: Sequence[Any] | None = None) float | dict[str, NDArray[float64]] | BatchResult#

Compute the present value of a joint-life annuity-due (prepayable) for three lives.

Parameters:
  • ages (Sequence[float] or tuple[Sequence[float], Sequence[float], Sequence[float]]) – Ages of the three lives [x, y, z]. Must have exactly 3 elements. In batch mode, pass a triple of N-element arrays: (np.array([x0, …]), np.array([y0, …]), np.array([z0, …])).

  • tables_yz (Sequence[LifeTable] or Sequence[Sequence[LifeTable]] or None, optional) –

    Life tables for the second and third lives [y, z]. Three modes in batch:

    • None: uses self for all three lives in all policies.

    • [LifeTable, LifeTable]: 2 shared tables (y and z) for all N policies.

    • [[t_y0, t_z0], …] of length N: per-policy; each inner list must have exactly 2 elements. Raises ValueError if outer length != N.

  • ts (float or NDArray[np.float64], optional) – Shift (displacement) in years from origin (default 0.0). In batch mode, may be a scalar or an array of length N. Pandas and Polars Series are accepted; values are converted to float64. Scalar int literals (e.g. n=10) are coerced to float.

  • d (float or NDArray[np.float64], optional) – Deferment period in years (default 0.0). In batch mode, may be a scalar or an array of length N. Pandas and Polars Series are accepted; values are converted to float64. Scalar int literals (e.g. n=10) are coerced to float.

  • n (float or NDArray[np.float64] or None, optional) – Total duration of the annuity (in years) from contract start. If None, perpetual annuity up to limiting age. In batch mode, may be a scalar or an array of length N. Pandas and Polars Series are accepted; values are converted to float64. Scalar int literals (e.g. n=10) are coerced to float.

  • m (PaymentFrequencyLiteral, sequence of int, NDArray[np.int64], or array-like, optional) – Number of payments per year (1=annual, 12=monthly, etc., default 1). Integer-valued floats (e.g. 12.0) are coerced to int. In batch mode, may be a scalar (broadcast to all N) or an array/list of length N for per-policy payment frequencies; distinct values are processed in sub-groups by the vectorised batch engine. Pandas and Polars Series are accepted; values are converted to int64.

  • gr (GrowthRate, float, or None, optional) – Growth rate model (default None). A scalar float is auto-wrapped as GrowthRate(gr) (geometric growth by default). If None, no growth is applied. In batch mode, may be a scalar or a list of N values. Pandas and Polars Series are accepted without .to_numpy(). A numeric Series is converted to per-policy floats (auto-wrapped as GrowthRate where applicable). An object-dtype Series of GrowthRate instances is treated like a list of length N (including piecewise curves). Mixed numeric and object elements in one array-like sequence raise ValueError.

  • cashflow_amounts (Sequence[float], optional) – Custom payment amounts (one per payment period). When provided, disables growth (gr is ignored). Note: due-annuity methods do not accept cashflow_times; for custom payment timing use the postpayable counterpart (axyz()). Pandas and Polars Series are accepted; converted to NDArray[np.float64] internally.

  • ir (float, InterestRate, list[float or InterestRate], NDArray[np.float64], or None, optional) – Interest rate (constant or curve). If None, uses interest_rate. In batch mode, may be a scalar (broadcast to all N), a list of N float values, or a list of N InterestRate objects (one curve per policy). Pandas and Polars Series are accepted without .to_numpy(). A numeric Series is converted to per-policy floats (auto-wrapped as InterestRate where applicable). An object-dtype Series of InterestRate instances is treated like a list of length N (including piecewise curves). Mixed numeric and object elements in one array-like sequence raise ValueError.

  • return_flows (bool, optional) –

    If True, returns a cashflow decomposition dict instead of the present value. The dict schema differs between single-policy and batch:

    Single-policy (scalar ages): per-payment engine dict. Same keys as ax(); see that method’s return_flows parameter for the full key reference.

    Batch (array ages): aggregate portfolio dict {'time_grid', 'expected_cf', 'pv_cf', 'total_pv'}. total_pv == np.sum(pv_cf) is always satisfied. Raises ValueError in discrete_simplified / continuous_simplified modes. Default is False.

  • t_output (NDArray[np.float64] or None, optional) – External bucketing grid for aggregate flow output. When provided, per-policy present-value contributions — already discounted at their exact payment times — are accumulated into the nearest bucket via np.searchsorted without re-discounting. Controls only the shape of expected_cf and pv_cf; total_pv is always the exact sum of all per-payment present values and is identical to the t_output=None result. Typical use: regulatory templates that require a fixed set of time buckets (e.g., annual IFRS 17 disclosure, Solvency II SCR projection). Only effective when return_flows=True; raises ValueError if return_flows=False. Default is None. Pandas and Polars Series are accepted; converted to NDArray[np.float64] before use.

Returns:

  • float – When ages[0] is scalar and return_flows=False.

  • NDArray[np.float64] – When ages[0] is an array or list and return_flows=False. Shape (N,).

  • dict – When return_flows=True. Single-policy: per-payment engine dict (same keys as ax()). Batch: aggregate portfolio dict {'time_grid', 'expected_cf', 'pv_cf', 'total_pv'}.

  • BatchResult – When on_error='nan'.

Raises:
  • ValueError – If ages does not contain exactly 3 elements. If tables_yz is provided but does not contain exactly 2 LifeTable instances. If any age is outside [0, w] for corresponding table. If return_flows=True and m is a vector with heterogeneous values. If t_output is not None and return_flows=False. If t_output is not None and input is scalar (non-batch mode). If on_error is not 'raise' or 'nan'. If on_error='nan' is passed with scalar x (or ages for joint-life methods). Use a list or array of ages for batch mode. If record_ids is not None and input is scalar (non-batch mode).

  • TypeError – If ages contains non-numeric values. If tables_yz contains non-LifeTable instances. If any numeric argument is bool.

Notes

Boolean values for age, duration, shift, deferment, and rate arguments raise TypeError. Accepted types for each parameter are described in Parameters above.

Joint-life annuity-due for three lives, under the independence assumption:

\[\ddot{a}_{xyz}^{(m)} = \frac{1}{m} \sum_{k=0}^{\infty} v^{k/m} \cdot {}_{k/m}p_x \cdot {}_{k/m}p_y \cdot {}_{k/m}p_z\]
  • Payments occur at the BEGINNING of each period (due/prepayable).

  • Annuity continues while ALL THREE lives survive (joint-life status).

  • Uses independence assumption for joint survival probabilities.

  • cashflow_times is not supported for annuities-due; cashflow_amounts scales benefits on the standard due payment grid. Use axyz() for custom payment times.

  • When gr is provided, the growth factor at the j-th payment interval is (1+g)^(j // m) — anniversaries counted from the first payment, not the contract origin. GrowthRate.shifted(ts) consumes int(ts) anniversary years; fractional ts is actuarially correct (revaluations operate on complete policy years). See Growth Rate Conventions for the full reference.

Batch mode#

Batch mode is activated when either of these conditions is met:

  1. ages[0] is a list or NDArray (age-triggered batch): pass ages as a triple of N-element arrays (x_arr, y_arr, z_arr), each (N,).

  2. ages[0] is scalar but any of n, ts, d, ir, gr, or m is a list/tuple/NDArray of length N (param-triggered batch): scalar ages broadcast to N copies.

In both cases the method returns NDArray[np.float64] of shape (N,).

  • Per-policy parameters ts, d, n, ir, and gr may each be a scalar or an array of length N.

  • m may be a scalar (broadcast to all N) or an array/list of length N for per-policy payment frequencies; the batch engine groups policies by unique m value and vectorises each group independently.

  • Pandas and Polars Series are accepted for all per-policy parameters (n, ts, d, ir, gr, m, cashflow_amounts, t_output, and each per-life age array within ages) without calling .to_numpy(); they are converted internally via the array protocol.

  • on_error='nan' marks invalid policies with np.nan.

See also

LifeTable.axyz

Joint-life annuity-immediate for three lives.

LifeTable.äjoint

Joint-life annuity-due for multiple lives.

LifeTable.äxy

Joint-life annuity-due for two lives.

LifeTable.äx

Single-life annuity-due.

Examples

>>> from lactuca import LifeTable
>>> # Three lives with same table
>>> table = LifeTable('PER2020_Ind_1o', 'm', cohort=1960)
>>> table.interest_rate = 0.03
>>> pv = table.äxyz([65, 60, 55], n=20, m=12)
>>> # Three lives with different tables
>>> table_m, table_f1, table_f2 = LifeTable('PER2020_Ind_1o', ('m', 'f', 'f'), cohort=1960)
>>> table_m.interest_rate = 0.03
>>> pv = table_m.äxyz([65, 62, 58], [table_f1, table_f2], n=15, m=12)

Batch mode:

>>> import numpy as np
>>> x_arr = np.array([60.0, 65.0])
>>> y_arr = np.array([55.0, 60.0])
>>> z_arr = np.array([50.0, 55.0])
>>> table.äxyz((x_arr, y_arr, z_arr), n=20, ir=0.03)
property calculation_mode: str#

Return the current calculation mode from config.

Returns:

One of 'discrete_precision', 'discrete_simplified', 'continuous_precision', or 'continuous_simplified'.

Return type:

str

Notes

  • Controls how annuities, insurances, and endowments are calculated.

  • Discrete modes use commutation functions (integer ages, annual periods).

  • Continuous modes use numerical integration (fractional ages supported).

  • Precision modes preserve maximum accuracy; simplified modes use approximations.

  • Set via config.calculation_mode = 'mode_name'.

See also

Config

Global configuration singleton for all LifeTable settings.

Examples

>>> from lactuca import LifeTable, Config, config
>>> lt = LifeTable('PASEM2010', 'm')
>>> lt.calculation_mode
'discrete_precision'
>>> config.calculation_mode = 'continuous_precision'
>>> lt.calculation_mode
'continuous_precision'
>>> Config().reset()
property decimals: _DecimalsConfig#

Access decimal precision settings for life table actuarial functions.

This property provides convenient attribute-based access to global decimal precision configuration. All values are proxied dynamically from the Config singleton — no state is stored locally in the table instance. Accessing decimals.ix or decimals.ox raises AttributeError (not meaningful for life tables).

Returns:

Decimal-precision proxy with attributes for life-table actuarial columns. Available attributes:

  • lx : int — Survival function \(\ell_x\) precision

  • dx : int — Deaths \(d_x\) precision

  • qx : int — Mortality rate \(q_x\) precision

  • px : int — Survival probability \(p_x\) precision

  • tpx : int — Multi-year survival \({}_t p_x\) precision

  • tqx : int — Multi-year mortality \({}_t q_x\) precision

  • ex : int — Life expectancy \(\mathring{e}_x\) precision

  • Dx, Nx, Sx : int — Commutation functions precision

  • Cx, Mx, Rx : int — Insurance commutation functions precision

  • Lx, Tx : int — Stationary population and total future lifetime precision

  • ax, Ax : int — Annuities and insurances precision

Return type:

object

Raises:

AttributeError – If decimals.ix or decimals.ox are accessed (disability incidence and exit rates are not available for LifeTable).

Notes

  • Read-only proxy: self.decimals.xxx = value raises AttributeError by design; use Config().decimals.xxx = value to change precision globally.

  • Global scope: Changes to Config.decimals are immediately visible through all table instances.

  • For permanent changes, use Config().decimals.lx = value or Config().set('decimals_lx', value).

Examples

>>> from lactuca import LifeTable, Config
>>> lt = LifeTable('PASEM2010', 'm')
>>> Config().decimals.annuities = 4
>>> lt.decimals.annuities
4
>>> Config().reset()

See also

Config.decimals

Global decimal precision configuration.

property force_integer_ts: bool#

Return whether fractional ts (shift) values are rejected.

Returns:

True if fractional ts raises ValueError; False otherwise.

Return type:

bool

Notes

  • When True, any non-integer ts raises ValueError at validation time.

  • When False, fractional ts is silently accepted. A UserWarning is emitted only when a GrowthRate is active, because growth factors use whole-year anniversary indices and a fractional shift would be applied to an integer-indexed schedule.

  • Set via config.force_integer_ts = True/False.

See also

Config

Global configuration singleton for all LifeTable settings.

Examples

>>> from lactuca import LifeTable, Config
>>> lt = LifeTable('PASEM2010', 'm')
>>> lt.force_integer_ts
False
>>> Config().force_integer_ts = True
>>> lt.force_integer_ts
True
>>> Config().reset()
property force_mortality_method: str#

Return the current force of mortality method from config.

Returns:

One of 'finite_difference', 'spline', or 'kernel'.

Return type:

str

Notes

  • Controls how the force of mortality \(\mu_x\) is approximated from discrete survival probabilities in continuous calculation modes.

  • 'finite_difference': finite difference approximation (default).

  • 'spline': spline-based approximation.

  • 'kernel': kernel smoothing approximation.

  • Used in continuous_precision and continuous_simplified modes.

  • Set via config.force_mortality_method = 'method_name'.

See also

Config

Global configuration singleton for all LifeTable settings.

Examples

>>> from lactuca import LifeTable, Config
>>> lt = LifeTable('PASEM2010', 'm')
>>> lt.force_mortality_method
'finite_difference'
>>> Config().force_mortality_method = 'spline'
>>> lt.force_mortality_method
'spline'
>>> Config().reset()
property interest_rate: InterestRate | None#

Get the default interest rate for annuity/insurance calculations.

Returns:

Default interest rate object stored on the table, or None if no default rate has been configured. Constant scalar inputs are normalized to InterestRate when assigned.

Return type:

InterestRate or None

Notes

  • Provides default interest rate for ax, Ax, nEx, and related methods.

  • Individual calculation methods can override with their ir parameter.

  • None value requires explicit ir argument in all calculation methods.

  • Float values assigned through __init__() or the setter are automatically converted to constant InterestRate curves.

See also

InterestRate

Interest rate class supporting constant and variable rate curves.

Examples

>>> from lactuca import LifeTable
>>> from lactuca import InterestRate
>>> lt = LifeTable('PASEM2010', 'm')
>>> lt.interest_rate = 0.03  # Constant rate
>>> isinstance(lt.interest_rate, InterestRate)
True
>>> lt.interest_rate = InterestRate(terms=[5, 10], rates=[0.02, 0.025, 0.03])  # Variable curve
>>> isinstance(lt.interest_rate, InterestRate)
True
property mortality_placement: str#

Return the current mortality placement from config.

Returns:

One of 'beginning', 'mid', or 'end'.

Return type:

str

Notes

  • Controls the discounting exponent \(\alpha\) in \(C_x = v^{x+\alpha} d_x\).

  • 'beginning': \(\alpha = 0\); benefit discounted as \(v^x d_x\).

  • 'mid': \(\alpha = 0.5\); benefit discounted as \(v^{x+1/2} d_x\).

  • 'end': \(\alpha = 1\); benefit discounted as \(v^{x+1} d_x\).

  • Affects Cx() commutation function and all insurance values derived from it.

  • Set via config.mortality_placement = 'placement_name'.

See also

Config

Global configuration singleton for all LifeTable settings.

LifeTable.Cx

Commutation function \(C_x = v^{x+\alpha} \cdot d_x\) affected by this setting.

Examples

>>> from lactuca import LifeTable, Config
>>> lt = LifeTable('PASEM2010', 'm')
>>> lt.mortality_placement
'mid'
>>> Config().mortality_placement = 'end'
>>> lt.mortality_placement
'end'
>>> Config().reset()
property table_type: str#

Return the type of table.

Returns:

Always 'life' for LifeTable instances.

Return type:

str

Examples

>>> from lactuca import LifeTable
>>> lt = LifeTable('PASEM2010', 'm')
>>> lt.table_type
'life'

Inherited from DecrementTable#

The following members are inherited from DecrementTable without modification. See the DecrementTable reference for full documentation of each member.

Actuarial methods#

lx

Return lx values (number of lives at age x) for the specified age(s).

px

Return probability of surviving one period (px) at age x.

qx

Return probability of decrement at age x.

tpx

Return interval survival probability tpx for all (x, t) combinations.

tqx

Return interval death probability tqx for all (x, t) combinations.

dx

Return \(d_x\) values (number of decrements between ages \(x\) and \(x+1\)).

Modification#

modify_qx

Apply modifications to the table's decrement rates.

reset_modifications

Restore decrement to the original base values and clear all caches.

copy

Return a deep copy of the instance.

Display#

view_data

Return the computed instance data, optionally excluding normalized padding rows.

head

Return the first n rows of the computed instance data.

tail

Return the last n rows of the computed instance data, always ending at w.

State#

sex

Current sex used for calculations.

cohort

Current cohort (year of birth) for generational tables.

duration

Return the current policy duration for select or non-select tables.

unisex_blend

Male weight used to blend male/female rates into unisex rates.

w

Current upper age limit (omega), accounting for modifications.

modified

True if a modification is currently active on this table.

modifications_applied

List of modification descriptors applied in the current call.

is_select

Return True if the underlying table is a select-ultimate table.

metadata_pending

True when required metadata has not yet been supplied via configure() or setters.

Deferred construction#

configure

Configure table metadata atomically with a single rebuild.

batch_update

Context manager to batch multiple setter assignments into one rebuild.

See also TableRegistry and configure_all() in Table registry utilities and Deferred construction: pending, configure, and TableRegistry in Using Actuarial Tables.

Table metadata#

table_name

Name of the underlying actuarial table.

table

Access the underlying TableSource instance with table data and metadata.

generational

Whether the table includes cohort-based improvement factors.

base_year

Base year for generational improvement.

omega

Original terminal age from metadata (immutable).

start_age

Minimum defined age in the original table.

description

Human-readable table description from metadata.

valid_sexes

Valid sexes for this table (e.g. ['m', 'f']).

sex_independent

Whether rates are identical for both sexes.

generational_formula_type

Generational improvement formula type.

select

Whether this is a select-ultimate table.

select_period

Select period in years.

select_improvement_diagonal

Calendar-year index for generational improvement on select columns.

start_duration

Minimum integer duration.

mi_by_duration

Whether improvement factors are stored by policy duration.

mi_structure

Mortality improvement column layout tag.

grid_years

Projection calendar years for year-indexed MI tables.

file_name

Basename of the loaded table file.

file_path

Absolute path to the loaded table file.

data

Raw underlying table data (all columns, all sexes).

metadata

Raw metadata dictionary from the table file.