reservingmodels

Claims development and stochastic reserve estimation: chain-ladder and the deterministic methods re-exported from actuarialpy, Mack’s analytic standard errors, and the over-dispersed-Poisson bootstrap of the full predictive reserve distribution, with residual diagnostics.

The deterministic triangle engine — chain-ladder development, completion factors, Bornhuetter–Ferguson / Benktander / Cape Cod, and Mack standard errors — lives in actuarialpy.reserving, where those primitives already sit because actuarialpy.Experience.complete() and the projection and pricing packages build on them. reservingmodels re-exports them as aliases (reservingmodels.ChainLadder is actuarialpy.ChainLadder) so a reserving analyst has one import for the whole workflow, and adds the stochastic layer on top. It depends on actuarialpy directly for that reason; it composes with risksim through the .sample() protocol, with no dependency in either direction.

Empirical tail measures follow the ecosystem VaR/TVaR estimators, and every simulation accepts the shared rng argument (None, a seed, or a Generator) — see Conventions.

The full features run end to end, with every number shown, in Example 11: the reserve, with a distribution.

Triangles and the deterministic engine

A triangle is a square K × K cumulative development array — origins on the index, developments 0 K-1 on the columns, the unobserved lower-right as NaN. The deterministic methods are the re-exported actuarialpy primitives:

import reservingmodels as rv

triangle = rv.datasets.taylor_ashe()      # canonical benchmark; reserve 18,680,856

cl = rv.ChainLadder.fit(triangle)         # volume-weighted development
cl.project(triangle)                      # per-origin ultimate and IBNR
cl.mack_standard_errors(triangle)         # Mack (1993) distribution-free SE

develop_ultimate applies Bornhuetter–Ferguson, Benktander, or Cape Cod with a supplied a-priori; completion_factors / apply_completion cover the health completion-factor (lag) workflow; make_completion_triangle builds a triangle from transaction-grain claims. Because these are aliases, a pattern fitted through reservingmodels is the same object actuarialpy and the other packages use. Their full reference lives on the actuarialpy page; the API section below documents only what reservingmodels adds.

The bootstrap: a predictive distribution

The chain ladder gives one number; the bootstrap gives its distribution. BootstrapODP implements the semiparametric over-dispersed-Poisson bootstrap of England & Verrall. Incremental claims follow an ODP with mean \(m_{ij}=x_i y_j\) and \(\mathrm{Var}[C_{ij}]=\phi\,m_{ij}\); the MLE of \(m\) equals the volume-weighted chain-ladder fitted incrementals, so this is the stochastic layer on top of the chain ladder — its mean reproduces the chain-ladder reserve. Each replicate resamples the degrees-of-freedom-adjusted Pearson residuals into a pseudo-triangle (estimation error), refits, and draws each future cell from \(\mathrm{Gamma}(m^\*/\phi,\ \phi)\) (process error).

boot = rv.BootstrapODP.fit(triangle)
boot.dispersion          # Pearson phi (52,601 on Taylor–Ashe)
boot.point_reserve       # the chain-ladder reserve — report this
dist = boot.reserve_distribution(size=100_000, rng=42)

Reading the distribution

reserve_distribution returns a ReserveDistribution: the simulated reserves with the point estimate and convenience summaries.

dist.point_reserve         # chain-ladder point estimate (the central figure)
dist.mean()                # bootstrap mean — ~1% high (ratio-estimator bias)
dist.prediction_error()    # SD of the predictive distribution
dist.quantile(0.99)        # empirical VaR (inverted-CDF)
dist.tvar(0.99)            # empirical TVaR (Acerbi–Tasche)
dist.to_frame()            # per-origin + total predictive exhibit

Two rules the API is built around. Report the point estimate, not the bootstrap mean — the mean carries the small upward bias of a product of ratio estimators, so point_reserve is the central figure and the bootstrap supplies the spread and the tail. And the prediction error need not match Mack: Mack assumes \(\mathrm{Var}\propto C\), the ODP assumes \(\mathrm{Var}\propto\) mean, so on Taylor–Ashe the bootstrap runs about 22% above Mack — a genuine difference between two variance models of the same quantity, not an error.

