easy_glm
Build an insurance GLM, turn it into rating tables, review it and score a
portfolio. easy_glm is for pricing teams that want a regularised model and a
portable table-based scorer without losing the link between the two.
Start with a frequency model. The recommended route is:
- Fit a small, intelligible model with
EasyGLM.fit. - Use the workflow API when you need deliberate design choices, validation and reproducible artefacts.
- Review a saved
.easyglmscorer or open the workbench.
The public French motor sample used below is included in a source checkout, so the examples run offline there. If you installed from PyPI, replace the sample path with your own Polars frame. Every Python block on this page is tested in order.
Install
pip install easy_glm
# Optional browser workbench and relativity editor
pip install "easy_glm[ui]"
Start here: a first frequency model
The data you need
Use a Polars DataFrame with one policy period per row. It needs:
- a non-negative claim count;
- positive exposure, used both as the frequency denominator and the credibility weight;
- predictor columns; and
- a train/holdout flag:
1for fitting and0for validation.
The bundled example calls the count ClaimNb and time on risk Exposure.
With divide_target_by_weight=True, the model fits claim frequency:
ClaimNb / Exposure.
This first model uses four familiar motor variables. EasyGLM.fit chooses
banded factors for numeric variables and treats text variables as
categoricals. It is the recommended default while you are learning the book.
from pathlib import Path
import numpy as np
import polars as pl
import easy_glm
DATA = Path("tests/fixtures/french_motor_50k.parquet")
df = pl.read_parquet(DATA)
rng = np.random.default_rng(42)
df = df.with_columns(
pl.Series("traintest", rng.random(len(df)) < 0.7, dtype=pl.Int64)
)
PREDICTORS = ["DrivAge", "Region", "BonusMalus", "Density"]
model = easy_glm.EasyGLM.fit(
data=df,
target="ClaimNb",
model_type="Poisson",
predictors=PREDICTORS,
weight_col="Exposure",
train_test_col="traintest",
divide_target_by_weight=True,
alpha=0.001, # fixed penalty for a quick iteration; use cv=5 for production selection
)
print(model)
print(f"Base rate: {model.rate_model.base_rate:.5f}")
The result contains a base rate and one table per factor. The relativity is a
multiplier on the base rate; 1.00 is the selected base band or level. For a
frequency model, expected claims for a policy period are exposure × base
rate × every applicable relativity. One table value is not a price by itself.
print(model.relativities["BonusMalus"].select("label", "relativity", "exposure"))
holdout = df.filter(pl.col("traintest") == 0)
expected = model.rate_model.predict(holdout)
ae = holdout["ClaimNb"].sum() / expected.sum()
print(f"Holdout A/E: {ae:.3f}")
Read this first. Overall A/E compares observed and expected claims. Values near 1 are a calibration check, not evidence that every factor is well modelled. Review results by factor before changing a price.
Save the scorer when you want to review or deploy it. A .easyglm file is
portable JSON; it contains the tables and does not need glum to score.
model.rate_model.to_json("my_model.easyglm")
print("Wrote my_model.easyglm")
Rare, unseen and missing categorical values score through the table's Other / Unknown row. Check that row when you review a new portfolio: it is the model's safe fallback, not evidence that the new value has its own fitted relativity.
Next: a model you can explain and validate
Use the lower-level pipeline when you need to control how factors are built. It separates three decisions:
| Decision | Recommended starting point | Why it matters |
|---|---|---|
| Factor form | Numeric variables as bands; text as categoricals | Gives tables that are easy to inspect |
| Penalty | cv=5 for a model you expect to use |
Chooses the amount of shrinkage from the data |
| Shape | Add monotonicity only where the business relationship warrants it | Prevents an implausible fitted curve |
Here Density is a smooth piecewise-linear curve, while BonusMalus is
constrained to rise. Build the specification on training data only: that
prevents holdout information affecting knots or category levels.
from easy_glm import DesignSpec, fit_glm, rate_tables, to_rate_model
train = df.filter(pl.col("traintest") == 1)
spec = DesignSpec.from_data(
train,
PREDICTORS,
weight_col="Exposure",
linear=["Density"],
)
fit = fit_glm(
train,
spec,
target="ClaimNb",
family="poisson",
weight_col="Exposure",
divide_target_by_weight=True,
alpha=0.001, # tutorial shortcut; use cv=5 when selecting a production penalty
monotone={"BonusMalus": "increasing"},
)
tables = rate_tables(fit)
rate_model = to_rate_model(fit, exposure_col="Exposure")
print(tables["Density"].select("label", "relativity", "exposure").head(4))
Validate on the holdout. Inspect the spread of A/E by band or level, not a second total across bins: the bins already add back to overall A/E.
from easy_glm.workflow import ae_by_variable
actual = holdout["ClaimNb"].to_numpy()
expected = rate_model.predict(holdout)
weight = holdout["Exposure"].to_numpy()
by_band = ae_by_variable(holdout, "BonusMalus", actual, expected, weight)
print(by_band.select("label", "exposure", "ae").head(5))
print(f"A/E range: {by_band['ae'].min():.2f}–{by_band['ae'].max():.2f}")
Watch-out. Tables reproduce the fitted GLM exactly before manual edits. If you cap, smooth or round a relativity, total expected claims can move. Use the review tools to measure that change, then rebalance the base rate if the commercial objective is to keep the portfolio total unchanged.
For a complete, commented version of this workflow, run:
python examples/advanced_pipeline.py
Review a fitted model
Keep fitting and review separate. The review example reads the scorer written above rather than fitting a fresh model. It prints factor-level A/E, shows how to make and assess a table change, and retains a snapshot for comparison.
python examples/exploring_fit.py my_model.easyglm
To score a new portfolio or map different source column names to the saved model, use the scorer example:
python examples/scoring_editor.py my_model.easyglm
The scoring promise is simple: the unedited RateModel gives the same
per-unit predictions as the fitted GLM, including nulls and unseen categories.
You can check it whenever you need an audit trail:
per_unit_model = to_rate_model(fit, exposure_col="Exposure")
assert np.allclose(
per_unit_model.predict(holdout, exposure_col=None),
fit.predict(holdout),
rtol=1e-10,
)
print("Table scorer agrees with the fitted model.")
Use the workbench
The workbench is the visual route through the same workflow: set data roles,
choose factor designs, fit, inspect diagnostics, review tables and export a
scorer. Start in the browser, or run
python examples/easy_glm_demo.py to create a ready-to-open project file.
easy-glm-workbench
This opens an empty workbench. To open an existing project, pass its file:
easy-glm-workbench project.easyglm-project.json
# From a source checkout:
python -m easy_glm.app project.easyglm-project.json
For a browser-based review of an existing scorer, launch the editor from a fitted model. It opens a working copy; the original model is left unchanged.
model.rate_model.launch_editor(data=df, port=8501)
Specialist recipes
Choose these when the business problem calls for them, not as part of the first pass through the package.
| Need | Recipe | Key caveat |
|---|---|---|
| Rate change from current premium | examples/rate_change.py |
The premium becomes an offset; solve the base rate for the target loss ratio |
| Lapse or conversion | examples/lapse_model.py |
Tables are odds relativities and predictions are probabilities, not amounts |
| Large portfolio | examples/large_book.py --rows 1000000 |
The compact design is selected automatically for large books |
| Interactions | fit_two_stage and the interaction guide |
Main tables remain fixed; cells are adjustments on top |
| Command line and reproducible exports | easy-glm run project.json --out artefacts/ |
Artefact commands refit from the project data |
Where to go next
- Intermediate workflow example: design, validation, tables and a portable scorer.
- Review example: inspect a saved model and test a table adjustment.
- Scoring example: score new business and map source columns.
- Workbench project example: create a project file, then open it in the browser.
- Rate-change recipe, lapse recipe and large-book recipe.
- Workbench plan and technical checks for implementation detail and evidence behind the guarantees.
Command line
easy-glm validate project.json
easy-glm run project.json --out artefacts/
easy-glm export project.json --script --excel --report
Development
black . && ruff check . && mypy src/easy_glm/core src/easy_glm/workflow --ignore-missing-imports && pytest -q
MIT licensed. See LICENSE.
Release files for easy-glm 0.4.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| easy_glm-0.4.1.tar.gz | 381.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| easy_glm-0.4.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 645.4 kB
Release files / easy_glm-0.4.1.tar.gz
| Download URL | easy_glm-0.4.1.tar.gz |
|---|---|
| Size | 381.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d1c87deafb1f3acb40449924109a2a1f2e10b9811f7a97254d8247c0c709ac8d
|
|
BLAKE2b-256 checksum How to use checksums |
9326527a8623a72530b21ccbe5121db320b5a7296729cefa3376420790ca39b5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 3, 2026.
Transparency logRelease files / easy_glm-0.4.1-py3-none-any.whl
| Download URL | easy_glm-0.4.1-py3-none-any.whl |
|---|---|
| Size | 263.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
53ceeb12bb4d07ac03f6b4db35818fa1955fdbdcc2387e7b0e7edadc4ba52eb6
|
|
BLAKE2b-256 checksum How to use checksums |
abbe8ee17066b1d69f8c99027da13722fa2bd3407ba25a9f24af6bfc712ed72e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 3, 2026.
Transparency log