Metadata-Version: 2.4
Name: insurance-dro
Version: 0.1.0
Summary: Distributionally robust rate optimisation for UK personal lines — Wasserstein ambiguity sets, tractable CVXPY reformulations, and price-of-robustness curves for committee papers
Project-URL: Repository, https://github.com/burning-cost/insurance-dro
Project-URL: Issues, https://github.com/burning-cost/insurance-dro/issues
Author-email: Burning Cost <pricing.frontier@gmail.com>
License: MIT License
        
        Copyright (c) 2026 Burning Cost
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Keywords: CVXPY,DRO,ENBP,FCA,PS21/5,UK insurance,Wasserstein,actuarial,demand uncertainty,distributionally robust optimisation,insurance,personal lines,price of robustness,rate optimisation
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Financial and Insurance Industry
Classifier: Intended Audience :: Science/Research
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 :: Office/Business :: Financial
Classifier: Topic :: Scientific/Engineering :: Mathematics
Requires-Python: >=3.10
Requires-Dist: clarabel>=0.7
Requires-Dist: cvxpy>=1.4
Requires-Dist: numpy>=1.24
Requires-Dist: pandas>=2.0
Requires-Dist: scipy>=1.11
Provides-Extra: all
Requires-Dist: matplotlib>=3.7; extra == 'all'
Provides-Extra: dev
Requires-Dist: matplotlib>=3.7; extra == 'dev'
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
Requires-Dist: pytest>=7.0; extra == 'dev'
Provides-Extra: plot
Requires-Dist: matplotlib>=3.7; extra == 'plot'
Description-Content-Type: text/markdown

# insurance-dro

Distributionally robust rate optimisation for UK personal lines.

## The problem

Every insurance rate optimiser takes a demand model as a point estimate. You fit a conversion or lapse model, read off elasticities, and then optimise rates to maximise expected profit subject to ENBP constraints. The trouble is that the demand model is always wrong at the margin, and the optimiser fails precisely when you need it most — at renewal, when demand shifts, or when the model was fitted on a biased sample.

The standard fix — sensitivity analysis — tells you what happens if elasticity is ±X%. That's manual, incomplete, and doesn't give you a single number for the committee paper.

Distributionally robust optimisation (DRO) gives you something better: rates that are optimal against the *worst* demand distribution within a formal uncertainty set, together with the **price of robustness** — the percentage of expected profit you sacrifice to buy that protection.

## What this library does

- Wraps CVXPY to give tractable Wasserstein, KL, and chi-squared DRO reformulations
- Takes demand bootstrap samples directly from `insurance-elasticity` as the empirical distribution
- Enforces ENBP as a hard constraint on all output rates (FCA PS21/5 compliant by construction)
- Returns a price-of-robustness curve suitable for pricing committee papers
- Includes an audit trail with a plain-English FCA Consumer Duty statement

## Installation

```bash
pip install insurance-dro
```

For plotting:

```bash
pip install insurance-dro[plot]
```

## Quickstart

```python
import numpy as np
from insurance_dro import AmbiguitySet, RobustOptimiser, RobustnessFrontier, AuditTrail

# Bootstrap demand samples from insurance-elasticity (N policies, S scenarios)
N, S = 200, 500
rng = np.random.default_rng(42)
current_rates = rng.uniform(300, 600, N)
technical_price = current_rates * rng.uniform(0.65, 0.85, N)
demand_samples = rng.uniform(0.55, 0.90, (N, S))   # renewal probabilities
enbp = technical_price * 1.5

# Calibrate ambiguity radius from elasticity standard errors
# (plug in the SE column from insurance-elasticity DML output)
elasticity_se = rng.uniform(0.05, 0.20, N)
amb = AmbiguitySet.from_elasticity_se(elasticity_se=elasticity_se)
print(f"Wasserstein radius: {amb.radius:.4f}")

# Single solve
opt = RobustOptimiser(
    rates=current_rates,
    technical_price=technical_price,
    demand_samples=demand_samples,
    enbp=enbp,
    constraints={"max_increase": 0.25, "max_decrease": 0.10},
    ambiguity_set=amb,
)
result = opt.optimise()
print(f"Price of robustness: {result.price_of_robustness_pct:.1f}%")
print(result.summary())

# Full PoR frontier
frontier = RobustnessFrontier(
    rates=current_rates,
    technical_price=technical_price,
    demand_samples=demand_samples,
    enbp=enbp,
    constraints={"max_increase": 0.25},
    ambiguity_kind="wasserstein",
)
fr = frontier.sweep(n_points=20)
print(f"Elbow radius: {fr.elbow_radius:.4f}")

# Plot (requires matplotlib)
fig = frontier.plot(result=fr)
fig.savefig("por_curve.png")

# Audit trail for committee paper
trail = AuditTrail(
    result=result,
    ambiguity_set=amb,
    n_policies=N,
    n_scenarios=S,
    constraint_spec={"max_increase": 0.25, "max_decrease": 0.10},
)
print(trail.fca_summary())
trail.save("audit_trail.json")
```

