Skip to main content

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

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_dro-0.1.0.tar.gz (36.8 kB view details)

Uploaded Source

Built Distribution

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

insurance_dro-0.1.0-py3-none-any.whl (30.7 kB view details)

Uploaded Python 3

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

Hashes for insurance_dro-0.1.0.tar.gz
Algorithm Hash digest
SHA256 19e8c0909b8970d583c0279279bad36dd975eec5500bd64789953421444754ce
MD5 bf2e4f0b7f318b7b5a111cebcca7b977
BLAKE2b-256 5e486a9fa1e6baee828756b267075e8971ca4bd8c78293082fa43988a0da3610

See more details on using hashes here.

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

Hashes for insurance_dro-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 3a63af0cc39f3dd8b6d306c54278d0fa7cbffb58c17d5675455aa98947e2e169
MD5 bef2e7c0c610d91e04d5be18f76e6c34
BLAKE2b-256 b018d8e481ad7c77034669eb833b9f911c37c7e2704e08cd272b4e98a9f3f356

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