Skip to content
Back to the work

Predictive Modelling

RiskIQ: pricing auto insurance risk instead of guessing at it

A claim-likelihood model and a deployable underwriting function — where the simplest, most interpretable model beat every tree ensemble we tried.

Context
MMA 867 — Queen’s University
Role
Solution architecture — EDA, model interpretation, deployable underwriting tool
Timeline
2026
Pythonscikit-learnLogistic RegressionEnsemble benchmarksClassification metrics
0.887
ROC-AUC, best model (logistic regression)
10,000
policyholder records, 18 variables
4
models benchmarked head-to-head

Problem

Blanket, demographic-class premiums overcharge safe drivers and undercharge risky ones — adverse selection that bled Canadian auto insurers roughly $1.2B in Alberta alone in a recent fiscal year.

Approach

Benchmarked logistic regression against decision tree, random forest and gradient boosting on 10,000 records; picked the winner on ROC-AUC, not just accuracy; wrapped it in a scoring function built for a real intake form.

Result

Logistic regression wins outright — 0.887 ROC-AUC, ahead of every tree ensemble — and ships as a `score_customer()` function that turns a raw customer record into a probability and a risk tier.

The business problem, with a number attached

Canadian auto insurers have been pricing risk with a blunt instrument. Broad demographic rating classes assume risk shifts slowly by group — age band, postal code, vehicle class — and charge everyone in a class the same premium. That assumption broke: claim severity and auto-theft payouts both climbed sharply in recent years, and insurers using blanket pricing were absorbing losses averaging 18% more in claims and legal overhead than they collected in premiums, a gap that ran to roughly $1.2B in Alberta alone.

Blanket pricing also self-selects against the insurer. Overcharge a safe driver relative to their actual risk and they leave for a competitor pricing them correctly. Undercharge a risky one and they stay. The book quietly gets worse every renewal cycle. A per-policy risk score, applied at quote time, is the direct countermeasure — and the point of this project was to build one, prove it works, and make it usable by an actual intake process rather than just by a notebook.

The data

10,000 policyholder records, 18 variables spanning demographics (age, gender, education, income bracket, marital status), driving history (experience, past accidents, speeding violations, DUIs), and the vehicle itself (year, type, ownership status, annual mileage). The target — outcome — is whether the policy generated a claim: 31.3% did, 68.7% didn’t. Moderate imbalance, not the extreme rarity you’d see in fraud detection, but enough that raw accuracy is a bad optimization target — a model that predicts “no claim” for everyone still scores nearly 69%.

Roughly 10% of records were missing credit_score and annual_mileage. Rather than drop those rows or silently impute and move on, we added a credit_score_missing flag before median-imputing — missingness on a credit file is itself a risk signal (thin or no credit history correlates with risk in ways a silently-imputed median would erase), and the flag let the model use that signal instead of losing it.

Four models, one honest comparison

We benchmarked logistic regression against a decision tree, a random forest and gradient boosting on an identical train/test split, and evaluated on precision, recall, F1 and ROC-AUC — never on accuracy alone, for the reason above.

Model comparison, held-out test set (ROC-AUC)
Model comparison by ROC-AUC on the held-out test set Horizontal bar chart. Logistic Regression scores highest at 0.8865 ROC-AUC, ahead of Gradient Boosting, Random Forest and Decision Tree, all within two points of each other. 0.85 0.86 0.87 0.88 0.89 0.90 Logistic Regression Logistic Regression: 0.8865 ROC-AUC 0.8865 Gradient Boosting Gradient Boosting: 0.8814 ROC-AUC 0.8814 Random Forest Random Forest: 0.8762 ROC-AUC 0.8762 Decision Tree Decision Tree: 0.8729 ROC-AUC 0.8729
Logistic regression — the plainest, most interpretable model in the lineup — won outright. Accuracy 0.831, precision 0.732, recall 0.726, F1 0.729. No tree ensemble made up the gap, which is a genuinely convenient result for a use case that needs to explain itself to a regulator.

