Appendix C — Case studies

For all case studies, follow the general strategy of the workflow chapter (Chapter 12): fit a base model derived from the question, check its residuals, and adjust the model to the problems you find (Table B.10).

C.1 Hurricanes

In Jung et al. (2014), the authors claim that “Female hurricanes are deadlier than male hurricanes”.

Specifically, they analyze the number of hurricane fatalities, and claim that there is an effect of the femininity of the name on the number of fatalities, correcting for several possible confounders. They interpret the result as causal (including mediators), claiming that giving only male names to hurricanes would considerably reduce death toll.

The data is available in DHARMa.

library(DHARMa)
library(mgcv)
?hurricanes
str(hurricanes)
Classes 'tbl_df', 'tbl' and 'data.frame':   92 obs. of  14 variables:
 $ Year                    : num  1950 1950 1952 1953 1953 ...
 $ Name                    : chr  "Easy" "King" "Able" "Barbara" ...
 $ MasFem                  : num  6.78 1.39 3.83 9.83 8.33 ...
 $ MinPressure_before      : num  958 955 985 987 985 960 954 938 962 987 ...
 $ Minpressure_Updated_2014: num  960 955 985 987 985 960 954 938 962 987 ...
 $ Gender_MF               : num  1 0 0 1 1 1 1 1 1 1 ...
 $ Category                : num  3 3 1 1 1 3 3 4 3 1 ...
 $ alldeaths               : num  2 4 3 1 0 60 20 20 0 200 ...
 $ NDAM                    : num  1590 5350 150 58 15 ...
 $ Elapsed_Yrs             : num  63 63 61 60 60 59 59 59 58 58 ...
 $ Source                  : chr  "MWR" "MWR" "MWR" "MWR" ...
 $ ZMasFem                 : num  -0.000935 -1.670758 -0.913313 0.945871 0.481075 ...
 $ ZMinPressure_A          : num  -0.356 -0.511 1.038 1.141 1.038 ...
 $ ZNDAM                   : num  -0.439 -0.148 -0.55 -0.558 -0.561 ...

Some plots:

plot(hurricanes$MasFem, hurricanes$NDAM, cex = 0.5, pch = 5)
points(hurricanes$MasFem, hurricanes$NDAM, cex = hurricanes$alldeaths/20,
       pch = 4, col= "red")

The original model from the paper fits a negative binomial, using mgcv. I suppose the reason is mainly that glmmTMB was not available at the time, and implementations of the negative binomial, in particular MASS::glm.nb and lme4::glmer.nb often had convergence problems.

originalModelGAM = gam(alldeaths ~ MasFem * (Minpressure_Updated_2014 + NDAM),
    data = hurricanes, family = nb, na.action = "na.fail")
summary(originalModelGAM)

Family: Negative Binomial(0.736) 
Link function: log 

Formula:
alldeaths ~ MasFem * (Minpressure_Updated_2014 + NDAM)

Parametric coefficients:
                                  Estimate Std. Error z value Pr(>|z|)    
(Intercept)                      7.014e+01  2.003e+01   3.502 0.000462 ***
MasFem                          -5.986e+00  2.529e+00  -2.367 0.017927 *  
Minpressure_Updated_2014        -7.008e-02  2.060e-02  -3.402 0.000669 ***
NDAM                            -3.845e-05  2.945e-05  -1.305 0.191735    
MasFem:Minpressure_Updated_2014  6.124e-03  2.603e-03   2.352 0.018656 *  
MasFem:NDAM                      1.593e-05  3.756e-06   4.242 2.21e-05 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1


R-sq.(adj) =  -3.61e+03   Deviance explained = 57.4%
-REML = 357.56  Scale est. = 1         n = 92
CautionTasks
  • Confirm that you get the same results as in the paper. It makes sense to translate their model to glmmTMB. Note that the nb parameterization of mgcv corresponds to nbinom2 in glmmTMB. You will get different results when choosing nbinom1
  • Inspect the fitted model for potential problems, in particular perform a residual analysis of the model, including residuals against all predictors, and improve the model if you find problems.
  • Forget what they did. Go back to start, do a causal analysis like we did (Chapter 7), and do your own model, diagnosing all residual problems that we discussed (Section 2.4, Section 9.3, Chapter 10, Chapter 11). Do you think there is an effect of femininity?

This is the model fit by Jung et al. (2014), fit with glmmTMB

library(DHARMa)
library(glmmTMB)

hurricanes$sNDAM = scale(hurricanes$NDAM)
m1 = glmmTMB(alldeaths ~ MasFem*
                             (Minpressure_Updated_2014 + sNDAM),
                           data = hurricanes, family = nbinom2)
summary(m1)
 Family: nbinom2  ( log )
Formula:          alldeaths ~ MasFem * (Minpressure_Updated_2014 + sNDAM)
Data: hurricanes

      AIC       BIC    logLik -2*log(L)  df.resid 
    660.7     678.4    -323.4     646.7        85 


Dispersion parameter for nbinom2 family (): 0.787 

Conditional model:
                                 Estimate Std. Error z value Pr(>|z|)    
(Intercept)                     69.661590  23.425598   2.974 0.002942 ** 
MasFem                          -5.855078   2.716589  -2.155 0.031138 *  
Minpressure_Updated_2014        -0.069870   0.024251  -2.881 0.003964 ** 
sNDAM                           -0.494094   0.455968  -1.084 0.278536    
MasFem:Minpressure_Updated_2014  0.006108   0.002813   2.171 0.029901 *  
MasFem:sNDAM                     0.205418   0.061956   3.316 0.000915 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Note that the model above scales only NDAM (and the paper does not say whether they scaled), but as we are looking for main effects in the presence of interactions, we should at least center the predictors (Section 2.10.3); here, we also scale them, which makes the effects comparable.

hurricanes$sMasFem = scale(hurricanes$MasFem)
hurricanes$sMinpressure = scale(hurricanes$Minpressure_Updated_2014)
m2 = glmmTMB(alldeaths ~ sMasFem * (sMinpressure + sNDAM),
                           data = hurricanes, family = nbinom2)
summary(m2)
 Family: nbinom2  ( log )
Formula:          alldeaths ~ sMasFem * (sMinpressure + sNDAM)
Data: hurricanes

      AIC       BIC    logLik -2*log(L)  df.resid 
    660.7     678.4    -323.4     646.7        85 


Dispersion parameter for nbinom2 family (): 0.787 

Conditional model:
                     Estimate Std. Error z value Pr(>|z|)    
(Intercept)            2.5034     0.1231  20.341  < 2e-16 ***
sMasFem                0.1237     0.1210   1.022 0.306923    
sMinpressure          -0.5425     0.1603  -3.384 0.000715 ***
sNDAM                  0.8988     0.2190   4.105 4.05e-05 ***
sMasFem:sMinpressure   0.3758     0.1731   2.171 0.029901 *  
sMasFem:sNDAM          0.6629     0.1999   3.316 0.000915 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

now main effect is n.s.; it’s a bit dodgy, but if you read in the main paper, they do not claim a significant main effect, they mainly argue via ANOVA and significance at high values of NDAM, so let’s run an ANOVA. car::Anova() gives type II tests, which, unlike type III, do not depend on the centering (see Section 3.2.4 and Caution 3.1):

car::Anova(m2)
Analysis of Deviance Table (Type II Wald chisquare tests)

Response: alldeaths
                       Chisq Df Pr(>Chisq)    
sMasFem               1.9495  1  0.1626364    
sMinpressure          7.1285  1  0.0075868 ** 
sNDAM                14.6100  1  0.0001322 ***
sMasFem:sMinpressure  4.7150  1  0.0299011 *  
sMasFem:sNDAM        10.9929  1  0.0009146 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

In the ANOVA we see that MasFem still n.s. but interactions, and if you would calculate effect of MasFem at high NDAM, it is significant. Something like that is argued in the paper. We can emulate this by changing NDAM centering to high NDAM, which gives us a p-value for the main effect of MasFem at high values of NDAM

hurricanes$highcenteredNDAM = hurricanes$NDAM - max(hurricanes$NDAM)

m3 = glmmTMB(alldeaths ~ sMasFem * (sMinpressure + highcenteredNDAM),
                           data = hurricanes, family = nbinom2)
summary(m3)
 Family: nbinom2  ( log )
Formula:          alldeaths ~ sMasFem * (sMinpressure + highcenteredNDAM)
Data: hurricanes

      AIC       BIC    logLik -2*log(L)  df.resid 
    660.7     678.4    -323.4     646.7        85 


Dispersion parameter for nbinom2 family (): 0.787 

Conditional model:
                           Estimate Std. Error z value Pr(>|z|)    
(Intercept)               7.210e+00  1.149e+00   6.275 3.50e-10 ***
sMasFem                   3.595e+00  1.041e+00   3.455 0.000551 ***
sMinpressure             -5.425e-01  1.603e-01  -3.384 0.000715 ***
highcenteredNDAM          6.949e-05  1.693e-05   4.105 4.05e-05 ***
sMasFem:sMinpressure      3.758e-01  1.731e-01   2.171 0.029904 *  
sMasFem:highcenteredNDAM  5.125e-05  1.546e-05   3.316 0.000915 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Now we see the significant main effect that they report. Note, however, that the significant difference is only there for high NDAM, i.e. what we do here is to project the effect of the interaction on the main effect. An alternative to do the same thing would be an effects plot (Section 2.3), or to specifically use predict() (Section 2.11) to calculate differences and CIs at high NDAM values.

library(effects)
plot(allEffects(m3, partial.residuals = T))

OK, this means we can replicate the results of the paper, even if concentrating the entire analysis exclusive on high NDAM seems a bit cherry-picking. Choosing the evaluation point after looking at the data inflates the false-positive risk, and at the maximum NDAM the estimate rests on very few storms. Another way to phrase the result is that we don’t find a main effect of MasFem. However, to be fair: the current results do say that there is a significant difference at high NDAM, and such a difference, if it existed, would be important.

But we haven’t done residual checks yet. Let’s do that for the original model of the paper (originalModelGAM):

res <- simulateResiduals(originalModelGAM)
plot(res)

plotResiduals(res, hurricanes$NDAM)

plotResiduals(res, hurricanes$MasFem)

plotResiduals(res, hurricanes$Minpressure_Updated_2014)

No significant deviation in the general DHARMa plot, but residuals ~ NDAM looks funny, which was also pointed out by Bob O’Hara in a blog post after publication of the paper. Let’s try to correct this - transforming NDAM with ^0.2 (found by trying a few powers; 0.1 to 0.2 fit best) does a great job:

