Lesson 10 of 13

Gamma and Tweedie Regression

In Lesson 9 you tamed counts that piled up at zero. The thing you predicted was still a whole number: how many claims, how many repairs. Now the outcome changes character again. It becomes an amount of money: strictly positive, stretched out to the right by a few very large values, and sometimes floored at an exact zero.

Meet Nadia, who prices motor insurance. She has last year's book of business, thousands of policies, and for each one two numbers she has to model. First, when a driver did make a claim, how big was it: a few hundred pounds for a scraped bumper, occasionally many thousands for a write-off. Second, across every policy on the books, what did the company pay out in total: for most drivers, exactly nothing.

Those two outcomes have shapes an ordinary straight line cannot respect. The three curves below are the tools built for them. Gamma is for the positive, skewed claim amount. Tweedie is for the total loss with its wall of zeros. Toggle between them to see the shape each one is built to fit.

By the end of this lesson you will be able to:

  • Say why ordinary regression is the wrong tool for a positive, right-skewed amount, and fit a Gamma model instead
  • Read a Gamma model's coefficients as multipliers on the mean
  • Recognise an outcome that is zero-or-positive, insurance-style loss, and model it with a Tweedie GLM

Prerequisites: you can fit and read a linear model, and you have met the GLM idea, the log link, and reading exp(coef) as a multiplier in the count-model lessons. Comfortable with logs and exponents (exp is always positive).

Why the straight line fails

A claim amount is not a measurement on the whole number line

Nadia's first outcome is amount, the size of a claim in pounds. It has three habits that ordinary least squares (the straight-line lm you know) cannot honour:

  • It is never negative. A claim cannot cost minus 400 pounds, yet a regression line runs happily below zero.
  • It is right-skewed. Most claims are small, a few are enormous, so the mean sits well above the median. A symmetric bell curve is the wrong picture.
  • Its spread grows with its size. Small claims barely vary; large ones swing by thousands. Ordinary regression assumes the noise is the same size everywhere.

Here is Nadia's book, built right here so every line on this page runs in interactive R. Each policy draws a Poisson number of claims (how often it claims) and a Gamma amount per claim (how big), and the total loss is those claims added up. Hold on to how this is built, it returns at the end of the lesson.

RInteractive R
set.seed(2026) n <- 4000 car_value <- round(runif(n, 5, 45), 1) # car value, thousands of pounds young <- rbinom(n, 1, 0.30) # 1 = main driver under 25 # Each policy: a Poisson number of claims (how often), each a Gamma amount (how big). # The total loss is those claims summed. Most policies never claim, so loss = 0. claims_per <- rpois(n, exp(-1.9 + 0.55 * young + 0.010 * car_value)) sev_mean <- exp(6.4 + 0.022 * car_value + 0.35 * young) # mean claim size, pounds loss <- numeric(n); amt <- c(); acv <- c(); ayg <- c() for (i in seq_len(n)) { k <- claims_per[i] if (k > 0) { a <- rgamma(k, shape = 2, scale = sev_mean[i] / 2) loss[i] <- sum(a) amt <- c(amt, a); acv <- c(acv, rep(car_value[i], k)); ayg <- c(ayg, rep(young[i], k)) } } yf <- function(z) factor(z, levels = c(0, 1), labels = c("25+", "under25")) policies <- data.frame(loss = round(loss), car_value = car_value, young = yf(young)) claims <- data.frame(amount = round(amt), car_value = acv, young = yf(ayg)) c(policies = nrow(policies), claims = nrow(claims), zero_loss = round(mean(policies$loss == 0), 3)) #> policies claims zero_loss #> 4000.000 957.000 0.791

  

Of 4000 policies, only 957 ever produced a claim. The histogram below plots a representative sample of those claim amounts: a tall stack of small claims and a thin tail of rare, expensive ones (the largest of all 957 reaches 8462 pounds). The mean claim (1283) sits far above the median (999) precisely because that tail drags it up. This lopsided, floored-at-zero shape is exactly what a straight line gets wrong.

Fit a straight line anyway and watch two things break. Its point guess is fine, but the symmetric noise it assumes makes its 95% prediction interval dip well below zero, a real, sizeable chance of a negative claim, which is impossible:

RInteractive R
lm_fit <- lm(amount ~ car_value + young, data = claims) predict(lm_fit, data.frame(car_value = 8, young = "25+"), interval = "prediction") #> fit lwr upr #> 1 564.1949 -1309.925 2438.315

  

And the spread is not constant. Split the claims into three groups by their fitted size and look at the standard deviation in each: it climbs from 611 to 1344 as the mean climbs. But look at the last column, the ratio of spread to mean stays almost fixed near 0.72:

RInteractive R
claims$bucket <- cut(predict(lm_fit), 3, labels = c("cheap", "mid", "dear")) aggregate(amount ~ bucket, claims, function(z) c(mean = round(mean(z)), sd = round(sd(z)), cv = round(sd(z) / mean(z), 2))) #> bucket amount.mean amount.sd amount.cv #> 1 cheap 848.00 611.00 0.72 #> 2 mid 1198.00 851.00 0.71 #> 3 dear 1848.00 1344.00 0.73

  

That last fact, spread proportional to the mean, is not a nuisance. It is a fingerprint, and it points straight at the model we want.