That result mattered beyond the leaderboard. Interpretability isn’t a nice-to-have in insurance pricing — it’s close to a legal requirement. A regulator or an ombudsman asking why a specific customer was priced the way they were needs an answer better than “the gradient boosting model said so.” Logistic regression winning meant we didn’t have to trade performance for explainability — we got both from the same model.

What actually drives claim risk

With logistic regression as the production model, its standardised coefficients double as the interpretability layer the regulatory case needs. Sign and magnitude both matter here: a negative coefficient means that factor lowers claim odds.

Standardised coefficients, top 8 claim drivers
Standardised logistic regression coefficients, top 8 claim drivers Diverging bar chart centered on zero. Driving experience has the strongest negative coefficient, reducing claim odds. Vehicle year before 2015 has the strongest positive coefficient, raising claim odds. Driving experience Driving experience: -1.687 (standardised coefficient) -1.69 Vehicle year < 2015 Vehicle year < 2015: +0.776 (standardised coefficient) +0.78 Vehicle ownership (owned) Vehicle ownership (owned): -0.770 (standardised coefficient) -0.77 Gender Gender: +0.455 (standardised coefficient) +0.46 Past accidents Past accidents: -0.370 (standardised coefficient) -0.37 Speeding violations Speeding violations: +0.188 (standardised coefficient) +0.19 Married Married: -0.188 (standardised coefficient) -0.19 Annual mileage Annual mileage: +0.112 (standardised coefficient) +0.11 Reduces claim odds Raises claim odds
Driving experience dominates — by a wide margin, the single strongest lever, ahead of the vehicle itself. Owning your car outright (versus financing) also reduces odds, plausibly because financed vehicles skew toward newer drivers with less equity cushion. Gender and pre-2015 vehicle age are the two clearest odds-raising factors.

Threshold selection is a business decision, not a statistical one

The costs of the two error types are wildly asymmetric: underpricing a genuinely risky policy costs a claim payout; overpricing a safe one costs a customer to a competitor who prices them correctly. The operating threshold has to come from those relative costs, not from an F1-optimal default — a point the strategic report built the deployment recommendation around, not just the model itself.

The deployable piece

A notebook that produces a good ROC-AUC and stops is a homework assignment. The thing that makes this a solution rather than an exercise is score_customer() — a function that takes a raw record in the shape an intake form would actually produce (strings like "high school" or "before 2015", not pre-encoded features), and returns a claim probability and a risk tier:

def score_customer(record: dict, model=logit, scaler=scaler, ...):
    """
    Take a raw customer record (as it would come off the intake form),
    return claim probability and a risk tier.
    """
    r = dict(record)
    # ordinal encoding for education / income / driving_experience
    # credit_score: flag-then-impute if missing, using the training median
    # one-hot encoding for vehicle_year and vehicle_type
    # build the feature vector in trained column order, scale, predict
    ...
    return {"claim_probability": prob, "risk_tier": tier}

It handles the two things a real intake form guarantees will happen: missing credit_score, and categorical fields arriving as human-readable strings rather than model-ready codes. Both are handled the same way the training pipeline handled them, so a production score can never silently drift from how the model was actually fit. Wrapped behind a Google Form intake, it’s deployable as-is.

What I owned

Team Danforth split the pipeline into phases — data cleaning, feature engineering, hypothesis testing, modelling — across five people. My piece was the EDA on all 18 variables at the start, and the underwriting scoring tool at the end: taking a fitted model out of a notebook and making it something an intake process could actually call.

Limitations

  • Synthetic-adjacent dataset. The 10,000-record dataset is realistic but not a live book of business — real deployment needs validation against actual claims experience before any premium decision leans on it.
  • Static snapshot, not a monitored model. Risk profiles drift — driving habits, vehicle age, local claim severity all move over time. A production version needs scheduled retraining and drift monitoring, not a one-time fit.
  • Fairness review is unfinished. gender shows up as a meaningful coefficient, and any model that prices on demographic-adjacent features needs an explicit fairness and regulatory-compliance pass before it touches a real quote.