4  The Poisson regression

What do you say we expend our horizon, make Ms. Frizzle proud, and open the door to number bigger than one? Let’s take a look at how we could model count data next.

The Poisson regression is the GLM used when the response variable is the result of a -guess what- Poisson distribution1 and the link function is the log function. As previously stated, this is typically where you will start for population counts. The Poisson distribution is brought to us by -you guessed it- the great Siméon Denis Poisson, the same great French mathematician and physicist that also brought you the Poisson’s Equation, the Poisson Integral, and the Poisson Summation Formula. When you get that many things named after yourself, you know you’re cooking! He was pretty much the Simone Biles of his time.

4.1 Data

To see how it functions in practice, let’s take the example of the tarpies’ abundance in Lake Tarpon. We have sampled the fish population in 100 different sites and for each site we recorded the total abundance and the amount of food available. We hypothesize that fish abundance is related to food availability. Here are the data:

fish.data=data.frame(sitenumber=1:100, 
                     fish.population=c(7, 3, 7, 1, 9, 1, 0, 0, 33, 15, 0, 
                                       8, 10, 6, 6, 0, 5, 4, 0, 0, 18, 1, 
                                       0, 4, 9, 35, 7, 6, 52, 18, 2, 6, 0, 
                                       1, 1, 1, 17, 0, 1, 0, 1, 2, 3, 2, 1, 
                                       5, 21, 5, 3, 2, 0, 2, 12, 4, 0, 0, 3, 
                                       0, 0, 2, 4, 10, 13, 10, 2, 4, 15, 3, 
                                       4, 20, 2, 1, 33, 0, 3, 0, 6, 32, 15, 
                                       0, 18, 5, 1, 1, 1, 0, 0, 52, 2, 23, 0, 
                                       4, 8, 4, 6, 13, 4, 0, 0, 3) ,
                     # Standardized food availability: 
                     food=c(0.7726, 0.3052, 0.4091, -0.7749, 0.9618, -0.2377, 
                            -1.2015, -0.8639, 1.6418, 1.1006, -1.2936, 0.3786, 
                            0.5588, 0.3441, 0.4137, -0.625, 0.5609, -0.2873, 
                            -0.7354, -2.353, 1.3576, 0.0096, -1.6573, 0.5267, 
                            0.9805, 1.5259, 0.4241, 0.5559, 1.9227, 1.1616, 
                            -0.4877, 0.434, -1.3075, -1.2571, -0.4045, 0.3146, 
                            1.0496, -1.1584, -0.9643, -0.7425, -0.6268, -0.2768, 
                            -0.0564, 0.0732, -0.4332, 0.7553, 1.2763, 0.5868, 
                            0.0771, -0.2776, -0.1421, -0.5285, 0.8199, 0.11, 
                            -0.4764, -1.189, 0.3835, 0.1221, -0.327, -0.4722, 
                            0.4809, 0.9963, 0.8434, 0.9199, -0.2348, 0.308, 
                            1.0344, 0.0919, -0.0676, 1.317, -0.2041, -0.4657, 
                            1.565, -0.8463, 0.457, -1.1507, 1.0512, 1.7268, 
                            1.0127, -0.4697, 1.4311, 0.1237, -0.8073, -0.6794, 
                            -0.8374, -0.053, -1.2292, 1.8966, -0.3192, 1.4033, 
                            -0.6946, 0.1261, 1.1178, -0.0309, 0.9486, 0.9798, 
                            -0.4686, -1.9257, -1.4116, -0.1264) 
                     )

A quick look:

plot(fish.population~food,fish.data,xlab="Food",ylab="Tarpies abundance")
Figure 4.1: Fish abundance

4.2 Estimation

Impatient and just want to jump to doing a Poisson regression? No worries, I got you: you just use the “glm()” function and set the argument “family” to “poisson” (go figure…):

fish.res=glm(fish.population~food, family=poisson, data=fish.data)

To analyze the results, as usual, we can get a summary of our model’s results via the well-named function “summary()”.

summary(fish.res)

Call:
glm(formula = fish.population ~ food, family = poisson, data = fish.data)

Coefficients:
            Estimate Std. Error z value Pr(>|z|)    
(Intercept)  0.87677    0.07580   11.57   <2e-16 ***
food         1.59823    0.05891   27.13   <2e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

(Dispersion parameter for poisson family taken to be 1)

    Null deviance: 1090.685  on 99  degrees of freedom
Residual deviance:   92.837  on 98  degrees of freedom
AIC: 365.86

Number of Fisher Scoring iterations: 5

4.3 Diagnostics and Model performance

Before looking at the result though, we need to run a couple of checks…

4.3.1 Checking for overdispersion

An essential assumption underlying the Poisson distribution is that the mean and variance should be equal. If this is not the case, a different approach should be considered.

We can get a quick and rough idea of whether or not overdispersion is an issue for our model by calculating the Dispersion Ratio (i.e., the ratio of the residual deviance over the degrees of freedom) using information from the model summary:

fish.res$deviance/fish.res$df.residual  # deviance can also be extracted with deviance(fish.res)
[1] 0.9473164

If this ratio is close to one (say between 0.9 and 1.1), overdispersion should not be a problem. However, if it is significantly greater than 1, you are likely to be facing overdispersion and will need to call in the big guns to handle that situation.

You can compute an overdispersion test using the check_overdispersion() function from the performance package. This package is a gold mine to look at model performances, in case you had not figured that out yet.

