Introduction
Refinement is the explicit translation from statistically estimated model effects to the structure that will be reviewed, justified and implemented as a tariff. It can be motivated by stability, credibility, monotonicity, smoothness, sparse experience or an explicit implementation constraint.
Refinement is not arbitrary editing of inconvenient coefficients. Each adjustment should have an actuarial or operational rationale and should be reviewed against exposure, claim volume, observed experience and model diagnostics. Recording these decisions as ordered steps makes them reproducible and easier to review.
This vignette starts from a fitted unrestricted GLM and focuses on what happens next. The construction of frequency, severity and technical risk-premium models is covered in Getting Started. The wider role of refinement within the package is mapped in Pricing workflow and package building blocks.
The refinement architecture is:
| Stage | Meaning |
|---|---|
| Unrestricted GLM | Statistical starting point |
prepare_refinement() |
Create an editable specification around that model |
add_smoothing(), add_restriction(),
add_shrinkage(), add_relativities()
|
Record proposed actuarial adjustments |
summary() and autoplot()
|
Inspect the proposal before fitting |
refit() |
Reconstruct and fit the GLM under the stored specification |
rating_table() and audit_refinement()
|
Inspect the fitted tariff and its portfolio effect |
A compact unrestricted model
The unrestricted model is only the starting material for this
vignette. The following setup creates grouped age and bonus-malus
factors and fits a Poisson frequency GLM. The response is claim count
and log(exposure) is the offset.
library(insurancerating)
portfolio <- as.data.frame(MTPL)
age_breaks <- c(18, 25, 32, 39, 51, 58, 65, 84, 95)
portfolio$age_band <- cut(
portfolio$age_policyholder,
breaks = age_breaks,
include.lowest = TRUE
)
portfolio$bm_group <- cut(
portfolio$bm,
breaks = c(0, 4, 8, Inf),
labels = c("Low", "Medium", "High")
)
portfolio$bm_detail <- factor(as.character(portfolio$bm))
portfolio$zip <- factor(portfolio$zip)
unrestricted <- glm(
nclaims ~ age_band + zip + bm_group + offset(log(exposure)),
family = poisson(),
data = portfolio
)The initial tariff effects can be inspected before any actuarial adjustment:
head(rating_table(unrestricted, exposure = FALSE))
#> risk_factor level est_unrestricted
#> 1 (Intercept) (Intercept) 0.2735430
#> 2 age_band [18,25] 1.0000000
#> 3 age_band (25,32] 0.6853582
#> 4 age_band (32,39] 0.5540633
#> 5 age_band (39,51] 0.5197098
#> 6 age_band (51,58] 0.4381469These are the conditional effects estimated by the unrestricted GLM. An irregular effect is not by itself a reason to refine it: the analyst should first consider data quality, exposure definition, model specification, interactions and factor construction.
Preparing the refinement
refinement <- prepare_refinement(
unrestricted,
data = portfolio
)
refinement
#> <rating_refinement>
#> Base model: Poisson GLM (log link)
#> Steps: 0prepare_refinement() creates a
rating_refinement object containing the unrestricted model,
the corresponding retained model data and an initially empty ordered
step specification. It does not refit the GLM and does not change its
coefficients or fitted values.
This distinction is central to the API:
-
unrestrictedis a fitted statistical model; -
refinementis an editable proposal for the tariff structure; - the model returned later by
refit()is the fitted outcome under that proposal.
Keep the refinement object when iterating. A model returned by
refit() is a fitted result, not an editable specification.
Calling prepare_refinement() on that fitted result
deliberately starts a new workflow and does not reconstruct the earlier
steps.
Smoothing an ordered effect
Raw level effects for a grouped continuous variable may contain local movement that is weakly supported or unstable over time. Smoothing replaces that local pattern with an explicitly structured curve. It is most relevant when a gradual underlying relationship is plausible and neighbouring tariff levels should be interpreted coherently.
refinement <- refinement |>
add_smoothing(
model_variable = "age_band",
source_variable = "age_policyholder",
breaks = age_breaks,
smoothing = "spline",
k = 5,
weights = "exposure"
)The arguments distinguish two variables:
-
model_variableis the grouped factor included in the unrestricted GLM; -
source_variableis the underlying numeric portfolio variable used to estimate the smooth relationship.
breaks define the intervals in the resulting tariff
factor. Exposure weights give levels with more portfolio support more
influence. With a spline method, k controls the available
curve flexibility; it is a basis dimension rather than a fixed number of
fitted degrees of freedom.
The default unconstrained spline is suitable when the shape should
remain data-led. Increasing or decreasing shape constraints are
available when a directional assumption has a defensible actuarial
basis. The function reference for add_smoothing() describes
the full set of methods and their curvature interpretation.
All smoothing methods act directly on the tariff relativity. For
example, smoothing = "increasing_concave" means that the
relativity increases while its absolute increase becomes smaller as the
source variable rises. This may be useful where neighbouring estimates
are noisy or upper-tail exposure is sparse, provided the shape is
supported by an explicit actuarial rationale.
Inspecting the proposal
summary(refinement)
#> Refinement specification
#>
#> Package: insurancerating 0.8.1.9000
#> Created: 2026-08-21 09:37:22 UTC
#> Observations: 30,000
#> Family: poisson (log link)
#> Base formula:
#> nclaims ~ age_band + zip + bm_group + offset(log(exposure))
#> Offset: log(exposure)
#>
#> Refinement steps: 1
#> 1. Smoothing: age_band from age_policyholder (method: spline, k: 5, scale: relativity)
#> shape = spline; 8 intervals over 18 to 95
autoplot(
refinement,
variable = "age_band",
x_max = 90
)
This is a pre-refit plot. It compares the original fitted effect with the proposed smooth structure. The GLM has not yet been estimated under that structure.
If the curve needs adjustment, retain the specification and use
edit_smoothing() before refitting. This preserves the rest
of the ordered workflow rather than starting again from the fitted
result.
Refining a selected part of the smoothing
add_smoothing() defines the initial curve and its
structural assumptions. edit_smoothing() can subsequently
refine a selected interval in two distinct ways. These edits are
explicit tariff assumptions layered on the initial smoothing; they are
not additional statistical observations.
Each call to edit_smoothing() is stored as a separate
refinement step. If the same smoothing is edited more than once, the
edits are applied cumulatively in their recorded order. The
step argument in autoplot() can therefore be
used to inspect the curve after a particular edit without introducing a
separate revision argument.
Explicitly redirecting the curve
Use explicit values when the intended targets are known:
explicit_age_refinement <- refinement |>
edit_smoothing(
model_variable = "age_band",
from = 32,
to = 65,
from_value = 0.95,
to_value = 1.10,
control_positions = 51,
control_values = 1.02
)
autoplot(explicit_age_refinement, variable = "age_band", x_max = 90)
The stored edit redirects the selected interval through the supplied target values. This is appropriate when those values have a documented actuarial or implementation basis.
Applying a relative local adjustment
Use adjustment when the current shape is broadly
appropriate but one region should be somewhat higher or lower:
relative_age_refinement <- refinement |>
edit_smoothing(
model_variable = "age_band",
from = 32,
to = 65,
adjustment = 1.05
)
autoplot(
relative_age_refinement,
variable = "age_band",
x_max = 90,
show_initial_smoothing = TRUE
)
Here, adjustment = 1.05 raises the middle of the
selected region by up to 5% relative to the existing smoothing. The
multiplier starts at 1 at from, moves towards 1.05 over the
available smoothing points, and returns to 1 at to. The
unchanged curve is therefore retained outside the interval and no jump
is introduced at either boundary.
With show_initial_smoothing = TRUE, the plot includes
both the curve directly after add_smoothing() and the
cumulative curve at the selected step. This comparison changes only the
plot; it does not alter the stored refinement or the subsequent
refit.
For example, two local adjustments create two successive edit steps:
cumulative_age_refinement <- relative_age_refinement |>
edit_smoothing(
model_variable = "age_band",
from = 50,
adjustment = 1.02,
transition = "linear"
)
# Step 3 is the cumulative result of the initial smoothing and both edits.
autoplot(
cumulative_age_refinement,
step = 3,
x_max = 90,
show_initial_smoothing = TRUE
)
Selecting step = 2 would show the curve after the first
edit only. The initial comparison line remains the result of the
original add_smoothing() step in both plots.
For a tail adjustment, only one boundary is needed. With only
from, the adjustment runs from that value to the end of the
available smoothing range. With only to, it runs from the
beginning of the range to that value. The transition remains attached to
the original curve at the supplied boundary:
# Refine the upper tail
edit_smoothing(
refinement,
model_variable = "age_band",
from = 50,
adjustment = 1.05
)
# Refine the lower tail
edit_smoothing(
refinement,
model_variable = "age_band",
to = 35,
adjustment = 0.95
)With transition = NULL, the default, the transition
inherits the smoothing specification of the step being edited. For
example, an original smoothing = "increasing_concave" uses
the same structural shape logic while adapting the entry and exit to
their opposite directions. The resulting curve is checked against the
inherited monotonicity and curvature where applicable.
Two explicit alternatives are available:
# Continuous straight transitions
edit_smoothing(
refinement,
model_variable = "age_band",
from = 32,
to = 65,
adjustment = 1.05,
transition = "linear"
)
# Immediate changes at both boundaries
edit_smoothing(
refinement,
model_variable = "age_band",
from = 32,
to = 65,
adjustment = 1.05,
transition = "step"
)"linear" remains continuous. "step"
deliberately permits discontinuities and should therefore be selected
only when an immediate tariff change is intended. Relative adjustments
and explicit target values cannot be combined in one
edit_smoothing() call because they express different
instructions. They can be recorded in separate consecutive edit steps
when that sequence has a clear actuarial interpretation.
Adjusting the slope after an anchor
Sometimes the level of the smoothing is acceptable, while the
remaining increase above a selected value should be stronger or weaker.
In that case, slope_adjustment scales the change relative
to the smoothing value at from:
slope_refinement <- refinement |>
edit_smoothing(
model_variable = "age_band",
from = 50,
slope_adjustment = 1.10
)
autoplot(
slope_refinement,
variable = "age_band",
show_initial_smoothing = TRUE
)
The curve through age 50 is unchanged. Above age 50, each difference
from the relativity at age 50 is multiplied by 1.10. The curve therefore
remains continuous at the anchor, while its subsequent change is 10%
stronger. A value between 0 and 1 flattens the remaining effect. This is
different from adjustment, which changes the relative level
over a selected interval.
Each edit_smoothing() call records one type of
intervention: a relative level adjustment, a slope adjustment, or
explicit target/control-point values. When both level and slope require
adjustment, use two consecutive calls:
refinement |>
edit_smoothing(
model_variable = "age_band",
from = 30,
to = 50,
adjustment = 1.05
) |>
edit_smoothing(
model_variable = "age_band",
from = 50,
slope_adjustment = 1.10
)The refinement history then retains both actuarial choices as separate steps.
The transformation preserves the direction of an increasing or decreasing effect. For a shape-constrained curve, a value above 1 may nevertheless create a visible change in slope at the anchor and need not preserve global concavity or convexity across that exact point. This should therefore be treated as an explicit actuarial intervention and inspected before refitting.
The remainder of this vignette uses
relative_age_refinement as the current proposal:
refinement <- relative_age_refinementThe relativity plot shows the overall tariff shape:
autoplot(refinement, variable = "age_band")
Interpreting the premium effect
A relativity curve shows the shape of a continuous tariff effect, but
its practical magnitude is not always immediately clear. By default,
premium_change() reports how much modelled premium changes
when the source variable doubles from a selected starting value:
premium_change(
refinement,
variable = "age_band",
at = c(20, 25, 30, 35)
)
#> Premium change for age_policyholder
#>
#> Comparison: doubling
#> Basis: Effective smoothing curve
#>
#> From To Premium change
#> 20 40 -46.8%
#> 25 50 -39.0%
#> 30 60 -39.5%
#> 35 70 -33.8%For example, a reported value of 0.12 means that the smoothing
implies a 12% higher modelled premium when age doubles from that
starting value. The helper uses the current effective smoothing,
including preceding edit_smoothing() steps, and does not
extrapolate beyond its supported range.
Supplying increment instead asks the corresponding
fixed-increment question:
premium_change(
refinement,
variable = "age_band",
at = seq(20, 60, by = 5),
increment = 5
)
#> Premium change for age_policyholder
#>
#> Increment: 5
#> Basis: Effective smoothing curve
#>
#> From To Premium change
#> 20 25 -17.7%
#> 25 30 -17.9%
#> 30 35 -13.9%
#> 35 40 -8.6%
#> 40 45 -2.8%
#> 45 50 -2.9%
#> 50 55 -8.9%
#> 55 60 -10.5%
#> 60 65 -10.5%Each row now compares R(age + 5) with
R(age) using the current effective smoothing. The table
does not impose a pattern; it only translates the proposed relativity
curve into practical premium comparisons.
For an insured-amount smoothing, increment = 100000
would ask how much modelled premium changes for another 100,000 from
each displayed starting value. It might, for example, compare 100,000
with 200,000, then 200,000 with 300,000. Successive percentage changes
may decline, remain stable or increase depending on the fitted
relationship. The increment is a finite practical comparison, not a
derivative or slope, and no particular pattern is imposed by the
package.
By default, basis = "curve" evaluates the continuous
effective smoothing at the exact starting and comparison values. This is
the appropriate basis when the question concerns the shape or steepness
of the smoothing itself. To review the premium effect of the
implementable tariff classes instead, use:
premium_change(
refinement,
variable = "age_band",
at = c(20, 25, 30, 35),
basis = "segments"
)
#> Premium change for age_policyholder
#>
#> Comparison: doubling
#> Basis: Tariff segments
#>
#> From To Premium change
#> 20 40 -44.5%
#> 25 50 -44.5%
#> 30 60 -45.0%
#> 35 70 -34.0%With basis = "segments", the helper determines which
effective tariff interval contains each value and compares the
corresponding current segment relativities. The result can therefore be
0% when both values are in the same segment, even when the underlying
curve increases within that range. A change can also occur discretely
when a comparison crosses a segment boundary. The two bases answer
different questions: the curve describes the underlying smooth
relationship, while the segments describe the tariff that would be
applied.
The initial and edited relationships can be compared directly:
premium_change(
relative_age_refinement,
variable = "age_band",
at = c(20, 25, 30),
steps = c(1, 2)
) |>
as_gt()With two selected states, the table shows both premium changes and their difference in percentage points. This comparison is often easier to interpret than small visual differences between two relativity curves. It remains an interpretation of the proposed smoothing and does not alter the refinement.
Restricting selected levels
add_restriction() fixes tariff levels at supplied
relativities. A restriction may represent an implementation rule, a
supported external assumption or a deliberate response to an unstable
local estimate. It differs from smoothing: smoothing estimates a
structured pattern, whereas a restriction explicitly prescribes selected
values.
zip_restrictions <- data.frame(
zip = c("0", "3"),
zip_restricted = c(0.95, 1.10)
)
refinement <- refinement |>
add_restriction(zip_restrictions)Only ZIP levels 0 and 3 are supplied here. The other observed ZIP levels are fixed at their current effective relativities. Consequently, a partial restriction changes the selected values but still produces a complete fixed structure for that risk factor.
Repeated calls for the same restricted variable update matching
levels and retain earlier restrictions for levels not supplied again.
New levels may be added when they represent explicit tariff assumptions;
new risk factors require the corresponding portfolio column and an
explicit opt-in. These behaviours are documented in the
add_restriction() reference.
A new fixed tariff factor can also replace an existing standalone
model term. For example,
add_restriction(..., replaces = "postal_area") records that
the new classification substitutes for postal_area during
refit(), rather than adding a second multiplicative effect.
The replacement is shown in the refinement summary and audit.
Interactions and transformed terms must be revised explicitly in the
model specification because removing them is not an unambiguous level
restriction.
autoplot(refinement, variable = "zip")
Again, the plot shows the proposed restriction before
refit().
Shrinking a categorical effect
add_shrinkage() reduces the differences between the
current relativities of one categorical risk factor. It is useful when
the direction and ordering of an estimated effect are considered
informative, but the spread between levels is larger than is supported
by the available experience or the intended tariff. Unlike a
restriction, shrinkage does not prescribe individual level values.
refinement <- refinement |>
add_shrinkage(
model_variable = "bm_group",
credibility = 0.9,
weights = "exposure"
)credibility is the weight assigned to the current
logarithmic effect. A value of 0.9 retains 90 percent of each level’s
deviation from the common centre; a value of 1 leaves the effect
unchanged, while a value of 0 removes differences between levels. This
is a user-selected refinement parameter, not an automatically estimated
Buhlmann or Buhlmann-Straub credibility factor.
The common centre is determined using the selected weighting basis.
Exposure weights are appropriate here because bm_group is
part of a frequency model. For a severity model, claim count may be a
more relevant basis. Use weights = "equal" when every
factor level should receive equal weight rather than representing the
observed portfolio mix.
After shrinkage, the relativities are normalised so their exposure-weighted arithmetic mean remains equal to its value before shrinkage. The operation therefore reduces tariff differentiation without intentionally changing the weighted level of this risk factor. The later refit can still recalibrate the intercept or other free model effects.
autoplot(refinement, variable = "bm_group")
This remains a pre-refit comparison: it shows the current estimated effect and the proposed shrunken structure stored in the refinement specification.
Adding differentiation within model levels
add_relativities() addresses a different problem. An
unrestricted GLM may use a broad factor because its detailed levels are
individually too sparse for stable direct estimation. The refinement can
retain the broad parent effect while introducing documented
differentiation between selected sublevels.
The example splits the broad bonus-malus groups Low and
Medium into their observed detailed values:
bm_relativities <- relativities(
split_level(
"Low",
new_levels = c("1", "2", "3", "4"),
relativities = c(0.95, 0.98, 1.02, 1.05)
),
split_level(
"Medium",
new_levels = c("5", "6", "7", "8"),
relativities = c(0.96, 0.99, 1.02, 1.05)
)
)
refinement <- refinement |>
add_relativities(
model_variable = "bm_group",
split_variable = "bm_detail",
relativities = bm_relativities,
exposure = "exposure",
normalize = TRUE,
output_variable = "bm_tariff_segment"
)model_variable supplies the parent GLM effect.
split_variable identifies the detailed portfolio levels
within each parent. output_variable names the resulting
hybrid tariff factor; unsplit parent levels retain their existing model
effect.
With normalize = TRUE, the sublevel relativities are
normalised within each parent so their exposure-weighted average equals
one. The split therefore redistributes the parent effect without
changing its exposure-weighted level. Because shrinkage was added first,
these sublevel effects use the shrunken bm_group
relativities as their parent values. With
normalize = FALSE, the supplied relativities are applied
directly.
This operation is not equivalent to restriction or smoothing:
- smoothing regularises an ordered effect already represented by the model;
- restriction fixes selected tariff values;
- shrinkage reduces differences between categorical levels while retaining their ordering;
- additional relativities introduce finer differentiation inside a broader model level.
Step order matters. A restriction or shrinkage step added before
add_relativities() changes the parent coefficient used as
the basis for the split. A later restriction can instead adjust selected
levels of the derived output_variable.
Combining and reviewing refinements
The four operations now form one ordered specification:
summary(refinement)
#> Refinement specification
#>
#> Package: insurancerating 0.8.1.9000
#> Created: 2026-08-21 09:37:22 UTC
#> Observations: 30,000
#> Family: poisson (log link)
#> Base formula:
#> nclaims ~ age_band + zip + bm_group + offset(log(exposure))
#> Offset: log(exposure)
#>
#> Refinement steps: 5
#> 1. Smoothing: age_band from age_policyholder (method: spline, k: 5, scale: relativity)
#> shape = spline; 8 intervals over 18 to 95
#> 2. Smoothing edit: age_band (relative adjustment: 1.05 from 32 to 65, transition: inherited)
#> relative adjustment = 1.05; transition = inherited; cumulative from smoothing step 1
#> 3. Restriction: zip -> zip_restricted (4 levels)
#> 0 = 0.9500000; 1 = 0.9954865; 2 = 0.8971513; 3 = 1.1000000
#> 4. Shrinkage: bm_group (credibility: 0.9, weights: exposure, weighted mean preserved)
#> credibility = 0.9; weights = exposure; weighted mean preserved
#> 5. Relativities: bm_group split by bm_detail -> bm_tariff_segment (normalised: yes)
#> 2 parent levels split: Low, MediumThe summary records the base formula, package version, data size and
each refinement in evaluation order. It describes what will be applied;
it does not report a fitted refined model because refit()
has not yet been called.
The variable-specific autoplot() calls above evaluate
the stored steps in their recorded order. They therefore inspect the
intended tariff after any preceding adjustments without silently
changing the GLM. The step argument can be used when an
earlier stage of a longer specification needs to be reviewed
separately.
add_rebasing() can be inserted when required to change
which resulting level is displayed as one without changing ratios
between levels. Rebasing changes the representation of an effect,
whereas shrinkage changes its degree of differentiation.
Refitting the model
refined_model <- refit(
refinement,
intercept_only = TRUE
)
#> Warning in update_formula_remove(formula, old_term): Column 'bm_group' must be in model call.refit() applies the stored steps in order, constructs
the required tariff variables and offsets, updates the formula and calls
glm() with the original model family. It does more than
copy proposed relativities into an existing coefficient vector.
The treatment of the remaining model effects depends on
intercept_only:
- with
intercept_only = TRUE, unaffected existing effects are fixed as offsets and only the intercept is estimated. Their relative differences are preserved while the overall level is recalibrated; - with
intercept_only = FALSE, remaining free terms are estimated again. Their coefficients may change as the GLM finds a new joint optimum conditional on the fixed refinement steps.
An intercept-only refit is often appropriate for a controlled adjustment to an accepted tariff structure. Re-estimating the remaining free effects is more appropriate when the refinement is part of substantive model development and dependence between factors should be reconsidered.
The prescribed smoothing, restrictions and sublevel relativities remain explicit assumptions. Refitting does not turn them into unrestricted statistical estimates.
Inspecting the fitted result
After refit(), the result is a fitted GLM and can be
reviewed with the normal interpretation tools:
head(rating_table(refined_model, exposure = FALSE))
#> risk_factor level est_refined_model
#> 1 (Intercept) (Intercept) 0.2568384
#> 2 zip_restricted 0 0.9500000
#> 3 zip_restricted 1 0.9954865
#> 4 zip_restricted 2 0.8971513
#> 5 zip_restricted 3 1.1000000
#> 6 bm_group Medium 1.0440707This is the post-refit result. It is distinct from
the preview shown by autoplot(refinement):
Printing refined_model also reports the original and
refitted formulas, the model family, refit mode and stored refinement
steps before showing the regular GLM output.
Calibrating the final level
After the tariff structure has been refined and fitted, an externally selected overall calibration factor can be applied to a log-link model:
calibrated_model <- calibrate_model(
refined_model,
factor = 1.05
)
head(rating_table(calibrated_model, exposure = FALSE))
#> risk_factor level est_calibrated_model
#> 1 (Intercept) (Intercept) 0.2696803
#> 2 zip_restricted 0 0.9500000
#> 3 zip_restricted 1 0.9954865
#> 4 zip_restricted 2 0.8971513
#> 5 zip_restricted 3 1.1000000
#> 6 bm_group Medium 1.0440707calibrate_model() adds log(1.05) to the
intercept. Response-scale predictions therefore increase by exactly 5%,
while all non-intercept coefficients and the ratios between tariff
levels remain unchanged. The original refined_model is not
modified.
This differs from refit(intercept_only = TRUE). An
intercept-only refit estimates the overall level from the model data
conditional on the stored refinements. Calibration instead applies one
explicit total factor after that fit. It is consequently a final
model-level operation: further refinement and repeated calibration are
rejected. If the tariff structure changes, return to the retained
refinement specification, refit it and calibrate the new
result.
Auditing the portfolio effect
Individual coefficient changes are difficult to interpret
independently of the intercept, other model terms and portfolio mix.
audit_refinement() therefore compares predictions from the
unrestricted and refined models on the same observed portfolio
combinations.
refinement_audit <- audit_refinement(
refined_model,
exposure = "exposure",
metric = "frequency"
)
summary(refinement_audit)
#> Refinement audit
#>
#> Package: insurancerating 0.8.1.9000
#> Prepared: 2026-08-21 09:37:22 UTC
#> Refitted: 2026-08-21 09:37:29 UTC
#> Audited: 2026-08-21 09:37:29 UTC
#> Measure: frequency (per_exposure)
#> Exposure: exposure
#>
#> Original formula:
#> nclaims ~ age_band + zip + bm_group + offset(log(exposure))
#> Refitted formula:
#> nclaims ~ offset(log(bm_group_rel) + log(bm_group_shrunk) + log(zip_restricted) +
#> log(age_band_smooth) + log(exposure))
#>
#> Refinement steps: 5
#> 1. Smoothing: age_band from age_policyholder (method: spline, k: 5, scale: relativity)
#> shape = spline; 8 intervals over 18 to 95
#> 2. Smoothing edit: age_band (relative adjustment: 1.05 from 32 to 65, transition: inherited)
#> relative adjustment = 1.05; transition = inherited; cumulative from smoothing step 1
#> 3. Restriction: zip -> zip_restricted (4 levels)
#> 0 = 0.9500000; 1 = 0.9954865; 2 = 0.8971513; 3 = 1.1000000
#> 4. Shrinkage: bm_group (credibility: 0.9, weights: exposure, weighted mean preserved)
#> credibility = 0.9; weights = exposure; weighted mean preserved
#> 5. Relativities: bm_group split by bm_detail -> bm_tariff_segment (normalised: yes)
#> 2 parent levels split: Low, Medium
#>
#> Portfolio effect
#> Before: 0.137596
#> After: 0.137596
#> Change: -1.95122e-14 (-1.418e-11%)
#>
#> Largest level changes (10 of 24)
#> risk_factor level before after change
#> age_policyholder_smooth (84,95] 0.06942859 0.12783575 0.058407152
#> zip_restricted 3 0.13680279 0.15155033 0.014747533
#> zip_restricted 0 0.14020239 0.12759132 -0.012611074
#> bm_tariff_segment 8 0.14270460 0.15538814 0.012683531
#> bm_tariff_segment 4 0.13883017 0.15067141 0.011841241
#> age_policyholder_smooth [18,25] 0.26142311 0.24440223 -0.017020877
#> bm_tariff_segment 7 0.14278504 0.15180712 0.009022077
#> bm_tariff_segment 3 0.13764208 0.14514697 0.007504895
#> age_policyholder_smooth (65,84] 0.10100599 0.09551705 -0.005488934
#> age_policyholder_smooth (51,58] 0.11462184 0.11999825 0.005376415
#> change_ratio
#> 0.84125502
#> 0.10780140
#> -0.08994907
#> 0.08887962
#> 0.08529300
#> -0.06510854
#> 0.06318643
#> 0.05452471
#> -0.05434266
#> 0.04690568The audit reports the portfolio-level frequency before and after refinement and the corresponding changes by final risk-factor level. These are predictions from the complete model for the observed portfolio mix, not isolated coefficient differences.
The refinement and audit objects record package version, timestamps, formulas and ordered steps. This supports reproducibility, peer review and documentation of actuarial judgement. Organisational approval, source-data versioning and formal governance remain outside the package and should be handled by the user’s normal processes.
When to revisit the model
Refinement is not a substitute for correcting a poor model. If an implausible effect is caused by incorrect data, exposure errors, missing interactions, an inappropriate response definition or poor factor construction, the model or data preparation should be revisited first. Refinement is most defensible when the statistical model captures the main risk structure and the remaining adjustment has a clear tariff rationale.
Iterating without losing the specification
Retain both the editable specification and the fitted result:
refined_model <- refit(refinement)
refinement <- refinement |>
edit_smoothing(
model_variable = "age_band",
from = 32,
to = 65,
adjustment = 1.03,
transition = "linear"
)
updated_model <- refit(refinement)The update replaces the earlier local adjustment on this smoothing step. It is always calculated relative to the initial smoothing and is therefore not multiplied cumulatively by the previous value.
Calling add_smoothing(), add_restriction()
or another refinement function directly on refined_model is
deliberately not supported. Further adjustments belong on the retained
rating_refinement object so their ordering and origin
remain visible.
Summary
The specialist workflow is:
- start from an unrestricted fitted model;
- create one persistent specification with
prepare_refinement(); - add smoothing, restrictions, shrinkage or sublevel relativities with an explicit rationale;
- inspect the proposal with
summary()andautoplot(); - use
refit()to fit the model under that specification; - inspect the final tariff with
rating_table()and its portfolio effect withaudit_refinement().
This separates estimated effects, proposed actuarial adjustments and final fitted output without treating refinement as an automatic approval of the result.
Where to go next
- Getting Started constructs the initial pricing model used as the starting point for refinement.
- Pricing workflow and package building blocks places refinement within the wider package architecture.
- Model validation develops the diagnostics used to assess unrestricted and refined models.
