TableBuilder#

TableBuilder provides a fluent interface for constructing custom actuarial tables programmatically from raw decrement arrays and saving them as .ltk files with automatic metadata validation and integrity checking.

Supported decrement types:

Array

Symbol

Table type

qx_m / qx_f / qx_u

\(q_x\)

Mortality (LifeTable)

ix_m / ix_f / ix_u

\(i_x\)

Disability inception rate (DisabilityTable)

ox_m / ox_f / ox_u

\(o_x\)

Exit / turnover rate (ExitTable)

Generational tables additionally require improvement rate columns (mi_m, mi_f, and/or mi_u).

See also

Building Custom Table Files (.ltk) — Step-by-step guide to building custom tables.
Table Taxonomy — Overview of table types and column conventions.

class lactuca.TableBuilder(data: dict | DataFrame | DataFrame, *, table_name: str, table_type: Literal['life', 'disability', 'exit'], generational: object = False, base_year: object = None, decrement_scale_factor: object = 1, mi_scale_factor: object = 1, omega: object = None, start_age: object = None, sex_independent: object = False, generational_formula_type: Literal['exponential_improvement', 'linear_improvement', 'discrete_improvement', 'projected_improvement'] | None = None, description: str = '', select: object = False, select_period: object = None, grid_years: list[int] | tuple[int, ...] | NDArray[int64] | None = None, validate_strict: object = True)#

Bases: object

Build, validate, scale and persist actuarial tables.

The TableBuilder class provides deterministic validation, canonical serialization and integrity hashing for actuarial tables (life, disability, exit). It accepts input as a dict, pandas.DataFrame or polars.DataFrame and favors vectorized, float64 computations for numerical stability and speed.

Parameters:
  • data (Union[dict, pl.DataFrame, pd.DataFrame]) – Table data as a dict (keys=column names, values=list/array), Pandas DataFrame, or Polars DataFrame.

  • table_name (str) – Short name identifier for the table (e.g., “PERM2000P”, “DAV2004R”).

  • table_type (TableTypeLiteral) – Category of the actuarial table. Accepted values: "life", "disability", or "exit" (see VALID_TABLE_TYPES).

  • generational (bool, optional) – Whether the table includes generational improvement factors (default: False). Must be a strict bool (e.g. generational=1 raises TypeError).

  • base_year (Union[int, None], optional) – Base (reference) year for generational tables. Required when generational=True; must be None otherwise.

  • decrement_scale_factor (int, optional) – Public scale factor for decrement columns (qx, ix, etc.). Default: 1 (no scaling). Must be a power of 10 (1, 10, 100, 1000, …).

  • mi_scale_factor (int, optional) – Power-of-10 public scale factor for MI (mortality improvement) columns. Default: 1 (no scaling). Must be a power of 10 (1, 10, 100, 1000, …).

  • omega (Union[int, None], optional) – Limiting age (\(\omega\)), the last age index in the table. For life tables, \(q_{\omega}\) must equal 1.0 (enforced in validate() and at load). Exit and disability tables may have rates below 1 at \(\omega\). When None, inferred from the data.

  • start_age (Union[int, None], optional) – First age represented in the data. Defaults to 0 when None. Must be a non-negative integer ≤ omega.

  • sex_independent (bool, optional) – Whether the table is sex-independent/unisex (default: False). Must be a strict bool.

  • generational_formula_type (Union[GenerationalFormulaTypeLiteral, None], optional) – Improvement formula type for generational tables. One of "exponential_improvement", "linear_improvement", "discrete_improvement", or "projected_improvement". Required when generational=True.

  • description (str, optional) – Human-readable description of the table (default: “”).

  • select (bool, optional) – Whether to build a select-ultimate table (default: False). When True, select_period must also be provided. Must be a strict bool.

  • select_period (Union[int, None], optional) – Count of numbered select-duration columns per sex. Duration columns are named _s{sd} through _s{sd+N-1} plus _ult, where N = select_period and sd is the start duration (auto-detected from the minimum integer key present: 0 for CMI/UK-style tables such as AM92/AF92, 1 for most others). Must be ≥ 1. Required when select=True.

  • grid_years (list[int] or None, optional) – Calendar years present in a year-indexed MI grid (mi_m_YYYY / mi_f_YYYY columns). When None (default), inferred automatically from column names.

