This vignette builds a compact but fairly complete personal-auto
rating example. The purpose is not to recommend a particular rate plan.
Instead, the example shows how ratingtables can represent
and execute a range of common rating structures while keeping the factor
data separate from the calculation order.
The example has two coverages, "BI" and
"CL".
Driver-level rating uses four multiplicative factors:
Each driver receives an indicated factor for each coverage. Driver factors are then averaged by household and joined to the vehicle records.
Vehicle-level rating uses:
The example concludes by applying rate-change caps to a small number of coverage records and by inspecting the calculation trace.
ratingtables stores rating values in a normalized
long-format factor table. For this example all table-based terms are
either constants or one-way factors, so only one variable/level slot is
needed.
The following small helper keeps the example code readable.
coverages <- c("BI", "CL")
make_factor_rows <- function(term_name,
coverage,
term_value,
variable = NA_character_,
level = NA_character_) {
n <- length(term_value)
if (length(coverage) == 1L) {
coverage <- rep(coverage, n)
}
if (length(variable) == 1L) {
variable <- rep(variable, n)
}
if (length(level) == 1L) {
level <- rep(level, n)
}
data.frame(
coverage = as.character(coverage),
term_name = rep(term_name, n),
term_value = as.numeric(term_value),
variable1 = as.character(variable),
level1 = as.character(level),
stringsAsFactors = FALSE
)
}The driver plan uses ordinary multiplicative rating factors. The factors are intentionally simple and are included only to illustrate the workflow.
driver_factor_table <- rbind(
# Gender
make_factor_rows(
"gender", "BI",
c(1.07, 0.98),
"gender", c("M", "F")
),
make_factor_rows(
"gender", "CL",
c(1.04, 0.99),
"gender", c("M", "F")
),
# Marital status
make_factor_rows(
"marital_status", "BI",
c(1.08, 0.96),
"marital_status", c("single", "married")
),
make_factor_rows(
"marital_status", "CL",
c(1.05, 0.97),
"marital_status", c("single", "married")
),
# Driver age
make_factor_rows(
"driver_age", "BI",
c(1.40, 1.08, 1.00, 1.06),
"driver_age", c("18_24", "25_39", "40_64", "65_plus")
),
make_factor_rows(
"driver_age", "CL",
c(1.30, 1.06, 1.00, 1.05),
"driver_age", c("18_24", "25_39", "40_64", "65_plus")
),
# Prior chargeable claims
make_factor_rows(
"prior_chargeable_claims", "BI",
c(0.95, 1.15, 1.35),
"prior_chargeable_claims", c("0", "1", "2_plus")
),
make_factor_rows(
"prior_chargeable_claims", "CL",
c(0.96, 1.12, 1.30),
"prior_chargeable_claims", c("0", "1", "2_plus")
)
)
head(driver_factor_table)
#> coverage term_name term_value variable1 level1
#> 1 BI gender 1.07 gender M
#> 2 BI gender 0.98 gender F
#> 3 CL gender 1.04 gender M
#> 4 CL gender 0.99 gender F
#> 5 BI marital_status 1.08 marital_status single
#> 6 BI marital_status 0.96 marital_status marriedThe rating specification defines the calculation order separately from the factor values. Every driver term in this example is an exact factor lookup applied multiplicatively.
driver_spec <- data.frame(
coverage = rep(coverages, each = 4),
step_number = rep(1:4, times = length(coverages)),
term_name = rep(
c(
"gender",
"marital_status",
"driver_age",
"prior_chargeable_claims"
),
times = length(coverages)
),
value_source = "factor_lookup",
calculation_type = "multiplicative",
stringsAsFactors = FALSE
)
driver_plan <- new_rating_plan(
factor_table = driver_factor_table,
rating_spec = driver_spec,
coverages = coverages,
max_vars = 1,
policy_id_col = "driver_id"
)There are ten drivers belonging to five households. Some households have more than one driver, which allows the entity-aggregation workflow to be shown.
drivers <- data.frame(
driver_id = paste0("D", 1:10),
household_id = c(
"H1", "H1",
"H2", "H2", "H2",
"H3",
"H4", "H4",
"H5", "H5"
),
gender = c("M", "F", "F", "M", "M", "F", "M", "F", "M", "F"),
marital_status = c(
"single", "single",
"married", "married", "single",
"married",
"single", "single",
"married", "married"
),
driver_age = c(
"18_24", "40_64",
"40_64", "65_plus", "25_39",
"25_39",
"18_24", "25_39",
"40_64", "65_plus"
),
prior_chargeable_claims = c(
"0", "0",
"0", "1", "0",
"1",
"2_plus", "1",
"0", "0"
),
stringsAsFactors = FALSE
)
drivers
#> driver_id household_id gender marital_status driver_age
#> 1 D1 H1 M single 18_24
#> 2 D2 H1 F single 40_64
#> 3 D3 H2 F married 40_64
#> 4 D4 H2 M married 65_plus
#> 5 D5 H2 M single 25_39
#> 6 D6 H3 F married 25_39
#> 7 D7 H4 M single 18_24
#> 8 D8 H4 F single 25_39
#> 9 D9 H5 M married 40_64
#> 10 D10 H5 F married 65_plus
#> prior_chargeable_claims
#> 1 0
#> 2 0
#> 3 0
#> 4 1
#> 5 0
#> 6 1
#> 7 2_plus
#> 8 1
#> 9 0
#> 10 0rate_entities() applies a rating plan to entity-level
records and retains the same trace information available for policy- or
vehicle-level rating.
driver_result <- rate_entities(
entity_data = drivers,
plan = driver_plan
)
scored_drivers <- driver_result$rated_data
scored_drivers[
,
c(
"driver_id",
"household_id",
"gender",
"marital_status",
"driver_age",
"prior_chargeable_claims",
"indicated_BI",
"indicated_CL"
)
]
#> driver_id household_id gender marital_status driver_age
#> 1 D1 H1 M single 18_24
#> 2 D2 H1 F single 40_64
#> 3 D3 H2 F married 40_64
#> 4 D4 H2 M married 65_plus
#> 5 D5 H2 M single 25_39
#> 6 D6 H3 F married 25_39
#> 7 D7 H4 M single 18_24
#> 8 D8 H4 F single 25_39
#> 9 D9 H5 M married 40_64
#> 10 D10 H5 F married 65_plus
#> prior_chargeable_claims indicated_BI indicated_CL
#> 1 0 1.5369480 1.3628160
#> 2 0 1.0054800 0.9979200
#> 3 0 0.8937600 0.9218880
#> 4 1 1.2521568 1.1863488
#> 5 0 1.1856456 1.1112192
#> 6 1 1.1684736 1.1400682
#> 7 2_plus 2.1840840 1.8454800
#> 8 1 1.3145328 1.2340944
#> 9 0 0.9758400 0.9684480
#> 10 0 0.9473856 0.9679824Because the driver plan begins with multiplicative factors rather
than a dollar base rate, the indicated_BI and
indicated_CL values are rating relativities. Each is the
product of the four applicable driver factors.
The driver factors are averaged by household separately for BI and CL.
driver_avgs <- aggregate_entity_values(
rated_entity_data = scored_drivers,
group_col = "household_id",
value_cols = c("indicated_BI", "indicated_CL"),
aggregation = "mean",
output_names = c(
"avg_driver_factor_BI",
"avg_driver_factor_CL"
)
)
driver_avgs
#> household_id avg_driver_factor_BI avg_driver_factor_CL
#> 1 H1 1.2712140 1.1803680
#> 2 H2 1.1105208 1.0731520
#> 3 H3 1.1684736 1.1400682
#> 4 H4 1.7493084 1.5397872
#> 5 H5 0.9616128 0.9682152This produces one BI and one CL driver factor for each household. Those household-level values can now be treated like any other vehicle rating input.
The vehicle factor table contains constants, categorical factors, an interpolation curve, coefficients for the continuous calculations, and the final additive expense fee.
vehicle_factor_table <- rbind(
# Base rate: one value per coverage
make_factor_rows("base_rate", "BI", 220),
make_factor_rows("base_rate", "CL", 180),
# Territory
make_factor_rows(
"territory", "BI",
c(0.90, 1.00, 1.10, 1.20),
"territory", c("T1", "T2", "T3", "T4")
),
make_factor_rows(
"territory", "CL",
c(0.95, 1.00, 1.08, 1.15),
"territory", c("T1", "T2", "T3", "T4")
),
# Credit
make_factor_rows(
"credit", "BI",
c(0.90, 1.00, 1.10, 1.20),
"credit", c("A", "B", "C", "D")
),
make_factor_rows(
"credit", "CL",
c(0.92, 1.00, 1.08, 1.15),
"credit", c("A", "B", "C", "D")
),
# Underwriting level
make_factor_rows(
"underwriting_level", "BI",
c(0.92, 1.00, 1.20),
"underwriting_level",
c("preferred", "standard", "nonstandard")
),
make_factor_rows(
"underwriting_level", "CL",
c(0.95, 1.00, 1.15),
"underwriting_level",
c("preferred", "standard", "nonstandard")
),
# Vehicle-value interpolation curve
make_factor_rows(
"vehicle_value_factor", "BI",
c(0.95, 1.00, 1.05, 1.10, 1.15),
"vehicle_value",
c("10000", "20000", "30000", "40000", "50000")
),
make_factor_rows(
"vehicle_value_factor", "CL",
c(0.90, 0.98, 1.08, 1.18, 1.28),
"vehicle_value",
c("10000", "20000", "30000", "40000", "50000")
),
# Continuous additive coefficient:
# coefficient * geo_score is added to the running premium
make_factor_rows("geo_score_charge", "BI", 1.25),
make_factor_rows("geo_score_charge", "CL", 0.75),
# Continuous multiplicative coefficient:
# factor applied is 1 + coefficient * annual_mileage_000
make_factor_rows("mileage_adjustment", "BI", 0.004),
make_factor_rows("mileage_adjustment", "CL", 0.003),
# Additive expense fee
make_factor_rows("expense_fee", "BI", 18),
make_factor_rows("expense_fee", "CL", 12),
# Collision deductible / coverage option.
# "no coverage" is intentionally listed first. Under the current package
# design this term is placed last in the CL calculation so that a factor of
# zero leaves the completed CL premium at zero.
make_factor_rows(
"cl_deductible", "CL",
c(0.00, 1.00, 0.88),
"cl_deductible",
c("no coverage", "1000", "2000")
)
)
vehicle_factor_table
#> coverage term_name term_value variable1 level1
#> 1 BI base_rate 220.000 <NA> <NA>
#> 2 CL base_rate 180.000 <NA> <NA>
#> 3 BI territory 0.900 territory T1
#> 4 BI territory 1.000 territory T2
#> 5 BI territory 1.100 territory T3
#> 6 BI territory 1.200 territory T4
#> 7 CL territory 0.950 territory T1
#> 8 CL territory 1.000 territory T2
#> 9 CL territory 1.080 territory T3
#> 10 CL territory 1.150 territory T4
#> 11 BI credit 0.900 credit A
#> 12 BI credit 1.000 credit B
#> 13 BI credit 1.100 credit C
#> 14 BI credit 1.200 credit D
#> 15 CL credit 0.920 credit A
#> 16 CL credit 1.000 credit B
#> 17 CL credit 1.080 credit C
#> 18 CL credit 1.150 credit D
#> 19 BI underwriting_level 0.920 underwriting_level preferred
#> 20 BI underwriting_level 1.000 underwriting_level standard
#> 21 BI underwriting_level 1.200 underwriting_level nonstandard
#> 22 CL underwriting_level 0.950 underwriting_level preferred
#> 23 CL underwriting_level 1.000 underwriting_level standard
#> 24 CL underwriting_level 1.150 underwriting_level nonstandard
#> 25 BI vehicle_value_factor 0.950 vehicle_value 10000
#> 26 BI vehicle_value_factor 1.000 vehicle_value 20000
#> 27 BI vehicle_value_factor 1.050 vehicle_value 30000
#> 28 BI vehicle_value_factor 1.100 vehicle_value 40000
#> 29 BI vehicle_value_factor 1.150 vehicle_value 50000
#> 30 CL vehicle_value_factor 0.900 vehicle_value 10000
#> 31 CL vehicle_value_factor 0.980 vehicle_value 20000
#> 32 CL vehicle_value_factor 1.080 vehicle_value 30000
#> 33 CL vehicle_value_factor 1.180 vehicle_value 40000
#> 34 CL vehicle_value_factor 1.280 vehicle_value 50000
#> 35 BI geo_score_charge 1.250 <NA> <NA>
#> 36 CL geo_score_charge 0.750 <NA> <NA>
#> 37 BI mileage_adjustment 0.004 <NA> <NA>
#> 38 CL mileage_adjustment 0.003 <NA> <NA>
#> 39 BI expense_fee 18.000 <NA> <NA>
#> 40 CL expense_fee 12.000 <NA> <NA>
#> 41 CL cl_deductible 0.000 cl_deductible no coverage
#> 42 CL cl_deductible 1.000 cl_deductible 1000
#> 43 CL cl_deductible 0.880 cl_deductible 2000There are eight vehicle records. Each vehicle belongs to one of the households rated above.
vehicles <- data.frame(
vehicle_id = paste0("V", 1:8),
policy_id = c("P1", "P1", "P2", "P2", "P3", "P4", "P5", "P5"),
household_id = c("H1", "H1", "H2", "H2", "H3", "H4", "H5", "H5"),
territory = c("T1", "T2", "T3", "T4", "T2", "T4", "T3", "T4"),
credit = c("A", "B", "C", "D", "B", "C", "D", "C"),
underwriting_level = c(
"preferred",
"standard",
"nonstandard",
"standard",
"standard",
"nonstandard",
"nonstandard",
"preferred"
),
vehicle_value = c(
12500,
21750,
32000,
18000,
27500,
39000,
46500,
23500
),
geo_score = c(3.2, 5.8, 7.5, 2.1, 6.9, 8.0, 7.8, 4.7),
annual_mileage_000 = c(8, 12, 16, 10, 14, 18, 20, 7),
cl_deductible = c(
"1000",
"no coverage",
"2000",
"1000",
"no coverage",
"1000",
"2000",
"no coverage"
),
stringsAsFactors = FALSE
)
vehicles
#> vehicle_id policy_id household_id territory credit underwriting_level
#> 1 V1 P1 H1 T1 A preferred
#> 2 V2 P1 H1 T2 B standard
#> 3 V3 P2 H2 T3 C nonstandard
#> 4 V4 P2 H2 T4 D standard
#> 5 V5 P3 H3 T2 B standard
#> 6 V6 P4 H4 T4 C nonstandard
#> 7 V7 P5 H5 T3 D nonstandard
#> 8 V8 P5 H5 T4 C preferred
#> vehicle_value geo_score annual_mileage_000 cl_deductible
#> 1 12500 3.2 8 1000
#> 2 21750 5.8 12 no coverage
#> 3 32000 7.5 16 2000
#> 4 18000 2.1 10 1000
#> 5 27500 6.9 14 no coverage
#> 6 39000 8.0 18 1000
#> 7 46500 7.8 20 2000
#> 8 23500 4.7 7 no coverageJoin the household driver averages back to the vehicle data:
vehicles_with_driver_avgs <- join_entity_values(
parent_data = vehicles,
entity_values = driver_avgs,
by = "household_id"
)
vehicles_with_driver_avgs[
,
c(
"vehicle_id",
"household_id",
"avg_driver_factor_BI",
"avg_driver_factor_CL"
)
]
#> vehicle_id household_id avg_driver_factor_BI avg_driver_factor_CL
#> 1 V1 H1 1.2712140 1.1803680
#> 2 V2 H1 1.2712140 1.1803680
#> 3 V3 H2 1.1105208 1.0731520
#> 4 V4 H2 1.1105208 1.0731520
#> 5 V5 H3 1.1684736 1.1400682
#> 6 V6 H4 1.7493084 1.5397872
#> 7 V7 H5 0.9616128 0.9682152
#> 8 V8 H5 0.9616128 0.9682152Most rating elements are easier to inspect and maintain when they remain in the ordinary factor table and rating specification. Custom functions are therefore best treated as an escape hatch rather than the default way to express rating logic.
They can still be useful when reproducing an existing rater that contains conditional or interdependent logic. A rater-modernization project may also be a good opportunity to reconsider whether unusual legacy rules should continue to exist at all.
A custom function receives six arguments:
row: the one-row record being rated;coverage: the coverage currently being rated;current_premium: the running premium immediately before
the step;plan: the complete rating-plan object;spec_row: the rating-specification row for the current
step; andlookup: a helper that can retrieve values from the
plan’s factor table.The first custom function is intentionally simple. A high geographic score combined with nonstandard underwriting produces a 3 percent surcharge.
custom_high_score_surcharge <- function(
row,
coverage,
current_premium,
plan,
spec_row,
lookup
) {
score <- as.numeric(row$geo_score[[1]])
uw <- as.character(row$underwriting_level[[1]])
if (score >= 7 && uw == "nonstandard") {
1.03
} else {
1.00
}
}This function only needs fields from row. The other
arguments are still available because every custom function receives the
same interface.
The second custom function demonstrates a different use case. It looks up three ordinary rating factors that have already been used elsewhere in the rate calculation: territory, credit, and underwriting level.
If exactly two of the three factors are adverse, a 3 percent surcharge applies. If all three are adverse, a 7 percent surcharge applies.
legacy_composite_risk_adjustment <- function(
row,
coverage,
current_premium,
plan,
spec_row,
lookup
) {
territory_factor <- lookup("territory")$value
credit_factor <- lookup("credit")$value
underwriting_factor <- lookup("underwriting_level")$value
adverse_count <- sum(
c(
territory_factor > 1,
credit_factor > 1,
underwriting_factor > 1
)
)
if (adverse_count == 3) {
1.07
} else if (adverse_count == 2) {
1.03
} else {
1.00
}
}This is the kind of rule that may be awkward to encode as another normalized factor table. The function can query the factor table directly and combine the results using arbitrary R logic.
The flexibility is useful, but such logic should not automatically be viewed as good rating-plan design merely because it can be reproduced.
The two custom functions are placed next to each other in the calculation order.
For BI, the additive expense fee is the final step and the completed premium is rounded to the nearest dime. For CL, a deductible/coverage-option factor follows the expense fee and is the final step.
Under the current package design, every coverage listed in the rating
plan is rated for every record. The "no coverage" CL
deductible level therefore has a factor of zero and is deliberately
placed last. This produces a final CL premium of zero for vehicles that
did not select the coverage.
base_vehicle_terms <- c(
"base_rate",
"territory",
"average_driver_factor",
"credit",
"underwriting_level",
"vehicle_value_factor",
"geo_score_charge",
"mileage_adjustment",
"custom_high_score_surcharge",
"legacy_composite_risk_adjustment",
"expense_fee"
)
base_vehicle_value_sources <- c(
"factor_lookup",
"factor_lookup",
"input_value",
"factor_lookup",
"factor_lookup",
"interpolated_lookup",
"factor_lookup",
"factor_lookup",
"custom_function",
"custom_function",
"factor_lookup"
)
base_vehicle_calculation_types <- c(
"multiplicative",
"multiplicative",
"multiplicative",
"multiplicative",
"multiplicative",
"multiplicative",
"continuous_additive",
"continuous_multiplicative",
"multiplicative",
"multiplicative",
"additive"
)
make_vehicle_spec <- function(coverage, driver_input) {
terms <- base_vehicle_terms
value_sources <- base_vehicle_value_sources
calculation_types <- base_vehicle_calculation_types
input_vars <- c(
NA,
NA,
driver_input,
NA,
NA,
NA,
"geo_score",
"annual_mileage_000",
NA,
NA,
NA
)
lookup_vars <- c(
NA,
NA,
NA,
NA,
NA,
"vehicle_value",
NA,
NA,
NA,
NA,
NA
)
bounds <- c(
NA,
NA,
NA,
NA,
NA,
"error",
NA,
NA,
NA,
NA,
NA
)
custom_functions <- c(
NA,
NA,
NA,
NA,
NA,
NA,
NA,
NA,
"custom_high_score_surcharge",
"legacy_composite_risk_adjustment",
NA
)
if (coverage == "CL") {
terms <- c(terms, "cl_deductible")
value_sources <- c(value_sources, "factor_lookup")
calculation_types <- c(calculation_types, "multiplicative")
input_vars <- c(input_vars, NA)
lookup_vars <- c(lookup_vars, NA)
bounds <- c(bounds, NA)
custom_functions <- c(custom_functions, NA)
}
rounding_rules <- rep(NA_character_, length(terms))
rounding_rules[length(terms)] <- "nearest_dime"
data.frame(
coverage = coverage,
step_number = seq_along(terms),
term_name = terms,
value_source = value_sources,
calculation_type = calculation_types,
input_var = input_vars,
lookup_var = lookup_vars,
bounds = bounds,
custom_function = custom_functions,
rounding_rule = rounding_rules,
stringsAsFactors = FALSE
)
}
vehicle_spec <- rbind(
make_vehicle_spec("BI", "avg_driver_factor_BI"),
make_vehicle_spec("CL", "avg_driver_factor_CL")
)
vehicle_spec[
,
c(
"coverage",
"step_number",
"term_name",
"value_source",
"calculation_type",
"input_var",
"lookup_var",
"custom_function",
"rounding_rule"
)
]
#> coverage step_number term_name value_source
#> 1 BI 1 base_rate factor_lookup
#> 2 BI 2 territory factor_lookup
#> 3 BI 3 average_driver_factor input_value
#> 4 BI 4 credit factor_lookup
#> 5 BI 5 underwriting_level factor_lookup
#> 6 BI 6 vehicle_value_factor interpolated_lookup
#> 7 BI 7 geo_score_charge factor_lookup
#> 8 BI 8 mileage_adjustment factor_lookup
#> 9 BI 9 custom_high_score_surcharge custom_function
#> 10 BI 10 legacy_composite_risk_adjustment custom_function
#> 11 BI 11 expense_fee factor_lookup
#> 12 CL 1 base_rate factor_lookup
#> 13 CL 2 territory factor_lookup
#> 14 CL 3 average_driver_factor input_value
#> 15 CL 4 credit factor_lookup
#> 16 CL 5 underwriting_level factor_lookup
#> 17 CL 6 vehicle_value_factor interpolated_lookup
#> 18 CL 7 geo_score_charge factor_lookup
#> 19 CL 8 mileage_adjustment factor_lookup
#> 20 CL 9 custom_high_score_surcharge custom_function
#> 21 CL 10 legacy_composite_risk_adjustment custom_function
#> 22 CL 11 expense_fee factor_lookup
#> 23 CL 12 cl_deductible factor_lookup
#> calculation_type input_var lookup_var
#> 1 multiplicative <NA> <NA>
#> 2 multiplicative <NA> <NA>
#> 3 multiplicative avg_driver_factor_BI <NA>
#> 4 multiplicative <NA> <NA>
#> 5 multiplicative <NA> <NA>
#> 6 multiplicative <NA> vehicle_value
#> 7 continuous_additive geo_score <NA>
#> 8 continuous_multiplicative annual_mileage_000 <NA>
#> 9 multiplicative <NA> <NA>
#> 10 multiplicative <NA> <NA>
#> 11 additive <NA> <NA>
#> 12 multiplicative <NA> <NA>
#> 13 multiplicative <NA> <NA>
#> 14 multiplicative avg_driver_factor_CL <NA>
#> 15 multiplicative <NA> <NA>
#> 16 multiplicative <NA> <NA>
#> 17 multiplicative <NA> vehicle_value
#> 18 continuous_additive geo_score <NA>
#> 19 continuous_multiplicative annual_mileage_000 <NA>
#> 20 multiplicative <NA> <NA>
#> 21 multiplicative <NA> <NA>
#> 22 additive <NA> <NA>
#> 23 multiplicative <NA> <NA>
#> custom_function rounding_rule
#> 1 <NA> <NA>
#> 2 <NA> <NA>
#> 3 <NA> <NA>
#> 4 <NA> <NA>
#> 5 <NA> <NA>
#> 6 <NA> <NA>
#> 7 <NA> <NA>
#> 8 <NA> <NA>
#> 9 custom_high_score_surcharge <NA>
#> 10 legacy_composite_risk_adjustment <NA>
#> 11 <NA> nearest_dime
#> 12 <NA> <NA>
#> 13 <NA> <NA>
#> 14 <NA> <NA>
#> 15 <NA> <NA>
#> 16 <NA> <NA>
#> 17 <NA> <NA>
#> 18 <NA> <NA>
#> 19 <NA> <NA>
#> 20 custom_high_score_surcharge <NA>
#> 21 legacy_composite_risk_adjustment <NA>
#> 22 <NA> <NA>
#> 23 <NA> nearest_dimeCreate the completed vehicle rating plan and register both custom functions by name.
vehicle_plan <- new_rating_plan(
factor_table = vehicle_factor_table,
rating_spec = vehicle_spec,
coverages = coverages,
max_vars = 1,
policy_id_col = "vehicle_id",
custom_functions = list(
custom_high_score_surcharge = custom_high_score_surcharge,
legacy_composite_risk_adjustment = legacy_composite_risk_adjustment
)
)
vehicle_plan
#> <rating_plan>
#> coverages: BI, CL
#> factor rows: 43
#> spec rows: 23vehicle_result <- rate_policies_with_trace(
rating_data = vehicles_with_driver_avgs,
plan = vehicle_plan
)
rated_vehicles <- vehicle_result$rated_data
rated_vehicles[
,
c(
"vehicle_id",
"policy_id",
"household_id",
"territory",
"credit",
"underwriting_level",
"vehicle_value",
"geo_score",
"annual_mileage_000",
"cl_deductible",
"avg_driver_factor_BI",
"avg_driver_factor_CL",
"indicated_BI",
"indicated_CL"
)
]
#> vehicle_id policy_id household_id territory credit underwriting_level
#> 1 V1 P1 H1 T1 A preferred
#> 2 V2 P1 H1 T2 B standard
#> 3 V3 P2 H2 T3 C nonstandard
#> 4 V4 P2 H2 T4 D standard
#> 5 V5 P3 H3 T2 B standard
#> 6 V6 P4 H4 T4 C nonstandard
#> 7 V7 P5 H5 T3 D nonstandard
#> 8 V8 P5 H5 T4 C preferred
#> vehicle_value geo_score annual_mileage_000 cl_deductible avg_driver_factor_BI
#> 1 12500 3.2 8 1000 1.2712140
#> 2 21750 5.8 12 no coverage 1.2712140
#> 3 32000 7.5 16 2000 1.1105208
#> 4 18000 2.1 10 1000 1.1105208
#> 5 27500 6.9 14 no coverage 1.1684736
#> 6 39000 8.0 18 1000 1.7493084
#> 7 46500 7.8 20 2000 0.9616128
#> 8 23500 4.7 7 no coverage 0.9616128
#> avg_driver_factor_CL indicated_BI indicated_CL
#> 1 1.1803680 229.1 180.7
#> 2 1.1803680 321.3 0.0
#> 3 1.0731520 469.9 306.0
#> 4 1.0731520 393.9 274.9
#> 5 1.1400682 308.7 0.0
#> 6 1.5397872 818.4 557.0
#> 7 0.9682152 481.3 335.2
#> 8 0.9682152 301.0 0.0The final indicated premiums include the complete sequence of multiplicative, additive, continuous, interpolated, and custom calculations. Rounding is attached only to the final step for each coverage, so intermediate calculations retain their full precision and the completed premium is rounded to the nearest dime.
For vehicles with cl_deductible == "no coverage", the
final CL deductible factor is zero and indicated_CL is
therefore zero.
rated_vehicles[
rated_vehicles$cl_deductible == "no coverage",
c("vehicle_id", "cl_deductible", "indicated_BI", "indicated_CL")
]
#> vehicle_id cl_deductible indicated_BI indicated_CL
#> 2 V2 no coverage 321.3 0
#> 5 V5 no coverage 308.7 0
#> 8 V8 no coverage 301.0 0This is a workable representation with the current API, but it also illustrates a distinction between coverage selection and coverage rating. The engine still evaluates the earlier CL steps before the final zero factor is applied. A rating system with first-class coverage selection could instead skip the CL calculation entirely for these records.
The trace retains one row per rating step. The following subset focuses on the interpolated vehicle-value factor and the two custom calculations.
interesting_terms <- c(
"vehicle_value_factor",
"custom_high_score_surcharge",
"legacy_composite_risk_adjustment"
)
interesting_trace <- vehicle_result$term_trace[
vehicle_result$term_trace$term_name %in% interesting_terms,
c(
"record_id",
"coverage",
"step_number",
"term_name",
"value_source",
"calculation_type",
"input_value",
"looked_up_value",
"applied_value",
"lower_level",
"upper_level",
"lower_value",
"upper_value",
"interpolation_weight",
"value_before_step",
"value_after_step",
"custom_function"
)
]
interesting_trace
#> record_id coverage step_number term_name
#> 6 V1 BI 6 vehicle_value_factor
#> 9 V1 BI 9 custom_high_score_surcharge
#> 10 V1 BI 10 legacy_composite_risk_adjustment
#> 17 V1 CL 6 vehicle_value_factor
#> 20 V1 CL 9 custom_high_score_surcharge
#> 21 V1 CL 10 legacy_composite_risk_adjustment
#> 29 V2 BI 6 vehicle_value_factor
#> 32 V2 BI 9 custom_high_score_surcharge
#> 33 V2 BI 10 legacy_composite_risk_adjustment
#> 40 V2 CL 6 vehicle_value_factor
#> 43 V2 CL 9 custom_high_score_surcharge
#> 44 V2 CL 10 legacy_composite_risk_adjustment
#> 52 V3 BI 6 vehicle_value_factor
#> 55 V3 BI 9 custom_high_score_surcharge
#> 56 V3 BI 10 legacy_composite_risk_adjustment
#> 63 V3 CL 6 vehicle_value_factor
#> 66 V3 CL 9 custom_high_score_surcharge
#> 67 V3 CL 10 legacy_composite_risk_adjustment
#> 75 V4 BI 6 vehicle_value_factor
#> 78 V4 BI 9 custom_high_score_surcharge
#> 79 V4 BI 10 legacy_composite_risk_adjustment
#> 86 V4 CL 6 vehicle_value_factor
#> 89 V4 CL 9 custom_high_score_surcharge
#> 90 V4 CL 10 legacy_composite_risk_adjustment
#> 98 V5 BI 6 vehicle_value_factor
#> 101 V5 BI 9 custom_high_score_surcharge
#> 102 V5 BI 10 legacy_composite_risk_adjustment
#> 109 V5 CL 6 vehicle_value_factor
#> 112 V5 CL 9 custom_high_score_surcharge
#> 113 V5 CL 10 legacy_composite_risk_adjustment
#> 121 V6 BI 6 vehicle_value_factor
#> 124 V6 BI 9 custom_high_score_surcharge
#> 125 V6 BI 10 legacy_composite_risk_adjustment
#> 132 V6 CL 6 vehicle_value_factor
#> 135 V6 CL 9 custom_high_score_surcharge
#> 136 V6 CL 10 legacy_composite_risk_adjustment
#> 144 V7 BI 6 vehicle_value_factor
#> 147 V7 BI 9 custom_high_score_surcharge
#> 148 V7 BI 10 legacy_composite_risk_adjustment
#> 155 V7 CL 6 vehicle_value_factor
#> 158 V7 CL 9 custom_high_score_surcharge
#> 159 V7 CL 10 legacy_composite_risk_adjustment
#> 167 V8 BI 6 vehicle_value_factor
#> 170 V8 BI 9 custom_high_score_surcharge
#> 171 V8 BI 10 legacy_composite_risk_adjustment
#> 178 V8 CL 6 vehicle_value_factor
#> 181 V8 CL 9 custom_high_score_surcharge
#> 182 V8 CL 10 legacy_composite_risk_adjustment
#> value_source calculation_type input_value looked_up_value
#> 6 interpolated_lookup multiplicative 12500 0.96250
#> 9 custom_function multiplicative NA NA
#> 10 custom_function multiplicative NA NA
#> 17 interpolated_lookup multiplicative 12500 0.92000
#> 20 custom_function multiplicative NA NA
#> 21 custom_function multiplicative NA NA
#> 29 interpolated_lookup multiplicative 21750 1.00875
#> 32 custom_function multiplicative NA NA
#> 33 custom_function multiplicative NA NA
#> 40 interpolated_lookup multiplicative 21750 0.99750
#> 43 custom_function multiplicative NA NA
#> 44 custom_function multiplicative NA NA
#> 52 interpolated_lookup multiplicative 32000 1.06000
#> 55 custom_function multiplicative NA NA
#> 56 custom_function multiplicative NA NA
#> 63 interpolated_lookup multiplicative 32000 1.10000
#> 66 custom_function multiplicative NA NA
#> 67 custom_function multiplicative NA NA
#> 75 interpolated_lookup multiplicative 18000 0.99000
#> 78 custom_function multiplicative NA NA
#> 79 custom_function multiplicative NA NA
#> 86 interpolated_lookup multiplicative 18000 0.96400
#> 89 custom_function multiplicative NA NA
#> 90 custom_function multiplicative NA NA
#> 98 interpolated_lookup multiplicative 27500 1.03750
#> 101 custom_function multiplicative NA NA
#> 102 custom_function multiplicative NA NA
#> 109 interpolated_lookup multiplicative 27500 1.05500
#> 112 custom_function multiplicative NA NA
#> 113 custom_function multiplicative NA NA
#> 121 interpolated_lookup multiplicative 39000 1.09500
#> 124 custom_function multiplicative NA NA
#> 125 custom_function multiplicative NA NA
#> 132 interpolated_lookup multiplicative 39000 1.17000
#> 135 custom_function multiplicative NA NA
#> 136 custom_function multiplicative NA NA
#> 144 interpolated_lookup multiplicative 46500 1.13250
#> 147 custom_function multiplicative NA NA
#> 148 custom_function multiplicative NA NA
#> 155 interpolated_lookup multiplicative 46500 1.24500
#> 158 custom_function multiplicative NA NA
#> 159 custom_function multiplicative NA NA
#> 167 interpolated_lookup multiplicative 23500 1.01750
#> 170 custom_function multiplicative NA NA
#> 171 custom_function multiplicative NA NA
#> 178 interpolated_lookup multiplicative 23500 1.01500
#> 181 custom_function multiplicative NA NA
#> 182 custom_function multiplicative NA NA
#> applied_value lower_level upper_level lower_value upper_value
#> 6 0.96250 10000 20000 0.95 1.00
#> 9 1.00000 NA NA NA NA
#> 10 1.00000 NA NA NA NA
#> 17 0.92000 10000 20000 0.90 0.98
#> 20 1.00000 NA NA NA NA
#> 21 1.00000 NA NA NA NA
#> 29 1.00875 20000 30000 1.00 1.05
#> 32 1.00000 NA NA NA NA
#> 33 1.00000 NA NA NA NA
#> 40 0.99750 20000 30000 0.98 1.08
#> 43 1.00000 NA NA NA NA
#> 44 1.00000 NA NA NA NA
#> 52 1.06000 30000 40000 1.05 1.10
#> 55 1.03000 NA NA NA NA
#> 56 1.07000 NA NA NA NA
#> 63 1.10000 30000 40000 1.08 1.18
#> 66 1.03000 NA NA NA NA
#> 67 1.07000 NA NA NA NA
#> 75 0.99000 10000 20000 0.95 1.00
#> 78 1.00000 NA NA NA NA
#> 79 1.03000 NA NA NA NA
#> 86 0.96400 10000 20000 0.90 0.98
#> 89 1.00000 NA NA NA NA
#> 90 1.03000 NA NA NA NA
#> 98 1.03750 20000 30000 1.00 1.05
#> 101 1.00000 NA NA NA NA
#> 102 1.00000 NA NA NA NA
#> 109 1.05500 20000 30000 0.98 1.08
#> 112 1.00000 NA NA NA NA
#> 113 1.00000 NA NA NA NA
#> 121 1.09500 30000 40000 1.05 1.10
#> 124 1.03000 NA NA NA NA
#> 125 1.07000 NA NA NA NA
#> 132 1.17000 30000 40000 1.08 1.18
#> 135 1.03000 NA NA NA NA
#> 136 1.07000 NA NA NA NA
#> 144 1.13250 40000 50000 1.10 1.15
#> 147 1.03000 NA NA NA NA
#> 148 1.07000 NA NA NA NA
#> 155 1.24500 40000 50000 1.18 1.28
#> 158 1.03000 NA NA NA NA
#> 159 1.07000 NA NA NA NA
#> 167 1.01750 20000 30000 1.00 1.05
#> 170 1.00000 NA NA NA NA
#> 171 1.03000 NA NA NA NA
#> 178 1.01500 20000 30000 0.98 1.08
#> 181 1.00000 NA NA NA NA
#> 182 1.03000 NA NA NA NA
#> interpolation_weight value_before_step value_after_step
#> 6 0.250 208.4079 200.5926
#> 9 NA 211.1396 211.1396
#> 10 NA 211.1396 211.1396
#> 17 0.250 176.4107 162.2979
#> 20 NA 168.6506 168.6506
#> 21 NA 168.6506 168.6506
#> 29 0.175 279.6671 282.1142
#> 32 NA 303.2536 303.2536
#> 33 NA 303.2536 303.2536
#> 40 0.175 212.4662 211.9351
#> 43 NA 224.0713 224.0713
#> 44 NA 224.0713 224.0713
#> 52 0.200 354.7448 376.0295
#> 55 NA 410.0703 422.3724
#> 56 NA 422.3724 451.9385
#> 63 0.200 259.1070 285.0177
#> 66 NA 304.5935 313.7313
#> 67 NA 313.7313 335.6925
#> 75 0.800 351.8130 348.2949
#> 78 NA 364.9567 364.9567
#> 79 NA 364.9567 375.9054
#> 86 0.800 255.4638 246.2671
#> 89 NA 255.2774 255.2774
#> 90 NA 255.2774 262.9357
#> 98 0.750 257.0642 266.7041
#> 101 NA 290.7475 290.7475
#> 102 NA 290.7475 290.7475
#> 109 0.750 205.2123 216.4989
#> 112 NA 230.9842 230.9842
#> 113 NA 230.9842 230.9842
#> 121 0.900 609.5990 667.5109
#> 124 NA 726.2917 748.0804
#> 125 NA 748.0804 800.4461
#> 132 0.900 395.8701 463.1680
#> 135 NA 494.5030 509.3381
#> 136 NA 509.3381 544.9918
#> 144 0.650 335.1028 379.5040
#> 147 NA 420.3943 433.0061
#> 148 NA 433.0061 463.3165
#> 155 0.650 248.9223 309.9083
#> 158 NA 334.7038 344.7449
#> 159 NA 344.7449 368.8770
#> 167 0.350 256.9122 261.4081
#> 170 NA 274.7671 274.7671
#> 171 NA 274.7671 283.0101
#> 178 0.350 205.6315 208.7160
#> 181 NA 216.6980 216.6980
#> 182 NA 216.6980 223.1990
#> custom_function
#> 6 <NA>
#> 9 custom_high_score_surcharge
#> 10 legacy_composite_risk_adjustment
#> 17 <NA>
#> 20 custom_high_score_surcharge
#> 21 legacy_composite_risk_adjustment
#> 29 <NA>
#> 32 custom_high_score_surcharge
#> 33 legacy_composite_risk_adjustment
#> 40 <NA>
#> 43 custom_high_score_surcharge
#> 44 legacy_composite_risk_adjustment
#> 52 <NA>
#> 55 custom_high_score_surcharge
#> 56 legacy_composite_risk_adjustment
#> 63 <NA>
#> 66 custom_high_score_surcharge
#> 67 legacy_composite_risk_adjustment
#> 75 <NA>
#> 78 custom_high_score_surcharge
#> 79 legacy_composite_risk_adjustment
#> 86 <NA>
#> 89 custom_high_score_surcharge
#> 90 legacy_composite_risk_adjustment
#> 98 <NA>
#> 101 custom_high_score_surcharge
#> 102 legacy_composite_risk_adjustment
#> 109 <NA>
#> 112 custom_high_score_surcharge
#> 113 legacy_composite_risk_adjustment
#> 121 <NA>
#> 124 custom_high_score_surcharge
#> 125 legacy_composite_risk_adjustment
#> 132 <NA>
#> 135 custom_high_score_surcharge
#> 136 legacy_composite_risk_adjustment
#> 144 <NA>
#> 147 custom_high_score_surcharge
#> 148 legacy_composite_risk_adjustment
#> 155 <NA>
#> 158 custom_high_score_surcharge
#> 159 legacy_composite_risk_adjustment
#> 167 <NA>
#> 170 custom_high_score_surcharge
#> 171 legacy_composite_risk_adjustment
#> 178 <NA>
#> 181 custom_high_score_surcharge
#> 182 legacy_composite_risk_adjustmentFor the interpolation step, the trace records the lower and upper vehicle-value points, their factor values, and the interpolation weight. For custom functions, the trace records the function name and the value applied to the running premium.
A complete explanation for one vehicle and coverage can also be requested:
explain_rating(
rating_result = vehicle_result,
row_number = 1,
coverage = "BI"
)
#> row_number record_id coverage step_number term_name
#> 1 1 V1 BI 1 base_rate
#> 2 1 V1 BI 2 territory
#> 3 1 V1 BI 3 average_driver_factor
#> 4 1 V1 BI 4 credit
#> 5 1 V1 BI 5 underwriting_level
#> 6 1 V1 BI 6 vehicle_value_factor
#> 7 1 V1 BI 7 geo_score_charge
#> 8 1 V1 BI 8 mileage_adjustment
#> 9 1 V1 BI 9 custom_high_score_surcharge
#> 10 1 V1 BI 10 legacy_composite_risk_adjustment
#> 11 1 V1 BI 11 expense_fee
#> value_source calculation_type applied_value
#> 1 factor_lookup multiplicative 220.000000
#> 2 factor_lookup multiplicative 0.900000
#> 3 input_value multiplicative 1.271214
#> 4 factor_lookup multiplicative 0.900000
#> 5 factor_lookup multiplicative 0.920000
#> 6 interpolated_lookup multiplicative 0.962500
#> 7 factor_lookup continuous_additive 4.000000
#> 8 factor_lookup continuous_multiplicative 1.032000
#> 9 custom_function multiplicative 1.000000
#> 10 custom_function multiplicative 1.000000
#> 11 factor_lookup additive 18.000000
#> value_before_step value_after_step
#> 1 NA 220.0000
#> 2 220.0000 198.0000
#> 3 198.0000 251.7004
#> 4 251.7004 226.5303
#> 5 226.5303 208.4079
#> 6 208.4079 200.5926
#> 7 200.5926 204.5926
#> 8 204.5926 211.1396
#> 9 211.1396 211.1396
#> 10 211.1396 211.1396
#> 11 211.1396 229.1000Rate capping is applied after the indicated premium has been calculated.
For this compact example, the prior premiums are deliberately constructed so that only a few vehicle/coverage records fall outside the permitted range. This makes the capping behavior visible without turning the example into a large renewal analysis.
bi_change_targets <- c(
1.05, 1.02, 1.25, 0.98,
1.00, 1.03, 0.85, 1.04
)
cl_change_targets <- c(
1.04, 0.95, 1.03, 1.22,
1.00, 1.01, 0.92, 1.02
)
prior_vehicle_premium <- data.frame(
vehicle_id = rated_vehicles$vehicle_id,
prior_BI = rated_vehicles$indicated_BI / bi_change_targets,
prior_CL = rated_vehicles$indicated_CL / cl_change_targets,
stringsAsFactors = FALSE
)
rated_vehicles_capped <- apply_caps(
rating_data = rated_vehicles,
prior_data = prior_vehicle_premium,
by = "vehicle_id",
coverages = coverages,
max_increase = 0.15,
max_decrease = 0.10
)
rated_vehicles_capped$cap_applied_BI <-
abs(
rated_vehicles_capped$capped_BI -
rated_vehicles_capped$indicated_BI
) > 1e-8
rated_vehicles_capped$cap_applied_CL <-
abs(
rated_vehicles_capped$capped_CL -
rated_vehicles_capped$indicated_CL
) > 1e-8
rated_vehicles_capped[
,
c(
"vehicle_id",
"prior_BI",
"indicated_BI",
"capped_BI",
"cap_applied_BI",
"prior_CL",
"indicated_CL",
"capped_CL",
"cap_applied_CL"
)
]
#> vehicle_id prior_BI indicated_BI capped_BI cap_applied_BI prior_CL
#> 1 V1 218.1905 229.1 229.1000 FALSE 173.7500
#> 2 V2 315.0000 321.3 321.3000 FALSE 0.0000
#> 3 V3 375.9200 469.9 432.3080 TRUE 297.0874
#> 4 V4 401.9388 393.9 393.9000 FALSE 225.3279
#> 5 V5 308.7000 308.7 308.7000 FALSE 0.0000
#> 6 V6 794.5631 818.4 818.4000 FALSE 551.4851
#> 7 V7 566.2353 481.3 509.6118 TRUE 364.3478
#> 8 V8 289.4231 301.0 301.0000 FALSE 0.0000
#> indicated_CL capped_CL cap_applied_CL
#> 1 180.7 180.700 FALSE
#> 2 0.0 0.000 FALSE
#> 3 306.0 306.000 FALSE
#> 4 274.9 259.127 TRUE
#> 5 0.0 0.000 FALSE
#> 6 557.0 557.000 FALSE
#> 7 335.2 335.200 FALSE
#> 8 0.0 0.000 FALSETo focus only on records where at least one coverage was capped:
rated_vehicles_capped[
rated_vehicles_capped$cap_applied_BI |
rated_vehicles_capped$cap_applied_CL,
c(
"vehicle_id",
"prior_BI",
"indicated_BI",
"capped_BI",
"prior_CL",
"indicated_CL",
"capped_CL"
)
]
#> vehicle_id prior_BI indicated_BI capped_BI prior_CL indicated_CL capped_CL
#> 3 V3 375.9200 469.9 432.3080 297.0874 306.0 306.000
#> 4 V4 401.9388 393.9 393.9000 225.3279 274.9 259.127
#> 7 V7 566.2353 481.3 509.6118 364.3478 335.2 335.200This vignette uses one coherent rating workflow to demonstrate
several parts of ratingtables:
The standard table-driven calculations should generally remain the preferred representation when they are sufficient. Custom functions are most useful as a controlled escape hatch for logic that is genuinely difficult to represent cleanly in the ordinary specification, particularly when recreating or validating an existing rater.
Because the factor data, calculation specification, custom code, and example rating records are all ordinary R objects, each component can be inspected, tested, version-controlled, and modified independently.