Skip to main content

Actuarial Neural Additive Model for insurance pricing — interpretable, monotone-constrained, with Poisson/Tweedie/Gamma distributional losses

Project description

insurance-anam

Tests

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

insurance_anam-0.1.1.tar.gz (158.7 kB view details)

Uploaded Source

Built Distribution

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

insurance_anam-0.1.1-py3-none-any.whl (34.3 kB view details)

Uploaded Python 3

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

Hashes for insurance_anam-0.1.1.tar.gz
Algorithm Hash digest
SHA256 843fd5a07306272ef100eb57c0f9f4679d37fcce3c431bdb71344b4568698718
MD5 b4fef478302c43f40b9f7b6d142b3ac4
BLAKE2b-256 54aa39240e850befdb4951fdac1b002fa39e788cbbd7b46d85a6e1bea7aa8872

See more details on using hashes here.

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

Hashes for insurance_anam-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 bd9d651415b27f60e3ac75803dd193b8762c1bfbf2ce9e9187808736fa83c1fd
MD5 b87a00e135e52f716511610676a0e2fe
BLAKE2b-256 ab168173d7bca0a56b33680e009e17cf03cc570506a458ae2ffa8377c3a20ca1

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