Notes

Deterministic on-disk format:

Saved files include a SHA-256 integrity fingerprint computed over a canonical representation of data and metadata, so identical logical content always produces the same hash across platforms and Python versions.

Uniform internal representation:

Regardless of the input type (polars.DataFrame, pandas.DataFrame, or dict), all data is converted to and stored as a polars.DataFrame after initialization, enabling uniform vectorized hashing and serialization for all input forms.

Metadata and column-order preservation:

The metadata "columns" field records the explicit column order used when saving so loaders can restore the exact same order on load.

Auditable scaling:

Public scale factors (decrement_scale_factor, mi_scale_factor) are recorded in applied_* metadata fields when applied; get_unscaled_data() reverses applied scaling without mutating the instance.

See also

read_table

Read a .ltk file and return a (DataFrame, metadata) pair.

TableBuilder.from_file

Load a TableBuilder instance from a .ltk file.

TableBuilder.from_payload

Reconstruct a TableBuilder from a payload dict.

Examples

Create and save from a Polars DataFrame:

>>> import polars as pl
>>> from lactuca import TableBuilder, read_table
>>> df = pl.DataFrame({"qx_m": [0.01, 0.02]})
>>> tb = TableBuilder(df, table_name="mortality", table_type="life", omega=1)
>>> tb.validate()
True
>>> tb.save(file_name="mortality.ltk", path="/path/to/tables", overwrite=True)

Load a table from disk:

>>> tb2 = TableBuilder.from_file("mortality.ltk", path="/path/to/tables", strict=True)

Read payload without strict integrity enforcement:

>>> df_loaded, meta = read_table("mortality.ltk", path="/path/to/tables", validate=False)

Work with unscaled (original) values:

>>> original = tb.get_unscaled_data()

Quick summary for logging:

>>> print(tb.summary())
classmethod from_file(file_name: str | PathLike[str], path: str | Path | None = None, strict: bool = True) TableBuilder#

Load a TableBuilder instance from a .ltk file on disk.

When strict=True, integrity or metadata problems raise ValueError; when strict=False, best-effort recovery is attempted and warnings are emitted instead of exceptions.

Parameters:
  • file_name (str or os.PathLike) – Filename (may include or omit the .ltk extension). Accepts pathlib.Path / pathlib.WindowsPath.

  • path (Union[str, Path, None], optional) – Directory to look for the file; when None the configured tables_path is used.

  • strict (bool, optional) – When True integrity/metadata problems raise ValueError; when False best-effort recovery is attempted.

Returns:

Initialized instance with data (polars.DataFrame) and metadata restored.

Return type:

TableBuilder

Raises:
  • FileNotFoundError – If the file does not exist at the resolved path.

  • ValueError – If the file does not have a valid .ltk signature; if a parsing or integrity (hash mismatch) error occurs when strict=True; if critical metadata fields (table_name, table_type, omega) are missing or have invalid values; if generational_formula_type is not a recognised literal; or if omega or start_age are not non-negative integers.

See also

TableBuilder.save

Persist a TableBuilder instance to a .ltk file.

read_table

Read a .ltk file and return a (DataFrame, metadata) pair.

TableBuilder.from_payload

Reconstruct a TableBuilder instance from a payload dict.

Examples

Load a table in strict mode (raises on integrity errors):

>>> from lactuca import TableBuilder
>>> tb = TableBuilder.from_file("PERM2000P.ltk", strict=True)

Load a table with best-effort recovery:

>>> from lactuca import TableBuilder
>>> tb = TableBuilder.from_file("PERM2000P.ltk", strict=False)
classmethod from_payload(payload: dict, *, strict: bool = True) TableBuilder#

Reconstruct a TableBuilder from a payload dict.

Accepts any dict with a "data" key — including a dict obtained by deserializing a .ltk file or a manually constructed payload. Column names are lower-cased, year-keyed MI sub-dicts and select-duration sub-dicts are expanded into flat columns, and all remaining keys are treated as metadata fields forwarded to __init__() for validation.