Diagnostics

The bootstrap is only as trustworthy as the ODP assumptions: that the Pearson residuals carry no pattern in origin, development, or calendar direction.

rv.pearson_residuals(triangle)     # tidy: origin, development, calendar, residual
rv.residual_summary(triangle)      # count, mean, std, min, max
rv.calendar_year_effects(triangle) # mean residual by calendar diagonal

A trend across calendar is the classic warning that one development pattern does not describe every diagonal — a claims-inflation or process change the chain ladder, and therefore the bootstrap, would miss. When the model holds the residual standard deviation sits near \(\sqrt{\phi}\) and the mean near zero.

Reserve risk as capital

A fitted BootstrapODP exposes .sample(size, rng) — one draw is one simulated total unpaid amount — so it is a risksim portfolio component with no cross-dependency:

import risksim as rs

port = rs.Portfolio([rs.PortfolioItem("reserve_risk", boot)])
sim = port.simulate(100_000, rng=7)
rs.metrics.tvar(sim.gross_losses, 0.99)     # capital held for the reserve

Aggregating several reserving segments, and imposing a rank correlation across them, is then the standard risksim workflow (the dependence mechanics are Example 9).

API reference

reservingmodels: claims development and stochastic reserve estimation.

One place for the reserving workflow. The deterministic triangle primitives – chain ladder, completion factors, Bornhuetter-Ferguson / Benktander / Cape Cod, and Mack’s analytic standard errors – are re-exported from actuarialpy.reserving (they are shared calculation primitives the projection and pricing packages also build on). On top of them this package adds the stochastic layer: the over-dispersed-Poisson bootstrap of the full predictive reserve distribution, plus residual diagnostics.

A fitted BootstrapODP exposes .sample(size, rng), so it drops straight into risksim as a portfolio component – reserve risk aggregated with prospective underwriting risk, with no dependency between the packages.

Quick start:

import reservingmodels as rv

triangle = rv.datasets.taylor_ashe()      # cumulative K x K triangle

# deterministic point estimate (re-exported chain ladder)
cl = rv.ChainLadder.fit(triangle)
ult = cl.project(triangle)                # per-origin ultimate and IBNR

# stochastic: the predictive distribution
boot = rv.BootstrapODP.fit(triangle)
dist = boot.reserve_distribution(size=100_000, rng=42)
round(dist.point_reserve), round(dist.prediction_error())

Part of the OpenActuarial ecosystem.

class BootstrapODP(fitted_incr: ndarray, residuals: ndarray, dispersion: float, obs_mask: ndarray, latest_dev: ndarray, point_ibnr: ndarray, origins: list)[source]

Bases: object

England-Verrall over-dispersed-Poisson bootstrap fitted to a triangle.

Fit with fit() on a cumulative development triangle, then call sample() for reserve draws (the risksim seam) or reserve_distribution() for a summarized predictive distribution.

classmethod fit(triangle: DataFrame | ndarray) BootstrapODP[source]

Fit the ODP model to a cumulative development triangle.

triangle is a square K x K cumulative triangle – a DataFrame (origins on the index, developments on the columns) or a 2-D array – with the unobserved lower-right as NaN. This is the same input reservingmodels.ChainLadder takes; build one from a claims listing with reservingmodels.make_completion_triangle() or from an actuarialpy.Experience via reservingmodels.integrations.actuarialpy.triangle_from_experience().

property point_reserve: float

The chain-ladder reserve (sum of per-origin IBNR).

reserve_distribution(size: int = 100_000, rng: Generator | int | None = None) ReserveDistribution[source]

Materialize a summarized predictive distribution of the reserve.

sample(size: int = 1, rng: Generator | int | None = None) ndarray[source]

Draw size predictive total-reserve outcomes -> shape (size,).

