Python for Actuarial Students · Course 3
Lesson 3

GLMs: Severity & Pure Premium Modeling

How much claims cost, and what a policy should cost: Gamma and inverse Gaussian severity, the two-part pure premium, and the Tweedie.

Lesson 3 · OverviewBy the end of this lesson you can…

  • Fit Gamma and inverse Gaussian GLMs with a log link to claim severity
  • Compare severity families with residuals and likelihood
  • Combine frequency and severity into a pure premium: E[loss] = E[N] × E[X]
  • Turn two GLMs into a multiplicative rating algorithm
  • Fit a Tweedie GLM to pure premium in one step and choose its power
  • Check a model on the holdout year with actual ÷ expected
75 min
Concepts & worked examples
75 min
Lab: a prospective rating algorithm

Companion notebook: lesson-03-severity-pure-premium.ipynb — worked examples and self-checking exercises.

Part 1

Severity Models

Continuous, strictly positive, right-skewed.

The dataOne row per liability claim

claims = pd.read_csv("data/motor/claims.csv")
claims["incurred"] = claims["paid"] + claims["outstanding"]
liab = claims[claims["coverage"] == "LIAB"].merge(
    train[["vehicle_id", "vehicle_style", "age_band",
           "urban", "log_miles", "channel", "driver_age"]],
    on="vehicle_id", how="inner", validate="many_to_one")
liab["young"] = liab["driver_age"] < 25
len(liab)       # 4,041 claims from policy years 2021–2024
Mean · median incurred12,817 · 6,647
Coefficient of variation1.36
Claims at or above the 100,000 limit51
Histogram of liability incurred per claim on a log scale, roughly bell-shaped with a spike at the policy limit

Gamma GLMConstant coefficient of variation

$$Y \sim \text{Gamma}(\mu, \phi) \qquad \mathrm{Var}(Y) = \phi\,\mu^2 \qquad \ln\mu = \mathbf{x}^\top\beta$$

gamma_log = sm.families.Gamma(sm.families.links.Log())
sev = smf.glm("incurred ~ C(vehicle_style, Treatment('Sedan'))"
              " + young + urban", data=liab,
              family=gamma_log).fit()

np.exp(sev.params)
sev.scale              # dispersion 1.832 → CV = √φ = 1.35
  • Standard deviation ∝ mean: a $10,000 claim has 10× the spread of a $1,000 claim
  • Specify the log link explicitly: the Gamma’s default is the inverse link
TermRelativity95% CITruth
Base (Sedan, 25+, non-urban)11,25310,156–12,468—
Sports1.2041.035–1.4001.30
Young driver (< 25)1.2021.085–1.3311.15
Urban1.1071.014–1.2101.10
Hatchback0.9280.814–1.0571.00
SUV1.0170.909–1.1391.00

Severity is noisy: intervals are ±15% wide. Adding age bands, mileage, and channel adds nothing (F = 1.20, p = 0.29).

Family choiceGamma, inverse Gaussian, or lognormal?

Q-Q plots of scaled deviance residuals for the Gamma and inverse Gaussian severity models
FamilyV(μ)AICSports · young · urban
Gammaμ²85,001.11.204 · 1.202 · 1.107
Inverse Gaussianμ³84,687.61.203 · 1.200 · 1.109
Lognormal (log-OLS)—83,865.01.253 · 1.279 · 1.128

The claims were simulated lognormal, and the likelihood finds it; Gamma residuals bend away at both ends.

The relativities barely change between families. Gamma stays the default: its mean is right in dollars, and it pairs naturally with a Poisson frequency model.

Part 2

The Two-Part Pure Premium

How often × how much.

Frequency × severityCombining two GLMs

$$\text{Pure Premium} = \widehat{\text{Frequency}} \times \widehat{\text{Severity}}$$

Valid when claim count and claim size are independent given the features, the standard collective-risk assumption.

# predicted rate per car-year: offset of zero = one year
train["freq_hat"] = freq.predict(train, offset=np.zeros(len(train)))
train["sev_hat"] = sev.predict(train)
train["pp_hat"] = train["freq_hat"] * train["sev_hat"]

(train["pp_hat"] * train["exposure"]).sum()   # 51,773,072
train["inc_liab"].sum()                       # 51,794,591

Because both use a log link, the pure premium is still multiplicative. Each factor’s pure-premium relativity is its frequency relativity × its severity relativity.

StyleFreq.Sev.Pure prem.
Sedan (base)1.0001.0001.000
Minivan0.8560.8960.767
Pickup1.0051.0501.055
Hatchback1.2130.9281.125
SUV1.2241.0171.246
Sports1.7281.2042.080

Rating algorithmFrom two GLMs to a rating manual

$$\text{PP} = \underbrace{447.03}_{\text{base}} \times f_{\text{style}} \times f_{\text{age}} \times f_{\text{young}} \times f_{\text{urban}} \times \Big(\tfrac{\text{miles}}{12{,}000}\Big)^{0.488}$$

