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'.valuesholds computed results, shape(N,); invalid records arenp.nan.errorsis aBatchErrorReport;bool(errors)isTruewhen 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,);Truewhere 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).Nonewhen norecord_idswere provided.messages (list[str]) – Human-readable validation texts in the same format as the
on_error='raise'ValueErrormessages. Each entry describes one violated constraint (one batch validation rule), not one invalid record.len(messages)is therefore not required to equaln_errors; useinvalid_indicesandvalid_maskfor per-record positions.
Notes
bool(report)evaluates toTruewhen there are errors.to_dataframe()requirespolars; it is imported lazily.When subgroup reports are merged, per-index
messagesentries may be sparse; indices beyondlen(messages) - 1receive 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.0is rejected; boolean values raiseTypeError). Common values: 1 (annual), 12 (monthly).m=14is 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, withm=12(monthly),[3, 6, 9, 12]selects March, June, September, and December. All values must be integer-valued in the range[1, m]. Pythonint, NumPy integer scalars (e.g.np.int64(7)), lists, tuples, NumPy integer arrays (e.g.np.array([1, 7], dtype=np.int64)),pandas.Series, andpolars.Seriesare all accepted. Default isNone, which selects all periods from 1 tom— equivalent to passinglist(range(1, m + 1)). An empty sequence raisesValueError.
- 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,)whereNdepends onn,m, and the number of selected periods.- Return type:
NDArray[np.float64]
- Raises:
TypeError – If
normis a boolean, if any argument has an invalid numeric type, or ifselected_periodscontains non-integer values (e.g. floats).ValueError – If
nis negative, ifmis not in the set of valid payment frequencies, ifselected_periodsis an empty sequence, or ifselected_periodsis notNoneand any value is not an integer in[1, m].
See also
tiered_amountsAssign tiered cashflow amounts to payment times.
Notes
Fractional Years: When
nis not an integer, the function includes only those payment periods from the final fractional year that have payment time at mostn. For example, withn=2.7andm=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 because2.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
valuesaccording to which tier it falls in. Tier boundaries are inclusive on the right (t <= breakpoints[i]: tieri).- 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, orpolars.Series). Must contain finite values. Boolean values raiseTypeError.breakpoints (array-like of float) – Sorted tier boundaries (in years). Must be strictly ascending and contain finite values.
len(breakpoints)must equallen(values) - 1. Accepts the same 1-D array-like types astimes. Boolean values raiseTypeError.values (array-like of float) – Cashflow amounts for each tier.
len(values)must equallen(breakpoints) + 1. Must contain finite values. Accepts the same 1-D array-like types astimes. Boolean values raiseTypeError.
- Returns:
Array of shape
(len(times),)and dtypefloat64with 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, ifbreakpointsis not strictly ascending, or if a scalar input is not finite.
See also
payment_timesGenerate payment times for tiered schedules.
Notes
Tier assignment uses
numpy.searchsorted()withside='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.])