Actuarial Neural Additive Model for insurance pricing — interpretable, monotone-constrained, with Poisson/Tweedie/Gamma distributional losses
Project description
insurance-anam
Actuarial Neural Additive Model for insurance pricing. A production-quality Python library implementing the ANAM architecture from Laub, Pho, Wong (2025).
The problem
GLMs are interpretable and well-understood by actuaries and regulators. Neural networks fit better but are black boxes. EBMs and GAMs sit in between, but none of them natively support:
- Poisson/Tweedie/Gamma distributional losses (not MSE)
- Mathematically guaranteed monotonicity constraints
- Exposure weights handled correctly at the loss level
- Output that reads like a GLM factor table
ANAM fills this gap. It's a neural network that an actuary can present to Lloyd's, the PRA, or a reinsurer.
What ANAM is
One MLP subnetwork per feature. The model computes:
eta = bias + f_1(x_1) + f_2(x_2) + ... + f_p(x_p) [+ interactions]
mu = exp(eta + log(exposure))
y ~ Poisson(mu)
Because it's purely additive, every feature's contribution is visible in isolation — exactly like a GLM marginal effect plot. Because it's a neural network, the shape functions can capture non-linearity that a GLM would need polynomial or spline terms to approximate.
Actuarial-specific features:
- Poisson, Tweedie, and Gamma deviance losses
- Monotonicity constraints via Dykstra's projection algorithm (mathematically guaranteed, not post-hoc)
- Smoothness regularisation (second-order difference penalty)
- Exposure offset handled as
log(exposure)in the linear predictor — same as a GLM offset - Shape function export as Polars DataFrames for regulatory documentation
- sklearn-compatible API (
fit,predict,score)
Install
pip install insurance-anam
Requires Python >= 3.10, PyTorch >= 2.0.
Quick start
from insurance_anam import ANAM
model = ANAM(
loss="poisson",
monotone_increasing=["vehicle_age"],
monotone_decreasing=["ncd_steps"],
categorical_features=["region", "vehicle_type"],
hidden_sizes=[64, 32],
n_epochs=100,
verbose=10,
)
model.fit(X_train, y_train, sample_weight=exposure_train)
y_pred = model.predict(X_test, exposure=exposure_test)
# Shape functions (GLM-style marginal effects)
shapes = model.shape_functions()
shapes["vehicle_age"].plot()
# Export as Polars DataFrame for regulatory review
from insurance_anam import shapes_to_relativity_table
rel_table = shapes_to_relativity_table(shapes)
Monotonicity constraints
Monotonicity is enforced by the Dykstra projection algorithm: after each gradient step, the weight matrices in monotone-constrained subnetworks are clamped to the non-negative (or non-positive) orthant. For a ReLU network, this guarantees a non-decreasing (or non-increasing) output — not as a soft penalty, but as a hard constraint.
model = ANAM(
monotone_increasing=["vehicle_age", "bonus_malus"],
monotone_decreasing=["ncd_steps", "years_no_claims"],
loss="poisson",
)
Constraint is verified: after project_monotone_weights(), the output is guaranteed monotone for any input in the training range.
Loss functions
| Loss | Distribution | Use case |
|---|---|---|
"poisson" |
Poisson | Claim frequency |
"tweedie" |
Tweedie (power p) | Pure premium (frequency × severity) |
"gamma" |
Gamma | Claim severity (positive, right-skewed) |
"mse" |
Gaussian | Continuous targets |
Set tweedie_p (default 1.5) for the compound Poisson-Gamma mix. Values near 1 are Poisson-like; values near 2 are Gamma-like.
Shape function export
shapes = model.shape_functions(n_points=200)
# As a relativity table (GLM-equivalent multiplicative factors)
sf = shapes["driver_age"]
rel_df = sf.to_relativities(base_level=40.0) # base = 40-year-old driver
# As JSON for documentation systems
json_str = sf.to_json()
# Polars DataFrame
df = sf.to_polars()
Categorical features export as bar charts and category-indexed DataFrames.
Interaction terms
from insurance_anam import ANAM, InteractionConfig
model = ANAM(
interaction_pairs=[
("driver_age", "vehicle_age"),
("region", "vehicle_type"),
],
...
)
Interaction pairs can be screened from data:
from insurance_anam import select_interactions_correlation, select_interactions_residual
# Correlation-based screening (pre-fit)
pairs = select_interactions_correlation(X_train, feature_names, threshold=0.3, top_k=5)
# Residual-based screening (post-fit)
y_resid = y_train - model.predict(X_train)
pairs = select_interactions_residual(X_train, y_resid, feature_names, top_k=5)
Comparing to a GLM
from insurance_anam import compare_shapes_to_glm
# GLM log-relativities from your existing production model
glm_coefficients = {
"driver_age": {"25.0": 0.45, "40.0": 0.0, "65.0": 0.22},
"region": {"0": 0.0, "1": 0.18, "2": -0.09, "3": 0.28},
}
comparison = compare_shapes_to_glm(shapes, glm_coefficients)
print(comparison)
Feature importance
fi = model.feature_importance()
# Returns Polars DataFrame sorted by importance descending
Importance is the L2 norm of the subnetwork weights. A quick heuristic for feature selection — not a replacement for permutation importance.
Architecture
insurance_anam/
├── feature_network.py — FeatureNetwork, CategoricalFeatureNetwork
├── interaction_network.py — InteractionNetwork (pairwise)
├── model.py — ANAMModel (orchestrator)
├── losses.py — Poisson, Tweedie, Gamma, Bernoulli deviance + penalties
├── trainer.py — Training loop with early stopping + monotonicity projection
├── shapes.py — ShapeFunction, extract_shape_functions, plot_all_shapes
├── api.py — ANAM (sklearn wrapper)
└── utils.py — Interaction selection, GLM comparison, StandardScaler
Databricks notebook
A full worked example with synthetic data, shape function comparison to ground truth, and relativity export is in notebooks/anam_demo.py.
Citation
@article{laub2025anam,
title = {An Interpretable Deep Learning Model for General Insurance Pricing},
author = {Laub, Patrick J. and Pho, Tu and Wong, Bernard},
journal = {arXiv preprint arXiv:2509.08467},
year = {2025}
}
License
MIT. Built by Burning Cost.
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_anam-0.1.1.tar.gz.
File metadata
- Download URL: insurance_anam-0.1.1.tar.gz
- Upload date:
- Size: 158.7 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 |
843fd5a07306272ef100eb57c0f9f4679d37fcce3c431bdb71344b4568698718
|
|
| MD5 |
b4fef478302c43f40b9f7b6d142b3ac4
|
|
| BLAKE2b-256 |
54aa39240e850befdb4951fdac1b002fa39e788cbbd7b46d85a6e1bea7aa8872
|
File details
Details for the file insurance_anam-0.1.1-py3-none-any.whl.
File metadata
- Download URL: insurance_anam-0.1.1-py3-none-any.whl
- Upload date:
- Size: 34.3 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 |
bd9d651415b27f60e3ac75803dd193b8762c1bfbf2ce9e9187808736fa83c1fd
|
|
| MD5 |
b87a00e135e52f716511610676a0e2fe
|
|
| BLAKE2b-256 |
ab168173d7bca0a56b33680e009e17cf03cc570506a458ae2ffa8377c3a20ca1
|