hurricanes$sNDAM02 = scale(hurricanes$NDAM^0.2)
correctedModel = glmmTMB(alldeaths ~ sMasFem * (sMinpressure + sNDAM02),
                          data = hurricanes, family = nbinom2)

res <- simulateResiduals(correctedModel, plot = T)

plotResiduals(res, hurricanes$NDAM)

summary(correctedModel)
 Family: nbinom2  ( log )
Formula:          alldeaths ~ sMasFem * (sMinpressure + sNDAM02)
Data: hurricanes

      AIC       BIC    logLik -2*log(L)  df.resid 
    630.8     648.5    -308.4     616.8        85 


Dispersion parameter for nbinom2 family (): 1.11 

Conditional model:
                     Estimate Std. Error z value Pr(>|z|)    
(Intercept)           2.26430    0.10912  20.751  < 2e-16 ***
sMasFem               0.05156    0.10695   0.482    0.630    
sMinpressure         -0.03162    0.18141  -0.174    0.862    
sNDAM02               1.28961    0.18992   6.790 1.12e-11 ***
sMasFem:sMinpressure -0.02410    0.20343  -0.118    0.906    
sMasFem:sNDAM02       0.16045    0.20350   0.788    0.430    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
car::Anova(correctedModel)
Analysis of Deviance Table (Type II Wald chisquare tests)

Response: alldeaths
                       Chisq Df Pr(>Chisq)    
sMasFem               0.5732  1     0.4490    
sMinpressure          0.1255  1     0.7232    
sNDAM02              73.5010  1     <2e-16 ***
sMasFem:sMinpressure  0.0140  1     0.9057    
sMasFem:sNDAM02       0.6216  1     0.4304    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

All gone, only damage is doing the effect. As the interactions are n.s., the main effects would hardly change with a different centering.

What if we would have fit our own model? First of all, note that if hurricane names were given randomly, we wouldn’t have to worry about confounders (Section 7.2.1). However, this is not the case, hurricanes were only named randomly after 1978 or so.

plot(MasFem ~ Year, data = hurricanes)

So, we could either take the earlier data out, which would remove half of our data, or we have to worry about confounding with variables that change over time. The most obvious thing would be to take time itself (Year) in the model, to correct for temporal confounding.

Do we need other variables that are not confounders? There are two reasons to add them (the first is the idea of precision variables, Tip 8.1):

  • they have strong effects on the response - not adding them could lead to residual problems and increase residual variance, which increases uncertainties and cost power
  • we want to fit interactions.

I added NDAM to the model, because we saw earlier that it has a strong effect. I think it’s not unreasonable to check for an interaction as well.

As we have several observations per year, a conservative approach would be to add a RE on year. Note that we use year both as a fixed effect (to remove temporal trends) and a random intercept, which is perfectly fine, however.

newModel = glmmTMB(alldeaths ~ sMasFem * sNDAM02 + Year + (1|Year),
                           data = hurricanes, family = nbinom2)
summary(newModel)
 Family: nbinom2  ( log )
Formula:          alldeaths ~ sMasFem * sNDAM02 + Year + (1 | Year)
Data: hurricanes

      AIC       BIC    logLik -2*log(L)  df.resid 
    630.8     648.4    -308.4     616.8        85 

Random effects:

Conditional model:
 Groups Name        Variance  Std.Dev. 
 Year   (Intercept) 2.571e-07 0.0005071
Number of obs: 92, groups:  Year, 49

Dispersion parameter for nbinom2 family (): 1.11 

Conditional model:
                 Estimate Std. Error z value Pr(>|z|)    
(Intercept)     -2.542287  12.730846  -0.200    0.842    
sMasFem          0.073207   0.119273   0.614    0.539    
sNDAM02          1.309624   0.118106  11.089   <2e-16 ***
Year             0.002426   0.006423   0.378    0.706    
sMasFem:sNDAM02  0.179874   0.117191   1.535    0.125    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
car::Anova(newModel) # nothing regarding MasFem
Analysis of Deviance Table (Type II Wald chisquare tests)

Response: alldeaths
                   Chisq Df Pr(>Chisq)    
sMasFem           0.5563  1     0.4558    
sNDAM02         121.9237  1     <2e-16 ***
Year              0.1426  1     0.7057    
sMasFem:sNDAM02   2.3559  1     0.1248    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

The variance of the year random effect is estimated as practically zero (a boundary fit), so there is no extra year-to-year variation beyond the fixed effect of Year, and removing the random effect would not change the results. The results remain that there is no effect of MasFem!

The hurricanes are also a time series. The check for temporal autocorrelation and an AR1 model for these data are shown in the hurricane exercise of the chapter on correlation structures (Section 11.2.1); they show only a weak, negative autocorrelation that could also be a statistical fluke.

Jung, K. et al. (2014) “Female hurricanes are deadlier than male hurricanes,” Proceedings of the National Academy of Sciences, 111(24), pp. 8782–8787.

C.2 Researcher degrees of freedom — skin color and red cards

Silberzahn et al. (2018) published a “meta-analysis” in Advances in Methods and Practices in Psychological Science, where they had provided 29 teams with the same data set to answer one research question: “[W]hether soccer players with dark skin tone are more likely than those with light skin tone to receive red cards from referees”.

Spoiler: They found that the “[a]nalytic approaches varied widely across the teams, and the estimated effect sizes ranged from 0.89 to 2.93 (Mdn = 1.31) in odds-ratio units”, highlighting that different approaches in data analysis can yield significant variation in the results.

The data is in

library(EcoData)
?redCards
CautionTasks

Do a re-analysis of the data as if you were the 30th team to contribute the results to the meta-analysis. You can find the data in the ecodata package, dataset redCards.

  1. Response variable: ‘redCards’ (+‘yellowReds’?).
  2. primary predictors: ‘rater1’, ‘rater2’
  3. Multiple variables, potentially accounting for confounding, offsetting, grouping, … are included in the data.

The rater variable contains ratings of “two independent raters blind to the research question who, based on their profile photo, categorized players on a 5-point scale ranging from (1) very light skin to (5) very dark skin. Note that ‘rater1’ and ‘rater2’ in EcoData::redCards are already rescaled to the range 0 … 1, as described in the paper (”This variable was rescaled to be bounded by 0 (very light skin) and 1 (very dark skin) prior to the final analysis, to ensure consistency of effect sizes across the teams of analysts. The raw ratings were rescaled to 0, .25, .50, .75, and 1 to create this new scale.”)

When you’re done, have a look at the other modeling teams. Do you understand the models they fit? Note that the results are displayed in terms of odds ratios. Are your results within the range of estimates from the 29 teams in Silberzahn et al. (2018)?

Base model

There is no single “correct” analysis - that is the whole point of Silberzahn et al. (2018). The following is therefore not a fully worked-out solution, but one defensible base model to start from. Each row is a player-referee dyad, so red cards are a count out of the number of games the two shared; we use that as exposure via a log offset, take the mean of the two skin-tone ratings rescaled to 0-1 as the predictor, and add random effects for player and referee to respect the non-independence.

library(EcoData); library(glmmTMB); library(DHARMa)
d = redCards
d$skin  = (d$rater1 + d$rater2)/2     # mean of the two ratings (already on the 0..1 scale)
d = d[!is.na(d$skin) & d$games > 0, ]

m = glmmTMB(redCards ~ skin + (1|playerShort) + (1|refNum),
            offset = log(games), family = poisson, data = d)
summary(m)
 Family: poisson  ( log )
Formula:          redCards ~ skin + (1 | playerShort) + (1 | refNum)
Data: d
 Offset: log(games)

      AIC       BIC    logLik -2*log(L)  df.resid 
  15543.6   15582.5   -7767.8   15535.6    124617 

Random effects:

Conditional model:
 Groups      Name        Variance Std.Dev.
 playerShort (Intercept) 0.3659   0.6049  
 refNum      (Intercept) 0.1481   0.3849  
Number of obs: 124621, groups:  playerShort, 1585; refNum, 2978

Conditional model:
            Estimate Std. Error z value Pr(>|z|)    
(Intercept) -5.78507    0.05547  -104.3  < 2e-16 ***
skin         0.32961    0.10649     3.1  0.00197 ** 
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
exp(fixef(m)$cond["skin"])   # rate ratio, darkest (1) vs lightest (0) skin
    skin 
1.390424 
res = simulateResiduals(m); plot(res); testDispersion(res)


    DHARMa nonparametric dispersion test via sd of residuals fitted vs.
    simulated

data:  simulationOutput
dispersion = 1.0768, p-value = 0.008
alternative hypothesis: two.sided

The exponentiated skin coefficient is our effect in rate-ratio units (comparable to the paper’s odds ratios: red cards are rare, about 0.4% per player-referee game, and for rare events a rate ratio is close to an odds ratio). Compare it to the 0.89-2.93 range of odds ratios reported across the 29 teams. This bare model gives a rate ratio of about 1.4, which is inside the range of the 29 teams and close to their median (1.31). The study shows, however, that the “researcher degrees of freedom” (choice of exposure, random-effect structure, which rater, count vs. binomial, and - crucially - which confounders are included) can move the result substantially. From this base model, you would go on to address the slight overdispersion (Section 9.3) and to add plausible confounders (see Chapter 7; league, playing position, player skill, …) and see how much the estimate changes.

Silberzahn, R. et al. (2018) “Many analysts, one data set: Making transparent how variations in analytic choices affect results,” Advances in Methods and Practices in Psychological Science, 1(3), pp. 337–356.

C.3 Scouting ants

CautionTasks

Look at the dataset EcoData::scoutingAnts, and find out if there are really scouting ants in Lasius niger.

A base model should be:

library(EcoData)
dat = scoutingAnts[scoutingAnts$first.visit == 0,]
dat$ant_group = as.factor(dat$ant_group)
dat$ant_group_main = as.factor(dat$ant_group_main)

fit <- glm(went.phero ~ ant_group_main, data = dat, family = binomial)
summary(fit)

Call:
glm(formula = went.phero ~ ant_group_main, family = binomial, 
    data = dat)

Coefficients:
                                       Estimate Std. Error z value Pr(>|z|)    
(Intercept)                             0.67398    0.09775   6.895 5.39e-12 ***
ant_group_mainSecondvisit_1st_to_phero  0.16690    0.11371   1.468    0.142    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

(Dispersion parameter for binomial family taken to be 1)

    Null deviance: 2322.8  on 1874  degrees of freedom
Residual deviance: 2320.7  on 1873  degrees of freedom
AIC: 2324.7

