Inspecting Cash Flows#
The return_flows=True parameter causes any calculation method to return not just a scalar
present value but a full dict of the arrays that produced it. This is the primary
mechanism for regulatory traceability, BEL reconciliation, and debugging complex calculations
under IFRS 17 and Solvency II.
from lactuca import LifeTable
lt = LifeTable("PASEM2020_Rel_1o", "m", interest_rate=0.03)
result = lt.äx(65, n=20, m=12, return_flows=True)
# result is a dict — inspect all available keys:
print(list(result.keys()))
# ['payment_time', 'interest_rate', 'discount_factor', 'survival_probability',
# 'growth', 'payment_adjustment', 'present_value_raw', 'present_value']
# Reconstruct the scalar PV:
pv = result["present_value"].sum()
print(round(pv, 4)) # 13.2805
The structure of the returned dict depends on the calculation mode in effect:
Active mode |
Dict structure |
|---|---|
|
Flat arrays — see Annuity keys, Insurance keys, Pure endowment |
|
Integration-grid arrays — see Continuous mode keys |
|
Nested dicts (annuity/insurance); flat scalars (endowment) — see Simplified modes |
Annuity cash-flow keys (discrete_precision)#
All arrays contain one entry per payment interval. For \(m\) payments per year over \(n\) years that is \(m \times n\) entries (e.g. \(12 \times 20 = 240\) for the example below).
Key |
Description |
|---|---|
|
Payment times \(t_j = j/m + d\), \(j = 0, 1, \ldots, mn-1\) |
|
Effective annual rates at each \(t_j\) (piecewise-term aware) |
|
Discount factors \(v^{t_j}\) |
|
Survival probabilities \({}_{t_j}p_x\) |
|
Benefit growth factors at \(t_j\) (1.0 if no |
|
Fractional-final-payment scale (1.0 for full payments; < 1 for the last fractional period) |
|
\({}_{t_j}p_x \cdot v^{t_j} \cdot g(t_j) \cdot \text{adj}_j\) — element-wise product before frequency scaling |
|
Contribution of each payment to the total PV: \(\text{raw}_j / m\) |
The total annuity value is result["present_value"].sum().
Insurance cash-flow keys (discrete_precision)#
All arrays contain one entry per payment interval — same length semantics as the annuity table above (\(m \times n\) entries total).
Key |
Description |
|---|---|
|
Integer interval indices \(j = 0, 1, \ldots\) |
|
Policy-year starts for each interval: \(t_j = j/m + d\) |
|
Times at which the benefit is discounted: \(t_j +\) |
|
Effective annual rates at |
|
Discount factors \(v^{\text{discount\_time}}\) |
|
Raw per-interval death probabilities from the table |
|
Fractional-final-period scale |
|
Adjusted death probabilities used in the summation |
|
Benefit growth factors (1.0 if no |
|
PV contribution of each interval |
The total insurance value is result["present_value"].sum().
Pure-endowment cash-flow keys#
Because a pure endowment has a single payment at time \(n\), all dict values are scalars:
Key |
Description |
|---|---|
|
\(n\) — the maturity time |
|
\({}_{n}p_x\) (single-life) or \(\prod_i {}_{n}p_{x_i}\) (joint) |
|
\(v^n\) |
|
\(v^n \cdot {}_{n}p_x\) |
Note
Both discrete_precision and discrete_simplified return this same four-key structure.
For continuous_precision and continuous_simplified, see the sections below.
Continuous mode keys#
For continuous_precision, methods return integration grid arrays over \([d, d+n]\)
instead of per-payment arrays:
Annuity (continuous_precision):
All arrays are evaluated on a linearly-spaced integration grid over \([d,\, d+n]\).
Key |
Description |
|---|---|
|
Grid points \(t\) over \([d,\, d+n]\) (equals \([0, n]\) when \(d = 0\)) |
|
Effective annual rates at each grid point |
|
\(v^t\) at each grid point |
|
\({}_{t}p_x\) (raw \(\ell_x\), no rounding applied) |
|
Growth factors at each grid point |
|
\(v^t \cdot {}_t p_x \cdot g(t)\) — the integrand of \(\bar{a}_x\) |
Note
The continuous annuity dict does not include a "present_value" key.
Reconstruct \(\bar{a}_x\) by integrating the integrand array:
import numpy as np
from lactuca import LifeTable, config
lt = LifeTable("PASEM2020_Rel_1o", "m", interest_rate=0.03)
config.calculation_mode = "continuous_precision"
result = lt.äx(65, n=20, return_flows=True)
pv = np.trapezoid(result["integrand"], result["payment_time"])
print(round(pv, 4)) # 13.2532
config.calculation_mode = "discrete_precision"
Insurance (continuous_precision):
All arrays are evaluated on the same integration grid; present_value is a scalar.
Key |
Description |
|---|---|
|
Integration grid points |
|
\({}_{t}p_x\) |
|
\(\mu_{x+t}\) (numerically derived) |
|
\(v^t\) |
|
Benefit growth factors |
|
\(v^t \cdot {}_t p_x \cdot \mu_{x+t} \cdot g(t)\) |
|
Scalar — trapezoidal integral \(\bar{A}_x\) |
Endowment (continuous_precision):
The grid spans \([0, n]\) (endowments have no deferral concept).
payment_time, integral, and present_value are scalars; all other values are arrays.
Key |
Description |
|---|---|
|
\(n\) — maturity time (scalar) |
|
Integration grid \(t \in [0, n]\) |
|
\({}_t p_x\) (or joint-life equivalent) at each grid point |
|
Combined force of mortality \(\sum_i \mu_i(t)\) at each grid point |
|
Force of interest \(\delta(t)\) at each grid point |
|
\(\sum_i \mu_i(t) + \delta(t)\) — integrand of \(-\ln({}_{n}E_x)\) |
|
Scalar — \(\int_0^n [\sum_i \mu_i(t) + \delta(t)]\,\mathrm{d}t\) (trapezoidal) |
|
Scalar — \(e^{-\text{integral}} = {}_{n}E_x\) |
Simplified modes — nested structure#
discrete_simplified and continuous_simplified return nested dicts for annuity
and insurance methods.
For annuity methods the inner "due" and "immediate" components are
themselves full discrete_precision flow dicts.
For insurance methods the structure differs — see below.
For endowment methods, both modes return flat scalar dicts — see below.
discrete_simplified:
from lactuca import LifeTable, config
lt = LifeTable("PASEM2020_Rel_1o", "m", interest_rate=0.03)
config.calculation_mode = "discrete_simplified"
result = lt.äx(65, m=12, return_flows=True)
due_flows = result["due"] # full discrete_precision dict for annual äx
imm_flows = result["immediate"] # full discrete_precision dict for annual ax
pv_due = result["pv_due"] # scalar: sum of due component
pv_immediate = result["pv_immediate"] # scalar: sum of immediate component
coef_due = result["coef_due"] # Woolhouse weight for due component
coef_imm = result["coef_immediate"]
pv_final = result["interpolated"] # scalar PV: coef_due*pv_due + coef_imm*pv_immediate
print(f"coef_due={coef_due:.4f}, coef_imm={coef_imm:.4f}") # 0.5417, 0.4583
print(round(pv_final, 4)) # 15.6316
config.calculation_mode = "discrete_precision" # restore
Note
"pv_due" and "pv_immediate" are only present when m > 1.
For m = 1 the method delegates to discrete_precision and the returned dict has only
five keys: "due", "immediate", "coef_due" (= 1.0), "coef_immediate" (= 0.0),
and "interpolated". In this case "due" and "immediate" alias the same underlying
dict — mutations to one will be reflected in the other.
continuous_simplified:
from lactuca import LifeTable, config
lt = LifeTable("PASEM2020_Rel_1o", "m", interest_rate=0.03)
config.calculation_mode = "continuous_simplified"
# Integer n — fractional sub-dict is empty
result = lt.äx(65, n=20, return_flows=True)
print(list(result.keys()))
# ['due', 'immediate', 'interpolated', 'fractional']
print(round(result["interpolated"], 4)) # 13.2555
print(result["fractional"]) # {}
# Fractional n — fractional sub-dict holds the two-point quadrature tail
result2 = lt.äx(65, n=20.5, return_flows=True)
frac = result2["fractional"]
print(list(frac.keys()))
# ['payment_time', 'interest_rate', 'discount_factor',
# 'survival_probability', 'growth', 'present_value']
print(round(result2["interpolated"], 4)) # 13.4242
config.calculation_mode = "discrete_precision" # restore
Key |
Type |
Description |
|---|---|---|
|
dict |
Annual annuity-due flows over the integer portion \(k = \lfloor n \rfloor\) (a full |
|
dict |
Annual annuity-immediate flows over \(k = \lfloor n \rfloor\) (a full |
|
float |
\(\bar{a}_{x:\overline{n}|} \approx \tfrac{1}{2}\bigl(\ddot{a}_{x:\overline{k}|} + a_{x:\overline{k}|}\bigr) + \tfrac{s}{2}\bigl(v^k\,{}_kp_x + v^n\,{}_np_x\bigr)\) where \(k = \lfloor n \rfloor\), \(s = n - k\); reduces to \(\tfrac{1}{2}\bigl(\ddot{a}_{x:\overline{n}|} + a_{x:\overline{n}|}\bigr)\) when \(n \in \mathbb{Z}\) — see Calculation Modes for derivation |
|
dict |
Two-point trapezoidal flows for the fractional tail \([k,\,n]\); empty dict |
Note
When "fractional" is populated (fractional \(n\)), its six inner arrays are Python lists of two
elements — not NumPy arrays. Use sum(frac["present_value"]) or
np.add(*frac["present_value"]) rather than frac["present_value"].sum().
Insurance (discrete_simplified, m > 1):
For insurance methods the Woolhouse approximation calculates \(A_x^{(m)}\) by interpolating
between two annual values at ages \(x\) and \(x+1\). The structure is flat — no "due"/"immediate":
Key |
Type |
Description |
|---|---|---|
|
dict |
Full |
|
dict |
Full |
|
float |
Woolhouse weight \(\frac{m-1}{2m}\) |
|
float |
Scalar \(A_x^{(m)} \approx A_{x}^{(1)} + \tfrac{m-1}{2m}\bigl(A_{x+1}^{(1)} - A_{x}^{(1)}\bigr)\) |
Insurance (continuous_simplified):
Interpolates \(\bar{A}_x\) as the arithmetic mean of two continuous_precision evaluations —
one at age \(x\), one at age \(x+1\). All three dict values are scalars:
Key |
Type |
Description |
|---|---|---|
|
float |
\(\bar{A}_x\) from |
|
float |
\(\bar{A}_{x+1}\) from |
|
float |
\(\tfrac{1}{2}(\bar{A}_x + \bar{A}_{x+1})\) |
Endowment (discrete_simplified):
Returns the same four-key flat scalar structure as discrete_precision — see
Pure-endowment cash-flow keys.
Endowment (continuous_simplified):
Uses average-force approximation \({}_{n}E_x \approx e^{-n(\bar{\delta} + \sum_i \bar{\mu}_i)}\). All five dict values are scalars:
Key |
Type |
Description |
|---|---|---|
|
float |
Maturity time \(n\) |
|
float |
\(\bar{\delta} = -\ln(v^n)/n\) |
|
float |
\(\sum_i \bar{\mu}_i = \sum_i(-\ln({}_np_{x_i})/n)\) |
|
float |
\(\bar{\delta} + \sum_i \bar{\mu}_i\) |
|
float |
\(\exp\!\bigl(-n\cdot(\bar{\delta}+\sum_i\bar{\mu}_i)\bigr)\) |
Decomposing and verifying a present value#
return_flows=True makes it possible to inspect every factor that contributed to a
present value and verify that they compose exactly as expected. This kind of
component-level traceability is the building block for model validation tasks —
such as auditing an actuarial cash-flow model or cross-checking a single benefit
stream inside a larger IFRS 17 / Solvency II projection.
Note
A real BEL under IFRS 17 / Solvency II covers a full portfolio (many policyholders), multiple cash-flow types (benefits, premiums, expenses), multiple decrement causes (mortality, lapse, disability …), and a Risk Adjustment / Risk Margin on top of the pure present value. The example below isolates one benefit stream for a single policy: it demonstrates the decomposition pattern, not a complete BEL calculation.
import numpy as np
from lactuca import LifeTable
lt = LifeTable("PASEM2020_Rel_1o", "m", interest_rate=0.03)
x, n, m = 65, 20, 12
flows = lt.äx(x, n=n, m=m, return_flows=True)
# 240 entries — one per payment (m=12 per year × n=20 years)
t = flows["payment_time"] # t_j = j/12, j = 0, …, 239
v_t = flows["discount_factor"] # v^{t_j}
tpx = flows["survival_probability"] # _{t_j}p_x
g_t = flows["growth"] # 1.0 everywhere (no gr=)
adj = flows["payment_adjustment"] # 1.0 for all full payments
pv_per_pmnt = flows["present_value"] # = tpx * v_t * g_t * adj / m
pv = pv_per_pmnt.sum()
print(f"PV: {pv:.4f}") # PV: 13.2805
# Verify that each element matches its formula component-by-component
reconstructed = tpx * v_t * g_t * adj / m
np.testing.assert_allclose(pv_per_pmnt, reconstructed, rtol=1e-12)
print("Components verified ✓")
summary() — audit snapshots#
Every main Lactuca object implements summary(), returning a human-readable snapshot
of its current state. This is the standard audit header for IFRS 17 and Solvency II
model documentation.
Class |
What |
|---|---|
|
Table name, sex, ω, active modifications, decimals, sample \(q_x\) values |
|
Same header as |
|
Table name, age range, sample incidence/recovery rates |
|
Table name and lapse-rate sample |
|
Rate type (constant / piecewise), rate values, scenario name if multi-scenario |
|
Growth type (geometric / arithmetic), rate values |
|
Builder configuration and included tables |
from lactuca import LifeTable, InterestRate
lt = LifeTable("PASEM2020_Rel_1o", "m")
print(lt.summary())
# LifeTable: PASEM2020_Rel_1er.orden
# Sex: m
# Table: PASEM2020_Rel_1er.orden | Type: life
# Age range: 0-109 | Valid sexes: f, m
# Generational: False
# w (current): 109
# Modified: False
# Decimals: qx=15
# Sample qx values (first 5):
# qx(0) = 0.0020038
# ...
# Sample qx values (last 5):
# ...
# qx(109) = 1.0
ir = InterestRate(0.03)
print(ir.summary())
# InterestRate: constant rate = 0.030000
See also#
Calculation Modes — which mode to choose for regulatory calculations
Prospective Reserves and the ts Parameter — the
ts=parameter and mid-year reserve calculationsActuarial Formulas — mathematical definitions of each quantity
Numerical Precision — float64 precision policy for the arrays returned