prospicio-math 0.0.4

Numerical engine: special functions, small dense linear algebra, root finding, quadrature, optimization.
Documentation

prospicio

prospicio is a library for actuarial and risk models. The core is in Rust. The Python and R packages call the same Rust code, so all three languages give the same numbers.

What prospicio does

  • Reserving. Make loss triangles. Estimate reserves with Chain Ladder, Mack, Bornhuetter–Ferguson, Cape Cod, Clark, the ODP bootstrap and the Bayesian chain ladder. Calculate the one-year view for capital.
  • Aggregate losses and reinsurance. Combine a claim count and a claim size into an aggregate loss distribution. Apply layers, towers, reinstatements and other treaty terms.
  • Pricing. Price a layer or a tower with a risk load. Calculate credibility and the overall rate indication.
  • Distributions. Use parametric distributions, copulas and extreme value methods. Save a distribution as JSON and load it again.
  • Models. Fit GLMs, GAMs, elastic nets, neural networks and Bayesian models through one model interface.

The tests in validation/ compare the results with reference implementations, for example R ChainLadder, chainladder-python, actuar, SciPy and statsmodels.

How the crates work together

The prospicio crates in three layers: the packages, the actuarial methods and the foundation

An arrow means "uses". All arrows point down, so the crates have no dependency cycles.

Foundation

  • prospicio-core holds the error type, the random-number streams and the calendar periods. All crates use it.
  • prospicio-math is the numerical engine: special functions, linear algebra, splines and root finding.
  • prospicio-prob holds the distributions and PredictiveDistribution. The method crates read and write these types, so the result of one crate can go into another crate.

Actuarial methods

  • prospicio-aggregate builds aggregate loss distributions and reinsurance towers.
  • prospicio-pricing prices treaties from the aggregate models. It also holds credibility and ratemaking.
  • prospicio-models defines the interface that every model engine uses.
  • prospicio-glm fits GLMs, GAMs and elastic nets through that interface.
  • prospicio-bayes samples Bayesian models with NUTS and checks the draws.
  • prospicio-nn trains neural networks with the Burn library.
  • prospicio-reserving holds the Triangle and the reserving methods. It uses glm for the ODP GLM. It uses bayes only when you turn on the bayes feature.

Packages

  • prospicio is the Rust umbrella crate. It re-exports the other crates. The bayes, nn and arrow features are optional.
  • prospicio-python (with python/) and prospicio-r (with R/prospicio) wrap the Rust code for Python and R. Neither package includes nn at this time.

Example: the price of a reinsurance layer

  1. prob supplies a claim size distribution and a claim count distribution.
  2. aggregate combines them into the aggregate loss distribution of the layer.
  3. pricing adds the risk load and gives the price.
  4. The Python or R package returns the result to you.

Where to read more

Path What
docs/guides/ Guides for actuaries, for example how a reinsurer arrives at a price
examples/ Notebooks that use the Python package from start to end
docs/architecture.md The plan for the library
docs/design/ Design notes for the core abstractions
validation/ Reference datasets and the parity tests
knowledge/ Facts collected while building the library

Build and test

Prerequisites

macOS / Linux Windows (PowerShell)
Rust ≥ 1.85 curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh winget install Microsoft.VisualStudio.2022.BuildTools --override "--quiet --wait --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended" then winget install Rustlang.Rustup
uv (Python) curl -LsSf https://astral.sh/uv/install.sh | sh winget install astral-sh.uv
R ≥ 4.2 from CRAN R from CRAN, Rtools matching your R version, then rustup target add x86_64-pc-windows-gnu
Quarto (docs only) from quarto.org winget install Posit.Quarto

Open a new terminal after installing so cargo, uv and R are on PATH.

Rust

All pure-Rust crates and the parity suite:

cargo test

Python (uv)

cd python
uv sync              # creates .venv, installs dev deps, builds the Rust extension
uv run pytest tests
uv run python        # a Python shell with prospicio installed

uv run rebuilds the extension automatically when Rust sources change.

R

Without a checkout, with Rust (cargo, rustc 1.85 or later) installed:

install.packages(c("remotes", "S7"))
remotes::install_github("EKtheSage/prospicio", subdir = "R/prospicio")

remotes builds a source tarball of R/prospicio; the package's bootstrap.R first copies the Rust crates it needs into src/rust/, so the tarball compiles on its own (third-party crates still come from crates.io).