Number of Fisher Scoring iterations: 4

For me, it made sense to change the contrasts of the possible confounders (see Section 2.6.1) into something more easily interpretable:

  • Did the side in which the pheromone was stay constant or not (testing for a directional persistence of the ants)

  • Was the pheromone in the left or the right arm (testing for a directional preference)

dat$directionConst = ifelse(dat$Treatment %in% c("LL", "RR"), T, F)
dat$directionPhero = as.factor(ifelse(dat$Treatment %in% c("LL", "RL"), "left", "right"))

Together with the orientation of the Maze, this makes 3 possible directional confounders, and the main predictor (if the Ant went to the pheromone in the first visit).

The treatments and orientations are nearly balanced by the setup, but the main predictor is a behavior of the ants, and ants do not behave randomly. Its distribution across the design variables is not balanced:

table(dat$Treatment, dat$ant_group_main)
    
     Secondvisit_1st_not_phero Secondvisit_1st_to_phero
  LL                        87                      414
  LR                       127                      358
  RL                       135                      334
  RR                       119                      301
round(prop.table(table(dat$Treatment, dat$ant_group_main), 1), 2)
    
     Secondvisit_1st_not_phero Secondvisit_1st_to_phero
  LL                      0.17                     0.83
  LR                      0.26                     0.74
  RL                      0.29                     0.71
  RR                      0.28                     0.72

Most ants (75%) went to the pheromone on their first visit, but this share differs between treatments (83% in LL, where the pheromone was on the left side in both runs, against 71 to 74% in the others). The main predictor is therefore weakly but visibly associated with the design variables (see Section 3.2.3.2 for why this matters even in balanced designs).

Adding an RE on colony is logical, and then let’s run the model:

library(lme4)
fit1<-glmer(went.phero ~ ant_group_main
             + directionConst
             + directionPhero
             + Orientation
             + (1|Colony),family="binomial", 
             data=dat)
summary(fit1)
Generalized linear mixed model fit by maximum likelihood (Laplace
  Approximation) [glmerMod]
 Family: binomial  ( logit )
Formula: went.phero ~ ant_group_main + directionConst + directionPhero +  
    Orientation + (1 | Colony)
   Data: dat

      AIC       BIC    logLik -2*log(L)  df.resid 
   2192.5    2225.7   -1090.2    2180.5      1869 

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-4.6870 -1.0451  0.4836  0.6876  1.0318 

Random effects:
 Groups Name        Variance Std.Dev.
 Colony (Intercept) 0.9284   0.9636  
Number of obs: 1875, groups:  Colony, 15

Fixed effects:
                                       Estimate Std. Error z value Pr(>|z|)    
(Intercept)                              1.9505     0.3330   5.857 4.72e-09 ***
ant_group_mainSecondvisit_1st_to_phero   0.1311     0.1248   1.050  0.29352    
directionConstTRUE                      -1.1465     0.2409  -4.760 1.94e-06 ***
directionPheroright                     -0.5680     0.1984  -2.863  0.00420 ** 
Orientationright                        -0.3493     0.1222  -2.859  0.00425 ** 
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr) a__S_1 dCTRUE drctnP
ant_g_S_1__ -0.252                     
drctnCnTRUE -0.413 -0.062              
drctnPhrrgh -0.441 -0.001  0.306       
Orinttnrght -0.182 -0.093 -0.074  0.085

Surprisingly, we find large effects of the other variables. Because of these large effects, we also test for interactions with the experimental treatment

fit2<-glmer(went.phero ~ ant_group_main * (
             + directionConst
             + directionPhero
             + Orientation)
             + (1|Colony),family="binomial", 
             data=dat)
summary(fit2)
Generalized linear mixed model fit by maximum likelihood (Laplace
  Approximation) [glmerMod]
 Family: binomial  ( logit )
Formula: went.phero ~ ant_group_main * (+directionConst + directionPhero +  
    Orientation) + (1 | Colony)
   Data: dat

      AIC       BIC    logLik -2*log(L)  df.resid 
   2131.2    2181.0   -1056.6    2113.2      1866 

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-6.7899 -0.8936  0.4857  0.5984  1.9020 

Random effects:
 Groups Name        Variance Std.Dev.
 Colony (Intercept) 0.9497   0.9745  
Number of obs: 1875, groups:  Colony, 15

Fixed effects:
                                                           Estimate Std. Error
(Intercept)                                                  2.7384     0.3914
ant_group_mainSecondvisit_1st_to_phero                      -0.7658     0.2764
directionConstTRUE                                          -1.9420     0.3065
directionPheroright                                         -1.8133     0.3008
Orientationright                                             0.2322     0.2345
ant_group_mainSecondvisit_1st_to_phero:directionConstTRUE    1.0466     0.2639
ant_group_mainSecondvisit_1st_to_phero:directionPheroright   1.4752     0.2665
ant_group_mainSecondvisit_1st_to_phero:Orientationright     -0.8261     0.2668
                                                           z value Pr(>|z|)    
(Intercept)                                                  6.997 2.61e-12 ***
ant_group_mainSecondvisit_1st_to_phero                      -2.771  0.00559 ** 
directionConstTRUE                                          -6.337 2.35e-10 ***
directionPheroright                                         -6.029 1.65e-09 ***
Orientationright                                             0.990  0.32208    
ant_group_mainSecondvisit_1st_to_phero:directionConstTRUE    3.967 7.29e-05 ***
ant_group_mainSecondvisit_1st_to_phero:directionPheroright   5.537 3.08e-08 ***
ant_group_mainSecondvisit_1st_to_phero:Orientationright     -3.096  0.00196 ** 
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr) an__S_1__ dCTRUE drctnP Ornttn a__S_1__:C a__S_1__:P
ant_g_S_1__ -0.532                                                     
drctnCnTRUE -0.433  0.241                                              
drctnPhrrgh -0.512  0.369     0.186                                    
Orinttnrght -0.282  0.428    -0.100  0.008                             
a__S_1__:CT  0.217 -0.443    -0.592  0.043  0.015                      
an__S_1__:P  0.343 -0.529     0.007 -0.733 -0.049 -0.072               
an__S_1__:O  0.225 -0.526     0.089  0.034 -0.845 -0.021      0.046    

We now find that there is an interaction with the main predictor, and there could be effects. We can also look at this visually.

plot(allEffects(fit2))

The results are difficult to interpret. The large effects of the design variables suggest that the ants did not behave randomly with respect to the maze (a bias of the setup). The main predictor is a behavior of the same ants and weakly associated with these design variables (see the table above), so its effect is hard to separate from them.

For our education, we can also look at the residual plots. I will use fit1, because there was a misfit:

res <- simulateResiduals(fit1, plot = T)

As we would expect significant interactions, we would probably see something if we plot residuals against predictors or their interactions, but I want to show you something else:

We will not see dispersion problems in a 0/1 binomial, but actually, this is a k/n binomial, just that the data are not prepared as such.

Either way, in DHARMa, you can aggregate residuals by a grouping variable.

res2 <- recalculateResiduals(res, group = dat$Colony)

Now, we essentially check k/n data, and we see that there is overdispersion, which is caused by the misfit.

plot(res2)

testDispersion(res2)


    DHARMa nonparametric dispersion test via sd of residuals fitted vs.
    simulated

data:  simulationOutput
dispersion = 1.0233, p-value = 0.832
alternative hypothesis: two.sided

Let’s do the same for model 2, which included the interactions.

res <- simulateResiduals(fit2, plot = T)

res2 <- recalculateResiduals(res, group = dat$Colony)

plot(res2)
Warning in newton(lsp = lsp, X = G$X, y = G$y, Eb = G$Eb, UrS = G$UrS, L = G$L,
: Fitting terminated with step failure - check results carefully

testDispersion(res2)


    DHARMa nonparametric dispersion test via sd of residuals fitted vs.
    simulated

data:  simulationOutput
dispersion = 1.0422, p-value = 0.736
alternative hypothesis: two.sided

Which largely removes the problem!

C.4 Snails

library(EcoData)
library(glmmTMB)
library(lme4)
library(DHARMa)
?EcoData::snails

Look at the Snails data set in the EcoData package, and find out which environmental and/or seasonal predictors i) explain the total abundance and ii) the infection rate of Bulinus spp.

The snails data set in the EcoData package includes observations on the distribution of freshwater snails and their infection rates ( schistosomiasis (a parasite)).

The first scientific question is that their abundance depends on the water conditions. The second scientific question is that their infection rate depends on the water conditions and seasonal factors

The data also contains data on other environmental (and seasonal factors). You should consider if it is useful to add them to the analysis.

Species: BP_tot (Biomphalaria pfeifferi), BF_tot (Bulinus forskalii), BT_tot (Bulinus truncatus)

Number of infected individuals: BP_pos_tot, BF_pos_tot, BT_pos_tot

Total abundance of Bulinus spp.: Bulinus_tot

Total number of infected Bulinus spp.: Bulinus_pos_tot

CautionTasks
  1. Model the total abundance of Bulinus spp. (Bulinus_tot)

  2. Model the infection rate of Bulinus spp. (Bulinus_pos_tot) (k/n binomial)

  3. Optional: Model the species individually (BP_tot, BF_tot, BT_tot)

  4. Optional: Fit a multivariate (joint) species distribution model (see Section 11.6)

Total abundance of Bulinus (GLMM)

Prepare+scale data:

library(lme4)
library(glmmTMB)
library(DHARMa)
data = EcoData::snails
data$sTemp_Water = scale(data$Temp_Water)
data$spH = scale(data$pH)
data$swater_speed_ms = scale(data$water_speed_ms)
data$swater_depth = scale(data$water_depth)
data$sCond = scale(data$Cond)
data$swmo_prec = scale(data$wmo_prec)
data$syear = scale(data$year)
data$sLat = scale(data$Latitude)
data$sLon = scale(data$Longitude)
data$sTemp_Air = scale(data$Temp_Air)

# remove rows with NAs in the variables used (see text)
rows = rownames(model.matrix(Bulinus_tot~sTemp_Water + spH + sLat + sLon + sCond + seas_wmo+ swmo_prec + swater_speed_ms + swater_depth +sTemp_Air+ syear + duration + locality + site_irn + coll_date, data = data))
data = data[rows, ]

We remove the rows with NAs in the variables we use beforehand (with model.matrix()), so that all models use the same rows and can be compared (Section 6.1). We do the same for the seed bank data.

Our hypothesis is that the abundance of Bulinus species depends on the water characteristics, e.g. site_type, Temp_water, pH, Cond, swmo_prec, water_speed_ms, and water_depth. We will set the length of the collection duration as an offset.