Parameters:
  • payload (dict) – A dict containing at least a "data" key whose value is a column dict, a list of row dicts, or a pandas.DataFrame / polars.DataFrame. All remaining keys are treated as metadata fields (table_name, table_type, generational, base_year, omega, start_age, select, select_period, grid_years, decrement_scale_factor, mi_scale_factor, sex_independent, generational_formula_type, description).

  • strict (bool, optional) – When True (default), validate() runs in strict mode during construction (any issue raises ValueError). When False, validation issues emit UserWarning and construction still returns the instance.

Returns:

A fully initialized instance with data restored as a polars.DataFrame and all metadata attributes set.

Return type:

TableBuilder

Raises:
  • TypeError – If payload is not a dict, or if payload["data"] has a type that cannot be converted to a polars.DataFrame.

  • ValueError – If payload does not contain a "data" key; if nested column dicts remain after all expansion steps (indicates mismatched select / select_period metadata); or if the resulting polars.DataFrame cannot be constructed from the data.

See also

TableBuilder.from_file

Load a TableBuilder directly from a .ltk file on disk.

TableBuilder.save

Persist a TableBuilder to a .ltk file.

read_table

Low-level reader returning (DataFrame, metadata) pair.

Examples

Round-trip via payload dict:

>>> import polars as pl
>>> from lactuca import TableBuilder
>>> df = pl.DataFrame({"qx_m": [0.001, 0.002, 0.003]})
>>> tb = TableBuilder(df, table_name="TestLife", table_type="life", omega=2)
>>> payload = {"data": df, "table_name": "TestLife", "table_type": "life", "omega": 2}
>>> tb2 = TableBuilder.from_payload(payload)
get_unscaled_data() DataFrame#

Return the table data with applied public scale factors reversed.

Any previously applied decrement and MI (mortality improvement) scale factors are undone so the values reflect the original (unscaled) rates.

The TableBuilder instance is not modified.

Returns:

The table data as a polars.DataFrame with decrement and MI columns multiplied by the corresponding applied scale factors to restore original values. When no scaling was applied, a clone() of self.data is returned so callers cannot mutate the instance.

Return type:

polars.DataFrame

See also

TableBuilder.save

Persist table with applied scale factors.

TableBuilder

Main class documenting scale factor behavior.

Examples

Get original unscaled data:

>>> from lactuca import TableBuilder
>>> tb = TableBuilder.from_file("PERM2000P.ltk")
>>> original_df = tb.get_unscaled_data()
>>> print(original_df["qx_m"])
head(n: int = 10, include_age: bool = True, show_normalized: bool = False) DataFrame#

Return the first n rows of meaningful table data, optionally with age column.

By default, excludes normalized rows. Use show_normalized=True to include padding.

Parameters:
  • n (int, default 10) – Number of rows to return.

  • include_age (bool, default True) – If True, prepend an ‘age’ column.

  • show_normalized (bool, default False) – If True, start from age 0 (including normalized padding). If False (default), start from start_age (meaningful data only).

Returns:

The first n rows. If include_age=True, includes an ‘age’ column.

Return type:

polars.DataFrame

See also

TableBuilder.tail

View last n rows.

TableBuilder.view_data

View full table data.

Examples

View first 5 meaningful rows (default):

>>> from lactuca import TableBuilder
>>> tb = TableBuilder.from_file("GAM71.ltk")
>>> print(tb.head(5))
shape: (5, 3)
┌─────┬──────────┬──────────┐
│ age │ qx_f     │ qx_m     │
├─────┼──────────┼──────────┤
│ 5   │ 0.000234 │ 0.000456 │
│ 6   │ 0.000193 │ 0.000424 │
│ 7   │ 0.000162 │ 0.000403 │
│ 8   │ 0.000143 │ 0.000392 │
│ 9   │ 0.000132 │ 0.000391 │
└─────┴──────────┴──────────┘
save(file_name: str | PathLike[str] | None = None, path: str | Path | None = None, overwrite: bool = False) None#

Persist the TableBuilder instance to a .ltk file atomically with an integrity hash.

Parameters:
  • file_name (str, os.PathLike, or None, optional) – Target filename. If None, the table_name attribute is used. Accepts pathlib.Path / pathlib.WindowsPath. Must be a valid filename (no path separators or invalid characters).

  • path (str, path-like, or None, optional) – Directory in which to save the file. Accepts pathlib.Path. If None the configured Config.tables_path is used. The resolved directory must be inside the configured tables path.

  • overwrite (bool, optional) – When False (default) an existing file with different content causes FileExistsError. When True, existing files (including malformed .ltk files) will be replaced.