## Ambiguity sets

### Wasserstein (recommended)

The 1-Wasserstein (earth-mover) metric defines a ball of distributions around the empirical demand distribution. Radius eps is in demand probability units.

```python
amb = AmbiguitySet(kind='wasserstein', radius=0.05)
```

The dual reformulation (Esfahani & Kuhn 2018) is an LP. For a linear profit function, the robust objective is simply:

```
mean profit - eps * max(rates)
```

This is the standard result for Wasserstein DRO with linear loss functions.

### KL divergence

```python
amb = AmbiguitySet(kind='kl', radius=0.5)
```

Uses a log-sum-exp CVXPY formulation. Tends to be more conservative than Wasserstein for the same nominal coverage.

### Chi-squared

```python
amb = AmbiguitySet(kind='chi2', radius=1.0)
```

Equivalent to mean + sqrt(rho) * std of the profit distribution. SOCP-tractable.

### Calibration from elasticity standard errors

```python
amb = AmbiguitySet.from_elasticity_se(
    elasticity_se=0.12,   # SE of your DML elasticity estimate
    coverage=0.95,         # confidence level
    base_demand=0.75,      # renewal probability at current rates
)
```

This maps the elasticity standard error to a Wasserstein radius using:
```
eps = z_{alpha/2} * base_demand * max(SE) / 2
```

## Constraints

```python
constraints = {
    "max_increase": 0.25,    # rates can rise by at most 25%
    "max_decrease": 0.10,    # rates can fall by at most 10%
    "volume_neutral": True,  # preserve total expected demand within ±5%
}
```

ENBP is always enforced as a hard constraint — you cannot disable it.

## Diagnostics

```python
from insurance_dro import diagnostics

# PoR table for committee paper appendix
df = diagnostics.por_table(frontier_result)

# Rate comparison chart
fig = diagnostics.rate_comparison_plot(result, current_rates=current_rates)

# Policy-level sensitivity table
df = diagnostics.sensitivity_table(results_list, radii_list, policy_idx=5)
```

## Design decisions

**CVXPY + CLARABEL.** Open source, ships with cvxpy >=1.4, no licence needed. The Wasserstein LP and chi-squared SOCP are fast enough for N=500 policies in under a second. KL is slower due to the exponential cone.

**Wasserstein over phi-divergence.** ACM ICAIF 2024 showed that phi-divergence DRO is overly conservative for insurance pricing (the worst-case distribution is too pessimistic relative to what actually happens). Wasserstein has better out-of-sample performance guarantees for bounded continuous distributions.

**Linearised demand.** The demand function is linearised around the current operating point using first-order Taylor expansion. This makes the profit objective linear in the rate variable, which is what lets the full DRO problem be written as an LP or SOCP. The approximation is valid for rate changes up to ±30%.

**Price of robustness as the primary output.** Not "better rates" — the PoR. The committee wants to know what the robustness costs, not just what the robust rates are.

## References

- Esfahani & Kuhn (2018). Data-Driven Distributionally Robust Optimization Using the Wasserstein Metric. *Mathematical Programming* 171(1-2), 115-166.
- Mohajerin Esfahani & Kuhn (2018) — same paper, often cited as "MOK 2018" in OR.
- ACM ICAIF 2024. Parametric Phi-Divergence-Based Distributionally Robust Optimization for Insurance Pricing. DOI: 10.1145/3768292.3770404.
- Lam (2019). Recovering Best Statistical Guarantees via the Empirical Distributionally Robust Optimization. *Operations Research* 67(4).

## Related libraries

- `insurance-elasticity` — DML price elasticity estimation; generates the bootstrap demand samples used here
- `insurance-optimise` — nominal (non-robust) rate optimiser