model1 = glm(Bulinus_tot~
                 offset(log(duration)) + site_type + sTemp_Water + spH +
                 sCond + swmo_prec + swater_speed_ms + swater_depth,
              data = data,  family = poisson)
summary(model1)

Call:
glm(formula = Bulinus_tot ~ offset(log(duration)) + site_type + 
    sTemp_Water + spH + sCond + swmo_prec + swater_speed_ms + 
    swater_depth, family = poisson, data = data)

Coefficients:
                   Estimate Std. Error z value Pr(>|z|)    
(Intercept)        0.328925   0.006417  51.258  < 2e-16 ***
site_typecanal.3  -0.161047   0.010287 -15.656  < 2e-16 ***
site_typepond     -0.837273   0.022624 -37.009  < 2e-16 ***
site_typerice.p   -1.378799   0.027252 -50.595  < 2e-16 ***
site_typeriver    -1.730850   0.032842 -52.703  < 2e-16 ***
site_typerivulet  -1.757255   0.041545 -42.298  < 2e-16 ***
site_typespillway -1.679141   0.048544 -34.590  < 2e-16 ***
sTemp_Water       -0.089050   0.004435 -20.080  < 2e-16 ***
spH                0.036653   0.004501   8.144 3.82e-16 ***
sCond              0.072787   0.004979  14.620  < 2e-16 ***
swmo_prec         -0.098717   0.006337 -15.577  < 2e-16 ***
swater_speed_ms   -0.181606   0.007695 -23.600  < 2e-16 ***
swater_depth      -0.113600   0.005998 -18.940  < 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: 131410  on 2071  degrees of freedom
Residual deviance: 116126  on 2059  degrees of freedom
AIC: 122311

Number of Fisher Scoring iterations: 6

As the sites are nested within localities, we will set a nested random intercept (see Tip 4.2) on site_irn within locality. Also potential confounders are collection date (coll_date), the season (wet or dry months, seas_wmo), year, and maybe other environmental factors such as the air temperature?.

model2 = glmer(Bulinus_tot~
                 offset(log(duration)) + site_type + sTemp_Water + spH +
                 sCond + swmo_prec + swater_speed_ms + swater_depth +
                 sTemp_Air + seas_wmo + (1|year) + (1|locality/site_irn) +  (swater_depth|coll_date),
              data = data,  family = poisson)
summary(model2)
Generalized linear mixed model fit by maximum likelihood (Laplace
  Approximation) [glmerMod]
 Family: poisson  ( log )
Formula: Bulinus_tot ~ offset(log(duration)) + site_type + sTemp_Water +  
    spH + sCond + swmo_prec + swater_speed_ms + swater_depth +  
    sTemp_Air + seas_wmo + (1 | year) + (1 | locality/site_irn) +  
    (swater_depth | coll_date)
   Data: data

      AIC       BIC    logLik -2*log(L)  df.resid 
  48873.9   48992.3  -24415.9   48831.9      2051 

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-11.997  -2.448  -0.963   1.285  41.809 

Random effects:
 Groups            Name         Variance Std.Dev. Corr 
 coll_date         (Intercept)  1.8754   1.3694        
                   swater_depth 1.2494   1.1178   0.29 
 site_irn:locality (Intercept)  0.7207   0.8489        
 locality          (Intercept)  0.8320   0.9121        
 year              (Intercept)  0.1612   0.4015        
Number of obs: 2072, groups:  
coll_date, 191; site_irn:locality, 89; locality, 20; year, 5

Fixed effects:
                   Estimate Std. Error z value Pr(>|z|)    
(Intercept)       -0.971166   0.362146  -2.682  0.00733 ** 
site_typecanal.3  -0.078376   0.238294  -0.329  0.74223    
site_typepond      0.291195   0.408641   0.713  0.47610    
site_typerice.p   -0.774498   0.339485  -2.281  0.02253 *  
site_typeriver    -0.532071   0.422256  -1.260  0.20765    
site_typerivulet  -0.449224   0.556215  -0.808  0.41929    
site_typespillway -0.421987   0.560119  -0.753  0.45122    
sTemp_Water       -0.055637   0.018135  -3.068  0.00215 ** 
spH               -0.029211   0.009002  -3.245  0.00117 ** 
sCond             -0.015815   0.009496  -1.666  0.09581 .  
swmo_prec         -0.212245   0.099788  -2.127  0.03342 *  
swater_speed_ms   -0.128273   0.009434 -13.596  < 2e-16 ***
swater_depth      -0.290785   0.084163  -3.455  0.00055 ***
sTemp_Air         -0.102238   0.013631  -7.500 6.36e-14 ***
seas_wmowet       -0.057576   0.204253  -0.282  0.77803    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Check residuals:

res = simulateResiduals(model2, plot = TRUE)
DHARMa:testOutliers with type = binomial may have inflated Type I error rates for integer-valued distributions. To get a more exact result, it is recommended to re-run testOutliers with type = 'bootstrap'. See ?testOutliers for details
DHARMa:testOutliers with type = binomial may have inflated Type I error rates for integer-valued distributions. To get a more exact result, it is recommended to re-run testOutliers with type = 'bootstrap'. See ?testOutliers for details

Does not look great -> dispersion problems -> switch to -> negative binomial distribution:

model3 = glmmTMB(Bulinus_tot~
                 offset(log(duration)) + site_type + sTemp_Water + spH +
                 sCond + swmo_prec + swater_speed_ms + swater_depth +
                 sTemp_Air + seas_wmo + (1|year) + (1|locality/site_irn) +  (swater_depth|coll_date),
              data = data,  family = nbinom2)
summary(model3)
 Family: nbinom2  ( log )
Formula:          
Bulinus_tot ~ offset(log(duration)) + site_type + sTemp_Water +  
    spH + sCond + swmo_prec + swater_speed_ms + swater_depth +  
    sTemp_Air + seas_wmo + (1 | year) + (1 | locality/site_irn) +  
    (swater_depth | coll_date)
Data: data

      AIC       BIC    logLik -2*log(L)  df.resid 
  14137.0   14261.0   -7046.5   14093.0      2050 

Random effects:

Conditional model:
 Groups            Name         Variance Std.Dev. Corr 
 year              (Intercept)  0.32980  0.5743        
 site_irn:locality (Intercept)  0.59947  0.7743        
 locality          (Intercept)  0.54314  0.7370        
 coll_date         (Intercept)  0.66067  0.8128        
                   swater_depth 0.02441  0.1563   0.16 
Number of obs: 2072, groups:  
year, 5; site_irn:locality, 89; locality, 20; coll_date, 191

Dispersion parameter for nbinom2 family (): 0.433 

Conditional model:
                  Estimate Std. Error z value Pr(>|z|)    
(Intercept)       -0.48794    0.37886  -1.288 0.197773    
site_typecanal.3  -0.14448    0.24459  -0.591 0.554728    
site_typepond      0.09447    0.43016   0.220 0.826172    
site_typerice.p   -0.92636    0.36321  -2.550 0.010757 *  
site_typeriver    -0.82008    0.42509  -1.929 0.053709 .  
site_typerivulet  -0.98675    0.54309  -1.817 0.069231 .  
site_typespillway -0.41259    0.56965  -0.724 0.468888    
sTemp_Water       -0.10038    0.09361  -1.072 0.283532    
spH               -0.07251    0.05265  -1.377 0.168487    
sCond              0.12188    0.05953   2.047 0.040613 *  
swmo_prec         -0.10279    0.07948  -1.293 0.195908    
swater_speed_ms   -0.12390    0.03418  -3.625 0.000289 ***
swater_depth      -0.18908    0.05497  -3.440 0.000583 ***
sTemp_Air         -0.01520    0.08007  -0.190 0.849444    
seas_wmowet       -0.03143    0.17994  -0.175 0.861355    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Check residuals:

res = simulateResiduals(model3, plot = TRUE)

Residuals look better but there is still a dispersion problem.

Let’s use the dispformula:

model4 = glmmTMB(Bulinus_tot~
                 offset(log(duration)) + site_type + sTemp_Water + spH +
                 sCond + swmo_prec + swater_speed_ms + swater_depth +
                 sTemp_Air + seas_wmo + (1|year) + (1|locality/site_irn) +  (swater_depth|coll_date),
                 dispformula = ~swater_speed_ms + swater_depth+sCond,
              data = data,  family = nbinom2)
summary(model4)
 Family: nbinom2  ( log )
Formula:          
Bulinus_tot ~ offset(log(duration)) + site_type + sTemp_Water +  
    spH + sCond + swmo_prec + swater_speed_ms + swater_depth +  
    sTemp_Air + seas_wmo + (1 | year) + (1 | locality/site_irn) +  
    (swater_depth | coll_date)
Dispersion:                   ~swater_speed_ms + swater_depth + sCond
Data: data

      AIC       BIC    logLik -2*log(L)  df.resid 
  14138.2   14279.1   -7044.1   14088.2      2047 

Random effects:

Conditional model:
 Groups            Name         Variance Std.Dev. Corr 
 year              (Intercept)  0.32157  0.5671        
 site_irn:locality (Intercept)  0.60194  0.7758        
 locality          (Intercept)  0.53655  0.7325        
 coll_date         (Intercept)  0.65362  0.8085        
                   swater_depth 0.01958  0.1399   0.14 
Number of obs: 2072, groups:  
year, 5; site_irn:locality, 89; locality, 20; coll_date, 191

Conditional model:
                  Estimate Std. Error z value Pr(>|z|)    
(Intercept)       -0.46934    0.37644  -1.247 0.212485    
site_typecanal.3  -0.15293    0.24497  -0.624 0.532443    
site_typepond      0.05419    0.43145   0.126 0.900045    
site_typerice.p   -0.95968    0.36331  -2.641 0.008254 ** 
site_typeriver    -0.84089    0.42561  -1.976 0.048184 *  
site_typerivulet  -1.01483    0.54260  -1.870 0.061439 .  
site_typespillway -0.45053    0.57035  -0.790 0.429575    
sTemp_Water       -0.09780    0.09342  -1.047 0.295146    
spH               -0.07259    0.05246  -1.384 0.166427    
sCond              0.12399    0.06344   1.954 0.050647 .  
swmo_prec         -0.10189    0.07895  -1.290 0.196899    
swater_speed_ms   -0.16899    0.04665  -3.623 0.000292 ***
swater_depth      -0.17509    0.05627  -3.112 0.001861 ** 
sTemp_Air         -0.01550    0.08014  -0.193 0.846589    
seas_wmowet       -0.04867    0.17952  -0.271 0.786290    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Dispersion model:
                 Estimate Std. Error z value Pr(>|z|)    
