Chapter 25 Solution to GLM exercise

theme_set(theme_bw())
d<-read.csv("/home/aqm/course/data/HedisteCounts.csv")

25.1 Visualise the data

The best way to look at these data quickly is by plotting histograms of the counts. I’ll use ggplot and facet wrapping

ggplot(d, aes(x=Count)) +geom_histogram(binwidth=3,fill="grey", col="black") +facet_wrap(~Substrate)

25.2 Simple non parametric

So its very obvious that the data are not normally distributed and there are lots of zeros. So, an undergraduate would probably use a Wilcoxon rank sum test (Mann Whitney in SPSS) in order to get a p-value.

wilcox.test(d$Count~d$Substrate)
## 
##  Wilcoxon rank sum test with continuity correction
## 
## data:  d$Count by d$Substrate
## W = 2006, p-value = 0.0005577
## alternative hypothesis: true location shift is not equal to 0

Great! We’ve found a significant difference. However, where is the effect size?

Let’s get the means anyway.

d%>% group_by(Substrate) %>% summarise(mean(Count))
## # A tibble: 2 x 2
##   Substrate `mean(Count)`
## * <fct>             <dbl>
## 1 Mud                3.8 
## 2 Sand               1.02

Looks like there are around four times as many ragworm in the mud than the sand. But we can’t say that based on the non-parametric test.

25.3 T test

Let’s do a T test

t.test(d$Count~d$Substrate)
## 
##  Welch Two Sample t-test
## 
## data:  d$Count by d$Substrate
## t = 3.561, df = 62.74, p-value = 0.0007115
## alternative hypothesis: true difference in means is not equal to 0
## 95 percent confidence interval:
##  1.221265 4.345402
## sample estimates:
##  mean in group Mud mean in group Sand 
##           3.800000           1.016667

Yippee! It’s significant again. But my tutor is gong to tell me to put it into SPSS and SPSS will tell me that the test isn’t valid.

25.4 One way Anova

mod<-lm(data=d,Count~Substrate)
anova(mod)
## Analysis of Variance Table
## 
## Response: Count
##            Df  Sum Sq Mean Sq F value    Pr(>F)    
## Substrate   1  211.28  211.28  14.451 0.0002385 ***
## Residuals 108 1578.98   14.62                      
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Test residuals for normality

shapiro.test(residuals(mod))
## 
##  Shapiro-Wilk normality test
## 
## data:  residuals(mod)
## W = 0.7562, p-value = 3.152e-12

Sugar. Not good.

Test for homogeneity of variance?

bartlett.test(residuals(mod)~d$Substrate)
## 
##  Bartlett test of homogeneity of variances
## 
## data:  residuals(mod) by d$Substrate
## Bartlett's K-squared = 39.177, df = 1, p-value = 3.871e-10

Not valid. So, I’ll have to use the wilcox test?

25.5 Poisson regression

Now I’m a master’s student I’m more sophisticated. I’ll use a GLM of the poisson family with a log link function. That will convince my tutor!

mod<-glm(data=d,Count~Substrate,family="poisson")
summary(mod)
## 
## Call:
## glm(formula = Count ~ Substrate, family = "poisson", data = d)
## 
## Deviance Residuals: 
##     Min       1Q   Median       3Q      Max  
## -2.7568  -1.4259  -1.4259   0.4656   5.8335  
## 
## Coefficients:
##               Estimate Std. Error z value Pr(>|z|)    
## (Intercept)    1.33500    0.07255  18.402   <2e-16 ***
## SubstrateSand -1.31847    0.14716  -8.959   <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: 600.35  on 109  degrees of freedom
## Residual deviance: 505.17  on 108  degrees of freedom
## AIC: 654.94
## 
## Number of Fisher Scoring iterations: 6

Oops. I’ve spotted over dispersion. Should have seen that one coming from the histograms.

25.6 Negative binomial

But… I’m a very savvy MSC student because I took the AQM course. I have an answer to that. I’ll use a glm of the negative binomial family.

