# Wald and Gamma Regression (Australian insurance claims 2004-2005)#

[1]:

import arviz as az
import bambi as bmb
import matplotlib.pyplot as plt
import numpy as np

[2]:

az.style.use("arviz-darkgrid")
np.random.seed(1234)


## Load and examine Vehicle insurance data#

In this notebook we use a data set consisting of 67856 insurance policies and 4624 (6.8%) claims in Australia between 2004 and 2005. The original source of this dataset is the book Generalized Linear Models for Insurance Data by Piet de Jong and Gillian Z. Heller.

[3]:

data = bmb.load_data("carclaims")

[3]:

veh_value exposure clm numclaims claimcst0 veh_body veh_age gender area agecat
0 1.06 0.303901 0 0 0.0 HBACK 3 F C 2
1 1.03 0.648871 0 0 0.0 HBACK 2 F A 4
2 3.26 0.569473 0 0 0.0 UTE 2 F E 2
3 4.14 0.317591 0 0 0.0 STNWG 2 F D 2
4 0.72 0.648871 0 0 0.0 HBACK 4 F C 2

Let’s see the meaning of the variables before creating any plot or fitting any model.

• veh_value: Vehicle value, ranges from \$0 to \$350,000.

• exposure: Proportion of the year where the policy was exposed. In practice each policy is not exposed for the full year. Some policies come into force partly into the year while others are canceled before the year’s end.

• clm: Claim occurrence. 0 (no), 1 (yes).

• numclaims: Number of claims.

• claimcst0: Claim amount. 0 if no claim. Ranges from \$200 to \$55922.

• veh_body: Vehicle body type. Can be one of bus, convertible, coupe, hatchback, hardtop, motorized caravan/combi, minibus, panel van, roadster, sedan, station wagon, truck, and utility.

• veh_age: Vehicle age. 1 (new), 2, 3, and 4.

• gender: Gender of the driver. M (Male) and F (Female).

• area: Driver’s area of residence. Can be one of A, B, C, D, E, and F.

• agecat: Driver’s age category. 1 (youngest), 2, 3, 4, 5, and 6.

The variable of interest is the claim amount, given by "claimcst0". We keep the records where there is a claim, so claim amount is greater than 0.

[4]:

data = data[data["claimcst0"] > 0]


For clarity, we only show those claims amounts below \$15,000, since there are only 65 records above that threshold. [5]:  data[data["claimcst0"] > 15000].shape[0]  [5]:  65  [6]:  plt.hist(data[data["claimcst0"] <= 15000]["claimcst0"], bins=30) plt.title("Distribution of claim amount") plt.xlabel("Claim amount ($)");


And this is when you say: “Oh, there really are ugly right-skewed distributions out there!”. Well, yes, we’ve all been there :)

In this case we are going to fit GLMs with a right-skewed distribution for the random component. This time we will be using Wald and Gamma distributions. One of their differences is that the variance is proportional to the cubic mean in the case of the Wald distribution, and proportional to the squared mean in the case of the Gamma distribution.

## Wald family#

The Wald family (a.k.a inverse Gaussian model) states that

$\begin{array}{cc} y_i \sim \text{Wald}(\mu_i, \lambda) & g(\mu_i) = \mathbf{x}_i^T\beta \end{array}$

where the pdf of a Wald distribution is given by

$f(x|\mu, \lambda) = \left(\frac{\lambda}{2\pi}\right)^{1/2}x^{-3/2}\exp\left\{ -\frac{\lambda}{2x} \left(\frac{x - \mu}{\mu} \right)^2 \right\}$

for $$x > 0$$, mean $$\mu > 0$$ and $$\lambda > 0$$ is the shape parameter. The variance is given by $$\sigma^2 = \mu^3/\lambda$$. The canonical link is $$g(\mu_i) = \mu_i^{-2}$$, but $$g(\mu_i) = \log(\mu_i)$$ is usually preferred, and it is what we use here.

## Gamma family#

The default parametrization of the Gamma density function is

$\displaystyle f(x | \alpha, \beta) = \frac{\beta^\alpha x^{\alpha -1} e^{-\beta x}}{\Gamma(\alpha)}$

where $$x > 0$$, and $$\alpha > 0$$ and $$\beta > 0$$ are the shape and rate parameters, respectively.

