Skip to main content

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

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 here
  • insurance-optimise — nominal (non-robust) rate optimiser

Metadata

Release files for insurance-dro 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for insurance-dro 0.1.0
File Size Uploaded
insurance_dro-0.1.0.tar.gz 36.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for insurance-dro 0.1.0
File Interpreter ABI Platform
insurance_dro-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 67.5 kB

Release files / insurance_dro-0.1.0.tar.gz

Download URL insurance_dro-0.1.0.tar.gz
Size 36.8 kB
Tags Source
SHA-256 checksum
How to use checksums
19e8c0909b8970d583c0279279bad36dd975eec5500bd64789953421444754ce
BLAKE2b-256 checksum
How to use checksums
5e486a9fa1e6baee828756b267075e8971ca4bd8c78293082fa43988a0da3610
Upload date
Uploaded using Trusted Publishing?
What is 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}

Release files / insurance_dro-0.1.0-py3-none-any.whl

Download URL insurance_dro-0.1.0-py3-none-any.whl
Size 30.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
3a63af0cc39f3dd8b6d306c54278d0fa7cbffb58c17d5675455aa98947e2e169
BLAKE2b-256 checksum
How to use checksums
b018d8e481ad7c77034669eb833b9f911c37c7e2704e08cd272b4e98a9f3f356
Upload date
Uploaded using Trusted Publishing?
What is 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}

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page