Return type:

None

Raises:
  • ValueError – If the table fails pre-save validation (see validate()); if the provided filename is invalid; or if an existing file has an invalid TableBuilder signature and overwrite is False.

  • FileExistsError – If a file already exists with different content and overwrite is False.

  • OSError – For unexpected filesystem errors during atomic write/replace.

Notes

  • Each .ltk file contains a deterministic SHA-256 integrity hash computed over the payload data and metadata.

  • The sha256 field covers all metadata (excluding the hash itself) and the data, ensuring reproducible integrity checks across platforms.

  • The save is performed atomically to prevent partial writes.

  • A UserWarning is emitted after each completed write ("Table saved to <path>"); non-fatal conditions (file already up-to-date, replacing malformed file when overwrite=True) are also reported via UserWarning.

See also

TableBuilder.from_file

Load a TableBuilder instance from a .ltk file.

read_table

Read a .ltk file and return a (DataFrame, metadata) pair.

TableBuilder.validate

Validate the table before saving.

Examples

Save with default filename (table_name):

>>> from lactuca import TableBuilder
>>> import polars as pl
>>> df = pl.DataFrame({"qx_m": [0.01, 0.02]})
>>> tb = TableBuilder(df, table_name="Test", table_type="life", omega=1)
>>> tb.save(path="/path/to/tables")

Save with explicit filename and allow overwrite:

>>> tb.save(file_name="custom_name.ltk", path="/path/to/tables", overwrite=True)
summary() str#

Return a human-readable, multi-line summary of the table metadata and key statistics.

The summary includes:

  • Table label (type and name)

  • Valid sexes

  • Age range (start_ageomega) with explicit start_age and omega values

  • Whether the table is sex-independent (only when sex_independent=True)

  • Applied decrement scale factor (with brief explanation when different from 1); for generational tables, also the MI (mortality improvement) scale factor

  • Whether the table is generational and, if so, base_year and generational formula type

  • Optional description when present

Returns:

A multi-line string intended for display in logs, REPLs or reports. The method does not modify the TableBuilder instance.

Return type:

str

See also

TableBuilder.view_data

View the underlying table data as a DataFrame.

Examples

Display detailed table summary:

>>> from lactuca import TableBuilder
>>> tb = TableBuilder.from_file("PERM2000P.ltk")
>>> print(tb.summary())
Life Table: PERM2000P
Valid sexes: m, f
Age range: 0–120 (start_age=0, omega=120)
...
tail(n: int = 10, include_age: bool = True, show_normalized: bool = False) DataFrame#

Return the last n rows of the table data, optionally with age column.

Always shows rows ending at omega. Use show_normalized parameter to control which range is considered.

Parameters:
  • n (int, default 10) – Number of rows to return from the end of the table.

  • include_age (bool, default True) – If True, prepend an ‘age’ column showing ages ending at omega.

  • show_normalized (bool, default False) – If True, consider full normalized range (0 to omega). If False (default), consider only meaningful range (start_age to omega).

Returns:

The last n rows. If include_age=True, includes an ‘age’ column.

Return type:

polars.DataFrame

See also

TableBuilder.head

View first n rows.

TableBuilder.view_data

View full table data.

Examples

View last 5 rows (default, always from meaningful data):

>>> from lactuca import TableBuilder
>>> tb = TableBuilder.from_file("GAM71.ltk")
>>> print(tb.tail(5))
shape: (5, 3)
┌─────┬──────────┬──────────┐
│ age │ qx_f     │ qx_m     │
├─────┼──────────┼──────────┤
│ 106 │ 0.716944 │ 0.640345 │
│ 107 │ 0.760237 │ 0.695679 │
│ 108 │ 0.80763  │ 0.757522 │
│ 109 │ 0.860909 │ 0.806309 │
│ 110 │ 0.999999 │ 0.999999 │
└─────┴──────────┴──────────┘
validate(strict: bool = True) bool#

Validate the TableBuilder instance for correctness and consistency.

Parameters:

strict (bool, optional) – If True (default) any validation failure raises a ValueError. If False, validation issues are reported via UserWarning and the method returns False rather than raising.