But GLMs model the mean of the function, so we need to use an alternative parametrization where

$\begin{array}{ccc} \displaystyle \mu = \frac{\alpha}{\beta} & \text{and} & \displaystyle \sigma^2 = \frac{\alpha}{\beta^2} \end{array}$

and thus we have

$\begin{array}{cccc} y_i \sim \text{Gamma}(\mu_i, \sigma_i), & g(\mu_i) = \mathbf{x}_i^T\beta, & \text{and} & \sigma_i = \mu_i^2/\alpha \end{array}$

where $$\alpha$$ is the shape parameter in the original parametrization of the gamma pdf. The canonical link is $$g(\mu_i) = \mu_i^{-1}$$, but here we use $$g(\mu_i) = \log(\mu_i)$$ again.

## Model fit#

In this example we are going to use the binned age, the gender, and the area of residence to predict the amount of the claim, conditional on the existence of the claim because we are only working with observations where there is a claim.

"agecat" is interpreted as a numeric variable in our data frame, but we know it is categorical, and we wouldn’t be happy if our model takes it as if it was numeric, would we?

We have two alternatives to tell Bambi that this numeric variable must be treated as categorical. The first one is to wrap the name of the variable with C(), and the other is to pass the same name to the categorical argument when we create the model. We are going to use the first approach with the Wald family and the second with the Gamma.

The C() notation is taken from Patsy and is encouraged when you want to explicitly pass the order of the levels of the variables. If you are happy with the default order, better pass the name to categorical so tables and plots have prettier labels :)

### Wald#

[7]:

model_wald = bmb.Model("claimcst0 ~ C(agecat) + gender + area", data, family = "wald", link = "log")
fitted_wald = model_wald.fit(tune=2000, target_accept=0.9)

Auto-assigning NUTS sampler...
Initializing NUTS using jitter+adapt_diag...
Multiprocess sampling (4 chains in 4 jobs)
NUTS: [Intercept, C(agecat), gender, area, claimcst0_lam]

100.00% [12000/12000 05:03<00:00 Sampling 4 chains, 0 divergences]
Sampling 4 chains for 2_000 tune and 1_000 draw iterations (8_000 + 4_000 draws total) took 305 seconds.

[8]:

az.plot_trace(fitted_wald);

[9]:

az.summary(fitted_wald)

[9]:

mean sd hdi_3% hdi_97% mcse_mean mcse_sd ess_bulk ess_tail r_hat
Intercept 7.726 0.097 7.550 7.910 0.002 0.002 1577.0 1891.0 1.0
C(agecat)[2] -0.169 0.102 -0.362 0.021 0.003 0.002 1637.0 2029.0 1.0
C(agecat)[3] -0.262 0.099 -0.459 -0.088 0.002 0.002 1645.0 2056.0 1.0
C(agecat)[4] -0.268 0.098 -0.452 -0.083 0.002 0.002 1556.0 2084.0 1.0
C(agecat)[5] -0.381 0.106 -0.580 -0.185 0.003 0.002 1802.0 2212.0 1.0
C(agecat)[6] -0.324 0.122 -0.551 -0.091 0.003 0.002 2069.0 2201.0 1.0
gender[M] 0.154 0.051 0.061 0.252 0.001 0.001 5291.0 3415.0 1.0
area[B] -0.029 0.072 -0.161 0.112 0.001 0.001 3145.0 3180.0 1.0
area[C] 0.073 0.067 -0.054 0.197 0.001 0.001 2900.0 3091.0 1.0
area[D] -0.021 0.089 -0.176 0.150 0.001 0.001 3619.0 3143.0 1.0
area[E] 0.151 0.108 -0.045 0.361 0.002 0.001 3977.0 2989.0 1.0
area[F] 0.372 0.130 0.133 0.614 0.002 0.002 4057.0 2897.0 1.0
claimcst0_lam 723.059 15.188 694.638 751.442 0.212 0.150 5097.0 3026.0 1.0