(Intercept)     -0.838268   0.038472 -21.789   <2e-16 ***
swater_speed_ms -0.095345   0.044883  -2.124   0.0336 *  
swater_depth    -0.003945   0.042073  -0.094   0.9253    
sCond           -0.013696   0.037138  -0.369   0.7123    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Dispformula is significant. Residuals:

res = simulateResiduals(model4, plot = TRUE)

Dispersion tests are n.s.:

testDispersion(res)


    DHARMa nonparametric dispersion test via sd of residuals fitted vs.
    simulated

data:  simulationOutput
dispersion = 0.50462, p-value = 0.016
alternative hypothesis: two.sided
testZeroInflation(res)


    DHARMa zero-inflation test via comparison to expected zeros with
    simulation under H0 = fitted model

data:  simulationOutput
ratioObsSim = 1.0533, p-value = 0.072
alternative hypothesis: two.sided

So far, we have not included a spatial trend, so there could be spatial autocorrelation. Let’s check for it. Note that we simulate unconditionally here (simulateREs = "unconditional"): with the default conditional simulations, the fitted random effects of the sites would absorb the spatial pattern, and the test could not detect it (Tip 4.4).

## Spatial
res = simulateResiduals(model4, simulateREs = "unconditional")
res2 = recalculateResiduals(res, group = c(data$site_irn))
groupLocations = aggregate(cbind(data$sLat, data$sLon ), list( data$site_irn), mean)
testSpatialAutocorrelation(res2, x = groupLocations$V1, y = groupLocations$V2)


    DHARMa Moran's I test for distance-based autocorrelation

data:  res2
observed = 0.295785, expected = -0.011364, sd = 0.066951, p-value =
4.482e-06
alternative hypothesis: Distance-based autocorrelation

Significant! Following the rule from Section 11.1.1, we first try to remove a large-scale spatial trend, here with a quadratic surface in latitude and longitude:

model5 = glmmTMB(Bulinus_tot~
                 offset(log(duration)) + site_type + sTemp_Water + spH +
                 sCond + swmo_prec + swater_speed_ms + swater_depth +
                 sTemp_Air + seas_wmo + (1|year) + (1|locality/site_irn) +  (swater_depth|coll_date) +
                 sLat + sLon + I(sLat^2) + I(sLon^2) + sLat:sLon,
                 dispformula = ~swater_speed_ms + swater_depth+sCond,
              data = data,  family = nbinom2)
summary(model5)
 Family: nbinom2  ( log )
Formula:          
Bulinus_tot ~ offset(log(duration)) + site_type + sTemp_Water +  
    spH + sCond + swmo_prec + swater_speed_ms + swater_depth +  
    sTemp_Air + seas_wmo + (1 | year) + (1 | locality/site_irn) +  
    (swater_depth | coll_date) + sLat + sLon + I(sLat^2) + I(sLon^2) +  
    sLat:sLon
Dispersion:                   ~swater_speed_ms + swater_depth + sCond
Data: data

      AIC       BIC    logLik -2*log(L)  df.resid 
  14131.6   14300.7   -7035.8   14071.6      2042 

Random effects:

Conditional model:
 Groups            Name         Variance Std.Dev. Corr 
 year              (Intercept)  0.30443  0.5517        
 site_irn:locality (Intercept)  0.56861  0.7541        
 locality          (Intercept)  0.19679  0.4436        
 coll_date         (Intercept)  0.65155  0.8072        
                   swater_depth 0.01937  0.1392   0.11 
Number of obs: 2072, groups:  
year, 5; site_irn:locality, 89; locality, 20; coll_date, 191

Conditional model:
                  Estimate Std. Error z value Pr(>|z|)    
(Intercept)       -0.74595    0.46822  -1.593 0.111129    
site_typecanal.3  -0.24653    0.24066  -1.024 0.305652    
site_typepond      0.23740    0.41640   0.570 0.568595    
site_typerice.p   -1.19097    0.36532  -3.260 0.001114 ** 
site_typeriver    -0.75403    0.39689  -1.900 0.057450 .  
site_typerivulet  -0.62267    0.50711  -1.228 0.219492    
site_typespillway -0.34750    0.55301  -0.628 0.529759    
sTemp_Water       -0.08609    0.09333  -0.922 0.356303    
spH               -0.06893    0.05247  -1.314 0.188934    
sCond              0.12641    0.06337   1.995 0.046063 *  
swmo_prec         -0.10129    0.07884  -1.285 0.198846    
swater_speed_ms   -0.17071    0.04661  -3.663 0.000249 ***
swater_depth      -0.17726    0.05619  -3.155 0.001608 ** 
sTemp_Air         -0.01311    0.07989  -0.164 0.869626    
seas_wmowet       -0.06032    0.17948  -0.336 0.736816    
sLat               1.40631    1.17979   1.192 0.233262    
sLon               0.74087    1.10345   0.671 0.501962    
I(sLat^2)          4.47135    1.46797   3.046 0.002320 ** 
I(sLon^2)          5.72527    2.04908   2.794 0.005205 ** 
sLat:sLon         10.08212    3.32987   3.028 0.002464 ** 
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Dispersion model:
                 Estimate Std. Error z value Pr(>|z|)    
(Intercept)     -0.837938   0.038426 -21.807   <2e-16 ***
swater_speed_ms -0.095700   0.044878  -2.132    0.033 *  
swater_depth    -0.004602   0.041907  -0.110    0.913    
sCond           -0.016225   0.036875  -0.440    0.660    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
res = simulateResiduals(model5, simulateREs = "unconditional")
res2 = recalculateResiduals(res, group = c(data$site_irn))
testSpatialAutocorrelation(res2, x = groupLocations$V1, y = groupLocations$V2)


    DHARMa Moran's I test for distance-based autocorrelation

data:  res2
observed = 0.067369, expected = -0.011364, sd = 0.066933, p-value =
0.2395
alternative hypothesis: Distance-based autocorrelation
AIC(model4, model5)
       df      AIC
model4 25 14138.24
model5 30 14131.57

After de-trending, there is no significant spatial autocorrelation left, and the AIC also prefers the de-trended model, so we don’t need a spatial correlation structure. If autocorrelation had remained, we would add one, e.g. exp(0 + numFac | group) (see Section 11.3). Final residual check:

res = simulateResiduals(model5, plot = TRUE)

Residuals do not look perfect but I would say that we can stop here now.

Prevalence of Bulinus (GLMM)

We use the data prepared and scaled in the solution for the total abundance (run that one first; it also loads lme4, glmmTMB and DHARMa). Let’s start directly with all potential confounders:

model1 = glmmTMB(cbind(Bulinus_pos_tot, Bulinus_tot - Bulinus_pos_tot )~
                 offset(log(duration)) + site_type + sTemp_Water + spH +
                 sCond + swmo_prec + swater_speed_ms + swater_depth +
                 sTemp_Air + seas_wmo + (1|year) + (1|locality/site_irn) +  (swater_depth|coll_date),
              data = data,  family = binomial)
summary(model1)
 Family: binomial  ( logit )
Formula:          
cbind(Bulinus_pos_tot, Bulinus_tot - Bulinus_pos_tot) ~ offset(log(duration)) +  
    site_type + sTemp_Water + spH + sCond + swmo_prec + swater_speed_ms +  
    swater_depth + sTemp_Air + seas_wmo + (1 | year) + (1 | locality/site_irn) +  
    (swater_depth | coll_date)
Data: data

      AIC       BIC    logLik -2*log(L)  df.resid 
   1323.3    1441.7    -640.7    1281.3      2051 

Random effects:

Conditional model:
 Groups            Name         Variance Std.Dev. Corr 
 year              (Intercept)  0.04167  0.2041        
 site_irn:locality (Intercept)  1.79468  1.3397        
 locality          (Intercept)  0.91215  0.9551        
 coll_date         (Intercept)  1.40999  1.1874        
                   swater_depth 0.46082  0.6788   0.76 
Number of obs: 2072, groups:  
year, 5; site_irn:locality, 89; locality, 20; coll_date, 191

Conditional model:
                  Estimate Std. Error z value Pr(>|z|)    
(Intercept)       -9.71202    0.52321 -18.562  < 2e-16 ***
site_typecanal.3  -0.20119    0.52740  -0.381  0.70285    
site_typepond      2.30894    0.81865   2.820  0.00480 ** 
site_typerice.p   -0.21964    0.84882  -0.259  0.79582    
site_typeriver    -0.05417    0.99933  -0.054  0.95677    
site_typerivulet   1.09537    1.05508   1.038  0.29918    
site_typespillway  0.84839    1.37766   0.616  0.53802    
sTemp_Water        0.02948    0.16178   0.182  0.85543    
spH               -0.08128    0.10741  -0.757  0.44919    
sCond             -0.32655    0.10527  -3.102  0.00192 ** 
swmo_prec         -0.59112    0.32596  -1.813  0.06976 .  
swater_speed_ms    0.17271    0.08010   2.156  0.03107 *  
swater_depth      -0.27776    0.19217  -1.445  0.14835    
sTemp_Air         -0.23545    0.14630  -1.609  0.10753    
seas_wmowet       -0.36256    0.32166  -1.127  0.25969    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
res = simulateResiduals(model1, plot=TRUE)
DHARMa:testOutliers with type = binomial may have inflated Type I error rates for integer-valued distributions. To get a more exact result, it is recommended to re-run testOutliers with type = 'bootstrap'. See ?testOutliers for details
DHARMa:testOutliers with type = binomial may have inflated Type I error rates for integer-valued distributions. To get a more exact result, it is recommended to re-run testOutliers with type = 'bootstrap'. See ?testOutliers for details

The dispersion test is n.s. (if k/n data are overdispersed, use family = betabinomial or an observation-level random effect, Section 9.3.2).

Check for spatial autocorrelation (again with unconditional simulations, as for the abundance model):

## Spatial
res = simulateResiduals(model1, simulateREs = "unconditional")
res2 = recalculateResiduals(res, group = c(data$site_irn))
groupLocations = aggregate(cbind(data$sLat, data$sLon ), list( data$site_irn), mean)
testSpatialAutocorrelation(res2, x = groupLocations$V1, y = groupLocations$V2)


    DHARMa Moran's I test for distance-based autocorrelation

data:  res2
observed = 0.159982, expected = -0.011364, sd = 0.067031, p-value =
0.01058
alternative hypothesis: Distance-based autocorrelation

Significant! As before, we first try to remove a large-scale spatial trend:

