GAS (Generalised Autoregressive Score) models for dynamic insurance pricing
Project description
insurance-gas
GAS (Generalised Autoregressive Score) models for dynamic insurance pricing.
The problem
UK pricing actuaries re-fit GLMs annually or quarterly. Between model updates, the world moves — frequency rises after a cold winter, severity drifts with parts inflation, a new distribution channel shifts the portfolio mix. Static models miss these changes until the next update cycle.
GAS models solve this by updating a time-varying parameter at each observation using the score (gradient) of the log-likelihood as a forcing variable. The update is proportional to how surprising the observation is, given what we currently believe. This is economical: the likelihood is still a simple product of densities, so MLE is straightforward L-BFGS-B.
The GAS recursion for a time-varying parameter f (on the link scale) is:
f_{t+1} = omega + alpha * S(f_t) * nabla(y_t, f_t) + phi * f_t
where nabla is the score, S is the scaling matrix (inverse Fisher information), alpha controls how fast the filter reacts, and phi controls how persistent the current level is.
This is the approach of Creal, Koopman & Lucas (2013) and Harvey (2013). In R, gasmodel implements 36 distributions. This library provides the actuarially relevant subset in Python, with exposure offsets and pricing team output formats.
Installation
pip install insurance-gas
Quick start
from insurance_gas import GASModel
from insurance_gas.datasets import load_motor_frequency
data = load_motor_frequency(T=60, trend_break=True)
model = GASModel(
distribution="poisson", # claim frequency
p=1, q=1, # GAS(1,1)
scaling="fisher_inv", # optimal for this distribution class
)
result = model.fit(data.y, exposure=data.exposure)
print(result.summary())
# Time-varying rate (natural scale, claims per unit exposure)
result.filter_path.plot()
# Trend index for actuarial sign-off: base period = 100
result.trend_index.plot()
# Relativities vs the time-average
result.relativities(base="mean")
Distributions
| String key | Class | Use case | Link |
|---|---|---|---|
poisson |
PoissonGAS |
Claim frequency | log |
gamma |
GammaGAS |
Claim severity | log |
negbin |
NegBinGAS |
Overdispersed frequency | log |
lognormal |
LogNormalGAS |
Severity (heavy right tail) | identity on log-mean |
beta |
BetaGAS |
Loss ratios on (0,1) | logit |
zip |
ZIPGAS |
Zero-inflated frequency | log + logit |
Result object
model.fit() returns a GASResult with:
result.filter_path # pd.DataFrame: time-varying params at each t
result.trend_index # same, re-scaled to base=100
result.relativities() # ratios vs mean or first period
result.log_likelihood # total log-likelihood at MLE
result.aic, result.bic
result.params # fitted parameter dict
result.std_errors # from numerical Hessian
result.score_residuals # standardised: should be ~iid(0,1)
result.summary() # formatted coefficient table
result.forecast(h=6) # h-step-ahead forecast
result.diagnostics() # PIT test, Ljung-Box, Dawid-Sebastiani
result.bootstrap_ci() # parametric bootstrap CI on filter path
Forecasting
fc = result.forecast(
h=12,
method="simulate", # 'mean_path' or 'simulate'
quantiles=[0.1, 0.5, 0.9],
n_sim=1000,
)
fc.plot()
df = fc.to_dataframe() # h x (mean + quantile columns)
Panel data (multiple rating cells)
from insurance_gas import GASPanel
panel = GASPanel("poisson")
panel_result = panel.fit(
data, # DataFrame with period, cell_id, claims, exposure
y_col="claims",
period_col="period",
cell_col="vehicle_class",
exposure_col="exposure",
)
panel_result.trend_summary() # wide DataFrame: periods x cells
panel_result.filter_paths # dict[cell_id, DataFrame]
Diagnostics
diag = result.diagnostics()
print(diag.summary())
# PIT histogram (should be uniform for well-specified model)
diag.pit_histogram()
# ACF of score residuals (no remaining autocorrelation → model adequate)
diag.score_residuals_acf()
Scaling options
The scaling matrix S in the GAS recursion matters for how quickly the filter adapts and how robust it is to outliers:
unit— no scaling (raw score). Simplest; can over-react to outliers in heavy-tailed distributions.fisher_inv— multiply by inverse Fisher information. Optimal for distributions close to the Gaussian. Default.fisher_inv_sqrt— more robust; down-weights large scores more aggressively.
Design decisions
Why observation-driven rather than parameter-driven? State-space models (Kalman filter) require integrating out the latent state. GAS models are observation-driven: the filter is a deterministic function of past data, so the likelihood is a product of closed-form densities. MLE is a standard L-BFGS-B optimisation — no MCMC, no expectation-maximisation.
Why not use PyFlux? PyFlux (2017) implements GAS but is unmaintained and incompatible with modern Python. This library is built against Python 3.10+ and modern NumPy/SciPy.
Why expose relativities rather than filter parameters? Actuaries work in relativities. Exposing f_t = log(mu_t) would require them to exponentiate and normalise. The trend_index and relativities() output are in the same format as development factor analyses.
On standard errors: they come from the numerical Hessian of the log-likelihood. This is consistent but can be unreliable in small samples (< 30 observations). Use bootstrap_ci() for credible uncertainty quantification in small portfolios.
References
- Creal, D., Koopman, S.J. and Lucas, A. (2013). 'Generalized Autoregressive Score Models with Applications.' Journal of Applied Econometrics, 28(5):777–795.
- Harvey, A.C. (2013). Dynamic Models for Volatility and Heavy Tails. Cambridge University Press.
- Holy, V. and Zouhar, J. (2024). 'gasmodel: GAS models in R.' arXiv:2405.05073.
License
MIT
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_gas-0.1.1.tar.gz.
File metadata
- Download URL: insurance_gas-0.1.1.tar.gz
- Upload date:
- Size: 37.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 |
8ce0711cc10c065ec606b605f6de4a1dc01d12a4aaeb3d3c53f2ac2fd0d8b16f
|
|
| MD5 |
3fb80afbeb47305f23e564ad540f9ab6
|
|
| BLAKE2b-256 |
29be1b56fc8003bbd1fca6fbb265ff81acd8ae086edc34ccba7a9f0c90e16535
|
File details
Details for the file insurance_gas-0.1.1-py3-none-any.whl.
File metadata
- Download URL: insurance_gas-0.1.1-py3-none-any.whl
- Upload date:
- Size: 34.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 |
4063f510d531ed93a42ea043827f1a668d0c3e008d9df0fa9d495c76c48cb9a8
|
|
| MD5 |
17051ce0092cb14e7fa8fe0ecac06ea4
|
|
| BLAKE2b-256 |
a0b77fff6d7f455dd1412bbdd07b7b248100c3a216f68e4a2735586aa661655f
|