A Worked Auto Driver Averaging Rating Plan with ratingtables

library(ratingtables)

Overview

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:

  1. a base rate;
  2. territory;
  3. the household average driver factor;
  4. credit;
  5. underwriting level;
  6. an interpolated vehicle-value factor;
  7. a continuous additive geographic-score charge;
  8. a continuous multiplicative mileage adjustment;
  9. a custom high-score surcharge;
  10. a custom composite-risk adjustment;
  11. an additive expense fee;
  12. a Collision deductible/coverage-option factor; and
  13. final rounding.

The example concludes by applying rate-change caps to a small number of coverage records and by inspecting the calculation trace.

A helper for building factor-table rows

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
  )
}

Driver-level rating

Driver factor table

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 married

Driver rating specification

The 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"
)

Driver data

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                       0

Rate the drivers

rate_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.9679824

Because 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.

Average driver factors by household

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.9682152

This 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.

Vehicle-level rating

Vehicle factor table

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        2000

Vehicle data

There 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 coverage

Join 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.9682152

Custom rating functions

Most 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:

Custom function 1: high-score surcharge

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.

Custom function 2: legacy composite-risk adjustment

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.

Vehicle rating specification

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_dime

Create 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: 23

Rate the vehicles

vehicle_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.0

The 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            0

This 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.

Inspect interpolation and custom-function trace rows

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_adjustment

For 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.1000

Apply rate-change caps

Rate 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          FALSE

To 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.200

What this example demonstrates

This 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.