A single-vehicle private car model rated by territory, vehicle group, driver age and no claims discount.
Personal motor insurance covers a private car against third-party liability, and usually fire, theft and accidental damage. It is one of the most price-competitive lines in the UK, where most policies are bought on price comparison websites, so the rating model has to be fast, accurate and easy to change.
This model uses a multiplicative rating structure. A base rate is looked up by postcode area, then multiplied by relativities for the territory, the ABI vehicle group, the driver's age and the no claims discount.
Driver age is banded in a transform step, because banding is a comparison rather than arithmetic. Under-17 drivers are declined, and high vehicle groups or serious convictions are referred to an underwriter.
Three further factors refine the risk: annual mileage, how the car is used (social, commuting or business) and where it is parked overnight.
| Field | Label | Type | Default | Allowed values |
|---|---|---|---|---|
postcode_area | Postcode area | string | SW | SW, M, EH, B, N |
vehicle_group | Vehicle group | integer | 10 | 5, 10, 20, 30, 45 |
driver_age | Driver age | integer | 30 | 17, 21, 25, 40, 60, 75 |
ncd_years | No-claims discount years | integer | 0 | 0, 2, 5, 9 |
convictions_5y | Convictions in last 5 years | integer | 0 | 0, 1, 3 |
annual_mileage | Annual mileage | integer | 8000 | 3000, 8000, 12000, 20000 |
vehicle_use | Vehicle use | string | social_commuting | social_only, social_commuting, business |
overnight_parking | Overnight parking | string | driveway | garage, driveway, street |
Steps run in order. Each one can use the inputs and the outputs of every step before it.
Look up base rate and territory band by postcode area. Feeds the premium calculation downstream.
{
"id": "territory_rates",
"key": "territory_rates",
"step": "collection",
"name": "Territory base rate lookup",
"retain": true,
"collection": [
["postcode_area", "band", "base_rate", "territory_factor"],
["SW", "urban_high", 620, 1.35],
["M", "urban_mid", 480, 1.1],
["EH", "urban_mid", 460, 1.05],
["B", "urban_mid", 490, 1.12],
["N", "urban_high", 640, 1.4]
],
"format": {
"territory_base_rate": {
"key": "territory_base_rate",
"label": "Territory base rate",
"exp": "collection.filter(postcode_area=={{postcode_area}}).map(base_rate).first()",
"type": "decimal",
"def": 500
},
"territory_factor": {
"key": "territory_factor",
"label": "Territory factor",
"exp": "collection.filter(postcode_area=={{postcode_area}}).map(territory_factor).first()",
"type": "decimal",
"def": 1
},
"territory_band": {
"key": "territory_band",
"label": "Territory band",
"exp": "collection.filter(postcode_area=={{postcode_area}}).map(band).first()",
"type": "string",
"def": "unknown"
}
}
}Look up a load/discount by ABI vehicle group. Bigger group = higher premium.
{
"id": "vehicle_rates",
"key": "vehicle_rates",
"step": "collection",
"name": "Vehicle group factor lookup",
"retain": true,
"collection": [
["group_max", "vehicle_factor"],
[5, 0.85],
[15, 1],
[25, 1.2],
[35, 1.5],
[50, 2]
],
"format": {
"vehicle_factor": {
"key": "vehicle_factor",
"label": "Vehicle group factor",
"exp": "collection.filter(group_max>={{vehicle_group}}).map(vehicle_factor).first()",
"type": "decimal",
"def": 1
}
}
}Convert driver age into a rating factor. Young and very old drivers get a load. Banding is a comparison, so it lives in a transform exp (JavaScript); formulas stay arithmetic.
{
"id": "age_factor",
"key": "age_factor",
"step": "transform",
"name": "Driver age factor",
"retain": true,
"format": {
"age_factor_calc": {
"key": "age_factor_calc",
"label": "Driver age factor",
"exp": "{{driver_age}} < 25 ? 1.6 : {{driver_age}} < 35 ? 1.10 : {{driver_age}} < 65 ? 0.95 : 1.15",
"type": "decimal",
"def": 1
}
}
}Bigger NCD = larger discount, floored at 0.55.
{
"id": "ncd_factor_calc",
"key": "ncd_factor_calc",
"step": "calculation",
"name": "NCD discount factor",
"def": 1,
"formula": "max(1 - ({{ncd_years}} * 0.05), 0.55)"
}More miles means more exposure to accidents. Low mileage earns a credit and high mileage a load.
{
"id": "mileage_bands",
"key": "mileage_bands",
"step": "collection",
"name": "Annual mileage factor",
"retain": true,
"collection": [
["annual_mileage_min", "annual_mileage_max", "mileage_factor_rel"],
[0, 5000, 0.9],
[5001, 10000, 1],
[10001, 15000, 1.1],
[15001, 999999, 1.25]
],
"format": {
"mileage_factor": {
"key": "mileage_factor",
"label": "Annual mileage factor",
"exp": "collection.filter(annual_mileage_min<={{annual_mileage}}).filter(annual_mileage_max>={{annual_mileage}}).map(mileage_factor_rel).first()",
"type": "decimal",
"def": 1
}
}
}Business use adds time on the road at busy times. Social-only use earns a small credit.
{
"id": "vehicle_use_rates",
"key": "vehicle_use_rates",
"step": "collection",
"name": "Vehicle use factor",
"retain": true,
"collection": [
["vehicle_use_value", "vehicle_use_factor_rel"],
["social_only", 0.92],
["social_commuting", 1],
["business", 1.15]
],
"format": {
"vehicle_use_factor": {
"key": "vehicle_use_factor",
"label": "Vehicle use factor",
"exp": "collection.filter(vehicle_use_value=={{vehicle_use}}).map(vehicle_use_factor_rel).first()",
"type": "decimal",
"def": 1
}
}
}Where the car is kept overnight affects theft and vandalism risk. A locked garage earns a credit.
{
"id": "parking_rates",
"key": "parking_rates",
"step": "collection",
"name": "Overnight parking factor",
"retain": true,
"collection": [
["overnight_parking_value", "parking_factor_rel"],
["garage", 0.93],
["driveway", 1],
["street", 1.12]
],
"format": {
"parking_factor": {
"key": "parking_factor",
"label": "Overnight parking factor",
"exp": "collection.filter(overnight_parking_value=={{overnight_parking}}).map(parking_factor_rel).first()",
"type": "decimal",
"def": 1
}
}
}Base rate × territory × vehicle × age × NCD, rounded to 2dp. Multiplied by the annual mileage factor, vehicle use factor, overnight parking factor.
{
"id": "premium",
"key": "premium",
"step": "calculation",
"name": "Final premium",
"def": 500,
"formula": "round({{territory_base_rate}} * {{territory_factor}} * {{vehicle_factor}} * {{age_factor_calc}} * {{ncd_factor_calc}} * {{mileage_factor}} * {{vehicle_use_factor}} * {{parking_factor}}, 2)"
}Decline rules: under-17 drivers cannot be rated.
{
"id": "motor_exclusions",
"key": "motor_exclusions",
"step": "exclusion",
"name": "Motor exclusions",
"def": false,
"items": [
{
"id": "under_17",
"name": "Driver under 17",
"exp": "{{driver_age}} < 17",
"def": false
}
]
}Refer-to-underwriter rules: high value vehicle group or serious conviction history.
{
"id": "motor_refers",
"key": "motor_refers",
"step": "refer",
"name": "Motor refers",
"def": false,
"items": [
{
"id": "high_value_vehicle",
"name": "High value vehicle",
"exp": "{{vehicle_group}} > 40",
"def": false
},
{
"id": "multiple_convictions",
"name": "Multiple convictions",
"exp": "{{convictions_5y}} > 1",
"def": false
}
]
}This quote was priced by the Ptally rating engine. The trace below follows it through every step to the final result.
{
"postcode_area": "SW",
"vehicle_group": 15,
"driver_age": 21,
"ncd_years": 0,
"convictions_5y": 0,
"annual_mileage": 20000,
"vehicle_use": "business",
"overnight_parking": "street"
}territory_base_rate = 620 · territory_factor = 1.35 · territory_band = "urban_high"vehicle_factor = 121 < 25 ? 1.6 : 21 < 35 ? 1.10 : 21 < 65 ? 0.95 : 1.15 → 1.6max(1 - (0 * 0.05), 0.55) = 1mileage_factor = 1.25vehicle_use_factor = 1.15parking_factor = 1.12round(620 * 1.35 * 1 * 1.6 * 1 * 1.25 * 1.15 * 1.12, 2) = 2,156.11Driver under 17: not firedHigh value vehicle: not firedMultiple convictions: not firedSelect personal motor as the default model for a new project, then change the rates, rules and tests to match your product. Our consultants can do this for you, with no implementation fee.