Returns:

True when all validation checks pass. If strict is False and issues are found, returns False.

Return type:

bool

Raises:

ValueError – Raised when strict is True and a validation check fails.

Notes

The validation covers:

  • No null values in columns.

  • Column naming conventions for the configured table_type.

  • Value ranges for decrement columns (must be in \([0, 1]\)) and MI columns (must be non-negative).

  • Consistency requirements for generational tables (base_year, generational_formula_type, MI columns present for each valid sex).

  • Scale factors that are positive, non-zero, and a power of 10 (1, 10, 100, …).

Generational tables and \(q_x\) validity

For generational tables (generational=True) the validator additionally evaluates the configured improvement formula to verify that generated decrement probabilities remain finite and within the unit interval \([0, 1]\).

For cohort-based formulas (exponential_improvement, linear_improvement, discrete_improvement), representative cohorts (base year, base year + 100, base year + 250, base year + 500) are tested for each sex. Cohorts strictly before the base year are skipped because all three formulas clamp improvement to zero for those ages, leaving base rates unchanged — these are already verified by the decrement range check above. For projected_improvement, a single last-grid-year stress check is used instead (no cohort-based iteration).

In strict mode (strict=True) any out-of-range or non-finite value raises ValueError. In non-strict mode a UserWarning is emitted and the method returns False.

Use this method before saving or publishing tables to ensure the table follows project conventions and actuarial expectations.

See also

TableBuilder.save

Persist validated table to disk.

TableBuilder

Main class for building and validating actuarial tables.

Examples

Validate in strict mode (raises on errors):

>>> import polars as pl
>>> from lactuca import TableBuilder
>>> df = pl.DataFrame({"qx_m": [0.01, 0.02]})
>>> tb = TableBuilder(df, table_name="Test", table_type="life", omega=1)
>>> tb.validate(strict=True)
True

Validate with warnings instead of exceptions:

>>> is_valid = tb.validate(strict=False)
>>> if not is_valid:
...     print("Table has validation issues (see warnings)")
view_data(include_age: bool = True, show_normalized: bool = False) DataFrame#

Return a copy of the table data for viewing, optionally with age column.

By default, excludes zero-padded rows for ages below start_age. Use show_normalized=True to see all rows.

Parameters:
  • include_age (bool, default True) – If True, prepend an ‘age’ column showing ages.

  • show_normalized (bool, default False) – If True, show all rows including normalized padding (ages 0 to start_age-1). If False (default), show only meaningful data (ages start_age to omega).

Returns:

Table data. If include_age=True, includes an ‘age’ column as the first column.

Return type:

polars.DataFrame

See also

TableBuilder.head

View first n rows with optional age column.

TableBuilder.tail

View last n rows with optional age column.

TableBuilder.start_age

Get the starting age from original table definition.

Examples

View meaningful data only (default, excludes normalized padding):

>>> from lactuca import TableBuilder
>>> tb = TableBuilder.from_file("GAM71.ltk")
>>> df = tb.view_data()
>>> print(df)
shape: (106, 3)
┌─────┬──────────┬──────────┐
│ age │ qx_f     │ qx_m     │
├─────┼──────────┼──────────┤
│ 5   │ 0.000234 │ 0.000456 │
│ 6   │ 0.000193 │ 0.000424 │
│ ... │ ...      │ ...      │
└─────┴──────────┴──────────┘

View all rows including normalized padding:

>>> df_all = tb.view_data(show_normalized=True)
>>> print(df_all.head(6))
shape: (6, 3)
┌─────┬──────────┬──────────┐
│ age │ qx_f     │ qx_m     │
├─────┼──────────┼──────────┤
│ 0   │ 0.0      │ 0.0      │
│ 1   │ 0.0      │ 0.0      │
│ 2   │ 0.0      │ 0.0      │
│ 3   │ 0.0      │ 0.0      │
│ 4   │ 0.0      │ 0.0      │
│ 5   │ 0.000234 │ 0.000456 │
└─────┴──────────┴──────────┘
property start_age: int#

First age for which the table contains meaningful data.

Returns the starting age as originally defined, regardless of the internal data layout. For example, GAM71 starts at age 5 and GAM94 starts at age 1; view_data() and head() default to showing data from this age onward.

