Metadata-Version: 2.4
Name: insurance-ilf
Version: 0.1.0
Summary: MBBEFD exposure curve fitting, Increased Limits Factors, and per-risk XL pricing for UK general insurance
Project-URL: Homepage, https://github.com/burning-cost/insurance-ilf
Project-URL: Repository, https://github.com/burning-cost/insurance-ilf
License: MIT
Keywords: ILF,MBBEFD,actuarial,excess-of-loss,exposure-curve,insurance,reinsurance
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Financial and Insurance Industry
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering :: Mathematics
Requires-Python: >=3.10
Requires-Dist: numpy>=1.24
Requires-Dist: pandas>=2.0
Requires-Dist: scipy>=1.10
Provides-Extra: dev
Requires-Dist: matplotlib>=3.6; extra == 'dev'
Requires-Dist: pytest-cov; extra == 'dev'
Requires-Dist: pytest>=7.0; extra == 'dev'
Provides-Extra: plots
Requires-Dist: matplotlib>=3.6; extra == 'plots'
Description-Content-Type: text/markdown

# 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

```bash
pip install insurance-ilf
```

Dependencies: NumPy, SciPy, Pandas. No R required.

---

## Quick start

### Swiss Re standard curves

```python
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

```python
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)

```python
# 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

```python
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

```python
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

```python
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
