Distributionally robust rate optimisation for UK personal lines — Wasserstein ambiguity sets, tractable CVXPY reformulations, and price-of-robustness curves for committee papers
Project description
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-elasticityas 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
pip install insurance-dro
For plotting:
pip install insurance-dro[plot]
Quickstart
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.
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
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
amb = AmbiguitySet(kind='chi2', radius=1.0)
Equivalent to mean + sqrt(rho) * std of the profit distribution. SOCP-tractable.
Calibration from elasticity standard errors
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
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
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 hereinsurance-optimise— nominal (non-robust) rate optimiser
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_dro-0.1.0.tar.gz.
File metadata
- Download URL: insurance_dro-0.1.0.tar.gz
- Upload date:
- Size: 36.8 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 |
19e8c0909b8970d583c0279279bad36dd975eec5500bd64789953421444754ce
|
|
| MD5 |
bf2e4f0b7f318b7b5a111cebcca7b977
|
|
| BLAKE2b-256 |
5e486a9fa1e6baee828756b267075e8971ca4bd8c78293082fa43988a0da3610
|
File details
Details for the file insurance_dro-0.1.0-py3-none-any.whl.
File metadata
- Download URL: insurance_dro-0.1.0-py3-none-any.whl
- Upload date:
- Size: 30.7 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 |
3a63af0cc39f3dd8b6d306c54278d0fa7cbffb58c17d5675455aa98947e2e169
|
|
| MD5 |
bef2e7c0c610d91e04d5be18f76e6c34
|
|
| BLAKE2b-256 |
b018d8e481ad7c77034669eb833b9f911c37c7e2704e08cd272b4e98a9f3f356
|