MBBEFD exposure curve fitting, Increased Limits Factors, and per-risk XL pricing for UK general insurance
Project description
insurance-ilf
MBBEFD exposure curve fitting, Increased Limits Factors (ILFs), and per-risk XL pricing for Python.
The problem
UK commercial property and liability pricing teams need to do two things that have no Python solution today:
-
Fit an exposure curve to claims data expressed as destruction rates (loss/MPL). The industry standard is the MBBEFD family from Bernegger (1997), implemented in the R
mbbefdpackage. There is no Python equivalent on PyPI. -
Price a per-risk XL layer using the exposure rating method — the methodology Lloyd's syndicates and reinsurers use when experience data is thin or the portfolio has changed. This requires a fitted exposure curve, a risk profile, and Clark's (2014) layer formula.
Without a library, every team builds this from scratch in Excel or reimplements the mathematics in NumPy without a tested, reusable structure. This library provides that structure.
What this library does
Five focused modules:
| Module | Contents |
|---|---|
insurance_ilf.distributions |
MBBEFDDistribution — pdf, cdf, ppf, rvs, exposure curve, LEV |
insurance_ilf.curves |
Swiss Re standard curves (Y1–Y4, Lloyd's), empirical exposure curve |
insurance_ilf.fitting |
MLE and moment-matching fitting with multi-start optimisation; truncated/censored data support |
insurance_ilf.pricing |
ILF tables, XL layer expected loss, per-risk XL treaty rating |
insurance_ilf.diagnostics |
KS/AD goodness-of-fit, PP/QQ plots, Lee diagram |
Install
pip install insurance-ilf
Dependencies: NumPy, SciPy, Pandas. No R required.
Quick start
Swiss Re standard curves
from insurance_ilf import swiss_re_curve
import numpy as np
# Y2 curve — standard commercial property
dist = swiss_re_curve(2.0)
print(f"Total loss probability: {dist.total_loss_prob():.1%}") # ~13%
print(f"G(0.5) = {dist.exposure_curve(0.5):.3f}") # ~0.74
Fit an exposure curve to claims data
from insurance_ilf import fit_mbbefd
import numpy as np
# Destruction rates: observed_loss / MPL, each in [0, 1]
destruction_rates = np.array([0.05, 0.12, 0.18, 0.31, 0.55, 0.87, 1.0, 0.22, ...])
result = fit_mbbefd(destruction_rates)
print(result)
# FittingResult(g=9.3, b=7.8, loglik=-45.2, aic=94.4, converged=True)
fitted_dist = result.dist
Fit with deductible and policy limit (truncated/censored data)
# Claims observed above a 5% deductible, capped at 80% of MPL
result = fit_mbbefd(
destruction_rates,
truncation=0.05, # deductible = 5% of MPL
censoring=0.80, # policy limit = 80% of MPL
)
ILF table
from insurance_ilf import ilf_table
table = ilf_table(
dist=fitted_dist,
limits=[1_000_000, 2_000_000, 5_000_000, 10_000_000],
basic_limit=1_000_000,
mpl=10_000_000,
)
print(table)
# limit lev ilf marginal_ilf
# 0 1000000 120340.2 1.000 1.000
# 1 2000000 175200.1 1.457 0.457
# 2 5000000 230100.4 1.913 0.456
# 3 10000000 252400.0 2.098 0.185
Per-risk XL treaty rating
import pandas as pd
from insurance_ilf import per_risk_xl_rate
# Risk profile from the ceding company
risk_profile = pd.DataFrame({
"sum_insured": [500_000, 1_000_000, 2_000_000, 5_000_000],
"premium": [200_000, 350_000, 280_000, 170_000],
"count": [400, 350, 140, 34],
})
result = per_risk_xl_rate(
risk_profile=risk_profile,
dist=fitted_dist,
attachment=1_000_000,
limit=1_000_000,
mpl_factor=1.0, # MPL = sum insured
)
print(f"Technical rate: {result['technical_rate']:.2%}")
print(f"Rate on line: {result['rol']:.3%}")
Goodness-of-fit
from insurance_ilf.diagnostics import GoodnessOfFit
gof = GoodnessOfFit(destruction_rates, fitted_dist)
print(gof.ks_test()) # {'statistic': 0.042, 'p_value': 0.82}
print(gof.ad_test()) # {'statistic': 0.31}
# With matplotlib:
import matplotlib.pyplot as plt
fig, axes = plt.subplots(1, 2, figsize=(10, 4))
gof.qq_plot(ax=axes[0])
gof.exposure_curve_plot(ax=axes[1])
plt.tight_layout()
plt.show()
The MBBEFD distribution
The MBBEFD (Maxwell-Boltzmann, Bose-Einstein, Fermi-Dirac) distribution class was introduced by Bernegger (1997) as an analytic framework for Swiss Re's four standard property exposure curves.
The distribution is mixed continuous-discrete:
- Continuous density on [0, 1) representing partial losses
- Point mass P(X=1) = 1/g representing total losses
The exposure curve G(x) = E[min(X,x)] / E[X] is the central object: it maps a fraction x of MPL to the proportion of expected loss that lies below x.
Swiss Re c-parameter maps a single scalar to the two-dimensional (g, b) space:
b = exp(3.1 − 0.15·c·(1+c))
g = exp(c·(0.78 + 0.12·c))
Standard curves:
| Curve | c | P(total loss) | Application |
|---|---|---|---|
| Y1 | 1.5 | ~23.7% | Light manufacturing, sprinkled |
| Y2 | 2.0 | ~13.0% | Standard commercial property |
| Y3 | 3.0 | ~3.3% | Heavy commercial |
| Y4 | 4.0 | ~0.6% | High-value industrial |
| Lloyd's | 5.0 | ~0.1% | Industrial complexes |
Design decisions
Why (g, b) not (a, b)? Bernegger's preferred parametrisation because 1/g has a direct interpretation as the total loss probability. The (a, b) form is available via MBBEFDDistribution.from_ab(a, b).
Why multi-start MLE? The log-likelihood surface is non-convex. Cycling through the five Swiss Re c-parameter starting points covers the plausible parameter space and avoids local optima in the vast majority of practical datasets. Differential evolution is used as a fallback for pathological cases.
Why no scipy.stats subclassing? The mixed discrete-continuous nature means the distribution cannot be a pure rv_continuous subclass. It would require a custom rv_discrete component for the total-loss atom, which doesn't integrate cleanly. The interface deliberately mirrors scipy.stats (pdf, cdf, ppf, rvs) without the inheritance.
Why Pandas for pricing output? Actuaries work in tabular form. An ILF table is naturally a DataFrame; a risk profile is naturally a DataFrame. Returning NumPy arrays here would be the wrong abstraction.
References
- Bernegger, S. (1997). "The Swiss Re Exposure Curves And The MBBEFD Distribution Class." ASTIN Bulletin, 27(1), pp99–111.
- Clark, D.R. (2014). "Basics of Reinsurance Pricing." CAS Study Note.
- Aigner, G. et al. (2024). "Modeling lower-truncated and right-censored insurance claims with an extension of the MBBEFD class." European Actuarial Journal. arXiv:2310.11471v2.
- R
mbbefdpackage v0.8.13 (Spedicato, Dutang et al.), CRAN.
Licence
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file insurance_ilf-0.1.0.tar.gz.
File metadata
- Download URL: insurance_ilf-0.1.0.tar.gz
- Upload date:
- Size: 130.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.10.8 {"installer":{"name":"uv","version":"0.10.8","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7bf4ca3449fce08ae5161648a4c950faf22bb1279647169c541c85e51a4c5c1b
|
|
| MD5 |
c4997ffb63d8c046956bf7345b0da734
|
|
| BLAKE2b-256 |
a287d750b860cdc93c9ae9221f81d4fcbdb0a57f43653c38b527bceafe30e5c1
|
File details
Details for the file insurance_ilf-0.1.0-py3-none-any.whl.
File metadata
- Download URL: insurance_ilf-0.1.0-py3-none-any.whl
- Upload date:
- Size: 25.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.10.8 {"installer":{"name":"uv","version":"0.10.8","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e34569bb9636f697a9d552878d37d87df61b5f4370f37bf69efc92f1043577d5
|
|
| MD5 |
408b848f86c3eab919fe694864cd2b01
|
|
| BLAKE2b-256 |
a9fbefd15f61e09b1de870377170863ecdac7a806a3b5f150b0578469139cfc6
|