From a checkout, at the repository root (a terminal where cargo --version works). The package's R API is built on S7, so install that first:

Rscript -e 'install.packages("S7")'
R CMD INSTALL R/prospicio
for f in R/prospicio/tests/*.R; do Rscript "$f"; done

Windows PowerShell: R is PowerShell's alias for Invoke-History, so call R.exe CMD INSTALL R\prospicio instead. To put R on PATH for your user (newest installed R; rerun after upgrading R), then open a new window:

$rbin = (Get-ChildItem "C:\Program Files\R" -Directory | Sort-Object Name | Select-Object -Last 1).FullName + "\bin"
[Environment]::SetEnvironmentVariable("Path", [Environment]::GetEnvironmentVariable("Path", "User") + ";$rbin", "User")

On Windows, R builds packages with Rtools' MinGW compiler, so the Rust code is compiled for the x86_64-pc-windows-gnu target (see R/prospicio/src/Makevars.win); the build stops with a message if that target is not installed.

Docs

Building a binding regenerates its docs (see "Documentation" in docs/architecture.md). Use these instead of the plain builds above whenever you change a doc comment or a public API:

Rscript -e 'install.packages(c("S7", "roxygen2", "pkgload", "pkgdown"))'  # once
cargo xtask python   # build + stubs, test, great-docs site in python/great-docs/_site
cargo xtask r        # roxygen2 man/ + NAMESPACE, install, test, pkgdown site in R/prospicio/docs
cargo xtask docs     # both plus rustdoc, collected into target/docs-site

The Python docs need Python 3.11+ (great-docs); cargo xtask python asks uv for one. Commit the regenerated python/prospicio/prospicio_native.pyi, R/prospicio/man/ and R/prospicio/NAMESPACE; never edit them by hand. cargo xtask docs --check fails if they are stale.

The same distribution, driven by the same Rust code, from all three:

import prospicio as ar
d = ar.distributions.Lognormal.from_mean_cv(1000, 0.5)
d.quantile(0.995), d.sample(3, seed=42, stream=0)
library(prospicio)
d <- lognormal_from_mean_cv(1000, 0.5)
quantile(d, 0.995); draws(d, 3, seed = 42, stream = 0)
d@meanlog; d@sdlog   # read-only S7 properties

Errors raised in Rust surface as Python ValueErrors and ordinary R errors.

Reserving

prospicio-reserving holds the four-axis, masked Triangle (docs/design/triangle.md), development factors, ChainLadder, Mack and the ODP bootstrap (OdpBootstrap), whose reserve distribution by origin is a PredictiveDistribution.

use prospicio_reserving::{ChainLadder, DevelopmentColumn, Grain, Long, Mack, Month, Triangle};

let origin = [2020, 2020, 2020, 2020, 2021, 2021, 2021, 2022, 2022, 2023].map(Month::january);
let tri = Triangle::from_long(&Long {
    keys: &[],
    origin: &origin,
    development: DevelopmentColumn::Age(&[12, 24, 36, 48, 12, 24, 36, 12, 24, 12]),
    values: &[(
        "paid",
        &[100.0, 150.0, 165.0, 170.0, 110.0, 170.0, 180.0, 120.0, 175.0, 130.0],
    )],
    origin_grain: Grain::Year,
    development_grain: Grain::Year,
    cumulative: true,
})?;
let cl = ChainLadder::default().fit(&tri, "paid")?;
let mack = Mack::default().fit(&tri, "paid")?;
assert!(cl.total_reserve() > 0.0 && mack.total_standard_error > 0.0);
println!("reserve {} ± {}", cl.total_reserve(), mack.total_standard_error);

Python and R expose the same objects:

from prospicio.reserving import Mack, OdpBootstrap, Triangle
tri = Triangle.from_long(origin, development, {"paid": paid})
Mack().fit(tri, "paid").total_standard_error
boot = OdpBootstrap(n_sims=10_000, seed=1).fit(tri, "paid")
boot.reserves.quantile(0.995)   # a PredictiveDistribution keyed by origin
tri <- triangle(long, "origin", "development", "paid")
mack(tri)@total_standard_error
boot <- odp_bootstrap(tri, n_sims = 10000, seed = 1)
quantile(boot@reserves, 0.995)  # a predictive_distribution keyed by origin

Segments are named by key columns. keys (Rust Long::keys, Python keys=, R keys =) lists them, and the long table comes back with them by name:

tri = Triangle.from_frame(df, "year", "age", ["paid", "incurred"], keys=["lob", "state"])
tri.keys, tri.index   # ['lob', 'state'], [('Auto', 'CA'), ...]
tri.to_frame()        # columns lob, state, origin, development, paid, incurred
tri <- triangle(df, "year", "age", c("paid", "incurred"), keys = c("lob", "state"))
tri@keys; tri@index   # a data.frame with columns lob and state
as.data.frame(tri)    # columns lob, state, origin, development, paid, incurred

Select segments and columns by key name, and sum segments over keys:

let auto = tri.select(&[("lob", &["Auto"]), ("state", &["CA", "NY"])])?;
let paid = tri.select_columns(&["paid"])?;
let by_lob = tri.group_by(&["lob"])?;   // sums states; group_by(&[]) is the total
tri.select(lob="Auto", state=["CA", "NY"], columns="paid")
ChainLadder().fit(tri.group_by(["lob"]).select(lob="Auto"), "paid")
subset(tri, lob = "Auto", state = c("CA", "NY"), columns = "paid")
chain_ladder(subset(aggregate(tri, keep = "lob"), lob = "Auto"), "paid")

R uses base generics (subset(), aggregate() with keep, as for a predictive_distribution) so it does not mask dplyr's filter(), select() or group_by().

Methods fit every segment at once, each on its own, and give long results with the key columns by name: one row per segment and origin, per segment (totals), or per segment and age (development factors). The bootstrap gives one joint PredictiveDistribution with the keys and origin as dimensions, so aggregating over origins keeps the dependence between segments:

let fits = Mack::default().fit_segments(&tri, "paid")?;
let by_origin = fits.to_long();          // keys, origin, latest, ..., standard_error
let totals = fits.totals();              // one row per segment
let factors = fits.development_table();  // keys, age, ldf, cdf, sigma, std_err
let auto_ca = fits.segment(&[("lob", "Auto"), ("state", "CA")])?;
let boot = OdpBootstrap::default().fit_segments(&tri, "paid")?;
let by_lob = boot.reserves.aggregate(&["lob"])?;
fit = Mack().fit(tri, "paid")      # any number of segments
fit.to_frame(); fit.totals_frame(); fit.development_frame()
fit.segment(lob="Auto", state="CA").ldf
OdpBootstrap(seed=1).fit(tri, "paid").reserves.aggregate(["lob"])
fit <- mack(tri, "paid")
as.data.frame(fit); totals_frame(fit); development_frame(fit)
segment(fit, lob = "Auto", state = "CA")@ldf
aggregate(odp_bootstrap(tri, "paid", seed = 1)@reserves, keep = "lob")

Per-origin fields (reserve, standard_error, ...) follow the rows of the long table, so a single-segment fit reads as before. Per-age fields (ldf, cdf, sigma, std_err), Mack's total standard errors and the bootstrap's scale, fitted and residuals need one segment; with several they raise an error that points to the frames or segment().

To read a triangle, view one segment and measure as an origin × development table, or summarise every segment and measure. A triangle with one segment and one measure prints as that table; otherwise it prints the summary, one row per segment and measure with its origins, valuation and latest diagonal total. The text comes from Rust, so both bindings print the same thing:

let grid = tri.view(&[("lob", "Auto"), ("state", "CA")], Some("paid"))?;
let rows = tri.summary().rows;   // n_origins, first/last origin, valuation, latest
println!("{tri}");               // the grid or the summary table
tri.view("paid", lob="Auto", state="CA")   # DataFrame: origins x ages
tri.summary()                              # one row per segment and measure
tri                                        # prints the grid or the summary
as.matrix(tri, lob = "Auto", state = "CA", column = "paid")  # origins x ages
summary(tri)                                                  # a data.frame
tri                                                           # same printout

R uses as.matrix() rather than a view() verb, which would mask tibble::view().

Every Chain Ladder and Mack value is checked against R ChainLadder and chainladder-python on RAA, GenIns and ABC in validation/; the ODP bootstrap is checked against R BootChainLadder.

Licence

Licensed under either of Apache License, Version 2.0 or MIT licence, at your option. Copyright (c) 2026 Ethan Kang.