from azoic.tariff import extract_tariff, recalibrate_for_total
portfolio = load_portfolio()
glm_config, _ = make_experiment_configs(str(PORTFOLIO_PATH))
glm_config = glm_config.model_copy(
update={"models": {"direct-tweedie-glm": glm_config.models["direct-tweedie-glm"]}}
)
glm_run, glm_estimators = run_experiment(glm_config, return_estimators=True)
train_frame = portfolio.iloc[list(glm_run.train_indices)].copy()
test_frame = portfolio.iloc[list(glm_run.test_indices)].copy()
y_train = train_frame["claim_amount"].to_numpy()
w_train = train_frame["exposure"].to_numpy()
y_test = test_frame["claim_amount"].to_numpy()
w_test = test_frame["exposure"].to_numpy()
glm_pipeline = glm_estimators["direct-tweedie-glm"]
fitted_preprocessing = glm_pipeline[:-1]
fitted_glm = glm_pipeline[-1]
special_predict_columns = [*FEATURES, "exposure", "claim_amount", "claim_count"]
transformed_test = fitted_preprocessing.transform(test_frame[special_predict_columns])11 Manual tariff recipes
A multiplicative tariff is a decision object: after the fitted model, an actuary may revise selected factors deliberately. Azoic supports that without a new model class — extract_tariff decomposes a fitted log-link GLM into a factor dictionary, apply_tariff scores a (possibly edited) dictionary, and recalibrate_for_total shifts the base to restore a portfolio total.
Treat judgemental factor changes as a revised tariff, not a tweak of the fitted model: editing the dictionary does not update the fitted pipeline or a previously exported workbook, and changing bin boundaries or group membership requires refitting the downstream model. Record the original factors and the reasons for each change.
11.1 Extract and reproduce
extract_tariff returns base_rate (intercept folded with reference-level coefficients), per-unit numeric coefficients, categorical relativities against a chosen reference level, and a mapping table. Applying the untouched dictionary on the same transformed features reproduces the fitted prediction.
tariff = extract_tariff(fitted_glm)
model_rate = np.asarray(glm_pipeline.predict(test_frame[[*FEATURES, "exposure"]]), dtype=float)
applied_rate = apply_tariff(tariff, transformed_test)
np.testing.assert_allclose(applied_rate, model_rate, rtol=1e-6, atol=1e-8)
display(pd.DataFrame(
{
"base_rate": [tariff["base_rate"]],
"numeric_features": [len(tariff["numeric"])],
"categorical_features": [len(tariff["categorical"])],
}
))
display(tariff["mapping"].head(15))| base_rate | numeric_features | categorical_features | |
|---|---|---|---|
| 0 | 48.05387 | 0 | 9 |
| feature | role | dtype | levels | n_levels | reference_level | |
|---|---|---|---|---|---|---|
| 0 | driver_age | categorical | category | (-inf, 21.5), [21.5, 24.5), [24.5, 27.5), [27.... | 7 | (-inf, 21.5) |
| 1 | vehicle_age | categorical | category | (-inf, 2.5), [2.5, 5.5), [5.5, 6.5), [6.5, 7.5... | 9 | (-inf, 2.5) |
| 2 | bonus_malus | categorical | category | (-inf, 51.5), [51.5, 57.5), [57.5, 63.5), [63.... | 7 | (-inf, 51.5) |
| 3 | density | categorical | category | (-inf, 18.5), [18.5, 375.5), [375.5, 2351.0), ... | 6 | (-inf, 18.5) |
| 4 | region | categorical | category | group_0, group_1, group_2, group_3, group_4, g... | 9 | group_0 |
| 5 | vehicle_brand | categorical | category | group_0, group_1, B1, B4, B5, B13, group_6, B1... | 9 | group_0 |
| 6 | veh_power | categorical | category | group_0, group_1, 7, group_3, 11, 10, 12, grou... | 9 | group_0 |
| 7 | veh_gas | categorical | category | 'Regular', 'Diesel', Other | 3 | 'Regular' |
| 8 | area | categorical | category | B, A, C, D, F, E, Other | 7 | B |
11.2 Revise one factor, then restore the total
Below, one grouped level of region gets a deliberate +10% factor. This local change affects only that segment and can change ranking between policies. The original training O/P is already close to, but not exactly, one (about 0.99817 with the default data); the edit changes that level further.
recalibrate_for_total then supplies a new base rate that balances the edited tariff to observed training claims. This second step is a common positive multiplier: it preserves the edited tariff’s ranking, not necessarily the original model’s ranking. We put the new base into a separate dictionary and freeze it for testing; neither the fitted model nor a workbook is updated.
revised = {
"base_rate": tariff["base_rate"],
"numeric": dict(tariff["numeric"]),
"categorical": {feat: dict(levels) for feat, levels in tariff["categorical"].items()},
}
feature = "region"
levels = revised["categorical"][feature]
edited_level = next(level for level, factor in levels.items() if not np.isclose(factor, 1.0))
levels[edited_level] *= 1.10
original_op = op_ratio(y_train, np.asarray(glm_pipeline.predict(
train_frame[[*FEATURES, "exposure"]]), dtype=float), w_train)
transformed_train = fitted_preprocessing.transform(train_frame[special_predict_columns])
revised_rate_train = apply_tariff(revised, transformed_train)
revised_op = op_ratio(y_train, revised_rate_train, w_train)
assert abs(revised_op - original_op) > 1e-3
new_base = recalibrate_for_total(
revised,
predicted_total=float(revised_rate_train @ w_train),
observed_total=float(y_train.sum()),
)
revised_recalibrated = {**revised, "base_rate": new_base}
restored_rate_train = apply_tariff(revised_recalibrated, transformed_train)
np.testing.assert_allclose(
op_ratio(y_train, restored_rate_train, w_train), 1, rtol=0, atol=1e-10
)
display(pd.DataFrame(
{
"variant": ["fitted GLM", "edited factor", "edited + recalibrated"],
"training_O/P": [
original_op,
revised_op,
op_ratio(y_train, restored_rate_train, w_train),
],
}
))| variant | training_O/P | |
|---|---|---|
| 0 | fitted GLM | 0.998170 |
| 1 | edited factor | 0.997147 |
| 2 | edited + recalibrated | 1.000000 |
The same totals check belongs on the held-out rows: recalibrating to training totals fixes the training level but does not claim to fix test calibration or any segment-level bias.
restored_rate_test = apply_tariff(revised_recalibrated, transformed_test)
display(pd.DataFrame(
{
"variant": ["fitted GLM", "edited factor", "edited + recalibrated"],
"test_O/P": [
op_ratio(y_test, model_rate, w_test),
op_ratio(y_test, apply_tariff(revised, transformed_test), w_test),
op_ratio(y_test, restored_rate_test, w_test),
],
"test_deviance": [
mean_tweedie_deviance(y_test / w_test, model_rate, sample_weight=w_test, power=1.5),
mean_tweedie_deviance(
y_test / w_test, apply_tariff(revised, transformed_test),
sample_weight=w_test, power=1.5,
),
mean_tweedie_deviance(
y_test / w_test, restored_rate_test, sample_weight=w_test, power=1.5,
),
],
}
))| variant | test_O/P | test_deviance | |
|---|---|---|---|
| 0 | fitted GLM | 1.022067 | 75.338331 |
| 1 | edited factor | 1.021074 | 75.346202 |
| 2 | edited + recalibrated | 1.023996 | 75.348905 |
For changes to model flexibility or sparse-risk pooling, return to the experiment controls. Choose those settings inside training data. Revisions prompted by the inspected holdout need fresh evaluation data; this illustration does not provide it.