Dynamic insurance pricing models: GAS score-driven filters and Bayesian change-point detection
Project description
insurance-dynamics
Dynamic insurance pricing models for UK pricing teams.
Two problems, one package: tracking how your loss ratios are changing right now (GAS), and detecting when they shifted structurally in the past (changepoint).
Merged from: insurance-gas (GAS score-driven models) and insurance-changepoint (BOCPD/PELT detection).
Blog post: Tracking Trend Between Model Updates with GAS Filters
The problems this solves
GAS models (insurance_dynamics.gas): Your GLM gives you a static relativity. Reality gives you frequency that drifts quarter by quarter. GAS (Generalised Autoregressive Score) models fit a time-varying parameter model to claim counts, severities, or loss ratios — producing a filter path that shows you how the underlying rate has moved, with confidence bands from parametric bootstrap.
Changepoint detection (insurance_dynamics.changepoint): You suspect something changed — whiplash rules, Ogden rate, a competitor exit. BOCPD tells you online whether this month looks like a break. PELT gives you retrospective break locations with bootstrap confidence intervals. The LossRatioMonitor combines both into a 'retrain / monitor' recommendation suitable for model governance.
Subpackages
insurance_dynamics.gas
GASModel— fits GAS(p,q) to a univariate series; Poisson, Gamma, NegBin, LogNormal, Beta, ZIPGASPanel— fits the same spec across a set of rating cellsgas_forecast— h-step-ahead forecasts with simulation intervalsbootstrap_ci— parametric bootstrap confidence intervals on the filter pathcompute_diagnostics— Dawid-Sebastiani score, PIT histograms, autocorrelation of score residuals
insurance_dynamics.changepoint
FrequencyChangeDetector— online BOCPD for claim frequency with exposure weightingSeverityChangeDetector— online BOCPD on log-severity using Normal-Gamma conjugateLossRatioMonitor— joint monitoring returning retrain/monitor recommendationRetrospectiveBreakFinder— offline PELT with bootstrap confidence intervalsUKEventPrior— UK insurance event calendar (Ogden, whiplash reform, FCA pricing review)ConsumerDutyReport— FCA PRIN 2A.9 evidence pack as HTML and JSON
Installation
uv add insurance-dynamics
Questions or feedback? Start a Discussion. Found it useful? A star helps others find it.
Quick start
The GAS example uses the built-in dataset loader — no data setup required:
from insurance_dynamics.gas import GASModel
from insurance_dynamics.gas.datasets import load_motor_frequency
data = load_motor_frequency(T=48)
model = GASModel("poisson")
result = model.fit(data.y, exposure=data.exposure)
print(result.summary())
result.filter_path.plot()
The changepoint monitor requires time series arrays. Here is a minimal self-contained example with 60 months of synthetic UK motor data including a regime shift at month 36:
import numpy as np
from insurance_dynamics.changepoint import LossRatioMonitor
rng = np.random.default_rng(42)
T = 60
# Monthly exposure (earned car years)
exposures = rng.uniform(800, 1200, T)
# Claim frequency: base 0.08/year, step up 37.5% at month 36
true_rate = np.where(np.arange(T) < 36, 0.08, 0.11)
counts = rng.poisson(true_rate * exposures)
# Mean severity (log-normal, slight upward drift)
mean_sev = 1500 * np.exp(0.003 * np.arange(T)) * rng.lognormal(0, 0.15, T)
# Period labels — sequential months across years
periods = [f"{2021 + i // 12}-{(i % 12) + 1:02d}" for i in range(T)]
monitor = LossRatioMonitor(lines=["motor"], uk_events=True)
result = monitor.monitor(
claim_counts=counts,
exposures=exposures,
mean_severities=mean_sev,
periods=periods,
)
print(result.recommendation) # 'retrain' or 'monitor'
Design decisions
GAS and changepoint detection are complementary tools. GAS smooths the signal continuously; BOCPD detects discrete jumps. Using both gives you a full picture: is the drift smooth (GAS will catch it) or structural (BOCPD will flag it)? The LossRatioMonitor is deliberately opinionated — it returns a binary recommendation, not a probability, because pricing teams need an action, not a number.
Databricks Benchmark
A full benchmark notebook is in databricks/benchmark_gas_vs_rolling.py. It benchmarks GAS filters against rolling window averages and a static Poisson GLM on synthetic UK motor frequency data with two regime changes: a gradual upward drift followed by a sharp step down.
Run it directly on Databricks serverless compute — no external data required.
Performance: GAS vs Rolling Windows vs Static GLM
Benchmarked on 72 months of synthetic UK motor claim frequency (4,500 vehicle-years/month, gradual drift in months 0-35, sharp step change at month 36). DGP: true rate rises from 0.065 to 0.090 during phase 1, then steps down to 0.055 at month 36. Results from databricks/benchmark_gas_vs_rolling.py (Databricks serverless, 2026-03-22, seed=42).
The key question: does GAS adapt faster than rolling windows after a regime change?
Adaptation speed (months to reach within 10% of true post-break rate):
- 3-month rolling: adapts within 3-4 months (window flushes fast but lags the full series)
- 6-month rolling: adapts within 5-7 months (slower flush of pre-break observations)
- GAS Poisson: adapts within 2-3 months (score-driven update reacts immediately)
GAS consistently wins on RMSE vs the true rate — the score-driven update uses the Poisson gradient, which is exposure-weighted and proportional to the surprise relative to the current estimate. Rolling windows give equal weight to all months in the window regardless of exposure.
On log-likelihood, GAS is strictly best — it was estimated by MLE on the full series, so it finds the parameters that maximise the probability of the observed sequence. This reflects better distributional calibration throughout, not just around breaks.
The honest caveat: on smooth drift, a 3-month rolling window is competitive with GAS. The advantage of GAS is clearest at sharp structural breaks and when the persistence parameter (phi) is not well-calibrated to a simple rolling window length. For a series with no breaks and no drift, a rolling average and GAS will converge to similar results.
| Method | MAE (post-break) | RMSE vs true (post-break) | Log-likelihood |
|---|---|---|---|
| Static GLM | Worst (blended) | Worst | Lowest |
| Rolling 6-month | Moderate | Moderate | N/A (not probabilistic) |
| Rolling 3-month | Better | Better | N/A |
| GAS Poisson | Best | Best | Highest |
When to use GAS: Monthly or quarterly aggregate claim data where the underlying rate may be drifting or subject to structural events. GAS adapts continuously; it does not require you to choose a window length.
When NOT to use GAS: Cross-sectional risk factor estimation (that is a GLM job). Series shorter than ~20 months (insufficient to estimate omega/alpha/phi reliably). Any context where an underwriter needs to reproduce the calculation in Excel.
Performance (Previous Benchmark)
Benchmarked against a static Poisson GLM (intercept-only and linear trend variants) on 60 months of synthetic UK motor frequency data with a known regime shift at month 36 (+37.5% step increase in claim frequency). Results from benchmarks/benchmark.py run locally (Raspberry Pi ARM64) on 2026-03-16.
| Model | MAE (all periods) | MAE (post-break) | RMSE vs true lambda (post-break) | Log-likelihood |
|---|---|---|---|---|
| GLM constant | 0.014980 | 0.018468 | 0.019104 | -289.0 |
| GLM trend | 0.009697 | 0.009918 | 0.008795 | -236.3 |
| GAS Poisson | 0.008438 | 0.010684 | 0.007540 | -231.7 |
GAS Poisson vs best static baseline (post-break):
- MAE overall: +13.0% improvement vs GLM trend
- MAE post-break: -7.7% (GLM trend has slightly lower post-break MAE on this DGP — within noise)
- RMSE vs true lambda: +14.3% improvement
- Log-likelihood: +4.6 (better distributional fit throughout the series)
- Post-break mean filter rate: 0.1059 (true = 0.110); converges to 0.1087 by month 48
The GAS filter wins clearly on RMSE against the true lambda schedule — it tracks the step more accurately than a blended linear trend. The MAE result at post-break is within stochastic noise: GAS has higher post-break MAE on this particular sample but lower MAE across all 60 periods, and the RMSE (which measures accuracy against the known true rate, not the noisy observations) consistently favours GAS. Log-likelihood improvement reflects a better-calibrated model throughout the series.
Related Libraries
| Library | What it does |
|---|---|
| insurance-trend | Forward trend projection — trend fits feed into the dynamic projection models |
| insurance-severity | Heavy-tail severity with composite Pareto models — dynamics models require severity projections for large loss exposure |
| insurance-monitoring | Model monitoring — tracks whether dynamic projections remain calibrated over time |
License
MIT
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_dynamics-0.1.4.tar.gz.
File metadata
- Download URL: insurance_dynamics-0.1.4.tar.gz
- Upload date:
- Size: 210.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
406f240906b3153d84eb690be317e3c066eb309733248bef2448a0a3ddfc5aaf
|
|
| MD5 |
2c23de54afb7cf1a0204e8fcd88009f6
|
|
| BLAKE2b-256 |
2a159d7bb99decddc6cc1a272493a09ad0dc25a6aa0f1903f74dcfee9fec2452
|
File details
Details for the file insurance_dynamics-0.1.4-py3-none-any.whl.
File metadata
- Download URL: insurance_dynamics-0.1.4-py3-none-any.whl
- Upload date:
- Size: 73.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
526589849e806e79806a39b8f90f8407ca93129103a69770fbfc215bf0b5e0cf
|
|
| MD5 |
b0fda9e85c5ebb46dd4bd973f8b1a820
|
|
| BLAKE2b-256 |
c274297d358c3f84984c6ebf86c2893c9f7969d99ee063559d19309d01a4f96a
|