Utilities#

Helper functions and return types for actuarial calculations that are not tied to a specific table instance.

Batch result types#

BatchResult and BatchErrorReport are the return types produced when on_error='nan' is passed to any batch calculation method. See Batch Calculations for usage examples.

class lactuca.BatchResult(values: NDArray[np.float64], errors: BatchErrorReport)#

Result of a batch actuarial calculation with on_error='nan'.

values holds computed results, shape (N,); invalid records are np.nan. errors is a BatchErrorReport; bool(errors) is True when any record was invalid.

Examples

>>> import numpy as np
>>> from lactuca import LifeTable
>>> lt = LifeTable('PASEM2010', 'm')
>>> lt.interest_rate = 0.03
>>> x_arr = np.array([65.0, -5.0, 70.0])
>>> values, report = lt.ax(x_arr, n=20, on_error='nan')
>>> report.n_errors
1
class lactuca.BatchErrorReport(n_errors: int, n_total: int, valid_mask: NDArray[bool], invalid_indices: NDArray[int64], record_ids: object, messages: list[str])#

Structured error report for batch calculations with on_error='nan'.

Produced by any of the 16 batch-capable actuarial functions when called with on_error='nan'. Contains all the information needed to identify, filter, and diagnose invalid input records.

Parameters:
  • n_errors (int) – Number of invalid records (rows where at least one constraint failed).

  • n_total (int) – Total number of input records.

  • valid_mask (NDArray[np.bool_]) – Boolean array of shape (N,); True where the record is valid.

  • invalid_indices (NDArray[np.int64]) – Integer positions of the invalid records, shape (K,).

  • record_ids (list or None) – User-supplied record identifiers for each invalid index (same order as invalid_indices). None when no record_ids were provided.

  • messages (list[str]) – Human-readable validation texts in the same format as the on_error='raise' ValueError messages. Each entry describes one violated constraint (one batch validation rule), not one invalid record. len(messages) is therefore not required to equal n_errors; use invalid_indices and valid_mask for per-record positions.

Notes

  • bool(report) evaluates to True when there are errors.

  • to_dataframe() requires polars; it is imported lazily.

  • When subgroup reports are merged, per-index messages entries may be sparse; indices beyond len(messages) - 1 receive an empty string.

to_dataframe() Any#

Return a Polars DataFrame with one row per invalid record.

Columns: idx, record_id.

Returns:

DataFrame with invalid record indices and optional user IDs.

Return type:

polars.DataFrame

invalid_indices: NDArray[int64]#
messages: list[str]#
n_errors: int#
n_total: int#
record_ids: list | None#
valid_mask: NDArray[bool]#

Payment schedule helpers#

payment_times() generates the vector of payment times (in years) for a given duration, payment frequency \(m\), and optional subset of periods within each year. It is used internally by all annuity and insurance calculation engines and is also available as a standalone tool for custom cashflow construction.

Use this function directly when you need to inspect or override the default payment grid that LifeTable methods would produce for a given \((n, m)\) combination, or when building Irregular Cashflows.

See also

Irregular Cashflows — Custom cashflow schedules for annuities and insurances.
Last Payment Adjustment — Fractional final payment handling.

lactuca.payment_times(n: object, m: Literal[1, 2, 3, 4, 6, 12, 14, 24, 26, 52, 365], selected_periods: Sequence[int] | None = None) NDArray[float64]#

Generate payment times (in years) for a given duration and payment frequency.

Payment times are computed following the formula \(t = k + p/m\), where \(k\) is the complete year number (0, 1, 2, …) and \(p\) is the selected period within that year.

Parameters:
  • n (float) – Total duration in years. Can be fractional (e.g., 3.5 years). Must be non-negative. Boolean values raise TypeError.

  • m (PaymentFrequencyLiteral) – Number of payment periods per year (payment frequency). Must be one of (1, 2, 3, 4, 6, 12, 14, 24, 26, 52, 365). Must be an integer type (4.0 is rejected; boolean values raise TypeError). Common values: 1 (annual), 12 (monthly). m=14 is accepted as an approximation for the Spanish “14 pagas” scheme.

  • selected_periods (sequence of int or None, optional) – Sequence of payment periods to include within each year. Periods are numbered from 1 to m. For example, with m=12 (monthly), [3, 6, 9, 12] selects March, June, September, and December. All values must be integer-valued in the range [1, m]. Python int, NumPy integer scalars (e.g. np.int64(7)), lists, tuples, NumPy integer arrays (e.g. np.array([1, 7], dtype=np.int64)), pandas.Series, and polars.Series are all accepted. Default is None, which selects all periods from 1 to m — equivalent to passing list(range(1, m + 1)). An empty sequence raises ValueError.

Returns:

Sorted array of payment times in years, with values in [0, n]. All times are deduplicated and sorted in ascending order. Array is always 1-dimensional with shape (N,) where N depends on n, m, and the number of selected periods.

Return type:

NDArray[np.float64]

Raises:
  • TypeError – If n or m is a boolean, if any argument has an invalid numeric type, or if selected_periods contains non-integer values (e.g. floats).

  • ValueError – If n is negative, if m is not in the set of valid payment frequencies, if selected_periods is an empty sequence, or if selected_periods is not None and any value is not an integer in [1, m].

See also

tiered_amounts

Assign tiered cashflow amounts to payment times.

Notes

Fractional Years: When n is not an integer, the function includes only those payment periods from the final fractional year that have payment time at most n. For example, with n=2.7 and m=4 (quarterly), payments at 0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0, 2.25, 2.5 are included, but 2.75 is excluded because 2.75 > 2.7.