library(MASS)
mod<-glm.nb(data=d,Count~Substrate)
summary(mod)
## 
## Call:
## glm.nb(formula = Count ~ Substrate, data = d, init.theta = 0.2727104969, 
##     link = log)
## 
## Deviance Residuals: 
##     Min       1Q   Median       3Q      Max  
## -1.2143  -0.9205  -0.9205   0.1156   1.6322  
## 
## Coefficients:
##               Estimate Std. Error z value Pr(>|z|)    
## (Intercept)     1.3350     0.2804   4.762 1.92e-06 ***
## SubstrateSand  -1.3185     0.3951  -3.337 0.000847 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## (Dispersion parameter for Negative Binomial(0.2727) family taken to be 1)
## 
##     Null deviance: 100.795  on 109  degrees of freedom
## Residual deviance:  89.795  on 108  degrees of freedom
## AIC: 385.39
## 
## Number of Fisher Scoring iterations: 1
## 
## 
##               Theta:  0.2727 
##           Std. Err.:  0.0608 
## 
##  2 x log-likelihood:  -379.3940

That looks much better.

Let’s try to understand the coefficients.

exp(coef(mod))
##   (Intercept) SubstrateSand 
##     3.8000000     0.2675439

Aha. The first is the mean for mud. What is the second one?

exp(coef(mod)[1]) * exp(coef(mod)[2])
## (Intercept) 
##    1.016667

Right. It’s the value that I need to multiply the count in mud by in order to get the count in sand.

Effect size with confidence intervals?

exp(confint(mod))
##                   2.5 %    97.5 %
## (Intercept)   2.2821868 6.9085692
## SubstrateSand 0.1216029 0.5794958

So the effect lies between 12% and 58%. We have an answer!

25.7 Presence absence

Converting to presence absence.

d$pres<-1*(d$Count>0)

Tabulate the data.

table(d$pres,d$Substrate)
##    
##     Mud Sand
##   0  23   44
##   1  27   16

Plot as bar chart. (An undergrad would use Excel)

d %>% group_by(Substrate,pres) %>% summarise(n=n()) %>%
  ggplot(aes(x=Substrate,y=n, fill=as.factor(pres))) + geom_col(position="dodge") 

Chi square test

It’s a 2x2 contingency table. I can do a chi squared!

chisq.test( table(d$pres,d$Substrate))
## 
##  Pearson's Chi-squared test with Yates' continuity correction
## 
## data:  table(d$pres, d$Substrate)
## X-squared = 7.4482, df = 1, p-value = 0.00635

Yippee! It’s significant. But … where is the effect size? Fit a binomial glm

mod<-glm(data=d,pres~Substrate,family="binomial")
summary(mod)
## 
## Call:
## glm(formula = pres ~ Substrate, family = "binomial", data = d)
## 
## Deviance Residuals: 
##     Min       1Q   Median       3Q      Max  
## -1.2462  -0.7876  -0.7876   1.1101   1.6259  
## 
## Coefficients:
##               Estimate Std. Error z value Pr(>|z|)   
## (Intercept)     0.1603     0.2838   0.565  0.57202   
## SubstrateSand  -1.1719     0.4071  -2.879  0.00399 **
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## (Dispersion parameter for binomial family taken to be 1)
## 
##     Null deviance: 147.21  on 109  degrees of freedom
## Residual deviance: 138.58  on 108  degrees of freedom
## AIC: 142.58
## 
## Number of Fisher Scoring iterations: 4
exp(coef(mod))
##   (Intercept) SubstrateSand 
##     1.1739130     0.3097643

Hang on… how do we interpret these exponentiated coeficients? They are odds.

table(d$pres,d$Substrate)
##    
##     Mud Sand
##   0  23   44
##   1  27   16

So 27 to 23 for presence in mud gives ..

27/23
## [1] 1.173913

Multiply this by the “relative risk” of finding ragworm in sand.

1.173913 * 0.3097643 
## [1] 0.3636363

Should be the same as

16/44
## [1] 0.3636364

Which it is.

So when we get confidence intervals the key value refers to the odds ratio.

exp(confint(mod))
##                   2.5 %   97.5 %
## (Intercept)   0.6735089 2.063209
## SubstrateSand 0.1369500 0.679671