If we look at the agecat variable, we can see the log mean of the claim amount tends to decrease when the age of the person increases, with the exception of the last category where we can see a slight increase in the mean of the coefficient (-0.307 vs -0.365 of the previous category). However, these differences only represent a slight tendency because of the large overlap between the marginal posteriors for these coefficients (see overlaid density plots for C(agecat).

The posterior for gender tells us that the claim amount tends to be larger for males than for females, with the mean being 0.153 and the credible interval ranging from 0.054 to 0.246.

Finally, from the marginal posteriors for the areas, we can see that F is the only area that clearly stands out, with a higher mean claim amount than in the rest. Area E may also have a higher claim amount, but this difference with the other areas is not as evident as it happens with F.

### Gamma#

[10]:

model_gamma = bmb.Model(
"claimcst0 ~ agecat + gender + area",
data,
family="gamma",
categorical="agecat",
)
fitted_gamma = model_gamma.fit(tune=2000, target_accept=0.9)

Auto-assigning NUTS sampler...
Initializing NUTS using jitter+adapt_diag...
Multiprocess sampling (4 chains in 4 jobs)
NUTS: [Intercept, agecat, gender, area, claimcst0_alpha]

100.00% [12000/12000 07:35<00:00 Sampling 4 chains, 0 divergences]
Sampling 4 chains for 2_000 tune and 1_000 draw iterations (8_000 + 4_000 draws total) took 455 seconds.

[11]:

az.plot_trace(fitted_gamma);

[12]:

az.summary(fitted_gamma)

[12]:

mean sd hdi_3% hdi_97% mcse_mean mcse_sd ess_bulk ess_tail r_hat
Intercept 7.716 0.063 7.602 7.833 0.002 0.001 1704.0 2240.0 1.0
agecat[2] -0.181 0.064 -0.301 -0.061 0.001 0.001 2019.0 2679.0 1.0
agecat[3] -0.276 0.063 -0.391 -0.158 0.001 0.001 1868.0 2520.0 1.0
agecat[4] -0.267 0.063 -0.386 -0.153 0.001 0.001 1815.0 2218.0 1.0
agecat[5] -0.388 0.069 -0.519 -0.262 0.001 0.001 2170.0 2401.0 1.0
agecat[6] -0.314 0.080 -0.460 -0.160 0.002 0.001 2278.0 2597.0 1.0
gender[M] 0.166 0.034 0.106 0.231 0.000 0.000 7281.0 3280.0 1.0
area[B] -0.024 0.050 -0.123 0.064 0.001 0.001 3097.0 3061.0 1.0
area[C] 0.071 0.047 -0.013 0.161 0.001 0.001 3206.0 3038.0 1.0
area[D] -0.016 0.063 -0.135 0.101 0.001 0.001 3786.0 2873.0 1.0
area[E] 0.153 0.068 0.028 0.286 0.001 0.001 3976.0 3126.0 1.0
area[F] 0.372 0.076 0.233 0.517 0.001 0.001 4503.0 2876.0 1.0
claimcst0_alpha 0.762 0.014 0.738 0.789 0.000 0.000 6496.0 3352.0 1.0

The interpretation of the parameter posteriors is very similar to what we’ve done for the Wald family. The only difference is that some differences, such as the ones for the area posteriors, are a little more exacerbated here.

## Model comparison#

We can perform a Bayesian model comparison very easily with az.compare(). Here we pass a dictionary with the InferenceData objects that Model.fit() returned and az.compare() returns a data frame that is ordered from best to worst according to the criteria used.

[13]:

models = {"wald": fitted_wald, "gamma": fitted_gamma}
df_compare = az.compare(models)
df_compare

[13]:

rank loo p_loo d_loo weight se dse warning loo_scale
wald 0 -38581.528593 12.987415 0.000000 1.0 106.066095 0.000000 False log
gamma 1 -39629.299736 27.090234 1047.771143 0.0 104.999828 35.741461 False log
[14]:

az.plot_compare(df_compare, insample_dev=False);


By default, ArviZ uses loo, which is an estimation of leave one out cross-validation. Another option is the widely applicable information criterion (WAIC). Since the results are in the log scale, the better out-of-sample predictive fit is given by the model with the highest value, which is the Wald model.

[15]:

%load_ext watermark
%watermark -n -u -v -iv -w

Last updated: Wed Jun 01 2022

Python implementation: CPython
Python version       : 3.9.7
IPython version      : 8.3.0

arviz     : 0.12.1
matplotlib: 3.5.1
sys       : 3.9.7 (default, Sep 16 2021, 13:09:58)
[GCC 7.5.0]
numpy     : 1.21.5
bambi     : 0.7.1

Watermark: 2.3.0