One draw is one simulated total unpaid amount, so a fitted BootstrapODP drops straight into risksim.PortfolioItem as an aggregate-loss component. rng is None (global state), an int seed, or a numpy.random.Generator threaded through a portfolio simulation.

sample_by_origin(size: int = 1, rng: Generator | int | None = None) ndarray[source]

Predictive reserve per origin -> shape (size, K).

class ReserveDistribution(reserves: ndarray, point_reserve: float, by_origin: ndarray | None = None, origins: list | None = None)[source]

Bases: object

A sample from the predictive distribution of the unpaid claims.

Wraps the simulated total reserves (and, optionally, the per-origin reserves) with the chain-ladder point estimate and convenience summaries. Risk measures use the same empirical estimators as the rest of the ecosystem (inverted-CDF VaR, Acerbi-Tasche TVaR).

prediction_error() float[source]

Standard deviation of the predictive distribution (the reserve SE).

quantile(q) ndarray | float[source]

Empirical VaR (lower-quantile / inverted-CDF convention).

to_frame(quantiles=(0.5, 0.75, 0.95, 0.99, 0.995)) DataFrame[source]

Per-origin and total predictive summary as a tidy table.

tvar(q: float) float[source]

Empirical tail value-at-risk (Acerbi-Tasche).

var(q) ndarray | float

Empirical VaR (lower-quantile / inverted-CDF convention).

pearson_residuals(triangle: DataFrame | ndarray) DataFrame[source]

Unscaled Pearson residuals of the ODP fit, one tidy row per cell.

Columns: origin, development (0-based development period), calendar (origin + development, the diagonal a cell sits on), actual, fitted, and residual = \((C_{ij}-\hat m_{ij})/\sqrt{\hat m_{ij}}\). A trend across calendar is the classic warning that a single development pattern does not describe every diagonal (a claims-inflation or process change), which the bootstrap would not capture.

residual_summary(triangle: DataFrame | ndarray) Series[source]

Scalar summary of the Pearson residuals: count, mean, std, min, max.

A mean far from zero or a heavy tail flags a poor ODP fit; the standard deviation is close to \(\sqrt{\phi}\) when the model holds.

calendar_year_effects(triangle: DataFrame | ndarray) DataFrame[source]

Mean Pearson residual by calendar diagonal.

One row per calendar period (origin + development) with the residual mean, std, and cell count. Systematic sign by diagonal is the signature of a calendar-year (inflation) effect the chain ladder ignores.

Fitting from an Experience

reservingmodels.integrations.actuarialpy builds the triangle — and a fitted bootstrap — straight from a claims-listing actuarialpy.Experience (one row per transaction, with an origin period and a valuation period):

from reservingmodels.integrations import actuarialpy as seam

triangle = seam.triangle_from_experience(exp, origin="origin_date",
                                         valuation="valuation_date")
boot = seam.fit_bootstrap_from_experience(exp, origin="origin_date",
                                          valuation="valuation_date")

Because reservingmodels re-exports actuarialpy it always has the dependency available, so this seam needs no optional extra.

Build reserving inputs from the canonical actuarialpy.Experience.

reservingmodels already requires actuarialpy (it re-exports the triangle primitives), so these helpers are always available. They turn a claims-listing Experience – one row per claim transaction, carrying an incurred/origin date and a paid amount – into a cumulative triangle and, in one step, a fitted BootstrapODP.

triangle_from_experience(exp, *, origin: str, valuation: str, amount_col: str | None = None, cumulative: bool = True) DataFrame[source]

Cumulative development triangle from a claims-listing Experience.

origin is the origin-period column (e.g. accident month) and valuation the transaction/valuation-period column; the paid amount is the bound expense role (or amount_col). Thin wrapper over actuarialpy.make_completion_triangle that resolves the amount column from the Experience’s roles.

fit_bootstrap_from_experience(exp, *, origin: str, valuation: str, amount_col: str | None = None) BootstrapODP[source]

Fit BootstrapODP straight from an Experience.