Returns:

First meaningful age in the table (0 when the table covers age 0).

Return type:

int

See also

TableBuilder.view_data

By default shows rows from start_age to omega.

TableBuilder.head

View the first n rows from start_age.

Examples

>>> from lactuca import TableBuilder
>>> tb = TableBuilder({"qx_m": [0.01, 0.02]}, table_name="Test",
...                   table_type="life", omega=6, start_age=5)
>>> tb.start_age
5
property tables_path: str#

Return the absolute path to the configured tables directory.

The value is taken from the global Config singleton. Treat this property as read-only.

Returns:

Absolute path to the directory where actuarial table files are stored.

Return type:

str

See also

Config.tables_path

Underlying configuration setting.

Examples

>>> from lactuca import TableBuilder
>>> tb = TableBuilder.from_file("GAM71.ltk")
>>> print(tb.tables_path)
/absolute/path/to/actuarial_tables

read_table#

Lightweight function for loading a .ltk file without building a full TableBuilder instance. Returns a (pl.DataFrame, dict) pair — a Polars DataFrame with the table data and a dict of normalised metadata — and supports both strict integrity-checking mode and best-effort recovery mode.

lactuca.read_table(file_name: str | PathLike[str], path: str | Path | None = None, validate: bool = False) tuple[DataFrame, dict]#

Read a .ltk file and return a (DataFrame, metadata) pair.

This function loads a TableBuilder .ltk file produced by TableBuilder.save(). Behavior depends on the validate flag:

  • validate=True (strict mode):

    • The file must exist and have a valid .ltk signature; otherwise ValueError is raised.

    • Parsing, structural, or SHA-256 integrity errors raise ValueError.

    • Returns a Polars DataFrame (scaled per metadata) and a normalized metadata dict (string values, lowercase keys).

  • validate=False (non-strict mode):

    • Invalid file signatures still raise ValueError.

    • Non-critical parsing or integrity issues emit UserWarning and may return an empty DataFrame with minimal metadata.

    • Hash mismatches or a missing sha256 metadata field emit UserWarning but do not raise.

    • Metadata normalization and legacy scaling are applied where possible.

Parameters:
  • file_name (str or os.PathLike) – Name of the .ltk file (may omit extension; must not include path separators). Accepts pathlib.Path / pathlib.WindowsPath.

  • path (str, path-like, or None, optional) – Directory in which to look for the file. Accepts pathlib.Path. If None, the configured Config.tables_path is used.

  • validate (bool, optional) – When True, integrity and parsing errors raise ValueError. When False, the function attempts best-effort recovery and emits warnings instead of raising for non-critical errors; missing table_name or table_type in metadata always raise regardless.

Returns:

A tuple (df, metadata) where df is a polars.DataFrame (scaled according to metadata) and metadata is a normalized dict with string values and lowercase keys.

Return type:

tuple[pl.DataFrame, dict]

Raises:
  • TypeError – If file_name is not a string.

  • ValueError – If file extension is invalid, signature invalid, critical metadata fields are missing, or (when validate=True) parsing/hash/metadata errors occur.

  • FileNotFoundError – If the specified file does not exist at the resolved path.

Warns:

UserWarning – When validate=False, emitted on hash mismatches, missing SHA-256, or parsing errors.

Notes

  • Invalid file signatures raise ValueError before parsing begins.

  • When validate=True, SHA-256 integrity mismatches raise ValueError.

  • When validate=False, non-critical integrity or parsing issues emit UserWarning and may return an empty DataFrame with minimal metadata.

  • Returned metadata keys are normalized to lowercase strings.

See also

TableBuilder.save

Save a table as a .ltk file with integrity hashing.

TableBuilder.from_file

Load a TableBuilder instance from a .ltk file.

TableBuilder

Main class for building and validating actuarial tables.

Examples

Read a table in strict mode (raises on errors):

>>> from lactuca import read_table
>>> df, meta = read_table("PERM2000P.ltk", validate=True)
>>> print(meta["table_name"])
PERM2000P

Read a table in recovery mode (warns on errors):

>>> from lactuca import read_table
>>> df, meta = read_table("PERM2000P.ltk", validate=False)
>>> if df.is_empty():
...     print("Failed to read table")