model2 = glmmTMB(cbind(Bulinus_pos_tot, Bulinus_tot - Bulinus_pos_tot )~
                 offset(log(duration)) + site_type + sTemp_Water + spH +
                 sCond + swmo_prec + swater_speed_ms + swater_depth +
                 sTemp_Air + seas_wmo + (1|year) + (1|locality/site_irn) +  (swater_depth|coll_date) +
                 sLat + sLon + I(sLat^2) + I(sLon^2) + sLat:sLon,
              data = data,  family = binomial)

res = simulateResiduals(model2, simulateREs = "unconditional")
res2 = recalculateResiduals(res, group = c(data$site_irn))
testSpatialAutocorrelation(res2, x = groupLocations$V1, y = groupLocations$V2)


    DHARMa Moran's I test for distance-based autocorrelation

data:  res2
observed = 0.098452, expected = -0.011364, sd = 0.067040, p-value =
0.1014
alternative hypothesis: Distance-based autocorrelation
AIC(model1, model2)
       df      AIC
model1 21 1323.300
model2 26 1328.605

After de-trending, the spatial autocorrelation is no longer significant, so we don’t need a spatial correlation structure. Note, however, that the AIC does not support the trend terms, i.e. the spatial signal was weak; keeping model1 would also be defensible.

Multivariate (joint) species distribution model

The species models are connected by their response to latent variable (unobserved environment). For that, we will transform our dataset with respect to species from wide (sp1, sp2, sp3) to long format (species abundances in one column and a second column telling us the group (species)). In the model then, we will separate the species and their responses by using ~0+Species + Species:(predictors).

The latent variable structure is set by the rr(…) object in the formula. We again use the data prepared in the solution for the total abundance (run that one first):

library(tidyverse)
── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
✔ dplyr     1.2.1     ✔ readr     2.2.0
✔ forcats   1.0.1     ✔ stringr   1.6.0
✔ ggplot2   4.0.3     ✔ tibble    3.3.1
✔ lubridate 1.9.5     ✔ tidyr     1.3.2
✔ purrr     1.2.2     
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::collapse() masks nlme::collapse()
✖ tidyr::expand()   masks Matrix::expand()
✖ dplyr::filter()   masks stats::filter()
✖ dplyr::lag()      masks stats::lag()
✖ tidyr::pack()     masks Matrix::pack()
✖ tidyr::unpack()   masks Matrix::unpack()
ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
dataLong =
  data %>% pivot_longer(cols = c("BP_tot", "BF_tot", "BT_tot"),
                      names_to = "Species",
                      values_to = "Abundance" )

numFac = numFactor(dataLong$sLat, dataLong$sLon)
group = factor(rep(1, nrow(dataLong)))
modelJoint = glmmTMB(Abundance~ 0 +
                 offset(log(duration)) + Species + Species:(site_type + 
                  sTemp_Water + spH + sCond + swmo_prec + swater_speed_ms + swater_depth +
                 sTemp_Air + seas_wmo) + (1|year) + (1|locality/site_irn) +  (swater_depth|coll_date:Species) + exp(0+numFac|group) + rr(Species + 0|locality:site_irn, d = 2),
                 dispformula = ~0+Species+Species:(swater_speed_ms + swater_depth+sCond),
              data = dataLong,  family = nbinom2)

Unconditional residuals:

plot(simulateResiduals(modelJoint, simulateREs = "unconditional"))

Conditional residuals:

pred = predict(modelJoint, re.form = NULL, type = "response")
pred_dispersion = predict(modelJoint, re.form = NULL, type = "disp")
simulations = sapply(1:1000, function(i) rnbinom(length(pred),size = pred_dispersion, mu =  pred))
res = createDHARMa(simulations, model.frame(modelJoint)[,1], pred)
plot(res)

C.5 Seed bank

library(EcoData)
library(glmmTMB)
library(lme4)
library(lmerTest)
library(DHARMa)
?EcoData::seedBank

The seedBank data set in the EcoData package includes observation on seed bank presence and size in different vegetation plots.

The scientific question is if the ability of plants to build a seed bank depends on their seed traits (see help).

The data also contains data on environmental factors and plant traits. You should consider if it is useful to add them to the analysis.

CautionTasks
  1. Fit an lm/lmm with SBDensity as response

  2. Bonus: Add phylogenetic correlation structure (see Section 11.4)

  3. Fit a glm/glmm with SBPA as response (Bonus: add phylogenetic correlation structure)

SBDensity (LMM)

Prepare+scale data:

data = as.data.frame(EcoData::seedBank)
data$sAltitude = scale(data$Altitude)
data$sSeedMass = scale(data$SeedMass)
data$sSeedShape = scale(data$SeedShape)
data$sSeedN = scale(data$SeedN)
data$sSeedPr = scale(data$SeedPr)
data$sDormRank = scale(data$DormRank)
data$sTemp = scale(data$Temp)
data$sHum = scale(data$Humidity)
data$sNitro = scale(data$Nitrogen)
data$sGrazing = scale(data$Grazing)
data$sMGT = scale(data$MGT)
data$sJwidth = scale(data$Jwidth)
data$sEpiStein = scale(data$EpiStein)
data$sMGR = scale(data$MGR)
data$sT95 = scale(data$T95)

# remove rows with NAs in the variables used (as for the snails)
rows = rownames(model.matrix(SBDensity~sAltitude + sSeedMass + sSeedShape + sSeedN +
                               sSeedPr + sDormRank + sTemp + sHum + sNitro + sMGT + 
                               sMGR + sEpiStein + sT95 +
                               sJwidth + sGrazing + Site + Species, data = data))
data = data[rows, ]

The response is highly skewed, so it makes sense to log-transform:

hist(data$SBDensity)

data$logSBDensity = log(data$SBDensity + 1)

Let’s fit a base model with our hypothesis that logSBDensity ~ sSeedMass (Seed Mass) + sSeedShape + sSeedN (Number of Seeds).

We set random intercepts on species and sites because we assume that there are species and site specific variations:

model1 = lmer(logSBDensity~
                sSeedMass + sSeedShape + sSeedN + 
                (1|Site) + (1|Species),
              data = data)

summary(model1)
Linear mixed model fit by REML. t-tests use Satterthwaite's method [
lmerModLmerTest]
Formula: logSBDensity ~ sSeedMass + sSeedShape + sSeedN + (1 | Site) +  
    (1 | Species)
   Data: data

REML criterion at convergence: 7531.9

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-3.6261 -0.4945 -0.0823  0.4542  3.1525 

Random effects:
 Groups   Name        Variance Std.Dev.
 Species  (Intercept) 3.0724   1.7528  
 Site     (Intercept) 0.4705   0.6859  
 Residual             3.6790   1.9181  
Number of obs: 1729, groups:  Species, 152; Site, 17

Fixed effects:
            Estimate Std. Error       df t value Pr(>|t|)    
(Intercept)   1.5314     0.2271  42.4393   6.743 3.23e-08 ***
sSeedMass    -0.3316     0.1273 175.6509  -2.604  0.00999 ** 
sSeedShape   -0.3175     0.1546 155.2402  -2.054  0.04168 *  
sSeedN       -0.1818     0.1289 165.3141  -1.410  0.16028    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
           (Intr) sSdMss sSdShp
sSeedMass  -0.037              
sSeedShape -0.034  0.168       
sSeedN     -0.031  0.039  0.077

Altitude and humidity may influence both which species (and thus which seed traits) occur at a site and the seed bank density, so we add them as potential confounders:

model2 = lmer(logSBDensity~
                sSeedMass + sSeedShape + sSeedN +
                 sAltitude + sHum + 
                (1|Site) + (1|Species),
              data = data)

summary(model2)
Linear mixed model fit by REML. t-tests use Satterthwaite's method [
lmerModLmerTest]
Formula: logSBDensity ~ sSeedMass + sSeedShape + sSeedN + sAltitude +  
    sHum + (1 | Site) + (1 | Species)
   Data: data

REML criterion at convergence: 7510.7

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-3.6468 -0.5089 -0.0720  0.4388  3.2140 

Random effects:
 Groups   Name        Variance Std.Dev.
 Species  (Intercept) 3.03405  1.7419  
 Site     (Intercept) 0.06929  0.2632  
 Residual             3.68099  1.9186  
Number of obs: 1729, groups:  Species, 152; Site, 17

Fixed effects:
             Estimate Std. Error        df t value Pr(>|t|)    
(Intercept)   1.65385    0.16664 113.38609   9.925  < 2e-16 ***
sSeedMass    -0.32886    0.12662 176.33088  -2.597   0.0102 *  
sSeedShape   -0.32165    0.15372 155.68190  -2.092   0.0380 *  
sSeedN       -0.18051    0.12816 165.92065  -1.408   0.1609    
sAltitude    -0.56452    0.10125  17.10538  -5.575 3.27e-05 ***
sHum          0.14280    0.09738  14.04198   1.466   0.1646    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
           (Intr) sSdMss sSdShp sSeedN sAlttd
sSeedMass  -0.052                            
sSeedShape -0.044  0.168                     
sSeedN     -0.042  0.039  0.077              
sAltitude  -0.058  0.007 -0.012 -0.006       
sHum       -0.004 -0.008 -0.003  0.004  0.512

Adding altitude and humidity barely changes the seed-trait estimates: seed mass and seed shape have negative effects on seed bank density, seed number has no clear effect.

Question:

What about the germination temperatures, T50/T95?

Answer:

They could be mediators (Seed Shape -> T95 -> SBDensity). For the total effect of the seed traits we leave them out; we would include them only to estimate the direct effect (Section 7.3).

Residual checks:

Check for missing random slopes (Section 4.2.2, Section 4.4.5):

plot(model2, resid(., scaled = TRUE) ~ fitted(.) | Species, abline = 0)

There seems to be a pattern!

plot(model2, resid(., scaled = TRUE) ~ sAltitude | Species, abline = 0)

The pattern seems to be caused by sAltitude (check plot(model2, resid(., scaled = TRUE) ~ sSeedMass | Species, abline = 0) )

Random slope model

Let’s add a random slope on sAltitude:

model3 = lmer(logSBDensity~
                sSeedMass + sSeedShape + sSeedN +
                sAltitude + sHum + 
                (1|Site) + (sAltitude|Species),
              data = data)

summary(model3)
Linear mixed model fit by REML. t-tests use Satterthwaite's method [
lmerModLmerTest]
Formula: logSBDensity ~ sSeedMass + sSeedShape + sSeedN + sAltitude +  
    sHum + (1 | Site) + (sAltitude | Species)
   Data: data

