prospicio
Actuarial and risk modeling on a Rust core, exposed to Python and R.
The plan: docs/architecture.md. Design notes for the core abstractions: docs/design/.
Layout
| Path | What |
|---|---|
crates/prospicio |
Umbrella crate: re-exports the others (prospicio::reserving, ...) |
crates/prospicio-core |
Error type, reproducible RNG streams |
crates/prospicio-math |
Numerical engine (Phase 0: normal special functions) |
crates/prospicio-prob |
Distributions (Phase 0: Distribution trait, Lognormal) |
crates/prospicio-reserving |
Reserving (v0.1 so far: Triangle, Chain Ladder, Mack, ODP bootstrap) |
crates/prospicio-aggregate |
Aggregate loss and reinsurance (empty until v0.3) |
crates/prospicio-python, python/ |
Python package prospicio (PyO3 + maturin) |
crates/prospicio-r, R/prospicio |
R package prospicio (extendr) |
validation/ |
Parity harness: reference datasets and results from SciPy, R ChainLadder and chainladder-python |
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:
Python (uv)
uv run rebuilds the extension automatically when Rust sources change.
R
From the repository root (a terminal where cargo --version works). The
package's R API is built on S7, so
install that first:
for; do ; 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:
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:
=
,
d <-
;
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 ;
let origin = .map;
let tri = from_long?;
let cl = default.fit?;
let mack = default.fit?;
assert!;
println!;
Python and R expose the same objects:
=
.
=
# a PredictiveDistribution keyed by origin
tri <-
@total_standard_error
boot <-
# 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:
=
, # ['lob', 'state'], [('Auto', 'CA'), ...]
# columns lob, state, origin, development, paid, incurred
tri <-
tri@keys; tri@index # a data.frame with columns lob and state
# columns lob, state, origin, development, paid, incurred
Select segments and columns by key name, and sum segments over keys:
let auto = tri.select?;
let paid = tri.select_columns?;
let by_lob = tri.group_by?; // sums states; group_by(&[]) is the total
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 = default.fit_segments?;
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?;
let boot = default.fit_segments?;
let by_lob = boot.reserves.aggregate?;
= # any number of segments
; ;
.
fit <-
; ;
@ldf
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?;
let rows = tri.summary.rows; // n_origins, first/last origin, valuation, latest
println!; // the grid or the summary table
# DataFrame: origins x ages
# one row per segment and measure
# prints the grid or the summary
# origins x ages
# 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.