Example 11: the reserve, with a distribution¶
A ten-year paid triangle, one reserve number every actuary reports, and the
question that number cannot answer on its own — how wrong could it be? This
page carries the Taylor–Ashe triangle from the chain-ladder point estimate to
the full predictive distribution of the unpaid claims, checks the model has the
right to run, reconciles the two error models, and turns the reserve into
capital in risksim. Development and the deterministic methods come from
actuarialpy (re-exported through reservingmodels); the distribution and
diagnostics are reservingmodels; the capital is risksim. Every number on
this page is the output of this exact fixed-seed run, pinned by a regression
test in the reservingmodels suite.
The triangle, and the number everyone reports¶
import reservingmodels as rv
triangle = rv.datasets.taylor_ashe()
cl = rv.ChainLadder.fit(triangle)
cl.mack_standard_errors(triangle).loc["Total"]
reserve |
Mack SE |
CV |
|---|---|---|
18,680,856 |
2,447,095 |
13.1% |
The chain ladder develops the triangle to an 18.7m reserve; Mack (1993) puts a distribution-free standard error on it without simulating anything. That is already more than a point estimate — but a single number and its SD do not tell you the shape of the tail, and the tail is where a reserve fails.
The whole distribution, not just its width¶
boot = rv.BootstrapODP.fit(triangle)
dist = boot.reserve_distribution(size=100_000, rng=42)
boot.dispersion # 52,601
dist.point_reserve # 18,680,856 — reproduces the chain ladder exactly
dist.mean() # 18,909,342 — the bootstrap mean, +1.2% (ratio bias)
dist.prediction_error() # 2,996,849
The bootstrap fits the over-dispersed Poisson whose mean surface is the chain
ladder, so point_reserve lands on 18,680,856 to the dollar; the estimated
dispersion \(\phi = 52{,}601\) matches the published fit. Report the point
estimate, not the bootstrap mean — the 1.2% gap is the known upward bias of a
product of ratio estimators, not signal. What the bootstrap adds is the
quantiles:
for q in (0.50, 0.75, 0.95, 0.99, 0.995):
dist.quantile(q)
dist.tvar(0.99)
quantile |
reserve |
|---|---|
50% (median) |
18,713,157 |
75% |
20,770,994 |
95% |
24,138,798 |
99% |
26,886,153 |
99.5% |
27,949,893 |
TVaR 99% |
28,458,966 |
The 99th percentile sits 8.2m — a third — above the booked reserve, and TVaR carries the average of the worst 1%. A 13% CV on the point estimate sounds mild; the 99.5% quantile is what a capital calculation actually consumes.
Per-origin, where the risk actually sits¶
dist.to_frame()
origin |
reserve |
se |
q95 |
q99 |
|---|---|---|---|---|
… |
||||
7 |
2,194,440 |
496,466 |
3,063,438 |
3,685,963 |
8 |
3,952,518 |
795,231 |
5,343,556 |
6,297,848 |
9 |
4,322,826 |
1,058,224 |
6,182,766 |
7,605,661 |
10 |
4,708,405 |
2,035,330 |
8,239,562 |
11,659,798 |
Total |
18,909,342 |
2,996,849 |
24,138,798 |
27,949,893 |
The uncertainty is not spread evenly. The youngest origin (10) has the second largest reserve but by far the largest relative error — its q99 is nearly 2.5× its mean — because it is developed off a single observation and the bootstrap refits the whole pattern each replicate. The total SE (2,996,849) is far below the sum of the origin SEs: the origins are estimated through one shared development pattern, so their errors are correlated, and the bootstrap captures that automatically.
Is the bootstrap even entitled to run?¶
rv.residual_summary(triangle)
rv.calendar_year_effects(triangle)
n |
mean |
std |
min |
max |
|---|---|---|---|---|
55 |
0.60 |
187.3 |
−403.8 |
533.2 |
The 55 Pearson residuals are centred near zero, and their spread sits in the
neighbourhood of \(\sqrt{\phi}\approx 229\) — the ODP variance assumption is
credible. calendar_year_effects shows no monotone drift across diagonals: the
largest-magnitude diagonal means fall on the one-, two-, and three-cell
diagonals, where a single residual dominates, not a systematic
calendar-year (inflation) trend that would break the single-pattern assumption.
When that check fails, the bootstrap’s tail is not trustworthy no matter how
tidy the quantiles look — which is the point of running it.
Mack vs the bootstrap¶
mack = cl.mack_standard_errors(triangle).loc["Total"]
proc = (boot.dispersion * boot.point_reserve) ** 0.5 # process SD
method |
reserve |
SE |
CV |
|---|---|---|---|
chain ladder / Mack |
18,680,856 |
2,447,095 |
13.1% |
ODP bootstrap |
18,909,342 |
2,996,849 |
15.9% |
The bootstrap SE runs 22% above Mack’s, and that is a feature, not a discrepancy: Mack assumes \(\mathrm{Var}(C_{i,k+1}\mid C_{i,k})\propto C_{i,k}\), the ODP assumes \(\mathrm{Var}\propto\) mean, and the two answer the same question under different variance laws. The bootstrap SE decomposes cleanly — estimation error 2,828,156 and process error \(\sqrt{\phi\cdot R}=991{,}281\) combine as \(\sqrt{2{,}828{,}156^2 + 991{,}281^2} = 2{,}996{,}849\) — so nothing is double-counted; the width is exactly estimation risk plus process risk. Having both numbers is the honest position: they bracket the reserve risk under two defensible models.
The reserve as capital¶
A fitted bootstrap is a risksim component — one .sample draw is one
simulated unpaid total — so the reserve joins a capital model with no
dependency between the packages:
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) # 28,318,860
booked reserve |
TVaR(99%) |
capital margin |
|---|---|---|
18,680,856 |
28,318,860 |
9,638,004 |
That last row is the whole point of the exercise stated in currency: you book
18.7m, but holding to a 99% tail-value standard needs roughly 9.6m more. The
portfolio TVaR reproduces the standalone distribution’s tail because the seam is
transparent — risksim simply consumed the reserve model — and from here a
second reserving segment aggregates in with a rank correlation exactly as
Example 9: two lines, one tail does it. The
reserve stops being a number and becomes a distribution you can hold capital
against.