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:
objectEngland-Verrall over-dispersed-Poisson bootstrap fitted to a triangle.
Fit with
fit()on a cumulative development triangle, then callsample()for reserve draws (therisksimseam) orreserve_distribution()for a summarized predictive distribution.- classmethod fit(triangle: DataFrame | ndarray) BootstrapODP[source]¶
Fit the ODP model to a cumulative development triangle.
triangleis a squareK x Kcumulative triangle – a DataFrame (origins on the index, developments on the columns) or a 2-D array – with the unobserved lower-right asNaN. This is the same inputreservingmodels.ChainLaddertakes; build one from a claims listing withreservingmodels.make_completion_triangle()or from anactuarialpy.Experienceviareservingmodels.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
sizepredictive total-reserve outcomes -> shape(size,).One draw is one simulated total unpaid amount, so a fitted
BootstrapODPdrops straight intorisksim.PortfolioItemas an aggregate-loss component.rngisNone(global state), anintseed, or anumpy.random.Generatorthreaded through a portfolio simulation.
- class ReserveDistribution(reserves: ndarray, point_reserve: float, by_origin: ndarray | None = None, origins: list | None = None)[source]¶
Bases:
objectA 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).
- 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.
- 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, andresidual= \((C_{ij}-\hat m_{ij})/\sqrt{\hat m_{ij}}\). A trend acrosscalendaris 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 residualmean,std, and cellcount. 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.originis the origin-period column (e.g. accident month) andvaluationthe transaction/valuation-period column; the paid amount is the bound expense role (oramount_col). Thin wrapper overactuarialpy.make_completion_trianglethat 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
BootstrapODPstraight from an Experience.