knitr::opts_chunk$set( collapse = TRUE, comment = "#>" )
In many pricing analyses, model estimation is followed by a translation step.
A fitted GLM may capture the structure of the portfolio well, while some fitted effects still need to be reviewed before they are used in a tariff.
Common reasons include:
For this reason, actuarial pricing work often distinguishes between:
insurancerating provides a staged refinement interface:
prepare_refinement()refit() to obtain the final fitted modelThis separation can make tariff adjustments easier to understand, reproduce, and audit.
Refinement can help when the estimated model output is useful, but the fitted coefficient pattern needs additional structure before it is used in a tariff.
Typical use cases include:
In many workflows, refinement is applied to the model that represents the final pricing signal, such as a premium or pure-premium model. In other cases, it may also be useful for selected frequency or severity effects. The relevant question is whether the adjusted coefficient pattern is intended to support the tariff structure that will be reviewed or implemented.
The example below starts from one common premium modelling setup:
library(insurancerating) library(dplyr) age_policyholder_frequency <- risk_factor_gam( data = MTPL, claim_count = "nclaims", risk_factor = "age_policyholder", exposure = "exposure" ) age_segments_freq <- derive_tariff_segments(age_policyholder_frequency) dat <- MTPL |> add_tariff_segments(age_segments_freq, name = "age_policyholder_freq_cat") |> mutate(across(where(is.character), as.factor)) |> mutate(across(where(is.factor), ~ set_reference_level(., exposure))) freq <- glm( nclaims ~ bm + age_policyholder_freq_cat, offset = log(exposure), family = poisson(), data = dat ) sev <- glm( amount ~ zip, weights = nclaims, family = Gamma(link = "log"), data = dat |> filter(amount > 0) ) premium_df <- dat |> add_prediction(freq, sev) |> mutate(premium = pred_nclaims_freq * pred_amount_sev) burn_unrestricted <- glm( premium ~ zip + bm + age_policyholder_freq_cat, weights = exposure, family = Gamma(link = "log"), data = premium_df )
Before refinement, inspect the unrestricted coefficient structure:
rating_table(burn_unrestricted) rating_table(burn_unrestricted) |> autoplot()
At this stage, the coefficients reflect the unrestricted model fit. This output is often informative by itself. If the pattern is too irregular, too granular or difficult to explain, a refinement step can be added explicitly.
Refinement begins with:
ref <- prepare_refinement(burn_unrestricted) ref
A rating_refinement object stores:
At this point, the model itself has not been refitted. The refinement object represents a proposed tariff adjustment structure, not yet the final fitted result.
This distinction is useful because refinement steps can be inspected before they are incorporated into the final model.
Smoothing can be used when a rating factor derived from a continuous variable contains local variation that is hard to justify in a tariff.
For example, a coefficient pattern such as:
may be statistically possible, but difficult to explain or maintain. Smoothing adds a more stable structure to the rating factor.
ref <- ref |> add_smoothing( model_variable = "age_policyholder_freq_cat", source_variable = "age_policyholder", breaks = seq(18, 95, 5), weights = "exposure" )
The key arguments are:
model_variable: the grouped variable present in the GLMsource_variable: the original continuous portfolio variablebreaks: the preferred commercial cut pointssmoothing: the smoothing specificationweights: optional weighting, typically exposureprint(ref) autoplot( ref, variable = "age_policyholder_freq_cat", x_max = 90, y_max = 1.5 )
This plot belongs to the pre-refit stage. It shows:
The purpose is to inspect the refinement step itself, before it is incorporated into the final fitted model.
Typical smoothing choices are:
"spline": polynomial-style smoothing"gam": flexible smooth curve"mpi": monotone increasing"mpd": monotone decreasingThe appropriate choice depends on the pricing context.
For example:
Restrictions can be used when coefficients need to follow a predefined structure.
Typical examples include:
Restrictions differ from smoothing:
zip_df <- data.frame( zip = c(0, 1, 2, 3), zip_adj = c(0.8, 0.9, 1.0, 1.2) ) ref <- ref |> add_restriction(restrictions = zip_df)
The restriction table must contain exactly two columns:
autoplot(ref, variable = "zip")
This shows the proposed restricted structure relative to the original fitted model.
In some cases, the fitted model uses a broad factor level, while portfolio or business knowledge suggests that more granular differentiation may be useful.
For example, a model may estimate one coefficient for "construction", while pricing practice distinguishes between:
This can be relevant when subgroup exposure is too limited to estimate stable coefficients directly.
relativities_activity <- relativities( split_level( "construction", c("residential_construction", "commercial_construction"), c(1.00, 1.15) ) ) ref <- ref |> add_relativities( model_variable = "business_activity", split_variable = "business_activity_split", relativities = relativities_activity, exposure = "exposure", normalize = TRUE )
If normalize = TRUE, the relativities are scaled so that their
exposure-weighted average remains equal to 1 within the original level.
This preserves the original model signal while introducing finer structure.
Refinement steps alter part of the model structure. Once these changes are applied, the remaining coefficients may also adjust.
For that reason, the sequence does not end with add_smoothing() or
add_restriction(). The final step is:
burn_refined <- refit(ref)
This refits the model while incorporating the documented refinement steps.
After refit, use rating_table():
rating_table(burn_refined)
At this point, the output no longer represents a proposed refinement plan. It represents the fitted coefficient structure after refinement.
The distinction is:
refit() --> inspect the refinement planrefit() --> inspect the fitted tariff structureIf smoothing, restrictions, and relativities have been applied, they are now embedded in the fitted model output.
rating_table(burn_refined) |> autoplot()
After refit, model structure can be extracted with extract_model_data():
md <- extract_model_data(burn_refined) head(md)
Observed model-point combinations can be obtained with rating_grid():
grid <- rating_grid(burn_refined) head(grid)
This is typically used for:
One possible refinement sequence is:
zip_df <- data.frame( zip = c(0, 1, 2, 3), zip_adj = c(0.8, 0.9, 1.0, 1.2) ) burn_refined <- prepare_refinement(burn_unrestricted) |> add_smoothing( model_variable = "age_policyholder_freq_cat", source_variable = "age_policyholder", breaks = seq(18, 95, 5), weights = "exposure" ) |> add_restriction(zip_df) |> refit() rating_table(burn_refined) rating_table(burn_refined) |> autoplot()
Legacy entry points remain available:
burn_refined_old <- burn_unrestricted |> smooth_coef( x_cut = "age_policyholder_freq_man", x_org = "age_policyholder", breaks = seq(18, 95, 5) ) |> restrict_coef(zip_df) |> refit_glm()
These are primarily maintained for backward compatibility.
For new code, the recommended interface is:
prepare_refinement() |> add_*() |> refit()
This keeps the sequence of tariff adjustments explicit.
The refinement interface helps separate:
This makes it easier to document and inspect adjustments before the model is refitted. In practice, this can support tariff structures that are:
For the underlying pricing concepts, see:
For an example sequence from portfolio analysis to fitted tariff, see:
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.