FactorFromValues
Base 447.030.03973 × 11,253Sedan, 40–49, non-urban, 12,000 mi
Age bandfrequency17–20: 2.062 · 21–24: 1.767 · 25–29: 1.279 · 75+: 1.680
Young driverseverityunder 25: 1.202
Urbanboth1.365 × 1.107 = 1.512
Mileagefrequencyelasticity 0.488

Three risks, priced

RiskFreq.Sev.Pure prem.
Sedan · 40–49 · rural · 12k mi0.039711,253447.03
Sports · 17–20 · urban · 15k mi0.215418,0283,883.87
Minivan · 50–64 · urban · 8k mi0.037011,164412.67

A premium is the pure premium plus expenses and profit: premium = PP ÷ permissible loss ratio. Lesson 8’s capstone turns these into rate indications.

ValidationDoes it hold up next year?

for d in (train, hold):
    d["pp_hat"] = (freq.predict(d, offset=np.zeros(len(d)))
                   * sev.predict(d))

expected = (hold["pp_hat"] * hold["exposure"]).sum()
hold["inc_liab"].sum() / expected       # A/E = 1.022

On policy year 2025, which the models never saw, actual losses were 2.2% above expected: 7,261,043 against 7,107,968.

Training yearsNon-urbanUrban
Liability premium per car-year1,1041,096
Actual pure premium627937
Model pure premium628935
Loss ratio56.8%85.4%

The current rating plan charges urban and rural drivers the same, yet urban cars cost 50% more. The model sees it; the rates don’t. That gap is what a pricing review exists to close.

Part 3

The Tweedie Model

Pure premium in a single GLM.

Compound Poisson–GammaA distribution with a mass at zero

$$S = \sum_{j=1}^{N} X_j,\quad N \sim \text{Poisson},\; X_j \sim \text{Gamma}$$

S is exactly zero when N = 0, and continuous otherwise. It belongs to the EDF with

$$\mathrm{Var}(S) = \phi\,\mu^p, \qquad 1 < p < 2$$

  • p → 1: Poisson-like (counts)
  • p → 2: Gamma-like (sizes)
  • Model pure premium directly, with exposure as a weight
Histogram of log pure premium for vehicles with a claim; 94.2 percent of vehicle-terms are exactly zero

statsmodelsFitting a Tweedie GLM

train["pp"] = train["inc_liab"] / train["exposure"]

tw = smf.glm("pp ~ C(vehicle_style, Treatment('Sedan'))"
             " + C(age_band, Treatment('40-49'))"
             " + urban + log_miles",
             data=train, var_weights=train["exposure"],
             family=sm.families.Tweedie(
                 var_power=1.6, link=sm.families.links.Log()),
            ).fit()
Power p1.31.41.51.61.71.8
Log-lik. (000s)−58.7−56.2−55.0−54.7−54.9−56.3

Choose p by profile likelihood: fit a grid, keep the best.

RelativityTweedieFreq × sev
Base431.53447.03
Sports1.9612.080
Age 17–202.6002.062 × 1.202 = 2.477
Age 21–242.2531.767 × 1.202 = 2.124
Urban1.5161.512
log_miles β0.4470.488

Holdout A/E: Tweedie 1.025, two-part 1.022.

Trade-offsTwo-part or Tweedie?

Frequency × severity

  • Shows why a segment is expensive: more claims, or bigger ones?
  • Different features in each part (age drives frequency, not severity)
  • Two models to fit, validate, and maintain
  • Needs claim-level data

Tweedie

  • One model, one set of relativities
  • Works on policy-level aggregate losses: no claim file needed
  • Assumes every factor moves frequency and severity together
  • The popular loss for gradient boosting (Lesson 7)

Many pricing teams build both: the two-part model for insight and filing support, the Tweedie as a cross-check. When they disagree, find out why.

Actuarial Lab · 75 minA prospective policy rating algorithm

  1. Build the claim-level severity table for policy years ≤ 2024
  2. Fit a Gamma log-link GLM; F-test the full against the parsimonious model
  3. Refit with inverse Gaussian; compare relativities and AIC
  4. Refit Lesson 2’s reduced Poisson model; predict frequency, severity, pure premium
  5. Write rate(style, age_band, young, urban, miles) from the fitted factors
  6. Check A/E on the 2025 holdout
  7. Fit a Tweedie over p = 1.3…1.8; compare with the two-part model

Answer key

Gamma base severity11,253.05
Gamma dispersion φ1.832
Base pure premium447.03
Sports · 17–20 · urban · 15k3,883.87
Training: expected ÷ actual0.9996
Holdout A/E (two-part)1.022
Best Tweedie power1.6

RecapLesson 3 checklist

  • I fit severity with a Gamma GLM and an explicit log link
  • I compare families by residuals and likelihood, and know relativities rarely move
  • I combine frequency and severity into a pure premium
  • I write the result as base rate × factors
  • I check expected vs actual on training and holdout data
  • I fit a Tweedie with exposure weights and choose its power
  • I know when to prefer two parts or one
  • I found the urban pricing gap
Up next
Lesson 4

Overfitting, Generalization & Regularization

The bias–variance trade-off, cross-validation, and Lasso, Ridge, and ElasticNet on a 60-feature health cost model.