REML criterion at convergence: 7216.1

Scaled residuals: 
     Min       1Q   Median       3Q      Max 
-2.77162 -0.39884 -0.08256  0.22365  3.06745 

Random effects:
 Groups   Name        Variance Std.Dev. Corr  
 Species  (Intercept) 2.61864  1.6182         
          sAltitude   1.31560  1.1470   -0.42 
 Site     (Intercept) 0.09023  0.3004         
 Residual             2.79664  1.6723         
Number of obs: 1729, groups:  Species, 152; Site, 17

Fixed effects:
             Estimate Std. Error        df t value Pr(>|t|)    
(Intercept)   1.52436    0.16898 106.79022   9.021 8.45e-15 ***
sSeedMass    -0.29923    0.13843 181.27154  -2.162 0.031960 *  
sSeedShape   -0.25730    0.14094 145.11888  -1.826 0.069969 .  
sSeedN       -0.15018    0.11145 147.11514  -1.347 0.179918    
sAltitude    -0.59612    0.15103  54.56729  -3.947 0.000228 ***
sHum          0.09296    0.10330  13.65567   0.900 0.383740    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
           (Intr) sSdMss sSdShp sSeedN sAlttd
sSeedMass  -0.042                            
sSeedShape -0.052  0.139                     
sSeedN     -0.045  0.037  0.086              
sAltitude  -0.286  0.040 -0.021 -0.003       
sHum        0.015 -0.009 -0.009  0.004  0.367

Residual checks:

plot(simulateResiduals(model3))

There seems to be dispersion problem.

Bonus: Modeling variance with glmmTMB

See Section 10.1.2 for modeling the variance.

model4 = glmmTMB(logSBDensity~
                sSeedMass + sSeedShape + sSeedN +
                sAltitude + sHum + 
                (1|Site) + (sAltitude|Species),
              dispformula = ~sAltitude + sHum,
              data = data)
summary(model4)
 Family: gaussian  ( identity )
Formula:          logSBDensity ~ sSeedMass + sSeedShape + sSeedN + sAltitude +  
    sHum + (1 | Site) + (sAltitude | Species)
Dispersion:                    ~sAltitude + sHum
Data: data

      AIC       BIC    logLik -2*log(L)  df.resid 
   7196.3    7267.2   -3585.2    7170.3      1716 

Random effects:

Conditional model:
 Groups   Name        Variance Std.Dev. Corr  
 Site     (Intercept) 0.07148  0.2674         
 Species  (Intercept) 2.53442  1.5920         
          sAltitude   1.30195  1.1410   -0.44 
 Residual                  NA      NA         
Number of obs: 1729, groups:  Site, 17; Species, 152

Conditional model:
            Estimate Std. Error z value Pr(>|z|)    
(Intercept)  1.51412    0.16438   9.211  < 2e-16 ***
sSeedMass   -0.28914    0.13762  -2.101   0.0356 *  
sSeedShape  -0.24086    0.13832  -1.741   0.0816 .  
sSeedN      -0.14539    0.10827  -1.343   0.1793    
sAltitude   -0.59734    0.14562  -4.102 4.09e-05 ***
sHum         0.09070    0.09475   0.957   0.3384    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Dispersion model:
            Estimate Std. Error z value Pr(>|z|)    
(Intercept)  0.51449    0.01856  27.713  < 2e-16 ***
sAltitude   -0.12343    0.02397  -5.148 2.63e-07 ***
sHum        -0.01575    0.02630  -0.599    0.549    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Bonus: SBDensity (LMM) with phylogenetic correlation structure

Prepare phylogeny:

library(ape)
library(geiger)
species = unique(data$Species)
species_df = data.frame(Species = species)
rownames(species_df) = species
obj = name.check(plantPhylo, species_df)

# drop rest of the species
phyl.upd = drop.tip(plantPhylo, obj$tree_not_data)
summary(phyl.upd)

Phylogenetic tree: phyl.upd 

  Number of tips: 152 
  Number of nodes: 140 
  Branch lengths:
    mean: 22.21203 
    variance: 624.9334 
    distribution summary:
   Min. 1st Qu.  Median 3rd Qu.    Max. 
  0.200   5.425  12.800  30.402 123.000 
  Root edge: 1 
  First ten tip labels: Tofieldia_pusilla 
                        Tofieldia_calyculata
                        Veratrum_album
                        Maianthemum_bifolium
                        Polygonatum_verticillatum
                        Juncus_monanthos
                        Luzula_glabrata
                        Luzula_sylvatica
                        Luzula_multiflora
                        Carex_sempervirens
  First ten node labels: N398 
                         N401
                         Tofieldiaceae
                         N573
                         N636
                         N1019
                         N1054
                         N1063
                         Juncaceae
                         Luzula
# check the names in the tree and in the data set
name.check(phyl.upd, species_df)
[1] "OK"
phyl.upd2 = multi2di(phyl.upd)

glmmTMB (not perfect because lack of support of specific phylogenetic correlation structures):

correlation_matrix = vcv(phyl.upd2)[unique(data$Species), unique(data$Species)]

###
#the following code was taken from https://github.com/glmmTMB/glmmTMB/blob/master/misc/fixcorr.rmd
as.theta.vcov <- function(Sigma,corrs.only=FALSE) {
    logsd <- log(diag(Sigma))/2
    cr <- cov2cor(Sigma)
    cc <- chol(cr)
    cc <- cc %*% diag(1 / diag(cc))
    corrs <- cc[upper.tri(cc)]
    if (corrs.only) return(corrs)
    ret <- c(logsd,corrs)
    return(ret)
}
corrs = as.theta.vcov(correlation_matrix, corrs.only=TRUE)
#####

data$dummy = factor(rep(0, nrow(data)))
nsp = length(unique(data$Species))
model5 = glmmTMB(logSBDensity~
                sSeedMass + sSeedShape + sSeedN + 
                sAltitude + sHum +
                (1|Site) + (sAltitude|Species) +
                (1+Species|dummy),
              dispformula = ~sAltitude + sHum,
              map=list(theta=factor(c(rep(0, 4), rep(1,nsp),rep(NA,length(corrs))) )),
              start=list(theta=c(rep(0, 4), rep(0,nsp),corrs)),
              data = data)
summary(model5)
 Family: gaussian  ( identity )
Formula:          logSBDensity ~ sSeedMass + sSeedShape + sSeedN + sAltitude +  
    sHum + (1 | Site) + (sAltitude | Species) + (1 + Species |      dummy)
Dispersion:                    ~sAltitude + sHum
Data: data

      AIC       BIC    logLik -2*log(L)  df.resid 
   7264.6    7324.6   -3621.3    7242.6      1718 

Random effects:

Conditional model:
 Groups   Name                         Variance Std.Dev. Corr               
 Site     (Intercept)                  1.022    1.011                       
 Species  (Intercept)                  1.022    1.011                       
          sAltitude                    1.022    1.011    0.01               
 dummy    (Intercept)                  2.394    1.547                       
          SpeciesAchillea_clavennae    2.394    1.547    0.00               
          SpeciesAchillea_millefolium  2.394    1.547    0.20 0.00          
          SpeciesAcinos_alpinus        2.394    1.547    0.28 0.00 0.20     
          SpeciesAconitum_tauricum     2.394    1.547    0.31 0.00 0.20 0.28
          SpeciesAdenostyles_alliariae 2.394    1.547    0.00 0.93 0.00 0.00
          SpeciesAgrostis_alpina       2.394    1.547    0.20 0.00 0.34 0.20
          SpeciesAgrostis_capillaris   2.394    1.547    0.31 0.00 0.20 0.28
          SpeciesAjuga_reptans         2.394    1.547    0.31 0.00 0.20 0.28
          SpeciesAlchemilla_vulgaris   2.394    1.547    0.20 0.00 0.26 0.20
          ...                          ...      ...      ...  ...  ...  ... 
 Residual                                 NA       NA                       
                             
                             
                             
                             
                             
                             
                             
                             
                             
 0.00                        
 0.20 0.00                   
 0.81 0.00 0.20              
 0.85 0.00 0.20 0.81         
 0.20 0.00 0.26 0.20 0.20    
 ...  ...  ...  ...  ...  ...
                             
Number of obs: 1729, groups:  Site, 17; Species, 152; dummy, 1

Conditional model:
            Estimate Std. Error z value Pr(>|z|)   
(Intercept)   1.6633     1.3864   1.200  0.23025   
sSeedMass    -0.3205     0.1166  -2.748  0.00599 **
sSeedShape   -0.3606     0.1331  -2.709  0.00674 **
sSeedN       -0.2142     0.1144  -1.872  0.06118 . 
sAltitude    -0.6441     0.3159  -2.039  0.04143 * 
sHum          0.1031     0.3041   0.339  0.73456   
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Dispersion model:
             Estimate Std. Error z value Pr(>|z|)    
(Intercept)  0.519708   0.018722  27.759  < 2e-16 ***
sAltitude   -0.120838   0.024140  -5.006 5.57e-07 ***
sHum        -0.009928   0.026301  -0.377    0.706    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
simulateResiduals(model5, plot = TRUE, simulateREs = "unconditional") # Unconditional simulations

Object of Class DHARMa with simulated residuals based on 250 simulations with refit = FALSE . See ?DHARMa::simulateResiduals for help. 
 
Scaled residual values: 0.836 0.808 0.916 0.956 0.848 0.852 0.252 0.296 0.264 0.256 0.248 0.264 0.24 0.244 0.232 0.184 0.216 0.288 0.796 0.796 ...

SBPA (GLMM)

We use the data prepared and scaled in the SBDensity solution (run that one first):

model1 = glmer(SBPA~
                sSeedMass + sSeedShape + sSeedN + 
                 sAltitude + sHum +
                (1|Site) + (sAltitude|Species),
              data = data, family = binomial())
