This document contains the statistical analyses accompanying the paper “Detection probabilities for sessile organisms” by Berberich et al (2016). It presents the R-code and results for full reproducibility.
The analysis is carried out in three parts:
We have three data sets: plots, nests, and sizes. plots contains the misidentification-corrected recorded nests (\(N^{obs}_{i, s}\)) for each of the 8 observers (in columns: O1 to O8) for each of the 16 plots (in rows: Plot 1 to Plot 16). As additional columns it contains the total number of different nests recorded at each plot, which is our lower bound (\(N^{obs}_s\)) for the true number of nests at each plot (\(\hat{N}_s\)).
plots <- read.csv("nestPlots.csv", row.names = 1)
The second data set, nests, is a long version of plots, in that it contains for each observer the information which nests he/she has detected (actual confirmed nests only).
nests <- read.csv("nestRecords.csv", row.names = 1)
The third data set contains the nest sizes estimated roughly from the photographs (height, in cm, along with the variables diameters, locations and forest setting).
sizes <- read.csv("nestSizes.csv", row.names = 1) # read file in again to get all nest sizes
sizes$Height[which(sizes$Height > 100)] <- 100 # moves 1 nest to smaller size
sizes$Diameter[which(sizes$Diameter > 200)] <- 200 # moves 3 nests
nestSize <- sizes[, 3]
Across all observers, nest size (height or diameter) or landscape setting (location, forest type) may affect detection. Here we use a GLM to find out. First, for each nest we compute how many observers detected it. Then we relate this proportion to nest size, etc.
# join tables (sorted in the same way):
detnetsize <- cbind.data.frame(rowSums(nests), 8 - rowSums(nests),
sizes)
summary(fmHeight <- glm(as.matrix(detnetsize[, 1:2]) ~ poly(Height,
2), family = quasibinomial, data = detnetsize))
Call:
glm(formula = as.matrix(detnetsize[, 1:2]) ~ poly(Height, 2),
family = quasibinomial, data = detnetsize)
Deviance Residuals:
Min 1Q Median 3Q Max
-3.1474 -1.4109 -0.1274 1.4317 4.1385
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) 0.01676 0.10023 0.167 0.86744
poly(Height, 2)1 6.73074 1.21899 5.522 1.52e-07 ***
poly(Height, 2)2 -3.77773 1.21330 -3.114 0.00223 **
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
(Dispersion parameter for quasibinomial family taken to be 2.673435)
Null deviance: 556.86 on 146 degrees of freedom
Residual deviance: 443.53 on 144 degrees of freedom
AIC: NA
Number of Fisher Scoring iterations: 4
predsHeight <- predict(fmHeight, newdata = data.frame(Height = 10:100),
se.fit = T)
summary(fmDiameter <- glm(as.matrix(detnetsize[, 1:2]) ~ poly(Diameter,
2), family = quasibinomial, data = detnetsize))
Call:
glm(formula = as.matrix(detnetsize[, 1:2]) ~ poly(Diameter, 2),
family = quasibinomial, data = detnetsize)
Deviance Residuals:
Min 1Q Median 3Q Max
-3.5483 -1.1682 -0.1953 1.4989 3.7833
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) 0.03257 0.10276 0.317 0.752
poly(Diameter, 2)1 6.66866 1.37642 4.845 3.24e-06 ***
poly(Diameter, 2)2 -0.26763 1.36859 -0.196 0.845
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
(Dispersion parameter for quasibinomial family taken to be 2.865521)
Null deviance: 556.86 on 146 degrees of freedom
Residual deviance: 477.70 on 144 degrees of freedom
AIC: NA
Number of Fisher Scoring iterations: 4
predsDiameter <- predict(fmDiameter, newdata = data.frame(Diameter = 50:200),
se.fit = T)
with(detnetsize, cor(Height, Diameter))
[1] 0.7910949
We can use a bubble plot to visualise this.
# pdf('Fig2-2panel.pdf', width=6, height=3)
par(mfrow = c(1, 2), mar = c(4, 4, 1, 1), tcl = -0.125, mgp = c(1.25,
0.25, 0))
cex.vec <- as.vector(table(rowSums(nests), sizes$Height))
x.vec <- rep(c(10, 50, 100), each = 8)
y.vec <- rep(seq(0.125, 1, by = 0.125), times = 3)
plot(x.vec, y.vec, las = 1, ylab = "proportion of observers finding this nest",
xlab = "height of nest [cm]", xlim = c(0, 150), pch = 16, cex = cex.vec/5,
axes = F, cex.lab = 0.75, font = 2)
legend("bottomright", bty = "n", pch = 16, pt.cex = sort(unique(cex.vec/5))[-1],
legend = paste(" ", sort(unique(cex.vec))[-1]), col = "darkgrey",
cex = 1)
axis(side = 1, at = c(10, 50, 100), cex.axis = 0.7, labels = c("10",
"50", "100+"))
axis(side = 2, las = 1, cex.axis = 0.7)
box()
lines(10:100, plogis(predsHeight$fit), lwd = 1, col = "grey")
lines(10:100, plogis(predsHeight$fit + 2 * predsHeight$se.fit), lwd = 1,
lty = 2, col = "grey")
lines(10:100, plogis(predsHeight$fit - 2 * predsHeight$se.fit), lwd = 1,
lty = 2, col = "grey")
# same for diameter:
par(mar = c(4, 2, 1, 3))
cex.vec1 <- as.vector(table(rowSums(nests), detnetsize$Diameter))
x.vec1 <- rep(c(50, 100, 150, 200), each = 8)
y.vec1 <- rep(seq(0.125, 1, by = 0.125), times = 4)
plot(x.vec1, y.vec1, las = 1, ylab = "", xlab = "Diameter of nest (cm)",
pch = 16, cex = cex.vec1/5, axes = F, xlim = c(45, 250), ylim = c(0.1,
1), cex.lab = 0.75, font = 2)
legend("bottomright", bty = "n", pch = 16, col = "darkgrey", pt.cex = sort(unique(cex.vec1/5))[-1],
legend = paste(" ", sort(unique(cex.vec1))[-1]), cex = 1)
axis(side = 1, at = c(50, 100, 150, 200), labels = c("50", "100",
"150", "200+"), cex.axis = 0.7)
axis(side = 2, las = 1, cex.axis = 0.7)
box()
lines(50:200, plogis(predsDiameter$fit), lwd = 1, col = "grey")
lines(50:200, plogis(predsDiameter$fit + 2 * predsDiameter$se.fit),
lwd = 1, lty = 2, col = "grey")
lines(50:200, plogis(predsDiameter$fit - 2 * predsDiameter$se.fit),
lwd = 1, lty = 2, col = "grey")
# dev.off()
Symbol size is proportional to the number of nests of that combination of size and numbers of observers that discovered it.
Since diameter and height are highly correlated, and size is the better predictor, we shall henceforth only use height to represent size.
All but three nests were recorded in spruce forest (one in pine, one in beech), and hence we would not expect to be able to detect effects of forest type. Similarly, location has several levels (13), but 95/147 data points are from fully surrounded by forest, rather than moss, thistle etc.
anova(glm(as.matrix(detnetsize[, 1:2]) ~ Forest, family = quasibinomial,
data = detnetsize), test = "F")
Analysis of Deviance Table
Model: quasibinomial, link: logit
Response: as.matrix(detnetsize[, 1:2])
Terms added sequentially (first to last)
Df Deviance Resid. Df Resid. Dev F Pr(>F)
NULL 138 530.50
Forest 2 7.509 136 522.99 1.1827 0.3096
anova(glm(as.matrix(detnetsize[, 1:2]) ~ Location, family = quasibinomial,
data = detnetsize), test = "F")
Analysis of Deviance Table
Model: quasibinomial, link: logit
Response: as.matrix(detnetsize[, 1:2])
Terms added sequentially (first to last)
Df Deviance Resid. Df Resid. Dev F Pr(>F)
NULL 146 556.86
Location 12 56.021 134 500.84 1.5007 0.1312
Neither location nor forest type adds significantly to explaining variation in detection, and both are hence omitted from further analyses.
For each nest, the probability of observing it depends on (a) the detection rate of the observer, (b) the size of the nest, and (c) plot characterisitcs. As shown in the last section, we have not recorded any useful measures of plot characateristics, so we leave out point (c) here.
We try two different models: fmm1 with an observer-specific detection curve, and fmm2 with the same detection curve for all observers, but an observer-specific intercept. The latter model will use fewer degrees of freedom. As this turns out to be the more appropriate model for our data, we plot these results.
# reformat data for analysis: all observers underneath each other:
part1 <- stack(nests)
colnames(part1) <- c("detected", "observer")
part2 <- do.call("rbind", replicate(8, sizes, simplify = FALSE))
dats <- cbind(part1, part2)
# head(dats)
library(lme4)
# fit a model with variable effect of nest height for each
# observer:
summary(fmm1 <- glmer(detected ~ (poly(Height, 2) | observer), family = binomial,
data = dats))
Generalized linear mixed model fit by maximum likelihood
(Laplace Approximation) [glmerMod]
Family: binomial ( logit )
Formula: detected ~ (poly(Height, 2) | observer)
Data: dats
AIC BIC logLik deviance df.resid
1525.7 1561.2 -755.8 1511.7 1169
Scaled residuals:
Min 1Q Median 3Q Max
-1.6253 -0.7663 0.6153 0.7934 1.8713
Random effects:
Groups Name Variance Std.Dev. Corr
observer (Intercept) 1.212 1.101
poly(Height, 2)1 379.454 19.480 -0.99
poly(Height, 2)2 129.448 11.378 1.00 -0.99
Number of obs: 1176, groups: observer, 8
Fixed effects:
Estimate Std. Error z value Pr(>|z|)
(Intercept) 1.028 0.480 2.14 0.0323 *
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
summary(fmm2 <- glmer(detected ~ poly(Height, 2) + (1 | observer),
family = binomial, data = dats))
Generalized linear mixed model fit by maximum likelihood
(Laplace Approximation) [glmerMod]
Family: binomial ( logit )
Formula: detected ~ poly(Height, 2) + (1 | observer)
Data: dats
AIC BIC logLik deviance df.resid
1498.2 1518.5 -745.1 1490.2 1172
Scaled residuals:
Min 1Q Median 3Q Max
-1.9525 -0.7399 0.5122 0.8212 1.7025
Random effects:
Groups Name Variance Std.Dev.
observer (Intercept) 0.1622 0.4028
Number of obs: 1176, groups: observer, 8
Fixed effects:
Estimate Std. Error z value Pr(>|z|)
(Intercept) 0.01972 0.15548 0.127 0.899
poly(Height, 2)1 19.76902 2.17022 9.109 < 2e-16 ***
poly(Height, 2)2 -11.09957 2.14672 -5.170 2.34e-07 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Correlation of Fixed Effects:
(Intr) p(H,2)1
ply(Hgh,2)1 0.000
ply(Hgh,2)2 0.003 -0.004
plot(10:100, plogis(predict(fmm2, newdata = data.frame(Height = 10:100,
observer = "O1"))), type = "l", las = 1, xlim = c(10, 100), ylim = c(0.2,
0.9), ylab = "detection rate", xlab = "nest height [cm]", axes = F)
axis(1, las = 1, at = c(10, 50, 100), labels = c("10", "50", "100+"))
axis(2, las = 1)
box()
for (j in 2:8) {
lines(10:100, plogis(predict(fmm2, newdata = data.frame(Height = 10:100,
observer = paste0("O", j)))), type = "l", las = 1)
}
All the analyses above are fine for analysing the correlation between detection of nests and various attributes, but they do not tell us anything about how many nests we have not seen. To answer that question, we turn to a very different approach. The above analyses have been useful, however, in guiding us which covariates to include in the following step.
Our data provide the following challenges:
Typical patch-occupancy data assume constant detection rates (“repeated within-season visits”) and focus on detection and occurrence of the (typically) animal at each plot. Instead, we want to estimate how many nests were not recorded at all. As ant nests (similar to trees, but in contrast to animals) don’t move, we can safely assume that occurrence (psi) is 1 if any observer has observed a nest.
We can handle the “overlooked nests”-issue by adding NA-records to our data set nests, which are then guessed (estimated) during the modelling procedure. This is called “data-augmentation”, which feels a bit like Bayesian magic, but isn’t. What the model does is to estimate for \(N^{aug}\) nests which were not observed, how likely it is that they are there, but were not observed. This can be achieved by realising that also the unobserved nests (and their sizes) are drawn from the same data model that we fit to the observed data. The main tuning parameter on top of a simpler patch-occupancy model is the number of nests we assume to be missing. (In the specific case, we shall assume \(N^{aug}=50\) overlooked nests, but the results do not change if we assume 20 or 200 instead.)
library(R2jags) # load access to JAGS
# augment the matrix with some unobserved nests:
Nunobserved <- 50
augnests <- rbind(as.matrix(nests), matrix(0, Nunobserved, 8))
jags.data <- list(Y = augnests, N = NROW(augnests), J = NCOL(augnests),
nestsize = c(ifelse(sizes$Height < 70, 0, 1), rep(NA, Nunobserved))) # categorise nest size into small (0) and large (1)
augAnalysis <- function() {
# the classical patch-occupancy model: loop through nests,
# observed plus augmented
for (i in 1:N) {
w[i] ~ dbern(omega) # realised nest probability
nestsize[i] ~ dbern(probnestsize) # either nest size 0 (small) or 1 (large)
for (j in 1:J) {
# loop through observers
Y[i, j] ~ dbern(P[i, j] * w[i]) # compute detection based on the members in the set and the guesses for the unobserved nests
logit(P[i, j]) <- detectrate[j] + betasize * nestsize[i] # nestsize effect on detection
}
}
# Priors and constraints:
for (j in 1:J) {
detectrate[j] ~ dnorm(0, 0.01) # flat but informative prior centred on p=0.5
# (note: this is at logit-scale, thus mu=0 -> p=0.5);
# curve(plogis(dnorm(x, 0, 10)), -20, 20)
}
omega ~ dunif(0, 1)
probnestsize ~ dbeta(1, 1)
betasize ~ dnorm(0, 0.01)
# derived parameters:
Ntruelythere <- sum(w) # number of nests across all plots
for (j in 1:J) {
# back-transformed detection rate per observer
detectionRateRealScale[j] <- exp(detectrate[j])/(1 + exp(detectrate[j]))
}
} # end of function
# inits<-function() list (w=c(rep(1, NROW(inventedData)), rep(0,
# Nunobserved)), betasize=rnorm(1),
# detectrate=rnorm(n=NCOL(inventedAugnests),1))
parms <- c("omega", "Ntruelythere", "detectionRateRealScale", "betasize",
"probnestsize")
ni <- 2000
nb <- ni/2
nc <- 3
nt <- 3 # 8000 will do for final estimation!
inits <- function() list(w = c(rep(1, NROW(nests)), rep(0, Nunobserved)),
betasize = rnorm(1), detectrate = rnorm(n = NCOL(augnests), 1))
# call JAGS
system.time(augJags <- jags(jags.data, inits, parms, model.file = augAnalysis,
n.chains = nc, n.thin = nt, n.iter = ni, n.burnin = nb, working.directory = getwd()))
module glm loaded
Compiling model graph
Resolving undeclared variables
Allocating nodes
Graph information:
Observed stochastic nodes: 1723
Unobserved stochastic nodes: 258
Total graph size: 4507
Initializing model
user system elapsed
10.375 0.050 10.501
plot(augJags)
augJags
Inference for Bugs model at "/var/folders/cc/3jfhfx190rb2ptxnqrqxj94m0000gp/T//Rtmp90xMhM/model293d6724c104.txt", fit using jags,
3 chains, each with 2000 iterations (first 1000 discarded), n.thin = 3
n.sims = 1002 iterations saved
mu.vect sd.vect 2.5% 25%
Ntruelythere 147.647 0.808 147.000 147.000
betasize 0.815 0.168 0.474 0.710
detectionRateRealScale[1] 0.365 0.041 0.287 0.337
detectionRateRealScale[2] 0.381 0.040 0.306 0.353
detectionRateRealScale[3] 0.434 0.041 0.358 0.406
detectionRateRealScale[4] 0.405 0.041 0.330 0.376
detectionRateRealScale[5] 0.489 0.042 0.403 0.464
detectionRateRealScale[6] 0.427 0.041 0.344 0.399
detectionRateRealScale[7] 0.618 0.042 0.536 0.589
detectionRateRealScale[8] 0.642 0.041 0.563 0.615
omega 0.747 0.031 0.683 0.726
probnestsize 0.174 0.029 0.120 0.153
deviance 1711.903 9.567 1699.116 1704.447
50% 75% 97.5% Rhat n.eff
Ntruelythere 147.000 148.000 150.000 1.004 1000
betasize 0.815 0.920 1.142 1.000 1000
detectionRateRealScale[1] 0.365 0.391 0.446 1.001 1000
detectionRateRealScale[2] 0.380 0.407 0.460 1.001 1000
detectionRateRealScale[3] 0.433 0.462 0.514 1.003 1000
detectionRateRealScale[4] 0.404 0.433 0.487 1.000 1000
detectionRateRealScale[5] 0.488 0.516 0.570 1.002 900
detectionRateRealScale[6] 0.427 0.455 0.505 1.000 1000
detectionRateRealScale[7] 0.619 0.647 0.698 1.001 1000
detectionRateRealScale[8] 0.642 0.669 0.722 1.002 940
omega 0.748 0.770 0.805 1.001 1000
probnestsize 0.172 0.193 0.239 1.000 1000
deviance 1710.874 1717.196 1734.277 1.001 1000
For each parameter, n.eff is a crude measure of effective sample size,
and Rhat is the potential scale reduction factor (at convergence, Rhat=1).
DIC info (using the rule, pD = var(deviance)/2)
pD = 45.8 and DIC = 1757.7
DIC is an estimate of expected predictive error (lower deviance is better).
The results show that large nests have a higher chance of being detected (notice the estimate for betasize of 0.819 at the link scale, representing the effect of going from small to large nests). Furthermore, we get per-observer observation estimates (at the real scale) between 0.365 and 0.642, i.e. almost a factor of 2. And, finally, we get an estimate of the total number of nests across all plots as 147.7 (95%-confidence interval up to 150), i.e. 1 to 3 nests overlooked.
Since we do not have any covariates at the plot level, we can distribute the overlooked nests across plots proportional to the number of nests observed there.
quants <- quantile(augJags$BUGSoutput$sims.list$Ntruelythere - 147,
c(0.025, 0.5, 0.975))
# So the number of nests per plot are:
estimated <- matrix(plots$Nmin, ncol = 3, nrow = 16, byrow = F) +
matrix(quants, ncol = 3, nrow = 16, byrow = T)/matrix(plots$Nmin,
ncol = 3, nrow = 16, byrow = F)
# in line with reason, but against maths, we assume for plot 4
# that 0/0=0, and get:
estimated[4, ] <- 0
colnames(estimated) <- c("lower CI", "median", "upper CI")
round(estimated, 2)
lower CI median upper CI
[1,] 24 24 24.12
[2,] 17 17 17.18
[3,] 8 8 8.38
[4,] 0 0 0.00
[5,] 18 18 18.17
[6,] 3 3 4.00
[7,] 1 1 4.00
[8,] 4 4 4.75
[9,] 10 10 10.30
[10,] 20 20 20.15
[11,] 1 1 4.00
[12,] 11 11 11.27
[13,] 4 4 4.75
[14,] 3 3 4.00
[15,] 12 12 12.25
[16,] 11 11 11.27
Essentially this indicates that with our eight observers, we have good faith of not having overlooked any nest!
One advantage of the above data-augmentation approach is that it allows us to model each nest separately and thereby include a covariate for the nest. If we omit the effect of nest size, the results are as follows:
augAnalysis2 <- function() {
# the classical patch-occupancy model: loop through nests,
# observed plus augmented
for (i in 1:N) {
w[i] ~ dbern(omega) # realised nest probability
for (j in 1:J) {
# loop through observers
Y[i, j] ~ dbern(P[i, j] * w[i]) # compute detection based on the members in the set and the guesses for the unobserved nests
logit(P[i, j]) <- detectrate[j] #+ betasize*nestsize[i] # no nestsize effect on detection
}
}
# Priors and constraints:
for (j in 1:J) {
detectrate[j] ~ dnorm(0, 0.01) # flat but informative prior centred on p=0.5
# (note: this is at logit-scale, thus mu=0 -> p=0.5);
# curve(plogis(dnorm(x, 0, 10)), -20, 20)
}
omega ~ dunif(0, 1)
# derived parameters:
Ntruelythere <- sum(w) # number of nests across all plots
for (j in 1:J) {
# back-transformed detection rate per observer
detectionRateRealScale[j] <- exp(detectrate[j])/(1 + exp(detectrate[j]))
}
} # end of function
# inits<-function() list (w=c(rep(1, NROW(inventedData)), rep(0,
# Nunobserved)), betasize=rnorm(1),
# detectrate=rnorm(n=NCOL(inventedAugnests),1))
parms <- c("omega", "Ntruelythere", "detectionRateRealScale")
inits <- function() list(w = c(rep(1, NROW(nests)), rep(0, Nunobserved)),
detectrate = rnorm(n = NCOL(augnests), 1))
# call JAGS
system.time(augJags2 <- jags(jags.data, inits, parms, model.file = augAnalysis2,
n.chains = nc, n.thin = nt, n.iter = ni, n.burnin = nb, working.directory = getwd()))
Warning in jags.model(model.file, data = data, inits =
init.values, n.chains = n.chains, : Unused variable "nestsize" in
data
Compiling model graph
Resolving undeclared variables
Allocating nodes
Graph information:
Observed stochastic nodes: 1576
Unobserved stochastic nodes: 206
Total graph size: 3428
Initializing model
user system elapsed
4.161 0.025 4.225
plot(augJags2)
augJags2
Inference for Bugs model at "/var/folders/cc/3jfhfx190rb2ptxnqrqxj94m0000gp/T//Rtmp90xMhM/model293d67a1defb.txt", fit using jags,
3 chains, each with 2000 iterations (first 1000 discarded), n.thin = 3
n.sims = 1002 iterations saved
mu.vect sd.vect 2.5% 25%
Ntruelythere 147.481 0.698 147.000 147.000
detectionRateRealScale[1] 0.398 0.039 0.322 0.372
detectionRateRealScale[2] 0.414 0.041 0.334 0.386
detectionRateRealScale[3] 0.468 0.041 0.386 0.439
detectionRateRealScale[4] 0.443 0.040 0.365 0.417
detectionRateRealScale[5] 0.521 0.041 0.440 0.494
detectionRateRealScale[6] 0.463 0.040 0.388 0.437
detectionRateRealScale[7] 0.642 0.040 0.560 0.618
detectionRateRealScale[8] 0.670 0.040 0.588 0.643
omega 0.746 0.031 0.685 0.727
deviance 1599.249 8.921 1588.160 1592.142
50% 75% 97.5% Rhat n.eff
Ntruelythere 147.000 148.000 149.000 1.002 1000
detectionRateRealScale[1] 0.397 0.424 0.478 1.001 1000
detectionRateRealScale[2] 0.413 0.441 0.493 1.003 700
detectionRateRealScale[3] 0.467 0.498 0.546 1.000 1000
detectionRateRealScale[4] 0.442 0.469 0.523 1.003 770
detectionRateRealScale[5] 0.521 0.548 0.597 1.002 760
detectionRateRealScale[6] 0.462 0.492 0.539 1.001 1000
detectionRateRealScale[7] 0.643 0.669 0.721 1.000 1000
detectionRateRealScale[8] 0.668 0.698 0.743 1.003 640
omega 0.748 0.766 0.805 1.001 1000
deviance 1596.627 1604.864 1619.851 1.002 1000
For each parameter, n.eff is a crude measure of effective sample size,
and Rhat is the potential scale reduction factor (at convergence, Rhat=1).
DIC info (using the rule, pD = var(deviance)/2)
pD = 39.8 and DIC = 1639.1
DIC is an estimate of expected predictive error (lower deviance is better).
That means that detection probabilities for the observers are slightly higher across the board, but the estimated number of nests hardly changes. In other words, while nest size affects detection probability, it does not greatly bias estimates of the total number of nests.
One disadvantage of the above analysis is that we essentially treat the data as if they were from one large plot, rather than from 16 different plots. If we wanted to estimate abundance of red wood ant nests at each plot, rather than across all plots, we have to employ an abundance model. The approach of Royle (2004: N-mixture models for estimating population size from spatially replicated counts. Biometrics 60:108) is in principle suitable for such data, except that in his case the observation matrix is very sparse (has many \(0\)s), while ours is dense (\(0\)s only in few observer-plot combinations):
plots
O1 O2 O3 O4 O5 O6 O7 O8 Nmin
Plot 1 12 14 12 9 10 11 12 13 24
Plot 2 1 5 5 7 7 7 9 12 17
Plot 3 4 5 2 5 4 5 7 6 8
Plot 4 0 0 0 0 0 0 0 0 0
Plot 5 9 7 7 6 13 8 14 14 18
Plot 6 0 0 0 0 0 0 3 2 3
Plot 7 0 0 1 0 0 0 1 0 1
Plot 8 2 2 2 3 4 2 3 3 4
Plot 9 3 3 3 4 3 5 5 7 10
Plot 10 6 5 10 8 9 8 9 10 20
Plot 11 0 0 0 1 0 0 0 0 1
Plot 12 7 6 9 7 9 6 10 8 11
Plot 13 1 1 0 1 1 3 3 3 4
Plot 14 2 2 1 2 2 2 3 3 3
Plot 15 5 5 8 5 8 4 7 10 12
Plot 16 7 6 9 7 7 7 9 8 11
In this case it seems more intuitive to model abundance of nests per plot as a binomial random variate, rather than the more involved mixture of N Poisson distributions. This is what our next model does.
library(unmarked)
obs <- matrix(as.character(1:8), 16, 8, byrow = T)
sitecovs <- data.frame(X = as.factor(1:16))
antsumf <- unmarkedFramePCount(y = as.matrix(plots[, -9]), obsCovs = list(observer = obs),
siteCovs = sitecovs)
(fit <- pcount(~observer ~ X, data = antsumf, K = 50, mixture = "P"))
Call:
pcount(formula = ~observer ~ X, data = antsumf, K = 50, mixture = "P")
Abundance:
Estimate SE z P(>|z|)
(Intercept) 3.760 0.269 13.9927 1.73e-44
X2 -0.530 0.289 -1.8360 6.64e-02
X3 -0.889 0.327 -2.7162 6.60e-03
X4 -15.401 351.669 -0.0438 9.65e-01
X5 -0.156 0.259 -0.6035 5.46e-01
X6 -2.497 0.607 -4.1172 3.83e-05
X7 -3.589 1.015 -3.5378 4.03e-04
X8 -1.500 0.415 -3.6128 3.03e-04
X9 -1.027 0.344 -2.9879 2.81e-03
X10 -0.352 0.275 -1.2818 2.00e-01
X11 -3.674 1.025 -3.5846 3.38e-04
X12 -0.402 0.279 -1.4398 1.50e-01
X13 -1.931 0.494 -3.9078 9.31e-05
X14 -1.719 0.456 -3.7726 1.62e-04
X15 -0.569 0.293 -1.9400 5.24e-02
X16 -0.438 0.282 -1.5501 1.21e-01
Detection:
Estimate SE z P(>|z|)
(Intercept) -1.3132 0.326 -4.034 5.49e-05
observer2 0.0425 0.206 0.206 8.37e-01
observer3 0.2033 0.202 1.004 3.15e-01
observer4 0.1246 0.204 0.611 5.41e-01
observer5 0.3519 0.200 1.757 7.90e-02
observer6 0.1839 0.203 0.907 3.64e-01
observer7 0.6556 0.202 3.248 1.16e-03
observer8 0.7189 0.203 3.534 4.10e-04
AIC: 449.1964
ests <- fit@estimates@estimates$det@estimates
# observer probabilities:
plogis(c(observer1 = unname(ests[1]), ests[2:8] + ests[1]))
observer1 observer2 observer3 observer4 observer5 observer6
0.2119490 0.2191383 0.2478838 0.2335077 0.2766223 0.2442902
observer7 observer8
0.3412760 0.3556458
# site estimates:
estplot <- fit@estimates@estimates$state@estimates
round(exp(c(X1 = unname(estplot[1]), estplot[2:16] + estplot[1])),
2)
X1 X2 X3 X4 X5 X6 X7 X8 X9 X10
42.97 25.29 17.66 0.00 36.74 3.54 1.19 9.58 15.38 30.20
X11 X12 X13 X14 X15 X16
1.09 28.75 6.23 7.70 24.32 27.73
Our model of the plots data as displayed above assumes that the number of nests observed in a plot s by observer i is a draw from a binomial distribution with a estimated population size \(\hat{N}_s^\text{true}\) for each plot s, and an estimated observation probability \(\hat{P}_i\) for observer i. Since we have eight observers and 16 plots, we can estimate both \(\hat{P}_i\) and \(\hat{N}_s\). For this Bayesian model we need to choose priors for detection probabilities and \(\hat{N}_s\). The latter has a lower bound at \(N^\text{min}_s = N^\text{obs}_s\), as there cannot be fewer nests than observed.
detectBinom <- function() {
# the detection model loop through observers loop through plots
for (i in 1:8) {
for (j in 1:16) {
Nobs[j, i] ~ dbin(Pi[i], Ntrue[j])
}
}
# Priors and constraints:
for (j in 1:16) {
# Ntrue must be an integer greater or equal Nobs:
Ntrue[j] ~ dpois(Nmin[j] * (1 + Propoverlooked[j]))
T(Nmin[j], )
# overlooked nests modelled as proportion of the number observed:
Propoverlooked[j] ~ dexp(shapeOverlooked)
}
shapeOverlooked ~ dgamma(1, 1)
# uninformative prior on detection:
for (i in 1:8) {
Pi[i] ~ dbeta(1, 1) # flat line
}
# compute another value of interest:
meanPropOverlooked <- mean(Propoverlooked)
} # end of function
jags.data <- list(Nobs = plots[, -9], Nmin = plots[, 9])
parametersBinom <- c("Ntrue", "Pi", "Propoverlooked", "shapeOverlooked",
"meanPropOverlooked")
ni <- 8000
nb <- ni/2
nc <- 3
nt <- 3
# call JAGS
system.time(antdetectBinom <- jags(jags.data, inits = NULL, parametersBinom,
model.file = detectBinom, n.chains = nc, n.thin = nt, n.iter = ni,
n.burnin = nb, working.directory = getwd()))
Compiling model graph
Resolving undeclared variables
Allocating nodes
Graph information:
Observed stochastic nodes: 128
Unobserved stochastic nodes: 41
Total graph size: 253
Initializing model
user system elapsed
5.779 0.054 5.906
plot(antdetectBinom)
(antdetectBinom)
Inference for Bugs model at "/var/folders/cc/3jfhfx190rb2ptxnqrqxj94m0000gp/T//Rtmp90xMhM/model293d4c0146f6.txt", fit using jags,
3 chains, each with 8000 iterations (first 4000 discarded), n.thin = 3
n.sims = 4002 iterations saved
mu.vect sd.vect 2.5% 25% 50%
Ntrue[1] 29.188 2.868 24.000 27.000 29.000
Ntrue[2] 18.515 1.507 17.000 17.000 18.000
Ntrue[3] 11.696 1.595 9.000 11.000 12.000
Ntrue[4] 0.000 0.000 0.000 0.000 0.000
Ntrue[5] 24.829 2.557 21.000 23.000 25.000
Ntrue[6] 3.129 0.363 3.000 3.000 3.000
Ntrue[7] 1.044 0.206 1.000 1.000 1.000
Ntrue[8] 6.197 1.096 4.000 5.000 6.000
Ntrue[9] 11.154 1.171 10.000 10.000 11.000
Ntrue[10] 21.774 1.742 20.000 20.000 21.000
Ntrue[11] 1.020 0.142 1.000 1.000 1.000
Ntrue[12] 18.773 2.236 15.000 17.000 19.000
Ntrue[13] 4.592 0.751 4.000 4.000 4.000
Ntrue[14] 4.921 0.955 3.000 4.000 5.000
Ntrue[15] 16.340 1.910 13.000 15.000 16.000
Ntrue[16] 18.100 2.141 14.000 17.000 18.000
Pi[1] 0.312 0.038 0.240 0.285 0.311
Pi[2] 0.322 0.038 0.250 0.296 0.322
Pi[3] 0.364 0.040 0.290 0.335 0.364
Pi[4] 0.343 0.040 0.268 0.315 0.342
Pi[5] 0.405 0.043 0.322 0.376 0.406
Pi[6] 0.359 0.040 0.283 0.332 0.359
Pi[7] 0.499 0.046 0.407 0.467 0.498
Pi[8] 0.519 0.048 0.428 0.486 0.519
Propoverlooked[1] 0.193 0.165 0.006 0.065 0.146
Propoverlooked[2] 0.151 0.140 0.004 0.048 0.109
Propoverlooked[3] 0.286 0.264 0.008 0.091 0.211
Propoverlooked[4] 0.332 0.391 0.008 0.082 0.203
Propoverlooked[5] 0.268 0.224 0.009 0.094 0.212
Propoverlooked[6] 0.217 0.224 0.005 0.062 0.149
Propoverlooked[7] 0.260 0.274 0.005 0.070 0.173
Propoverlooked[8] 0.297 0.301 0.007 0.081 0.206
Propoverlooked[9] 0.179 0.168 0.005 0.055 0.129
Propoverlooked[10] 0.148 0.135 0.004 0.046 0.108
Propoverlooked[11] 0.271 0.305 0.006 0.071 0.173
Propoverlooked[12] 0.412 0.327 0.015 0.161 0.336
Propoverlooked[13] 0.227 0.231 0.006 0.063 0.150
Propoverlooked[14] 0.304 0.309 0.009 0.090 0.206
Propoverlooked[15] 0.251 0.224 0.008 0.083 0.187
Propoverlooked[16] 0.384 0.308 0.013 0.145 0.310
meanPropOverlooked 0.261 0.102 0.113 0.190 0.244
shapeOverlooked 3.583 1.412 1.538 2.590 3.357
deviance 392.087 6.158 381.990 387.796 391.374
75% 97.5% Rhat n.eff
Ntrue[1] 31.000 35.000 1.002 2000
Ntrue[2] 19.000 22.000 1.002 1400
Ntrue[3] 13.000 15.000 1.002 1600
Ntrue[4] 0.000 0.000 1.000 1
Ntrue[5] 26.000 30.000 1.002 2100
Ntrue[6] 3.000 4.000 1.004 980
Ntrue[7] 1.000 2.000 1.009 1400
Ntrue[8] 7.000 9.000 1.001 4000
Ntrue[9] 12.000 14.000 1.002 1400
Ntrue[10] 23.000 26.000 1.005 1100
Ntrue[11] 1.000 1.000 1.002 4000
Ntrue[12] 20.000 24.000 1.003 1000
Ntrue[13] 5.000 6.000 1.001 2800
Ntrue[14] 5.000 7.000 1.003 850
Ntrue[15] 18.000 20.000 1.002 1400
Ntrue[16] 19.000 23.000 1.003 890
Pi[1] 0.337 0.389 1.003 710
Pi[2] 0.348 0.398 1.001 4000
Pi[3] 0.391 0.444 1.002 1600
Pi[4] 0.370 0.423 1.001 2500
Pi[5] 0.434 0.489 1.002 1100
Pi[6] 0.385 0.438 1.002 2000
Pi[7] 0.530 0.590 1.003 880
Pi[8] 0.551 0.615 1.003 1100
Propoverlooked[1] 0.281 0.602 1.003 1000
Propoverlooked[2] 0.211 0.528 1.002 2600
Propoverlooked[3] 0.407 0.954 1.002 1100
Propoverlooked[4] 0.434 1.425 1.002 1700
Propoverlooked[5] 0.384 0.846 1.002 1200
Propoverlooked[6] 0.294 0.808 1.002 1200
Propoverlooked[7] 0.355 1.023 1.001 2900
Propoverlooked[8] 0.418 1.107 1.002 1500
Propoverlooked[9] 0.253 0.623 1.004 710
Propoverlooked[10] 0.212 0.498 1.002 1600
Propoverlooked[11] 0.358 1.087 1.001 4000
Propoverlooked[12] 0.585 1.205 1.003 830
Propoverlooked[13] 0.314 0.835 1.003 890
Propoverlooked[14] 0.417 1.142 1.005 620
Propoverlooked[15] 0.363 0.828 1.001 2400
Propoverlooked[16] 0.554 1.130 1.001 4000
meanPropOverlooked 0.315 0.497 1.008 320
shapeOverlooked 4.317 7.022 1.005 510
deviance 395.694 406.091 1.001 4000
For each parameter, n.eff is a crude measure of effective sample size,
and Rhat is the potential scale reduction factor (at convergence, Rhat=1).
DIC info (using the rule, pD = var(deviance)/2)
pD = 19.0 and DIC = 411.1
DIC is an estimate of expected predictive error (lower deviance is better).
# estimated number of nests:
sum(antdetectBinom$BUGSoutput$mean$Ntrue)
[1] 191.2716
The key findings are that we overlook, according to this model, around 26% of nests (estimating the total as roughly 191 \(\pm\) 19); and that the detection rates vary between 0.312 and 0.520 among observers. These values look rather different to the previous data-augmentation model. This difference can be attributed to two changes in the model structure:
We can plot the estimated (x-axis) and observed number of nests per plot (y-axis):
par(mar = c(4, 5, 1, 1))
plot(antdetectBinom$BUGSoutput$mean$Ntrue, plots[, 9], type = "n",
ylab = expression(N[s]^{
obs
}), xlab = expression(hat(N)[s]), las = 1)
abline(0, 1)
text(antdetectBinom$BUGSoutput$mean$Ntrue, plots[, 9], cex = 1)
Clearly the correction applied is larger in plots with more ant nests, or, phrased differently, the adjustment is proportional. We can plot the posteriors and their mean:
par(mar = c(3, 4, 1, 1), mfrow = c(4, 4))
for (i in 1:16) {
dens <- antdetectBinom$BUGSoutput$sims.list$Ntrue[, i]
tdens <- table(dens)
plot(as.numeric(names(tdens)), tdens/length(dens), las = 1, col = "grey70",
type = "h", lwd = 3, ylab = "", xlab = "", xlim = c(min(max(0,
plots[i, 9] - 1), as.numeric(names(tdens))), max(c(1,
as.numeric(names(tdens))))), ylim = c(0, max(tdens/length(dens))),
axes = F)
axis(1, at = 0:40)
points(plots[i, 9], 0, pch = 17, cex = 1.5, col = "red", xpd = T)
legend("topright", bty = "n", legend = paste("plot", i), cex = 1.2)
lines(rep(antdetectBinom$BUGSoutput$mean$Ntrue[i], 2), c(0, 1),
col = "black", lwd = 1.5)
}
Histograms are Bayesian posteriors for \(\hat{N}_s\), with mean estimate indicated by the vertical black line, and the red triangle indicates \(N^\text{obs}_s\) for each plot.
Royle (2004) proposed to view observed abundances as mixtures of Poisson distributions, thus extending the patch-occupancy idea to abundances. As is the case for the previous Bayesian plot-level analysis, this approach cannot accommodate nest traits and is thus at the plot level.
library(unmarked)
obs <- matrix(as.character(1:8), 16, 8, byrow = T)
sitecovs <- data.frame(X = as.factor(1:16))
antsumf <- unmarkedFramePCount(y = as.matrix(plots[, -9]), obsCovs = list(observer = obs),
siteCovs = sitecovs)
(fit <- pcount(~observer ~ X, data = antsumf, K = 50, mixture = "P"))
Call:
pcount(formula = ~observer ~ X, data = antsumf, K = 50, mixture = "P")
Abundance:
Estimate SE z P(>|z|)
(Intercept) 3.760 0.269 13.9927 1.73e-44
X2 -0.530 0.289 -1.8360 6.64e-02
X3 -0.889 0.327 -2.7162 6.60e-03
X4 -15.401 351.669 -0.0438 9.65e-01
X5 -0.156 0.259 -0.6035 5.46e-01
X6 -2.497 0.607 -4.1172 3.83e-05
X7 -3.589 1.015 -3.5378 4.03e-04
X8 -1.500 0.415 -3.6128 3.03e-04
X9 -1.027 0.344 -2.9879 2.81e-03
X10 -0.352 0.275 -1.2818 2.00e-01
X11 -3.674 1.025 -3.5846 3.38e-04
X12 -0.402 0.279 -1.4398 1.50e-01
X13 -1.931 0.494 -3.9078 9.31e-05
X14 -1.719 0.456 -3.7726 1.62e-04
X15 -0.569 0.293 -1.9400 5.24e-02
X16 -0.438 0.282 -1.5501 1.21e-01
Detection:
Estimate SE z P(>|z|)
(Intercept) -1.3132 0.326 -4.034 5.49e-05
observer2 0.0425 0.206 0.206 8.37e-01
observer3 0.2033 0.202 1.004 3.15e-01
observer4 0.1246 0.204 0.611 5.41e-01
observer5 0.3519 0.200 1.757 7.90e-02
observer6 0.1839 0.203 0.907 3.64e-01
observer7 0.6556 0.202 3.248 1.16e-03
observer8 0.7189 0.203 3.534 4.10e-04
AIC: 449.1964
ests <- fit@estimates@estimates$det@estimates
# observer probabilities:
plogis(c(observer1 = unname(ests[1]), ests[2:8] + ests[1]))
observer1 observer2 observer3 observer4 observer5 observer6
0.2119490 0.2191383 0.2478838 0.2335077 0.2766223 0.2442902
observer7 observer8
0.3412760 0.3556458
# site estimates:
estplot <- fit@estimates@estimates$state@estimates
round(exp(c(X1 = unname(estplot[1]), estplot[2:16] + estplot[1])),
2)
X1 X2 X3 X4 X5 X6 X7 X8 X9 X10
42.97 25.29 17.66 0.00 36.74 3.54 1.19 9.58 15.38 30.20
X11 X12 X13 X14 X15 X16
1.09 28.75 6.23 7.70 24.32 27.73
Observer rates are estimated lower, and number of nests accordingly higher, than in the Bayesian model. Otherwise the model confirms the results of the previous models.
We can dispense with the Bayesian nature of the previous step as we are using uninformative priors only, and thus in this case use maximum likelihood to estimate the model parameters (observation rates and true number of nests per plot). Again we assume that the number of nests detected by observer \(i\) at plot \(s\) is a draw from a binomial distribution, i.e. \(N^{obs}_{i, s} \sim Binom(size=\hat{N}_s, p=\hat{P}_i)\). Thus, across the 16 plots and 8 observers we have to optimise 24 values to find the maximum likelihood fit for the 128 data points in plots.
Nobs <- plots[, -9] # remove Nmin
# define the maximum likelihood based on the 24 parameters:
toopt <- function(parms, Nobs = plots[, -9]) {
Ntrue <- exp(parms[1:16]) # ensure positive values
Pi <- plogis(parms[17:24]) # ensure values in [0,1]
Nsest <- tcrossprod(Ntrue, Pi)
-sum(dpois(as.matrix(Nobs), lambda = as.matrix(Nsest), log = T))
}
# use Nmin as start values for Ntrue and 0.5 for detection rates:
Nmin <- plots[, 9]
(op <- optim(par = c(log(Nmin + 1), rep(0.5, 8)), fn = toopt, method = "BFGS",
hessian = F))
$par
[1] 3.2723471 2.7100414 2.3773348 -13.9086521 3.0964573
[6] 0.3491841 -0.5671277 1.7842722 2.2362588 2.9141346
[11] -1.2602092 2.8668821 1.3047004 1.5729595 2.6909924
[16] 2.8340923 -0.6153202 -0.5634996 -0.3626869 -0.4619341
[21] -0.1688646 -0.3873130 0.2611425 0.3585991
$value
[1] 198.443
$counts
function gradient
91 43
$convergence
[1] 0
$message
NULL
# image(op$hessian) # after setting hessian=T reveals that there
# is no correlation between estimates; therefore we can compute
# errors for Nhat as sum of independent plots estimated detProb
# per observer:
round(plogis(op$par[17:24]), 2)
[1] 0.35 0.36 0.41 0.39 0.46 0.40 0.56 0.59
# estimated number of nests
round(bestguessNtrue <- exp(op$par[1:16]))
[1] 26 15 11 0 22 1 1 6 9 18 0 18 4 5 15 17
# compare to Nmin:
plots[, 9]
[1] 24 17 8 0 18 3 1 4 10 20 1 11 4 3 12 11
# estimated number of nests:
sum(bestguessNtrue)
[1] 168.1645
Maximum-likelihood estimates are somewhat higher than \(N^\text{obs}_s\) (= Nmin), but in plots 2, 6, 9 and 11 they are lower (than the observed number of nests). But the results are extremely similar to the Bayesian plot-level analysis and also compare well to the data-augmentation results (which are essentially identical to \(N^{obs}_s\)):
cor(cbind(mlEst = bestguessNtrue, BayesEst = antdetectBinom$BUGSoutput$mean$Ntrue,
dataAug = estimated[, 2]))
mlEst BayesEst dataAug
mlEst 1.0000000 0.9964866 0.9517471
BayesEst 0.9964866 1.0000000 0.9715177
dataAug 0.9517471 0.9715177 1.0000000
par(mar = c(5, 5, 1, 1))
plot(estimated[, 2], antdetectBinom$BUGSoutput$mean$Ntrue, xlab = expression(hat(N)[s]^{
augmented
}), ylab = expression(hat(N)[s]), las = 1)
points(estimated[, 2], bestguessNtrue, pch = 16)
abline(0, 1)
legend("bottomright", pch = c(1, 16), legend = c("Bayes estimates",
"ML estimates"), bty = "n", cex = 1.2)
The figure shows estimates from the site-level Bayesian and maximum likelihood approach (y-axis) against the nest-level data-augmentation results (x-axis). Line gives perfect accordance (1:1).
We think that we can thus use the site-level maximum likelihood approach in lieu of the Bayesian plot-level model, particularly when in the next step we re-run the analysis many times for different combinations of observers. This takes only seconds using the maximum likelihood approach, but would take many hours with the Bayesian plot-level model.
Computing confidence intervals or standard errors for the maximum likelihood estimates is a bit involved, as asymptotic errors (based on the Hessian matrix) are very unreliable for such small data sets. We thus use bootstrapping instead.
# draw, for each plot, with replacement from the nests data set:
plotNames <- substring(rownames(nests), 3, 4)
n.bs <- 1000 # number of bootstraps
detProb.bs <- matrix(NA, nrow = n.bs, ncol = 24)
for (n in 1:n.bs) {
plots.bs <- matrix(0, nrow = 16, ncol = 8)
rownames(plots.bs)[c(1:3, 5:16)] <- unique(plotNames)
rownames(plots.bs)[4] <- "04"
for (i in unique(plotNames)) {
thisPlot <- nests[which(plotNames == i), ]
plotBS <- thisPlot[sample(nrow(thisPlot), nrow(thisPlot),
replace = T), ]
plots.bs[rownames(plots.bs) == i, ] <- colSums(plotBS)
}
(op.bs <- optim(par = c(log(Nmin + 1), rep(0.5, 8)), fn = toopt,
Nobs = plots.bs, method = "BFGS"))
detProb.bs[n, 17:24] <- plogis(op.bs$par[17:24]) # detection rates
detProb.bs[n, 1:16] <- exp(op.bs$par[1:16]) # nest estimates
}
# observation rate estimates:
round(colMeans(detProb.bs[, 17:24]), 2)
[1] 0.34 0.35 0.40 0.38 0.45 0.40 0.55 0.58
round(apply(detProb.bs[, 17:24], 2, sd), 3)
[1] 0.058 0.058 0.064 0.063 0.071 0.064 0.095 0.107
round(apply(detProb.bs[, 17:24], 2, quantile, c(0.025, 0.975)), 3)
[,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8]
2.5% 0.249 0.264 0.307 0.280 0.343 0.304 0.418 0.429
97.5% 0.480 0.494 0.561 0.527 0.616 0.554 0.791 0.876
# nest number estimates:
round(colMeans(detProb.bs[, 1:16]), 2)
[1] 27.82 15.75 11.30 0.00 23.19 1.48 0.59 6.17 9.46 19.86
[11] 0.30 18.46 3.81 5.06 15.59 17.85
round(apply(detProb.bs[, 1:16], 2, sd), 3)
[1] 6.025 3.250 2.701 0.000 4.282 0.307 0.085 1.902 2.849 3.743
[11] 0.043 3.411 1.365 1.497 3.849 3.457
round(apply(detProb.bs[, 1:16], 2, quantile, c(0.025, 0.975)), 3)
[,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9]
2.5% 19.725 9.448 6.455 0 15.035 0.911 0.411 2.528 4.772
97.5% 41.494 21.926 16.801 0 31.554 2.078 0.741 10.044 15.354
[,10] [,11] [,12] [,13] [,14] [,15] [,16]
2.5% 12.562 0.205 12.078 1.718 2.051 8.018 11.101
97.5% 26.859 0.371 25.053 7.026 7.941 23.232 24.219
It is easy to simulate fewer observers by simply randomly drawing the desired number of observers from the data and repeating the (maximum-likelihood) analysis (the Bayesian would take quite a long time, since we have to repeat this random drawing many times).
k <- 3
Y <- Nobs[, c(1, 3, 5)]
tooptk <- function(parms, k) {
Ntrue <- exp(parms[1:16]) # ensure positive values
Pi <- plogis(parms[17:(17 + k - 1)]) # ensure values in [0,1]
estY <- tcrossprod(Ntrue, Pi)
-sum(dpois(as.matrix(Y), lambda = as.matrix(estY), log = T))
}
(opk <- optim(par = c(log(Nmin + 1), rep(0, k)), fn = tooptk, k = k,
control = list(maxit = 20000)))
$par
[1] 4.2022158 3.2148259 2.9396221 -1.6024937 4.0601943
[6] -2.4845207 0.6931455 2.7263550 2.8637990 3.8470075
[11] -2.5610937 3.8868345 1.3011575 2.2898649 3.7009533
[16] 3.7855818 -1.7505154 -1.5653785 -1.4278722
$value
[1] 67.59848
$counts
function gradient
10566 NA
$convergence
[1] 0
$message
NULL
round(exp(opk$par[1:16]), 1) # estimated number of nests
[1] 66.8 24.9 18.9 0.2 58.0 0.1 2.0 15.3 17.5 46.9 0.1 48.8
[13] 3.7 9.9 40.5 44.1
round(plogis(opk$par[17:(17 + k - 1)]), 2) # estimated detProb per observer
[1] 0.15 0.17 0.19
# this can be looped through 1000 times to get error bars for the
# estimates, always drawing a random set of observers; and then we
# repeat this with k=2 to k=8; here the example for k=3:
Nreps <- 10 # only for illustration the value is set low; set this to something larger (takes .3s per rep)
k <- 3
trueNmat3 <- matrix(NA, ncol = 16, nrow = Nreps)
for (i in 1:Nreps) {
# choose a random set of observers:
useTheseObs <- sample(8, k)
Y <- Nobs[, useTheseObs]
# compute the trueY based on this:
opk <- optim(par = c(log(Nmin + 1), rep(0, k)), fn = tooptk, k = 3,
control = list(maxit = 50000))
if (opk$convergence != 0)
stop("not converged!")
trueNmat3[i, ] <- exp(opk$par[1:16])
cat(i, " ")
}
1 2 3 4 5 6 7 8 9 10
boxplot(trueNmat3, las = 1, ylab = "estimated number of ant nests",
xlab = "plot", col = "grey70")
Note that these values are much higher when only few observers are used for estimation. Let us briefly try and understand why.
The estimated values of the observed \(N_{i, s}\) are the cross-product of the estimated number of nests in a plot, \(\hat{N}_s\) and each observers detection probability, \(P_i\). If we estimate high values for \(\hat{N}_s\), we can ‘compensate’ this by low values of \(P_i\). If variability is high (because we have only few observers), we have little to go on for estimating either value. There is now the choice between ‘many nest, poor detection rates’ and ‘few nests, high detection’. As it works out, in the binomial probability mass function, it is easier to accommodate highly variable data with ‘many nests, poor detection’. Thus, the poorer the data (read: the fewer observers), the more the estimation will overestimate the true number of nests.
Repeating this for different values of k will lead to Fig. 3 in the main text. We do not provide the code here, but it is near-trivial to adapt the above for any value of k.
As detailed in the main paper, we require two different probabilities to simulate more observers: (a) the detection rate \(P_i\) of each observer \(i\) (which we have from either the maximum-likelihood estimation or the Bayesian analysis); and (b) the probability that a second observer discovers a new (complementary) nest, \(P_c\). We compute \(P_c\) for each pair of observers, yielding a matrix from which to sample when simulating more observers (or indeed fewer).
# add overlooked species according to jack1 estimate (intermediate
# between Chao and jack2)
nestsAll <- rbind(as.matrix(nests), matrix(0, nrow = 25, ncol = 8))
# nests which second observers found but first overlooked
# (quantifying complementarity):
Pc.mat <- matrix(0, 8, 8)
colnames(Pc.mat) <- rownames(Pc.mat) <- colnames(nests)
for (i in 1:8) {
for (j in 1:8) {
tt <- table(nestsAll[, i], nestsAll[, j])
Pc.mat[i, j] <- tt[1, 2]/sum(tt[1, ]) # proportion of 0s turned into 1s
}
}
round(Pc.mat, 3) # note that this matrix is (obviously) not symmetric!
O1 O2 O3 O4 O5 O6 O7 O8
O1 0.000 0.159 0.265 0.257 0.283 0.239 0.442 0.496
O2 0.144 0.000 0.207 0.225 0.243 0.216 0.450 0.441
O3 0.194 0.146 0.000 0.204 0.223 0.223 0.350 0.447
O4 0.215 0.196 0.234 0.000 0.290 0.262 0.393 0.477
O5 0.147 0.116 0.158 0.200 0.000 0.137 0.358 0.411
O6 0.173 0.163 0.231 0.240 0.212 0.000 0.423 0.452
O7 0.182 0.208 0.130 0.156 0.208 0.221 0.000 0.351
O8 0.219 0.151 0.219 0.233 0.233 0.219 0.315 0.000
# mean Pc value:
mean(c(Pc.mat[lower.tri(Pc.mat)], Pc.mat[upper.tri(Pc.mat)])) # 0.2565
[1] 0.2568836
hist(c(Pc.mat[lower.tri(Pc.mat)], Pc.mat[upper.tri(Pc.mat)]), main = "",
xlab = "Pc")
# note that this is the complement to the value computed above in
# jointly.mat!!
We now have a matrix with values representing the probabilities of discovering new nests overlooked by one previous observer. The chance of \(k\) observers to all overlooking a nest is \((1-P_c)^k(1-P_i)\). We turn this into a little function and plot it for \(k\) from 1 to 10.
overlooked <- function(k, Pc = 0.2565, Pi = 0.42) {
# Pd is mean of Bayesian estimates returns the probability of
# having overlooked nests
(1 - Pc)^(k - 1) * (1 - Pi)
}
par(mar = c(5, 5, 1, 1))
plot(1:10, overlooked(1:10), type = "o", pch = 16, cex = 2, las = 1,
ylab = "probability of overlooking a nest", xlab = "number of observers",
lwd = 2, col = "grey40")
abline(h = 0.1, col = "grey", lty = 2)
Clearly, and obviously, the more observers we have, the lower is the chance of overlooking a nest. With 8 observers we cross to below 10% overlooked nests.
This plot ignores the variation around the detection and complementarity rates. So we now open the function to bootstrapping (i.e. sampling with replacement) \(P_i\) and \(P_c\) and run it on the real data (\(P_i\) from maximum likelihood).
overlookedBS <- function(k, Pc, Pi) {
# recursive problem!
if (k == 1)
return((1 - sample(Pi, 1))) # for one observer: 1-detection probability
# for two or more observers:
(1 - sample(Pc, 1)) * overlookedBS(k - 1, Pc, Pi)
}
# run a test for 10 observers, 1000 repetitions:
hist(replicate(1000, overlookedBS(10, Pc = c(Pc.mat[lower.tri(Pc.mat)],
Pc.mat[upper.tri(Pc.mat)]), Pi = plogis(op$par[17:24]))), main = "10 observers",
xlab = "proportion overlooked")
So we can now compute the overlooking probability for 1 to 10 (or more) observers.
simuObservers <- sapply(1:10, function(x) replicate(1000, overlookedBS(x,
Pc = c(Pc.mat[lower.tri(Pc.mat)], Pc.mat[upper.tri(Pc.mat)]),
Pi = c(0.338, 0.35, 0.394, 0.37, 0.439, 0.388, 0.533, 0.561))))
par(mar = c(5, 5, 1, 1))
boxplot(simuObservers, las = 1, whisklty = 1, col = "grey", ylab = "probability of overlooking any nest",
xlab = "number of observers")
abline(h = c(0.1, 0.05, 0.01), col = "darkgrey", lty = 2) # targets for accuracy
The black lines are the same values as in the previous dot-plot, but now we also get an estimate of the uncertainty around this value.
For completeness, we also include the more traditional way to estimate overlooked nests, building on coarse approximations of the ratio of rare and common events. Each observer’s records are a sample of the true nests at each plot. We can use non-parametric richness estimators to predict the number of nests across all plots. This is akin to having multiple recordings of a community and estimating the total number of species in the pool. The nests-data have to be transposed before analysis to have “species” (i.e. nest locations) as columns.
library(vegan)
specpool(t(nests))
Species chao chao.se jack1 jack1.se jack2 boot
All 147 163.7244 8.247339 172.375 11.88289 179.9821 159.8692
boot.se n
All 8.652339 8
As boot is frequently reported as underestimating the true richness, we shall only use Chao’s S, jack1 S and jack2 S as estimators of the true number of nests across all plots. Thus, we have sampled 147/164*100% = 90% (Chao), 85% (jack1) or 82% (jack2) of all nests, which suggests a high sampling coverage:
par(mar = c(4, 4, 1, 1))
plot(specaccum(t(nests)), xlab = "number of observers", ylab = "number of nests recorded",
las = 1, lwd = 2)
To visualise which ant nests were detected by who, we can plot the nests data as a matrix.
library(bipartite)
sizes <- read.csv("nestSizes.csv", row.names = 1) # read file in again to get all nest sizes
nestSize <- sizes$Height
par(oma = c(0, 0, 0, 0.1)) # create space for label to the right
# overlooked species according to jack2 estimate (see further
# below):
nests2 <- rbind(as.matrix(nests), matrix(0, nrow = 44, ncol = 8))
visweb(t(nests2), labsize = 5, prednames = F, preynames = F, clear = F)
# here comes the symbol/col for nest size:
points(((1:147) - 0.5)[order(rowSums(nests), decreasing = TRUE)],
rep(9, 147), pch = 16, col = "darkgrey", cex = 1.3 * ifelse(nestSize ==
10, 0.3, ifelse(nestSize == 50, 0.6, ifelse(nestSize == 100,
0.9, 1.2))))
# lines for number of observers/nest:
lines(c(22, 22), c(-1, 10), col = "darkgrey")
lines(c(22 + 16, 22 + 16), c(-1, 10), col = "darkgrey")
lines(c(22 + 16 + 10, 22 + 16 + 10), c(-1, 10), col = "darkgrey")
lines(c(22 + 16 + 10 + 8, 22 + 16 + 10 + 8), c(-1, 10), col = "darkgrey")
lines(c(22 + 16 + 10 + 8 + 11, 22 + 16 + 10 + 8 + 11), c(-1, 10),
col = "darkgrey")
lines(c(22 + 16 + 10 + 8 + 11 + 29, 22 + 16 + 10 + 8 + 11 + 29), c(-1,
10), col = "darkgrey")
lines(c(22 + 16 + 10 + 8 + 11 + 29 + 22, 22 + 16 + 10 + 8 + 11 + 29 +
22), c(-1, 10), col = "darkgrey")
lines(c(22 + 16 + 10 + 8 + 11 + 29 + 22 + 29, 22 + 16 + 10 + 8 + 11 +
29 + 22 + 29), c(-1, 10), col = "darkgrey")
# indicator for the 147 detected nests:
points(147, 9.7, pch = 25, cex = 1.5, bg = "black")
text(147, 11.5, "147", cex = 1) #expression(N[min])
# indicators for S_est and uncertainty:
lines(c(164, 164), c(-1, 8.5), col = "darkgrey") # S_chao
points(164, 9.2, pch = 25, cex = 1.5, bg = "black")
text(164 + 1, 11.5, expression(S[Chao]), cex = 1.2)
lines(c(172, 172), c(-1, 8.5), col = "darkgrey") # S_jack1
points(172, 9.2, pch = 25, cex = 1.5, bg = "black")
text(172 + 1, 11.5, expression(S[jack1]), cex = 1.2)
lines(c(180, 180), c(-1, 8.5), col = "darkgrey") # S_jack1
points(180, 9.2, pch = 25, cex = 1.5, bg = "black")
text(180 + 1, 11.5, expression(S[jack2]), cex = 1.2)
# add MLE:
points(171, -1, pch = 24, cex = 1.5, bg = "black")
text(171, -3, "MLE", cex = 1.2)
points(191, -1, pch = 24, cex = 1.5, bg = "black")
text(191 - 2, -3, "Bayes", cex = 1.2)
points(148, -1, pch = 24, cex = 1.5, bg = "black")
text(148, -3, "Patch occ.", cex = 1.2)
Dots above the nest indicate the nest’s size. Empty cells to the right were unrecorded by all observers. The best guess for \(\hat{N}_\text{true}\) of the different methods is indicated by a triangle alongside the method.
Platform, session and package information:
sessionInfo()
R version 3.2.3 (2015-12-10)
Platform: x86_64-apple-darwin13.4.0 (64-bit)
Running under: OS X 10.10.5 (Yosemite)
locale:
[1] en_GB.UTF-8/en_GB.UTF-8/en_GB.UTF-8/C/en_GB.UTF-8/en_GB.UTF-8
attached base packages:
[1] stats graphics grDevices utils datasets methods
[7] base
other attached packages:
[1] unmarked_0.11-0 Rcpp_0.12.5 reshape_0.8.5
[4] lme4_1.1-12 Matrix_1.2-6 truncnorm_1.0-7
[7] R2jags_0.5-7 rjags_4-6 coda_0.18-1
[10] bipartite_2.06 sna_2.3-2 vegan_2.3-5
[13] lattice_0.20-33 permute_0.9-0
loaded via a namespace (and not attached):
[1] formatR_1.4 nloptr_1.0.4 plyr_1.8.4
[4] tools_3.2.3 boot_1.3-18 digest_0.6.9
[7] evaluate_0.9 nlme_3.1-128 mgcv_1.8-12
[10] igraph_1.0.1 yaml_2.1.13 parallel_3.2.3
[13] spam_1.3-0 raster_2.4-18 stringr_1.0.0
[16] cluster_2.0.4 knitr_1.13 fields_8.4-1
[19] maps_3.1.0 grid_3.2.3 rmarkdown_0.9.6
[22] sp_1.1-1 minqa_1.2.4 magrittr_1.5
[25] codetools_0.2-14 htmltools_0.3.5 R2WinBUGS_2.1-21
[28] MASS_7.3-45 splines_3.2.3 abind_1.4-3
[31] stringi_1.1.1
The above code is licensed under CC-BY-SA 4.0.