Actuarial Context:

  • Annuities with custom payment frequencies (e.g., quarterly annuity payable in specific quarters)

  • Insurance premiums with irregular payment schedules (e.g., premiums only in January and July)

  • Endowment calculations with partial year coverage

Payment times are always expressed in years from time 0, consistent with actuarial notation where time is measured continuously from policy inception.

Examples

Generate all quarterly payments for 5 years (default: all periods):

>>> from lactuca import payment_times
>>> times = payment_times(n=5.0, m=4)
>>> times
array([0.25, 0.5 , 0.75, 1.  , 1.25, 1.5 , 1.75, 2.  , 2.25, 2.5 ,
       2.75, 3.  , 3.25, 3.5 , 3.75, 4.  , 4.25, 4.5 , 4.75, 5.  ])

Generate quarterly payment times over 5 years (explicit all quarters):

>>> times = payment_times(n=5.0, m=4, selected_periods=[1, 2, 3, 4])
>>> times
array([0.25, 0.5 , 0.75, 1.  , 1.25, 1.5 , 1.75, 2.  , 2.25, 2.5 ,
       2.75, 3.  , 3.25, 3.5 , 3.75, 4.  , 4.25, 4.5 , 4.75, 5.  ])

Generate quarterly payments for only March and September (periods 1 and 3 of a quarterly schedule):

>>> times = payment_times(n=3.0, m=4, selected_periods=[1, 3])
>>> times
array([0.25, 0.75, 1.25, 1.75, 2.25, 2.75])

Generate monthly payments for a fractional period (3.5 years, all months):

>>> times = payment_times(n=3.5, m=12, selected_periods=list(range(1, 13)))
>>> len(times)  # 12*3 + 6 = 42 payments
42
>>> times[-3:]  # Last three payments
array([3.33333333, 3.41666667, 3.5       ])

Generate semi-annual payments for only the first half of each year:

>>> times = payment_times(n=2.0, m=2, selected_periods=[1])
>>> times
array([0.5, 1.5])

Handle edge case: fractional year that cuts off some selected periods:

>>> times = payment_times(n=1.4, m=4, selected_periods=[1, 2, 3, 4])
>>> times  # From the final partial year (k=1): only period 1 (t=1.25) qualifies; period 2 would be at t=1.50 > n=1.4
array([0.25, 0.5 , 0.75, 1.  , 1.25])

Tiered cashflow amounts#

tiered_amounts() maps each payment time to a cashflow amount according to a step-up / step-down schedule defined by breakpoints and values. It is the recommended way to build piecewise-constant benefit schedules for use with ax().

See also

Irregular Cashflows — Step-up pension example and further use cases.

lactuca.tiered_amounts(times: object, breakpoints: object, values: object) NDArray[float64]#

Assign a cashflow amount to each payment time based on tier breakpoints.

Implements a step-up / step-down schedule: each payment time is mapped to one of the provided values according to which tier it falls in. Tier boundaries are inclusive on the right (t <= breakpoints[i]: tier i).

Parameters:
  • times (float or array-like of float) – Payment times in years (typically produced by payment_times()). A scalar or 1-D sequence is accepted (list, tuple, numpy.ndarray, pandas.Series, or polars.Series). Must contain finite values. Boolean values raise TypeError.

  • breakpoints (array-like of float) – Sorted tier boundaries (in years). Must be strictly ascending and contain finite values. len(breakpoints) must equal len(values) - 1. Accepts the same 1-D array-like types as times. Boolean values raise TypeError.

  • values (array-like of float) – Cashflow amounts for each tier. len(values) must equal len(breakpoints) + 1. Must contain finite values. Accepts the same 1-D array-like types as times. Boolean values raise TypeError.

Returns:

Array of shape (len(times),) and dtype float64 with the cashflow amount corresponding to each payment time.

Return type:

NDArray[np.float64]

Raises:
  • TypeError – If any argument is a boolean or has an invalid numeric type, or if any array input contains non-finite values.

  • ValueError – If len(values) != len(breakpoints) + 1, if breakpoints is not strictly ascending, or if a scalar input is not finite.

See also

payment_times

Generate payment times for tiered schedules.

Notes

Tier assignment uses numpy.searchsorted() with side='left', which implements the inclusive-right convention:

\[\text{tier}(t) = \#\{i : b_i < t\}\]

so that \(t \leq b_i\) maps to tier \(i\) and \(t > b_i\) maps to tier \(i+1\).

Examples

Three-tier step-up pension (tiers end at years 5, 10, and 15):

>>> from lactuca import payment_times, tiered_amounts
>>> times = payment_times(n=20, m=1)
>>> amounts = tiered_amounts(times, breakpoints=[5, 10, 15], values=[1.00, 1.10, 1.21, 1.331])
>>> amounts[:5]
array([1., 1., 1., 1., 1.])
>>> amounts[5:10]
array([1.1, 1.1, 1.1, 1.1, 1.1])
>>> amounts[10:15]
array([1.21, 1.21, 1.21, 1.21, 1.21])
>>> amounts[15:20]
array([1.331, 1.331, 1.331, 1.331, 1.331])

Two-tier benefit (12,000 for first 10 years, then 8,000):

>>> times2 = payment_times(n=20, m=1)
>>> tiered_amounts(times2, breakpoints=[10], values=[12_000.0, 8_000.0])
array([12000., 12000., 12000., 12000., 12000., 12000., 12000., 12000.,
       12000., 12000.,  8000.,  8000.,  8000.,  8000.,  8000.,  8000.,
        8000.,  8000.,  8000.,  8000.])