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](docs/images/crates.svg)

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/]docs/guides/ | Guides for actuaries, for example how a reinsurer arrives at a price |
| [examples/]examples/ | Notebooks that use the Python package from start to end |
| [docs/architecture.md]docs/architecture.md | The plan for the library |
| [docs/design/]docs/design/ | Design notes for the core abstractions |
| [validation/]validation/ | Reference datasets and the parity tests |
| [knowledge/]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]https://quarto.org/docs/get-started/ | `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:

```bash
cargo test
```

### Python (uv)

```bash
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:

```r
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](https://rconsortium.github.io/S7/), so
install that first:

```bash
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:

```powershell
$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](docs/architecture.md)). Use these instead of the
plain builds above whenever you change a doc comment or a public API:

```bash
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:

```python
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)
```

```r
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 `ValueError`s and ordinary R errors.

## Reserving

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

```rust
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:

```python
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
```

```r
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:

```python
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
```

```r
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:

```rust
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
```

```python
tri.select(lob="Auto", state=["CA", "NY"], columns="paid")
ChainLadder().fit(tri.group_by(["lob"]).select(lob="Auto"), "paid")
```

```r
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:

```rust
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"])?;
```

```python
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"])
```

```r
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:

```rust
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
```

```python
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
```

```r
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](LICENSE-APACHE) or
[MIT licence](LICENSE-MIT), at your option. Copyright (c) 2026 Ethan Kang.