So we have got a confidence interval for a meaningful quantity, but we have unfortunately entered the interpretabilty minefield thrown up by relative odds ratios. Still better than using a chi squared!

We end up by being able to state that the relative odds of finding ragworm present in “sand” cores compared to “mud” cores (note that the variable itself is based on a binary split) is between 14% and 68%. Compare this to the difference between the result for the analysis of abundance. It is surprisingly similar. This illustrates that there is not really that much information in the actual counts. Presence vs absence is just about as good a measure (statistically). Of course, if you want to make an estimate of the overall number of ragworm then you do need to count them.

Note that if you have been following stories on vaccine trials in the news, this is equivalent to the stats being reported for a RCT (randomised control trial) between vaccinated and placebo groups. This is because analyses of drug trials also tend to use logistic regression (i.e glms of the binomial family)

25.8 Chi squared test and poisson glms

As an aside, I mentioned that the chi squared test is essentially just a poisson glm. This is how you can use a poisson glm to get the same p-value. The input data are the tabulated counts.

d %>% group_by(pres,Substrate) %>% summarise(count=n()) -> tb
#tb<-as.data.frame(table(d$pres,d$Substrate)) ## base R tables need to be turned to df
#names(tb)<-c("pres","substrate","count")
tb$pres<-as.factor(tb$pres)
tb
## # A tibble: 4 x 3
## # Groups:   pres [2]
##   pres  Substrate count
##   <fct> <fct>     <int>
## 1 0     Mud          23
## 2 0     Sand         44
## 3 1     Mud          27
## 4 1     Sand         16

If you remember your introductory stats the chisquared test using a contingency table is a test for association. So the element that is of interest in the model is an interaction between presence and substrate.

mod<-glm(data=tb,count~pres*Substrate,family="poisson")
summary(mod)
## 
## Call:
## glm(formula = count ~ pres * Substrate, family = "poisson", data = tb)
## 
## Deviance Residuals: 
## [1]  0  0  0  0
## 
## Coefficients:
##                     Estimate Std. Error z value Pr(>|z|)    
## (Intercept)           3.1355     0.2085  15.037  < 2e-16 ***
## pres1                 0.1603     0.2838   0.565  0.57202    
## SubstrateSand         0.6487     0.2573   2.521  0.01170 *  
## pres1:SubstrateSand  -1.1719     0.4071  -2.879  0.00399 ** 
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## (Dispersion parameter for poisson family taken to be 1)
## 
##     Null deviance:  1.4819e+01  on 3  degrees of freedom
## Residual deviance: -4.8850e-15  on 0  degrees of freedom
## AIC: 28.367
## 
## Number of Fisher Scoring iterations: 3

The p value should be the same as the chi squared test, if the technical continuity correction used for contingency tables is not made.

chisq.test( table(d$pres,d$Substrate),correct = FALSE)
## 
##  Pearson's Chi-squared test
## 
## data:  table(d$pres, d$Substrate)
## X-squared = 8.5577, df = 1, p-value = 0.003441

Which it is. There is a very minor numerical difference due to rounding and minor differences in the actual mechanism of calculation, but the two numbers are practically equivalent. This equivalence applies to all situations, not just this data set. Notice it is also the same as the p-value for the binomial model.

The point being illustrated is that the assumption made by the chi squared test is that the counts follow a poisson distribution with a variance equal to the mean. This is used when testing for a difference between the observed number of counts and the expected number of counts under the null hypothesis of no association (interaction) between presence and substrate. So, the chi squared test is not even really a non parametric test!

Of the three possible ways of getting a p-value the most useful is clearly the binomial glm, as this provides confidence intervals. The poisson glm for the counts actually also could provide the same intervals, by looking at the interaction term, but this is rarely used in this context.

exp(confint(mod))
##                          2.5 %    97.5 %
## (Intercept)         14.8342716 33.720346
## pres1                0.6735089  2.063209
## SubstrateSand        1.1676593  3.219274
## pres1:SubstrateSand  0.1369500  0.679671