Models and evaluation API
The estimators share the scikit-learn protocol and keep special columns inside
X. Metrics calculate actuarial evidence; plot functions render it. Start with
model choice and fitting, then use the
diagnostics and visualization guide
for complete table and chart recipes.
Targets and explicit weights must be finite one-dimensional vectors with one
value per row. Model predictors must keep their fitted names and order;
predict and score reject renamed, reordered, missing, or extra predictors.
Prediction can omit exposure and frequency–severity outcome columns. Scoring
uses exposure by default; explicit sample_weight also works without exposure.
Weights must be non-negative with a positive total, and default exposure weights
must meet the one-day floor.
| Module | Public API | Purpose |
|---|---|---|
azoic.models |
RiskGLM, RiskGBM, FrequencySeverityModel |
scikit-learn-compatible pure-premium estimators with specials inside X |
azoic.metrics |
gini, lorenz, calibration_table, stability_table, one_way_table, double_lift_table, op_ratio, mean_tweedie_deviance, mean_poisson_deviance, mean_gamma_deviance |
Actuarial diagnostics as values and tables |
azoic.plots |
plot_lorenz, plot_lift, plot_calibration, plot_one_way, plot_double_lift, plot_actual_vs_predicted, model_colors, azoic_style |
Matplotlib rendering of the metric results |
Pure-premium model estimators: GLM (glum), GBM (LightGBM), freq x severity.
Special columns travel inside X per AGENTS.md rule 8 so pipelines, GridSearchCV
and cross_val_score keep working: pass exposure_col (and, for the
FrequencySeverityModel, claim_count_col / claim_amount_col) to the
estimator and it pops those columns from X inside fit / predict.
The exposure_col on a sub-estimator is really a weight column -- popped
from X and routed to the backend as sample_weight. The pure-premium
convention (PRD section 5 rule 1): y = claim_amount / exposure and
sample_weight = exposure.
FrequencySeverityModel.fit applies the actuarial severity filter (claim_count
0, since Gamma needs y > 0) -- never in user code (rule 3).
FrequencySeverityModel
Bases: RegressorMixin, BaseEstimator
Frequency x severity pure-premium meta-estimator.
Fits freq on per-row claim frequency (claim_count / exposure,
sample_weight exposure) and sev on per-claim severity
(claim_amount / claim_count, sample_weight claim_count) using only
rows where claim_count > 0 (Gamma needs y > 0). The filter lives here
per AGENTS.md rule 3, never in user code. Before deriving rates or fitting
either component, exposure must be positive and finite, counts and amounts
non-negative and finite, and count and amount zero or positive together.
Invalid outcomes raise ValueError even when y is omitted.
fit(X, y) accepts the same pure-premium rate target as RiskGLM
and RiskGBM and validates it against claim_amount / exposure.
y may be omitted because the outcome columns already travel inside
X. predict returns rates and score compares rate targets.
Sub-estimators are duck-typed: anything exposing
fit(X, y, sample_weight=...) and predict(X) works -- RiskGLM,
RiskGBM, sklearn's own GLM estimators, etc.
Parameters
freq, sev : estimator Sub-estimators for frequency and severity. Required. exposure_col, claim_count_col, claim_amount_col : str Names of the special columns inside X.
Attributes
freq_, sev_ : fitted sub-estimators (clones of freq / sev)
n_features_in_, feature_names_in_
Warns
UserWarning If a categorical level observed in the full training data has no positive-claim row available to fit severity.
score(X, y, sample_weight=None)
D^2 (Tweedie, p=1.5) on pure-premium rates, weighted by exposure.
y is the pure-premium rate target. Exposure from X is the
default weight; an explicit sample_weight overrides it and works
without an exposure column. Weights must be finite, non-negative,
one-dimensional, and not all zero; popped exposure must be at least
one day. Prediction may omit all special columns, but predictors must
retain their fitted names and order.
RiskGBM
Bases: RegressorMixin, BaseEstimator
Gradient boosted tree model on top of lightgbm.LGBMRegressor.
fit and score take per-exposure rate targets; predict returns
rates. Targets must be finite one-dimensional vectors with one value per
row. Prediction and scoring require the fitted predictor names and order.
When exposure_col is set, exposure supplies the default sample weight
and is excluded from the backend features; prediction may omit it.
Parameters
objective : str
LightGBM objective: "tweedie" (pure-premium default), "poisson",
"gamma", "regression", "rmse", ...
exposure_col : str | None
Column in X popped and forwarded to LightGBM as sample_weight.
tweedie_variance_power : float
Tweedie power; validated in __init__ to lie in [1.0, 2.0)
(PRD rule 4) when objective == "tweedie".
monotone_constraints : dict[str, int] | sequence[int] | None
Per-feature monotonicity for LightGBM's tree splits. dict keys are
post-exposure feature names (missing keys default to 0); sequence
must be one-dimensional with exactly that post-exposure feature count.
Values must be in {-1, 0, 1} (1 = increasing, -1 =
decreasing, 0 = unconstrained). Nonzero constraints on categorical
columns are rejected before LightGBM is invoked. None = no constraints.
num_leaves, max_depth, learning_rate, n_estimators, min_child_samples,
subsample, subsample_freq, colsample_bytree, reg_alpha, reg_lambda,
random_state, n_jobs, verbose
Forwarded explicitly to LightGBM (no **kwargs; LightGBM kwargs break
get_params per AGENTS.md).
Attributes
backend_ : LGBMRegressor The fitted LightGBM estimator. feature_names_in_, n_features_in_
score(X, y, sample_weight=None)
D^2 of the deviance matching objective (Tweedie power map).
Only objectives in _GBM_OBJECTIVE_POWER (poisson, gamma, the L2
family) plus tweedie are scorable this way; anything else raises --
pass an explicit scoring= to GridSearchCV / cross_val_score.
RiskGLM
Bases: RegressorMixin, BaseEstimator
Generalized linear model on top of glum.GeneralizedLinearRegressor.
fit and score take per-exposure rate targets; predict returns
rates. Targets must be finite one-dimensional vectors with one value per
row. Prediction and scoring require the fitted predictor names and order.
When exposure_col is set, exposure supplies the default sample weight
and is excluded from the backend features; prediction may omit it.
Parameters
family : str
Glum family name: "normal", "poisson", "gamma", "tweedie",
"binomial", "inverse.gaussian", "negative.binomial". "tweedie"`
defaults to power 1.5 unlesstweedie_poweris given.
link : str
Glum link:"auto","log","identity","sqrt", ...
exposure_col : str | None
Column in X to pop and forward to glum assample_weight(the
exposure-weight pure-premium convention). Other special columns
(e.g.claim_count/claim_amount) are NOT auto-stripped -- drop
them up-front (or wrap in aColumnTransformer) so they don't leak
as features.
tweedie_power : float | None
Tweedie variance power (used only whenfamily == "tweedie").
alpha, l1_ratio : float
Regularization strength and Elastic-Net mixing. Default0.001adds a
vanishing ridge so glum stays well-conditioned on wide designs; passalpha=0.0`` for an unpenalized MLE.
fit_intercept : bool
max_iter, gradient_tol : int | float | None
Glum solver controls.
random_state : int | None
Attributes
backend_ : GeneralizedLinearRegressor The fitted glum estimator. coef_, intercept_ : np.ndarray | float Re-exposed from the backend for tarification (M6). feature_names_in_, n_features_in_
Actuarial diagnostics: ranking, calibration, stability, one-way, and double-lift.
Pure-premium convention (see PRD.md section 5): y_true = claim_amount (aggregate) y_pred = predicted pure premium (rate per exposure unit) sample_weight = exposure
Deviances are re-exported from sklearn (never reimplemented) -- they already
accept sample_weight.
Concentration curves order policies by ascending predicted rate. Gini is twice the signed area from the curve to the diagonal: positive when claims tend to concentrate at higher predictions, negative for the reverse. Tied-score blocks are integrated, equivalently the Frees-Meyers-Cummings midrank formula. This is a prediction-ordered measure. Observed-rate ordering recovers the corresponding exposure-weighted pairwise inequality Gini.
Lorenz = namedtuple('Lorenz', ['exposure_pct', 'claims_pct', 'gini'])
module-attribute
Exposure-vs-claims concentration curve (actuarial ascending y_pred).
calibration_table(y_true, y_pred, sample_weight=None, *, groups=None, n_bins=10, claim_count=None)
Per-segment observed vs predicted pure premium, exposure, and O/P ratio.
y_true = claim_amount, y_pred = predicted pure premium, sample_weight =
exposure. When groups is None, segments are deciles of predicted risk,
exposure-weighted when sample_weight is supplied. Otherwise groups is an
array-like of one segment label per row; NaN labels form their own segment
(dropna=False). Columns: group, exposure, claim_amount,
predicted_claim_amount, observed_pure_premium, predicted_pure_premium,
o_p_ratio; plus claim_count when provided.
Constant predictions form one real group, including without weights.
double_lift_table(y_true, pred_a, pred_b, sample_weight=None, *, n_bins=10, label_a='model A', label_b='model B')
Per-decile observed pure premium ordered by the pred_a / pred_b ratio.
Policies are bucketed into exposure-weighted deciles of the ratio (high
ratio = A predicts a higher rate relative to B), and per decile the table reports
mean ratio, exposure, observed pure premium, and predicted pure premium
for both A and B. Compare each predicted rate with observations within
each ratio group; rising observations alone cannot select a model. Extreme
ratio groups contain the largest relative disagreements, whose credibility
depends on exposure and claims. Rates are absolute, not normalized by
portfolio means. Use azoic.plots.plot_double_lift to render.
Both predictions are floored at _RATIO_EPS to avoid division-by-zero
when a GBM objective returns exact zeros (Poisson on never-claimed rows).
Generated model columns must be distinct and must not match reserved output
columns (for example, label_a="observed" is invalid). Each segment must
sum to at least one day of exposure, as in calibration and one-way tables.
gini(y_true, y_pred, sample_weight=None)
Exposure-weighted concentration Gini (actuarial low-to-high convention).
y_true is aggregate claim amount, y_pred is predicted rate, and
sample_weight is exposure (unit exposure when omitted). Order by
ascending prediction and integrate cumulative claim share C(u) against
cumulative exposure share u: G = 1 - 2 * integral(C(u), u=0..1).
This is twice the signed area from the curve to the diagonal, within
[-1, 1] for non-negative claims and positive exposure. Negative values
indicate claims concentrated toward lower predictions; zero need not
imply random predictions. Observed-rate ordering gives a hindsight upper
bound, generally below one, equal to exposure-weighted inequality Gini.
Pairwise absolute-difference inequality Gini measures observed-rate dispersion independently of predictions. This implementation instead integrates tied-score blocks, equivalently the Frees-Meyers-Cummings midrank formula. Ties are independent of row order; positive scaling preserves Gini. Returns 0 when total claims or total exposure is 0.
lorenz(y_true, y_pred, sample_weight=None)
Return the ascending concentration curve plus the Gini.
exposure_pct and claims_pct both start at 0.0 and end at 1.0,
suitable for direct plotting. Policies are ordered by predicted rate,
with equal scores aggregated before integration. The curve may cross the
diagonal; Gini is twice its signed area to that diagonal. A lower curve
dominates another only when it is nowhere above it.
one_way_table(X, feature, y_true, y_pred, sample_weight=None, *, n_bins=10)
Per-level observed vs predicted pure premium for one feature.
Buckets rows by feature. Categorical / low-cardinality features are
kept at their actual level (grouped levels from AutoGrouper are
preserved as-is, including declared ordered-category order). Numeric features
with n_bins=None get one level
per unique raw value (natural-value one-way on test rows: driver age
has as many points as distinct values); an integer n_bins exposure-
weight quantiles numeric features into n_bins bins for near-
continuous features like density.
y_true = claim_amount, y_pred = predicted pure premium (rate),
sample_weight = exposure. Output columns:
level, level_label, level_center, exposure, claim_amount, predicted_claim_amount, observed_pure_premium, predicted_pure_premium, o_p_ratio.
level is the grouping key (integer code for numeric binned, raw value
for numeric unique-value mode and categorical). level_label is a
human-readable label (bin interval [a, b] for quantiled numeric, the
actual level otherwise). level_center is the exposure-weighted mean
of the raw feature within the bin (quantiled numeric only); the raw
value itself in unique-value mode; NaN for categorical or missing
values. Numeric missing rows retain their totals as Missing with
an undefined centre, and their exposure does not affect bin boundaries.
op_ratio(y_true, y_pred, sample_weight=None)
Portfolio observed/predicted pure-premium ratio (1.0 = perfect level).
observed = sum(claim_amount) / sum(exposure) predicted = sum(pure_premium * exposure) / sum(exposure)
stability_table(y_true, y_pred, sample_weight, *, periods)
Return exposure-weighted diagnostics for each naturally sorted period.
y_true is aggregate claim amount, y_pred is predicted pure premium,
and sample_weight is exposure. The fixed-power Tweedie deviance and
period-relative :math:D^2 use power=1.5. D^2 is NaN when the
period's exposure-weighted observed-mean prediction has zero deviance.
All inputs must be non-empty, one-dimensional, equal-length arrays without missing periods. Claim amounts must be non-negative and finite; predictions and exposures must be positive and finite.
Actuarial diagnostic plots: Lorenz, lift, calibration, one-way, double-lift.
All chart functions accept ax= to embed in a caller figure and path=
to save a PNG. ax=None triggers a guided layout built with
matplotlib.pyplot.subplot_mosaic (bottom exposure panel where useful).
Style is fivethirtyeight layered with :data:AZOIC_STYLE (CVD-safe Okabe-Ito
palette, white background, subtle grid); every function applies it to its own
figure, and :func:azoic_style lets callers style embedded grids identically.
Observed quantities are always the same grey (:data:OBSERVED); each model
keeps one color across all charts via :func:model_colors.
azoic_style()
Apply the azoic chart style (fivethirtyeight base + CVD-safe overrides).
model_colors(names)
Stable Okabe-Ito color per model name, consistent across all charts.
plot_actual_vs_predicted(y_true, y_pred, sample_weight=None, *, bins='log', gridsize=50, cmap=None, ax_lim=None, logx=False, logy=False, ax=None, path=None, title='Actual vs predicted')
Hexbin density of observed vs predicted rates with a residual panel.
y_true is aggregate claim amount, y_pred is predicted
pure-premium rate, and sample_weight is exposure. Observed amounts
are divided by exposure before plotting; omitted exposure means unit
exposure. Left panel: observed rate on x, predicted rate on y, y=x dashed
reference. Right panel: rate residuals (observed − predicted) vs predicted
rate with a zero reference. Density colouring is exposure-weighted
(Σ exposure per hex) when supplied, otherwise log counts; the two panels
get independent colorbars. cividis by default (CVD-safe).
Inputs must be finite, one-dimensional, and equal-length; claims must be
non-negative and exposure strictly positive for rate division.
Interpret credible exposure-weighted grouped mean residuals, not the
densest band in zero-heavy outcomes. A constant non-zero mean residual
indicates an additive discrepancy. If observed rate equals c * y_pred,
residuals equal (c - 1) * y_pred: a slope can be purely multiplicative
and removable by one factor. Review remaining curvature only after level.
logy=True uses a log y axis for positive predictions and a symmetric
log (symlog) y axis for signed residuals, with Matplotlib's default
linear threshold of 2 around zero. logy=False keeps linear y axes.
Callers can adjust the axes after plotting. bins="log" controls density
colour independently of the residual axis scale.
plot_calibration(table, *, ax=None, path=None, title='Calibration', logx=False, logy=False)
Scatter of observed vs predicted pure premium per segment.
table is the output of azoic.metrics.calibration_table. Point size
is proportional to segment exposure; the dashed y=x line is perfect
calibration. The decile-level observed-vs-paired view lives in
:func:plot_lift.
plot_double_lift(table, *, ax=None, path=None, label_a='model A', label_b='model B', color_a=None, color_b=None, title='Double-lift chart', exposure='panel', logy=False)
Per-decile observed pure premium, ordered by the pred_a / pred_b ratio.
table is the output of azoic.metrics.double_lift_table. Three
lines: observed (grey o-), predicted A (s--), predicted B
(^:), one color per model, per ratio decile (mean ratio on tick
labels). Compare each prediction with observations within each ratio group;
observed direction alone cannot select a model. Extreme groups capture
the largest relative disagreements; check their exposure and claims
credibility. Curves show absolute rates, without normalization by portfolio
means. Stand-alone draws a bottom exposure panel; embedded grids get
background bars.
plot_lift(table, *, color=None, label='predicted', ax=None, path=None, title='Lift chart', exposure='background', logy=False)
Observed vs predicted pure premium per exposure-balanced decile.
table is the output of azoic.metrics.calibration_table (deciles of
predicted risk by default). Observed is the grey o- line, predicted
the model-colored s-- line: the two should track in level
(calibration) while observed rises with the decile (ranking); crossings
expose deciles where the model over- or under-prices. Translucent
background bars show the exposure share per decile (sklearn-example
style).
plot_lorenz(y_true, y_pred, sample_weight=None, *, ax=None, path=None, title='Lorenz curve', label='model', color=None, show_oracle=False, show_shade=False)
Plot exposure-vs-claims concentration curves, one color per model.
y_pred is a single prediction array or a mapping {model_name:
predictions}. The diagonal (random ranking) is drawn exactly once; with
show_oracle=True the oracle ranks by observed rate (aggregate claims
divided by exposure; omitted weights mean unit exposure). Aggregate claims
and exposure still determine the cumulative shares. This hindsight ranking
bound is not a deployable model and need not have Gini one. It is overlaid
once as a grey dash-dot reference. Curves are sorted from safest
to riskiest (ascending prediction). Gini, shown in the legend, is twice
the signed area from each curve to the diagonal; even a positive-Gini
curve can cross the diagonal. Shading shows the area, not twice its value.
plot_one_way(table, *, color=None, label='predicted', ax=None, path=None, title='One-way observed vs predicted', xlabel='Feature value', exposure='panel', logy=False)
Per-level observed vs predicted pure premium with the exposure layout.
Use the output of azoic.metrics.one_way_table. With n_bins=None
the table has one level per unique value (natural numeric axis: driver
age has as many points as distinct test values). If any centre is missing,
all groups use labelled positions so the missing segment remains visible.
Observed is the grey o- line, predicted the model-colored s-- line.
Stand-alone (ax=None, default exposure="panel") a bottom exposure-share panel
is drawn via subplot_mosaic; pass exposure="background" when
embedding in a caller grid to underlay translucent exposure bars instead.