Loss cost trend analysis for UK personal lines insurance pricing
Project description
insurance-trend
Loss cost trend analysis for UK personal lines insurance pricing.
Blog post: Loss Cost Trend Analysis in Python
The problem
Every UK motor and household pricing actuary does loss cost trend analysis every quarter. The workflow is: take aggregate accident-period experience data, fit a log-linear trend to frequency and severity separately, project forward to the next rating period, and report a trend rate with a confidence interval.
Currently this is done in Excel, SAS, or bespoke R scripts. There is no Python library for it. chainladder-python handles reserving triangles but does nothing for pricing trend — it applies user-specified factors, it does not fit them from data.
The post-2021 inflationary environment has made this more urgent: UK motor claims inflation ran at 34% from 2019 to 2023 versus CPI of 21% — a 13 percentage point superimposed component that CPI alone does not capture. A library that cannot identify structural breaks (COVID lockdown, Ogden rate change) will produce misleading trend estimates.
Quick start
import numpy as np
from insurance_trend import LossCostTrendFitter
# 24 quarters of UK motor aggregate experience data (2019 Q1 – 2024 Q4)
# Synthetic: stable frequency pre-2021, then step-down (COVID recovery lag),
# then gradual recovery. Severity accelerates post-2021 (repair cost inflation).
rng = np.random.default_rng(42)
n = 24
periods = [
f"{yr}Q{q}"
for yr in range(2019, 2025)
for q in range(1, 5)
]
# Earned vehicle-years (growing book, slight seasonality)
earned_vehicles = (
12_000
+ np.arange(n) * 150
+ rng.normal(0, 200, n)
).clip(10_000, None)
# True frequency: stable ~0.085 pre-2021, drops to ~0.065 in 2021 (COVID),
# then recovers at +3% pa through 2024
t = np.arange(n)
freq_true = np.where(
t < 8,
0.085 + 0.001 * t,
0.065 * np.exp(0.007 * (t - 8)), # post-COVID recovery trend
)
claim_counts = rng.poisson(freq_true * earned_vehicles).astype(float)
# True severity: accelerates at +8% pa from 2022 (repair inflation)
base_severity = 3_800.0
sev_true = base_severity * np.where(
t < 12,
1.0 + 0.03 * t / 4,
(1.0 + 0.03) ** 3 * np.exp(0.08 * (t - 12) / 4), # post-2022 inflation
)
total_paid = claim_counts * rng.lognormal(np.log(sev_true), 0.15)
fitter = LossCostTrendFitter(
periods=periods,
claim_counts=claim_counts,
earned_exposure=earned_vehicles,
total_paid=total_paid,
)
result = fitter.fit(
detect_breaks=True, # auto-detect COVID, Ogden rate change
seasonal=True, # quarterly seasonal dummies
)
print(result.combined_trend_rate) # e.g. 0.085 — 8.5% pa loss cost trend
print(result.decompose()) # freq_trend, sev_trend, superimposed
print(result.summary())
With an ONS external index for severity deflation (requires network access):
from insurance_trend import LossCostTrendFitter, ExternalIndex
# Fetch ONS motor repair index (SPPI G4520, 2015=100)
motor_repair_idx = ExternalIndex.from_ons('HPTH')
fitter = LossCostTrendFitter(
periods=periods,
claim_counts=claim_counts,
earned_exposure=earned_vehicles,
total_paid=total_paid,
external_index=motor_repair_idx, # deflates severity; superimposed_inflation() gives residual
)
result = fitter.fit(detect_breaks=True, seasonal=True)
print(result.superimposed_inflation) # trend component not explained by ONS index
Classes
-
FrequencyTrendFitter— log-linear OLS on log(claims/exposure). Optional WLS, quarterly seasonal dummies, structural break detection via ruptures PELT, piecewise refitting on detected breaks, bootstrap CI, local linear trend alternative. -
SeverityTrendFitter— same as frequency, plus optional external index deflation. When an index is supplied, the fit runs on deflated severity andsuperimposed_inflation()gives the residual trend not explained by the index. -
LossCostTrendFitter— wraps the frequency and severity fitters, combines results, providesdecompose()andprojected_loss_cost(). -
ExternalIndex— fetches ONS time series from the public API (no auth required), with a catalogue of UK insurance-relevant codes. Also accepts user-supplied CSV for BCIS and other subscription data.
Why log-linear
The industry baseline. Fits log(y) = alpha + beta*t + seasonal + epsilon via OLS. The annual trend rate is exp(beta * periods_per_year) - 1. The model is transparent, easily explainable to a regulator, and fast enough to bootstrap 1000 replicates in under a second.
The local linear trend alternative (method='local_linear_trend') uses statsmodels UnobservedComponents with a Kalman filter — useful when the trend itself is changing, but requires longer series and is harder to explain.
Structural breaks
The ruptures PELT algorithm runs on the log-transformed series. If a break is detected, the library warns and refits piecewise. The trend rate from the final segment is what gets reported — this is the defensible choice for projection, since you are projecting from the current regime.
Pass changepoints=[8, 20] to impose known breaks (e.g. 2020 Q1, 2025 Q1) rather than using auto-detection.
ONS series catalogue
| Key | ONS code | Description |
|---|---|---|
motor_repair |
HPTH | SPPI G4520 Maintenance and repair of motor vehicles (2015=100) |
motor_insurance_cpi |
L7JE | CPI 12.5.4.1 Motor vehicle insurance |
vehicle_maintenance_rpi |
CZEA | RPI Maintenance of motor vehicles |
building_maintenance |
D7DO | CPI 04.3.2 Services for maintenance and repair of dwellings |
household_maintenance_weights |
CJVD | CPI Weights 04.3 Maintenance and repair |
For household severity, use D7DO as a free proxy. BCIS is more appropriate for reinstatement cost trend — load it via ExternalIndex.from_csv().
Inputs
Aggregate accident-period data. Minimum viable: 6 quarters. Recommended: 12–20 quarters.
| Column | Description |
|---|---|
periods |
Quarter identifiers, e.g. '2020Q1' |
claim_counts |
Number of claims in the period |
earned_exposure |
Earned exposure (vehicle-years, policy-years, etc.) |
total_paid |
Total paid claims |
Both pandas and Polars DataFrames/Series are accepted as inputs. All outputs are Polars.
Installation
pip install insurance-trend
Dependencies
pandas, numpy, statsmodels, scipy, ruptures, matplotlib, requests, polars.
No scikit-learn, TensorFlow, or PyTorch.
Mix adjustment
V1 does not include mix adjustment. If your portfolio composition has shifted (more young drivers, different vehicle types), apparent trends may reflect mix change rather than genuine inflation. Pre-process to mix-adjusted frequency/severity before passing to the fitters if this matters for your use case.
Scope
This library is for pricing trend — forward projection of aggregate accident-period data. It is not a reserving tool. Use chainladder-python for triangle development to ultimate; use insurance-trend for what comes after.
<<<<<<< Updated upstream
Databricks Notebook
A ready-to-run Databricks notebook benchmarking this library against standard approaches is available in burning-cost-examples.
Performance
Benchmarked against a fixed trend assumption (last-12-quarters exposure-weighted OLS on loss cost, standard industry practice) on synthetic UK motor data — 24 quarters with a known structural break at Q13, analogous to a COVID-style frequency collapse combined with post-break severity acceleration. Dataset: quarterly aggregate experience with Poisson frequency noise and log-normal severity noise around a known DGP.
| Metric | Fixed trend (baseline) | insurance-trend | Notes |
|---|---|---|---|
| Loss cost trend bias vs DGP (pa) | blended pre/post-break | post-break only | lower is better; baseline blends two regimes |
| Projection MAPE over +4 quarters | measured at runtime | measured at runtime | expected 10–25% improvement when break is recent |
| Projection error at +4Q | measured at runtime | measured at runtime | expected 15–30% improvement |
| Structural break detected | No | Yes (ruptures PELT) | to within ±2 quarters in a 24-quarter series |
| Frequency/severity decomposition | No | Yes | enables separate loading in reinsurance pricing |
| Fit time | <1s | <30s (500 bootstrap) | bootstrap dominates; reduce to 200 for exploration |
The improvement in trend bias is most pronounced when the structural break falls within the observation window and the pre- and post-break trend rates differ by more than 5 percentage points annually — the scenario the library was designed for. When experience is genuinely stable with no dislocations, the library produces a single segment and returns the same headline trend rate as the fixed approach, with the added benefit of a bootstrap confidence interval.
Run notebooks/benchmark.py on Databricks to reproduce.
Related Libraries
| Library | What it does |
|---|---|
| insurance-whittaker | Whittaker-Henderson graduation for development triangles — smooth the trends before forward projection |
| insurance-dynamics | Loss development models — trend projections inform the development assumptions in reserve models |
| insurance-causal-policy | SDID causal evaluation of rate changes — separates genuine market trends from the effects of pricing actions |
Stashed changes
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
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_trend-0.1.2.tar.gz.
File metadata
- Download URL: insurance_trend-0.1.2.tar.gz
- Upload date:
- Size: 162.6 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 |
69b3f6aae7c8389d6ac0a51632a86957dee873b1a11e8bcc81c1cf07f58a68ac
|
|
| MD5 |
f21e0b49e0d991f199ead5ca56b75f7b
|
|
| BLAKE2b-256 |
47d0167479dc006e323dd2e51d1a3b986992c2c8c1e49a04cf962e40df0d4076
|
File details
Details for the file insurance_trend-0.1.2-py3-none-any.whl.
File metadata
- Download URL: insurance_trend-0.1.2-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 |
25d27e319253ca1a683b92e5a8522ca5aa8a921918b8d5078269cdf90fc45d69
|
|
| MD5 |
f7a7762e173c819075ed5e8c1b1d63c4
|
|
| BLAKE2b-256 |
7fe62f46da921608743bcd81ed70d2ae53858ac34826ff497dd472d2afd0b1c6
|