library(performance)
check_overdispersion(fish.res)
# Overdispersion test

       dispersion ratio =  0.810
  Pearson's Chi-Squared = 79.399
                p-value =  0.915
No overdispersion detected.

It looks like here, we are good to go to use a Poisson distribution to model our tarpies data. Good.

4.3.2 A simple measure of fit

Next, we will want to take a look at the model fit. We can follow a similar approach to what we’ve already done for the logistic regression.

The null deviance and the corresponding degrees of freedom indicate a highly significant difference between fitted values and observed values:

1-pchisq(fish.res$null.deviance,fish.res$df.null) 
[1] 0

(Noticed how I directly extracted the “null.deviance” and the number of degrees of freedom “df.null” from the GLM output?)

Once the predictor is added though, it’s a different story!

1-pchisq(fish.res$deviance,fish.res$df.residual) 
[1] 0.6283983

Now, the fitted values are not significantly different from the observed values. We therefore cannot reject the null hypothesis (i.e., our model prediction and our observed data are similar).

4.3.3 Goodness of Fit: Likelihood Ratio Test

Another way to look at it is to perform a likelihood ratio test, and test directly if our model performs better than a Null model.

We can use the same tools as presented for the logistic regression, either using base R:

anova(fish.res)
Analysis of Deviance Table

Model: poisson, link: log

Response: fish.population

Terms added sequentially (first to last)

     Df Deviance Resid. Df Resid. Dev  Pr(>Chi)    
NULL                    99    1090.68              
food  1   997.85        98      92.84 < 2.2e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

or a dedicated package:

library(lmtest)
lrtest(fish.res)
Likelihood ratio test

Model 1: fish.population ~ food
Model 2: fish.population ~ 1
  #Df  LogLik Df  Chisq Pr(>Chisq)    
1   2 -180.93                         
2   1 -679.85 -1 997.85  < 2.2e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Here, we can confirm once again that adding our food covariate leads to a significant improvement in the fit of our model to our data. Nothing fishy here!

4.3.4 A look a the residuals

Let’s make sure our residuals are distributed as expected by simulating randomized quantile residuals.

library(performance)
library(qqplotr)

simulated_residuals <- simulate_residuals(fish.res)

plot(simulated_residuals)

Distribution of quantile residuals
check_residuals(simulated_residuals) 
OK: Simulated residuals appear as uniformly distributed (p = 0.705).

Nothing to see here, we can confidently jump to what we’re all here for. Inferences!

4.4 Inferences

4.4.1 A Closer look

summary(fish.res)

Call:
glm(formula = fish.population ~ food, family = poisson, data = fish.data)

Coefficients:
            Estimate Std. Error z value Pr(>|z|)    
(Intercept)  0.87677    0.07580   11.57   <2e-16 ***
food         1.59823    0.05891   27.13   <2e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

(Dispersion parameter for poisson family taken to be 1)

    Null deviance: 1090.685  on 99  degrees of freedom
Residual deviance:   92.837  on 98  degrees of freedom
AIC: 365.86

Number of Fisher Scoring iterations: 5

Looking at the summary of our model output, we see that we have statistically significant intercept and food effect. We can directly extract the corresponding coefficients from the GLM output:

fish.res$coefficients # or we can get the same information with 'coef(fish.res)'
(Intercept)        food 
  0.8767657   1.5982324 

We can extract from the specific coefficient value on the log-scale for our food effect.

fish.res$coefficients[["food"]]
[1] 1.598232

Which in turn can be used to express our population growth rate depending on the food availability. Since this coefficient is on the log scale, we need to use the inverse function of the log, i.e. the exponential, in order to get this information.

exp(fish.res$coefficients[["food"]]) 
[1] 4.944285

Every time we add one food unit on the standardized scale, the tarpies population will get 4.9 times larger. In a real life situation, we would also back transform the standardized “food” covariate to give a clearer ecological description of our dynamic.

4.4.2 Plotting the fit

As for the previous fitted models, we can plot the fitted line, but this time, let’s ‘prettify’ it with ggplot2 for good measure:

food.new <- seq(min(fish.data$food), max(fish.data$food), length.out = 100) # Creating new values of the covariates for which we want to predict the response variable 
resp.pred=data.frame(food=food.new)

resp.pred$pred <- predict(fish.res, newdata=resp.pred, type="response") # Predictions


library(ggplot2)

gg.poisson <- ggplot() +
  geom_point(aes(x=food,y=fish.population), data=fish.data) +
  geom_line(aes(x= food,y=pred ),col="red",data=resp.pred) + # Fitted values 
  labs( title="Fish abundance depending on food resources",
        x="Food",
        y=" Abundance")+
  theme_minimal()
 gg.poisson
Figure 4.2: Fish abundance in function of food resources

And compute and plot the prediction’s 95% confidence interval:

pred.linkscale <- predict(fish.res, newdata=resp.pred, se=T) 
pred.linkscale.CI2.5  = pred.linkscale$fit-1.96*pred.linkscale$se.fit
pred.linkscale.CI97.5 = pred.linkscale$fit+1.96*pred.linkscale$se.fit

resp.pred$CI2.5  = family(fish.res)$linkinv(pred.linkscale.CI2.5)
resp.pred$CI97.5 = family(fish.res)$linkinv(pred.linkscale.CI97.5)


gg.poisson.2 <- gg.poisson +
  geom_ribbon(aes(x=food, ymin = CI2.5,ymax = CI97.5),
              alpha=0.15,data=resp.pred)
gg.poisson.2
Figure 4.3: Fish abundance in function of food resources

I know… Beautiful, right?


  1. ↩︎