Skip to main content

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:

  1. 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 mbbefd package. There is no Python equivalent on PyPI.

  2. 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 mbbefd package v0.8.13 (Spedicato, Dutang et al.), CRAN.

Licence

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

insurance_ilf-0.1.0.tar.gz (130.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

insurance_ilf-0.1.0-py3-none-any.whl (25.2 kB view details)

Uploaded Python 3

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

Hashes for insurance_ilf-0.1.0.tar.gz
Algorithm Hash digest
SHA256 7bf4ca3449fce08ae5161648a4c950faf22bb1279647169c541c85e51a4c5c1b
MD5 c4997ffb63d8c046956bf7345b0da734
BLAKE2b-256 a287d750b860cdc93c9ae9221f81d4fcbdb0a57f43653c38b527bceafe30e5c1

See more details on using hashes here.

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

Hashes for insurance_ilf-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 e34569bb9636f697a9d552878d37d87df61b5f4370f37bf69efc92f1043577d5
MD5 408b848f86c3eab919fe694864cd2b01
BLAKE2b-256 a9fbefd15f61e09b1de870377170863ecdac7a806a3b5f150b0578469139cfc6

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page