Aggregate Loss and Reinsurance¶
An insurance portfolio produces a random number of claims of random size. What matters is the total:
heavytails.actuarial builds the distribution of \(S\) from a frequency model and
a severity distribution, and prices the reinsurance written on top of it.
A worked portfolio¶
Take a commercial property book: about ten claims a year, over-dispersed, with a Pareto severity attaching at 10,000 and a tail index of 1.9. Policies carry a 25,000 deductible and a 500,000 limit.
from heavytails import Pareto
from heavytails.actuarial import (
LayeredSeverity, NegativeBinomial, PolicyTerms,
compound_moments, panjer_recursion,
)
frequency = NegativeBinomial(r=4.0, beta=2.5) # mean 10, variance 35
severity = Pareto(alpha=1.9, xm=10_000.0) # mean 21,111
terms = PolicyTerms(deductible=25_000.0, limit=500_000.0)
The severity has a mean; the portfolio has no variance¶
With alpha = 1.9 the severity has a mean but no second moment, so the
aggregate variance does not exist. That is not a numerical inconvenience. Every
normal and translated-gamma approximation in the actuarial literature works by
matching two moments, and there is no second moment here to match. Reporting a
large finite number instead would let someone reach for one of them.
The policy limit changes this. A capped severity is bounded, so it has moments of every order:
Capping is what makes the arithmetic possible, and it is worth noticing that this is the same reason catastrophe reinsurance is always written with a limit.
Pricing the retained layer¶
layer.cdf(0.0) # 0.8246 -- most losses never reach the deductible
layer.mean() # 4,556.43 expected payment per loss
frequency.mean() * layer.mean() # 45,564.30 expected annual cost
The layer mean is a difference of limited expected values, \(E[X \wedge 525{,}000] - E[X \wedge 25{,}000]\), which stays exact even when the underlying severity is heavy enough to make direct integration awkward.
Per-loss or per-payment¶
LayeredSeverity has two bases, and choosing the wrong one is the classic error
in this calculation.
| Basis | Atom at zero | Pairs with |
|---|---|---|
per-loss |
yes, \(F(d)\) | the original frequency |
per-payment |
no | the frequency thinned by \(S(d)\) |
Both describe the same portfolio and give the same aggregate distribution:
per_payment = LayeredSeverity(severity, terms, basis="per-payment")
thinned = frequency.thin(layer.exceedance_probability)
panjer_recursion(frequency, layer, h=2_000.0, n=4_000)
panjer_recursion(thinned, per_payment, h=2_000.0, n=4_000) # identical
Mixing them — a per-payment severity with an unthinned frequency — overstates the expected aggregate by \(1/S(d)\), here a factor of 5.7. It produces a number that looks entirely plausible.
Two routes to the aggregate distribution¶
Panjer recursion¶
Exact given a discretised severity, and it returns the whole distribution rather than a sample:
aggregate = panjer_recursion(
frequency, layer, h=2_000.0, n=4_000, method="mean-preserving"
)
aggregate.mean() # 45,564.30, matching the closed form
aggregate.value_at_risk(0.99) # 418,000
aggregate.expected_shortfall(0.99) # 527,068
Two things go wrong with it, and both are reported rather than returned:
The recursion can start at zero. It begins from \(g_0 = P_N(f_0)\), which for a
Poisson is \(e^{\lambda(f_0-1)}\). For a large expected claim count that underflows
to exactly zero, and since every later probability is a multiple of it, the whole
output becomes zeros — a result that is not a distribution and not obviously
wrong. panjer_recursion raises instead.
The grid can be too short. A heavy-tailed severity puts real mass beyond any
finite grid. AggregateLoss carries truncated_mass and severity_tail_mass,
and ppf returns inf above 1 - truncated_mass rather than the last grid
point, which would look like an answer.
A policy limit removes the problem entirely: the capped severity fits inside the
grid, and truncated_mass here is \(3 \times 10^{-16}\).
Simulation¶
No grid to truncate and no \(g_0\) to underflow, at the cost of slower convergence:
from heavytails.actuarial import EmpiricalAggregate, simulate_aggregate_loss
sample = EmpiricalAggregate(
simulate_aggregate_loss(frequency, layer, 100_000, seed=1)
)
sample.mean() # 45,623
sample.value_at_risk(0.95) # 182,201 (Panjer: 182,000)
EmpiricalAggregate exposes the same interface, so the two routes are
interchangeable and both work with heavytails.risk.
For a severity with no mean at all — alpha below 1 — simulation is the only
route that runs.
Reinsurance¶
Per-risk excess of loss¶
The reinsurer pays \(\min(\max(X - r, 0), \ell)\) on each individual claim, so the expected annual cost is a difference of limited expected values. No grid, no simulation:
from heavytails.actuarial import excess_of_loss_premium
excess_of_loss_premium(frequency, severity, retention=100_000, limit=500_000)
# 11,199.23
excess_of_loss_premium(frequency, severity, retention=100_000, limit=None)
# 13,988.06
Layers are additive: pricing 5 to 20 and 20 to 50 gives exactly the same total as pricing 5 to 50.
On a severity with no mean the unlimited layer costs inf and the limited one
does not, which is the whole reason those covers carry limits.
This is the expected loss cost, not a premium
It carries no loading for expenses, risk margin or cost of capital. For a heavy-tailed layer the risk margin is usually the larger part of the price, precisely because the variance that would ordinarily size it does not exist.
Aggregate stop loss¶
The reinsurer pays whatever the total exceeds a retention:
Because the truncated mass sits entirely above the retention, the gridded value is
a lower bound. stop_loss_premium returns inf when what fell off the end would
move the answer by more than the tolerance, which defaults to 1%. A heavy tail
always truncates something, so the check is relative rather than absolute — an
absolute one would make the function useless on exactly the distributions it is
for.
Choosing a frequency model¶
| Model | Dispersion | Use when |
|---|---|---|
Binomial |
variance below mean | a closed group, at most one claim per policy |
Poisson |
variance equals mean | the default |
NegativeBinomial |
variance above mean | the claim rate is itself uncertain |
All three are the \((a,b,0)\) class, where \(p_k/p_{k-1} = a + b/k\) — which is exactly the property that makes Panjer's recursion possible, so the selection is not a matter of convenience.
The choice matters more than it appears. A Poisson with the same mean as the negative binomial above matches the expected aggregate to within a rounding error while understating the 99th percentile, so checking the mean will not catch it. Over-dispersion is the usual case: a gamma-mixed Poisson is negative binomial, so uncertainty about the rate produces it automatically.
Discretisation¶
Panjer needs the severity on an arithmetic grid, and h is the whole accuracy
story.
"mass"assigns each point the probability of the interval around it. Simple, fast, and slightly biased in the mean."mean-preserving"matches local moments, so the discretised mean is exact. Prefer it for pricing, where an error in the mean is an error in the premium.
Mean-preserving takes a second difference of limited expected values, which
amplifies any error in them by \(1/h\) and can push a weight negative. Clipping a
material amount would leave a biased grid that still sums to something plausible,
so discretise_severity raises instead.