Warning in checkConv(attr(opt, "derivs"), opt$par, ctrl = control$checkConv, : Model failed to converge with max|grad| = 0.00691219 (tol = 0.002, component 1)
  See ?lme4::convergence and ?lme4::troubleshooting.
summary(model1)
Generalized linear mixed model fit by maximum likelihood (Laplace
  Approximation) [glmerMod]
 Family: binomial  ( logit )
Formula: SBPA ~ sSeedMass + sSeedShape + sSeedN + sAltitude + sHum + (1 |  
    Site) + (sAltitude | Species)
   Data: data

      AIC       BIC    logLik -2*log(L)  df.resid 
   1368.9    1423.4    -674.4    1348.9      1719 

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-2.5936 -0.2658 -0.1017  0.2001  3.3347 

Random effects:
 Groups  Name        Variance Std.Dev. Corr  
 Species (Intercept) 8.16725  2.8578         
         sAltitude   4.38865  2.0949   -0.13 
 Site    (Intercept) 0.06798  0.2607         
Number of obs: 1729, groups:  Species, 152; Site, 17

Fixed effects:
            Estimate Std. Error z value Pr(>|z|)    
(Intercept)  -2.6285     0.4066  -6.465 1.01e-10 ***
sSeedMass    -1.3670     0.6431  -2.126   0.0335 *  
sSeedShape   -0.5813     0.3100  -1.875   0.0608 .  
sSeedN       -2.1359     1.3165  -1.622   0.1047    
sAltitude    -1.3935     0.3364  -4.142 3.44e-05 ***
sHum         -0.1025     0.1308  -0.784   0.4330    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
           (Intr) sSdMss sSdShp sSeedN sAlttd
sSeedMass   0.221                            
sSeedShape  0.033  0.062                     
sSeedN      0.336  0.060 -0.066              
sAltitude   0.132  0.142  0.079  0.012       
sHum        0.023  0.002 -0.003 -0.011  0.229
optimizer (Nelder_Mead) convergence code: 0 (OK)
Model failed to converge with max|grad| = 0.00691219 (tol = 0.002, component 1)
  See ?lme4::convergence and ?lme4::troubleshooting.

Model did not converge, but there is a trick which often helps (see Section 4.4.1). The default optimizer in lme4 is not the best optimizer, changing it to ‘bobyqa’ often helps with convergence issues

model1 = glmer(SBPA~
                sSeedMass + sSeedShape + sSeedN + 
                sAltitude + sHum +
                (1|Site) + (sAltitude|Species),
              data = data, family = binomial(),
              control = glmerControl(optimizer = "bobyqa"))
summary(model1)
Generalized linear mixed model fit by maximum likelihood (Laplace
  Approximation) [glmerMod]
 Family: binomial  ( logit )
Formula: SBPA ~ sSeedMass + sSeedShape + sSeedN + sAltitude + sHum + (1 |  
    Site) + (sAltitude | Species)
   Data: data
Control: glmerControl(optimizer = "bobyqa")

      AIC       BIC    logLik -2*log(L)  df.resid 
   1368.9    1423.4    -674.4    1348.9      1719 

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-2.5938 -0.2658 -0.1018  0.2001  3.3353 

Random effects:
 Groups  Name        Variance Std.Dev. Corr  
 Species (Intercept) 8.16321  2.8571         
         sAltitude   4.38183  2.0933   -0.13 
 Site    (Intercept) 0.06786  0.2605         
Number of obs: 1729, groups:  Species, 152; Site, 17

Fixed effects:
            Estimate Std. Error z value Pr(>|z|)    
(Intercept)  -2.6284     0.4066  -6.465 1.01e-10 ***
sSeedMass    -1.3693     0.6437  -2.127   0.0334 *  
sSeedShape   -0.5816     0.3100  -1.876   0.0606 .  
sSeedN       -2.1419     1.3182  -1.625   0.1042    
sAltitude    -1.3920     0.3362  -4.141 3.46e-05 ***
sHum         -0.1022     0.1307  -0.781   0.4345    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
           (Intr) sSdMss sSdShp sSeedN sAlttd
sSeedMass   0.222                            
sSeedShape  0.033  0.062                     
sSeedN      0.337  0.061 -0.066              
sAltitude   0.132  0.142  0.079  0.012       
sHum        0.023  0.002 -0.003 -0.011  0.229

Success, it converged! Note that silencing the warning by switching the optimizer is no evidence that the estimates are good: check that estimates agree across optimizers (allFit(model1)) and that the fit is not singular (isSingular(model1)).

Residual checks:

Check residuals:

res = simulateResiduals(model1, plot=TRUE)

Residuals look good!

Bonus: With phylogenetic correlation structure:

(This bonus part uses phyl.upd2 and the function as.theta.vcov() from the SBDensity bonus solution above, so run that one first.)

correlation_matrix = vcv(phyl.upd2)[unique(data$Species), unique(data$Species)]

corrs = as.theta.vcov(correlation_matrix, corrs.only=TRUE)

data$dummy = factor(rep(0, nrow(data)))
nsp = length(unique(data$Species))
model6 = glmmTMB(SBPA~
                sSeedMass + sSeedShape + sSeedN + 
                sAltitude + sHum +
                (1|Site) + (sAltitude|Species) +
                (1+Species|dummy),
              map=list(theta=factor(c(rep(0, 4), rep(1,nsp),rep(NA,length(corrs))) )),
              start=list(theta=c(rep(0, 4), rep(0,nsp),corrs)),
              family = binomial,
              data = data)
simulateResiduals(model6, plot = TRUE)

Object of Class DHARMa with simulated residuals based on 250 simulations with refit = FALSE . See ?DHARMa::simulateResiduals for help. 
 
Scaled residual values: 0.5748496 0.5526574 0.05684348 0.6166831 0.1098403 0.2653392 0.2043353 0.531952 0.3925722 0.2693407 0.07212984 0.2086342 0.4148425 0.5437874 0.5376287 0.2171818 0.7868062 0.3029527 0.5870516 0.7049796 ...

Conditional simulations:

pred = predict(model6, re.form = NULL, type = "response")
simulations = sapply(1:1000, function(i) rbinom(length(pred),1, pred))
res = createDHARMa(simulations, model.frame(model6)[,1], pred)
plot(res)

C.6 Snouter

CautionTasks

Fit one of the (continuous) responses in the snouter dataset, e.g. snouter1.1, against the predictors rain + djungle (see ?snouter); the coordinates are X and Y. Check for spatial autocorrelation and proceed to fitting a spatial model if needed (Section 11.3). See the data set’s help for details on the variables.

library(EcoData)
str(snouter)

Fit a base model, then check its residuals for spatial autocorrelation. We use snouter1.1 (a continuous response) here; the coordinates in this dataset are X and Y:

library(EcoData); library(glmmTMB); library(DHARMa)
fit = glmmTMB(snouter1.1 ~ rain + djungle, data = snouter)
res = simulateResiduals(fit); plot(res)

testSpatialAutocorrelation(res, x = snouter$X, y = snouter$Y)


    DHARMa Moran's I test for distance-based autocorrelation

data:  res
observed = 0.12556787, expected = -0.00090334, sd = 0.00141608, p-value
< 2.2e-16
alternative hypothesis: Distance-based autocorrelation

If the spatial test is significant, we first de-trend with a 2D spline (de-trend before adding a correlation structure, Section 11.1.1):

library(mgcv)
fit2 = gam(snouter1.1 ~ rain + djungle + te(X, Y), data = snouter)
res2 = simulateResiduals(fit2)
testSpatialAutocorrelation(res2, x = snouter$X, y = snouter$Y)


    DHARMa Moran's I test for distance-based autocorrelation

data:  res2
observed = 0.03162372, expected = -0.00090334, sd = 0.00141607, p-value
< 2.2e-16
alternative hypothesis: Distance-based autocorrelation

This changes residuals somewhat, but there is still significant spatial autocorrelation left. Thus, we have to move to a spatial model.

# TODO 

C.7 Volcano Island

CautionTasks

Analyze ?volcanoisland in the EcoData package. Perform an appropriate causal statistical analysis (see Chapter 7; including residual checks and everything) to understand the predictors that determine the value of

  • windObs (numeric)
  • lizards (1/0)
  • beetles2 (counts)
  • survivedOf20 (k out of 20)

There are various predictors, including plot, year, x, y that can be considered. Assume that all the issues that we talked about (random effects, Chapter 4; overdispersion, Section 9.3.2; zero-inflation, Section 9.3.3; spatial autocorrelation, Section 11.3) could appear in this data.

Hint: it is possible that the possibility to observe a species depends on wind, and it is possible that lizards eat beetles, so you can also consider these variables (or predictions for these variables) as predictors of the others.

Hint 2: binomial k/n data in R is specified as a response that is cbind(k, n-k), so you would write glm(cbind(survivedOf20, 20 - survivedOf20) ~ predictor, data = data, family = “binomial”)

Hint 3: lizards may be present but not observed (imperfect detection, see Hint 1). Occupancy models, which separate the true presence from the detection process, are introduced with the same data set in the chapter on occupancy models of Hartig (2024).

Hartig, F. (2024) “Introduction to Bayesian statistics with R.” Available at: https://florianhartig.github.io/LearningBayes/.

Base model

This is an open, integrative exercise, so the following is not a fully worked-out solution but a base model to start from for one of the responses (beetle counts). The other responses (windObs -> Gaussian / gls, lizards -> binomial, survivedOf20 -> k/n binomial) follow exactly the same recipe.

library(EcoData); library(glmmTMB); library(DHARMa)
d = volcanoisland

# beetles: counts -> start with Poisson and plot as RE, then diagnose
m0 = glmmTMB(beetles2 ~ windObs + lizards + (1|plot), family = poisson, data = d)
res = simulateResiduals(m0); plot(res)

testDispersion(res); testZeroInflation(res)


    DHARMa nonparametric dispersion test via sd of residuals fitted vs.
    simulated

data:  simulationOutput
dispersion = 2.6752, p-value < 2.2e-16
alternative hypothesis: two.sided


    DHARMa zero-inflation test via comparison to expected zeros with
    simulation under H0 = fitted model

data:  simulationOutput
ratioObsSim = 1.1159, p-value < 2.2e-16
alternative hypothesis: two.sided
# if over-dispersed / zero-inflated -> negative binomial + ZI term
m1 = glmmTMB(beetles2 ~ windObs + lizards + (1|plot),
             family = nbinom2, ziformula = ~1, data = d)

# then check for residual spatial autocorrelation on the coordinates
res1 = simulateResiduals(m1)
grp  = recalculateResiduals(res1, group = d$plot)
testSpatialAutocorrelation(grp,
      x = aggregate(x ~ plot, d, mean)$x,
      y = aggregate(y ~ plot, d, mean)$y)


    DHARMa Moran's I test for distance-based autocorrelation

data:  grp
observed = 0.205367, expected = -0.010101, sd = 0.017216, p-value <
2.2e-16
alternative hypothesis: Distance-based autocorrelation

Work through the diagnostic ladder (Table B.10) in order - functional form -> distribution -> dispersion -> zero-inflation -> correlation - adding structure only when a residual check demands it. Note the hint that lizards (predators) may belong in the beetle model, and that detectability may depend on windObs.