Trap-specific responses in spatial capture-recapture

R
capture-recapture
spatial
behavioural response
simulation
ecology tutorial
When animals learn one baited trap, spatial capture-recapture in R reads the pile-up of recaptures as a short sigma. A global Mb cannot fix it; Mbk can.
Author

Tidy Ecology

Published

2026-09-23

A small-mammal grid of 49 baited hair tubes runs for six nights, and the hairs left on the sticky patch inside each tube are genotyped, so each genotyped sample is a detection of a known animal. A wood mouse that leaves hair in tube C4 on the first night has found oats there, and on the nights that follow it turns up in C4 again, and again, while the tubes one row over, which sit just as close to where it lives, rarely record it. Nothing about the mouse’s home range has changed. What has changed is that one tube is now worth visiting and the others are not.

Spatial capture-recapture reads the spread of an animal’s captures across the grid as information about how far it ranges. Spatial capture-recapture from scratch builds the likelihood that does this, with a half-normal detection function whose scale sigma is estimated from where each animal was caught, and SCR sampling design and precision puts the point bluntly: “A capture at a single trap says an animal was nearby, but not how far it typically ranges.” A mouse that keeps returning to C4 is a pile of captures at a single trap (a detection at a hair tube counts as a capture here). The model has no way to know the pile is about bait, so it reads it as a mouse that does not range far.

The standard answer to a trap response is model Mb, which Royle, Chandler, Sollmann and Gardner (2014) carry into spatial models in their chapter on variation in encounter probability. It works on this site. Capture heterogeneity: Mt, Mb and Mh in R generates trap-shy data and fits a global Mb, in which capture probability changes once and everywhere after the first capture, and Mb is the one model whose estimate sits on the truth. The capture-heterogeneity post fits a trap response that has no address, and there the global model is the repair; on a grid, a global term can change how often an animal is caught but not where, and sigma stays short until the model knows which trap the animal learned. The secr package calls the trap-specific version bk, an animal by site learned response, and Schmidt, Graves, Pederson and Carroll (2022) met it in black bear hair corrals baited with a scent lure, where a trap-specific behavioural term was strongly supported at one of their study areas, and a behavioural term in the detection model raised density estimates relative to models without one. Schmidt and colleagues also fitted a model without the behavioural term to data simulated with it; their main text reports only the matched fits, and I have not seen the mismatched results in their appendix, so this post makes no claim about priority. It measures what ignoring the local response does to sigma in a controlled setting.

Two other posts mark the edges. Detection covariates and checking in SCR lets baseline detection differ between traps because some “use more attractive lures”, which is a fixed property of the trap shared by every animal; here the attraction belongs to one animal and one trap and switches on only after a capture. Disturbed animals in repeated counts is the same idea with no marks at all: a counted animal is detected afterwards with a different probability, and a count-only version of Mb repairs the N-mixture estimate. This post adds the address.

library(ggplot2)
library(patchwork)

te_paper  <- "#f5f4ee"
te_ink    <- "#16241d"
te_body   <- "#2c3a31"
te_forest <- "#275139"
te_rust   <- "#b5534e"
te_gold   <- "#c9b458"
te_line   <- "#dad9ca"

theme_datasheet <- function() {
  theme_minimal(base_size = 12) +
    theme(plot.background  = element_rect(fill = te_paper, colour = NA),
          panel.background = element_rect(fill = te_paper, colour = NA),
          panel.grid.major = element_line(colour = te_line, linewidth = 0.3),
          panel.grid.minor = element_blank(),
          text             = element_text(colour = te_body),
          plot.title       = element_text(colour = te_ink, face = "bold"),
          plot.subtitle    = element_text(colour = te_body),
          axis.text        = element_text(colour = te_body))
}

Where the later captures fall

The design is a 7 by 7 grid of proximity detectors run for six occasions, with the half-normal detection shape of Efford (2004) and sigma of 0.8 units. Proximity detectors let an animal be recorded at several traps on one occasion, as hair tubes and cameras do; live traps do not, and that case is not simulated (Honest limits). Activity centres are uniform over the grid plus a buffer of four sigma. At the base spacing of one unit, which is 1.25 sigma, the state space holds 150 animals, and that density is held fixed at every spacing used later, so a wider grid sits in a larger state space with proportionally more animals. Naive detection at the activity centre is 0.10.

Three truths share that design. With no response, detection never changes. With a global response, an animal’s detection at every trap rises after its first capture anywhere. With a trap-specific response, detection rises only at a trap where that animal has already been caught, and stays at the naive value at every other trap.

sigma_true <- 0.8                      # detection scale, grid units
n_side     <- 7                        # 7 x 7 proximity detectors
k_occ      <- 6                        # occasions
buffer     <- 4 * sigma_true           # state space beyond the outer traps
cell_target <- 0.6                     # mask cell size, grid units
p0_naive   <- 0.10                     # detection at the centre before capture
density_fix <- 150 / (6 + 2 * buffer)^2  # animals per unit area, held fixed

make_design <- function(spacing_sigma) {
  spacing <- spacing_sigma * sigma_true
  g   <- (0:(n_side - 1)) * spacing
  tr  <- as.matrix(expand.grid(x = g, y = g))
  lo  <- -buffer; hi <- max(g) + buffer
  n_cell <- round((hi - lo) / cell_target)
  mg  <- seq(lo, hi, length.out = n_cell + 1)
  mg  <- (mg[-1] + mg[-length(mg)]) / 2
  msk <- as.matrix(expand.grid(x = mg, y = mg))
  d2  <- outer(msk[, 1], tr[, 1], "-")^2 + outer(msk[, 2], tr[, 2], "-")^2
  list(tr = tr, d2 = d2, lo = lo, hi = hi, spacing = spacing,
       cell = (hi - lo) / n_cell, n_true = round(density_fix * (hi - lo)^2))
}

# one capture history array (animals x traps x occasions) for detected animals
simulate_caps <- function(des, p0, p0_after, response) {
  n_all <- des$n_true; n_tr <- nrow(des$tr)
  sx <- runif(n_all, des$lo, des$hi); sy <- runif(n_all, des$lo, des$hi)
  h  <- exp(-(outer(sx, des$tr[, 1], "-")^2 + outer(sy, des$tr[, 2], "-")^2) /
              (2 * sigma_true^2))
  at_trap <- matrix(FALSE, n_all, n_tr); any_trap <- rep(FALSE, n_all)
  y <- array(FALSE, c(n_all, n_tr, k_occ))
  for (k in seq_len(k_occ)) {
    resp <- switch(response,
                   local  = at_trap,
                   global = matrix(any_trap, n_all, n_tr),
                   none   = matrix(FALSE, n_all, n_tr))
    yk <- matrix(runif(n_all * n_tr) < ifelse(resp, p0_after, p0) * h, n_all, n_tr)
    y[, , k] <- yk
    at_trap  <- at_trap | yk
    any_trap <- any_trap | rowSums(yk) > 0
  }
  y[apply(y, 1, any), , , drop = FALSE]
}
des_base <- make_design(1.25)

The mask cell comes out at 0.590 units at the base spacing. The first thing to look at is the raw data, before any model: for every detected animal, where do its captures after the first occasion fall, measured from the trap where it was first caught? The response in this check is fourfold, detection 0.10 before and 0.40 after.

set.seed(2716)
n_pile <- 100
later_dist <- function(y, des) {
  out <- numeric(0)
  for (i in seq_len(dim(y)[1])) {
    yi <- y[i, , ]
    k1 <- which(colSums(yi) > 0)[1]
    j1 <- which(yi[, k1])[1]
    if (k1 == k_occ) next
    hits <- which(yi[, (k1 + 1):k_occ, drop = FALSE], arr.ind = TRUE)[, 1]
    out <- c(out, sqrt(colSums((t(des$tr[hits, , drop = FALSE]) - des$tr[j1, ])^2)) /
                  des$spacing)
  }
  out
}
pile_one <- function(response) {
  d <- unlist(lapply(seq_len(n_pile), function(r)
    later_dist(simulate_caps(des_base, p0_naive, 0.40, response), des_base)))
  cls <- cut(d, c(-0.01, 0.5, 1.2, 1.5, 2.1, Inf),
             labels = c("same trap", "1", "1.41", "2", "more than 2"))
  data.frame(response = response, dist = levels(cls),
             share = as.vector(table(cls)) / length(d), n_later = length(d))
}
pile <- do.call(rbind, lapply(c("none", "global", "local"), pile_one))
same_share <- setNames(pile$share[pile$dist == "same trap"],
                       pile$response[pile$dist == "same trap"])
n_later_all <- setNames(pile$n_later[pile$dist == "same trap"],
                        pile$response[pile$dist == "same trap"])
pile$dist <- factor(pile$dist, levels = c("same trap", "1", "1.41", "2", "more than 2"))
pile$response <- factor(pile$response, levels = c("none", "global", "local"),
                        labels = c("no response", "global response", "trap-specific response"))
ggplot(pile, aes(dist, share, fill = response)) +
  geom_col(position = position_dodge(width = 0.8), width = 0.75) +
  scale_fill_manual(values = c(te_gold, te_forest, te_rust), name = NULL) +
  labs(x = "distance from first-capture trap (trap spacings)",
       y = "share of later captures",
       title = "A learned trap collects the later captures") +
  theme_datasheet() +
  theme(legend.position = "top")
A grouped column chart on warm off-white paper headed A learned trap collects the later captures. The horizontal axis has five classes of distance from the first-capture trap: same trap, 1, 1.41, 2 and more than 2 trap spacings; the vertical axis is the share of later captures from 0 to 0.4. Gold columns for no response and dark green columns for a global response are nearly the same height in every class, about 0.16 at the same trap, 0.37 at one spacing, 0.21 at 1.41, 0.09 at 2 and 0.15 to 0.16 beyond. Red columns for the trap-specific response reach 0.39 at the same trap and are lower than the other two in every other class, at about 0.29, 0.15, 0.07 and 0.10.
Figure 1: Where an animal’s captures after its first occasion fall, measured from its first-capture trap in units of trap spacing, pooled over 100 simulated data sets for each truth. The response is fourfold after capture.

With no response, 16 per cent of later captures land back in the first-capture trap, a share set by the grid geometry alone. A global response leaves that share at 17 per cent: the animals are caught more often, 21361 later captures against 5318 over the same number of data sets, but they are caught in the same places. A trap-specific response moves the share to 39 per cent, and every other distance class loses what the first trap gains. That tighter spatial pattern is what a half-normal detection function will fit.

Three models on one likelihood

The likelihood is the conditional one of Borchers and Efford (2008): each detected animal’s capture history is integrated over a discrete mask of possible activity centres, and the product is divided by the probability of being detected at all, raised to the number detected. Abundance in the state space then follows as a Horvitz-Thompson estimate, the number detected divided by the mean detection probability over the mask. The three models differ only in which occasions count as “after the response” for each animal and trap: none of them in M0, every occasion after the animal’s first capture in the global Mb, and every occasion after the animal’s first capture at that particular trap in the trap-specific model, called Mbk here. The probability of being detected at all involves only naive detection in all three, because nothing has been learned before a first capture.

The code builds three tallies per animal and trap (naive captures, responded captures, responded occasions) so that the log likelihood on the mask is a few sums over the traps where each animal was caught.

tallies <- function(y, model) {
  n <- dim(y)[1]; n_tr <- dim(y)[2]
  n1 <- m1 <- b <- matrix(0, n_tr, n)
  seen_here <- matrix(FALSE, n, n_tr); seen_any <- rep(FALSE, n)
  for (k in seq_len(k_occ)) {
    yk <- y[, , k]
    resp <- switch(model,
                   M0  = matrix(FALSE, n, n_tr),
                   Mb  = matrix(seen_any, n, n_tr),
                   Mbk = seen_here)
    n1 <- n1 + t(yk & !resp)       # captures while naive
    m1 <- m1 + t(yk & resp)        # captures after the response
    b  <- b + t(resp)              # occasions after the response
    seen_here <- seen_here | yk
    seen_any  <- seen_any | rowSums(yk) > 0
  }
  list(n1 = n1, m1 = m1, b = b)
}

# the tallies are sparse: only traps where an animal was caught carry counts,
# so each product with a mask x trap matrix is a sum over those traps alone
to_triplet <- function(tab) {
  w <- which(tab != 0, arr.ind = TRUE)
  list(j = w[, 1], i = w[, 2], v = tab[w], n = ncol(tab))
}
sparse_prod <- function(lmat, trip) {
  out <- matrix(0, nrow(lmat), trip$n)
  if (length(trip$v) == 0) return(out)
  r <- rowsum(t(lmat[, trip$j, drop = FALSE]) * trip$v, trip$i)
  out[, as.integer(rownames(r))] <- t(r)
  out
}

fit_scr <- function(y, des, model, start = NULL) {
  tl <- tallies(y, model); n <- dim(y)[1]
  s_n1 <- to_triplet(tl$n1); s_m1 <- to_triplet(tl$m1)
  if (model == "Mb") b_animal <- tl$b[1, ] else s_b <- to_triplet(tl$b)
  nll <- function(th) {
    p0 <- plogis(th[1]); sg <- exp(th[2])
    p0_after <- if (model == "M0") p0 else plogis(th[3])
    h  <- exp(-des$d2 / (2 * sg^2))
    pr <- pmin(p0 * h, 1 - 1e-12); pr_after <- pmin(p0_after * h, 1 - 1e-12)
    l0 <- log1p(-pr); l0_after <- log1p(-pr_after)
    lf <- sparse_prod(log(pr) - l0, s_n1) + k_occ * rowSums(l0)
    if (model != "M0") {
      lf <- lf + sparse_prod(log(pr_after) - l0_after, s_m1)
      # under Mb the responded occasions are the same at every trap
      lf <- lf + if (model == "Mb") outer(rowSums(l0_after - l0), b_animal) else
        sparse_prod(l0_after - l0, s_b)
    }
    pdot <- 1 - exp(k_occ * rowSums(l0))
    mx <- lf[cbind(max.col(t(lf), ties.method = "first"), seq_len(n))]
    v  <- -(sum(mx + log(colSums(exp(lf - rep(mx, each = nrow(lf)))))) - n * log(sum(pdot)))
    if (is.finite(v)) v else 1e10
  }
  st <- if (is.null(start)) c(qlogis(0.1), log(1)) else start
  o  <- optim(st, nll, method = "BFGS", control = list(maxit = 500))
  p0 <- plogis(o$par[1]); sg <- exp(o$par[2])
  pdot <- 1 - exp(k_occ * rowSums(log1p(-pmin(p0 * exp(-des$d2 / (2 * sg^2)), 1 - 1e-12))))
  list(par = o$par, p0 = p0, sigma = sg,
       p0_after = if (model == "M0") p0 else plogis(o$par[3]),
       n_hat = n / mean(pdot), aic = 2 * o$value + 2 * length(o$par),
       conv = o$convergence)
}

fit_three <- function(y, des) {
  f0 <- fit_scr(y, des, "M0")
  st <- c(f0$par, f0$par[1])          # Mb and Mbk start from the M0 answer
  list(M0 = f0, Mb = fit_scr(y, des, "Mb", st), Mbk = fit_scr(y, des, "Mbk", st))
}

set.seed(4417)
y_ex <- simulate_caps(des_base, p0_naive, 0.40, "local")
fits_ex <- fit_three(y_ex, des_base)
ex_tab <- data.frame(model = names(fits_ex),
  sigma_ratio = sapply(fits_ex, function(f) f$sigma / sigma_true),
  p0 = sapply(fits_ex, `[[`, "p0"), p0_after = sapply(fits_ex, `[[`, "p0_after"),
  aic = sapply(fits_ex, `[[`, "aic"))
ex_tab$d_aic <- ex_tab$aic - min(ex_tab$aic)
print(ex_tab[, c("model", "sigma_ratio", "p0", "p0_after", "d_aic")], digits = 3, row.names = FALSE)
 model sigma_ratio    p0 p0_after d_aic
    M0       0.881 0.207    0.207  33.0
    Mb       0.881 0.116    0.244  22.5
   Mbk       1.019 0.115    0.383   0.0

One data set from the fourfold trap-specific truth catches 59 of the 150 animals. M0 puts sigma at 0.88 of its true value. The global Mb estimates detection after capture at 0.244 against 0.116 before, so it has found the response, and it still puts sigma at 0.88. The trap-specific model estimates 0.383 after and 0.115 before, puts sigma at 1.02, and is ahead of Mb by 22.5 AIC units. One data set is an anecdote; the next section repeats it.

Sigma shrinks, and the global term cannot move it

Four settings share the base spacing of 1.25 sigma. Two are trap-happy trap-specific responses, twofold (detection 0.10 to 0.20) and fourfold (0.10 to 0.40). One is trap-shy and trap-specific, halving detection from 0.30 to 0.15. The fourth is the control: a fourfold response that is global, so the truth is Mb. All design constants except the number of draws in the fourfold trap-specific cells at 1.25 and 2.0 sigma were fixed before any fits were run. The twofold and fourfold trap-specific cells get 40 simulated data sets each, the global control 20 and the trap-shy cell 10.

one_draw <- function(des, p0, p0_after, response) {
  y  <- simulate_caps(des, p0, p0_after, response)
  f  <- fit_three(y, des)
  traps_per <- mean(rowSums(apply(y, c(1, 2), any)))
  c(n_caught = dim(y)[1], traps = traps_per,
    s_M0 = f$M0$sigma, s_Mb = f$Mb$sigma, s_Mbk = f$Mbk$sigma,
    N_M0 = f$M0$n_hat, N_Mb = f$Mb$n_hat, N_Mbk = f$Mbk$n_hat,
    best = which.min(c(f$M0$aic, f$Mb$aic, f$Mbk$aic)),
    conv = f$M0$conv + f$Mb$conv + f$Mbk$conv)
}
run_cell <- function(id, spacing_sigma, p0, p0_after, response, n_draw) {
  des <- make_design(spacing_sigma)
  m <- t(replicate(n_draw, one_draw(des, p0, p0_after, response)))
  data.frame(cell = id, spacing_sigma = spacing_sigma, ratio = p0_after / p0,
             response = response, n_true = des$n_true, m)
}
t_cells <- proc.time()
set.seed(8203)
cells <- rbind(
  run_cell("A", 1.25, 0.10, 0.20, "local",  40),
  run_cell("B", 1.25, 0.10, 0.40, "local",  40),
  run_cell("E", 1.25, 0.10, 0.40, "global", 20),
  run_cell("F", 1.25, 0.30, 0.15, "local",  10),
  run_cell("C", 2.00, 0.10, 0.40, "local",  40),
  run_cell("D", 2.50, 0.10, 0.40, "local",  20))
t_cells <- (proc.time() - t_cells)[["user.self"]]
for (m in c("M0", "Mb", "Mbk")) {
  cells[[paste0("sr_", m)]] <- cells[[paste0("s_", m)]] / sigma_true
  cells[[paste0("nr_", m)]] <- cells[[paste0("N_", m)]] / cells$n_true
}
cell_sum <- do.call(rbind, lapply(split(cells, cells$cell), function(d) {
  data.frame(cell = d$cell[1], draws = nrow(d), n_true = d$n_true[1],
    n_caught = median(d$n_caught),
    traps = mean(d$traps),
    sr_M0 = median(d$sr_M0), sr_Mb = median(d$sr_Mb), sr_Mbk = median(d$sr_Mbk),
    mean_Mb = mean(d$sr_Mb), se_Mb = sd(d$sr_Mb) / sqrt(nrow(d)),
    lo_Mb = min(d$sr_Mb), hi_Mb = max(d$sr_Mb),
    lo_Mbk = min(d$sr_Mbk), hi_Mbk = max(d$sr_Mbk),
    gap = mean(d$sr_Mbk - d$sr_Mb), gap_se = sd(d$sr_Mbk - d$sr_Mb) / sqrt(nrow(d)),
    pick_M0 = mean(d$best == 1), pick_Mb = mean(d$best == 2), pick_Mbk = mean(d$best == 3),
    nr_M0 = median(d$nr_M0), nr_Mb = median(d$nr_Mb), nr_Mbk = median(d$nr_Mbk),
    nr_Mb_hi = max(d$nr_Mb), nr_Mb_n2 = sum(d$nr_Mb > 2), nr_Mbk_lo = min(d$nr_Mbk), nr_Mbk_hi = max(d$nr_Mbk),
    n_fail = sum(d$conv > 0))
}))
cs <- function(id, col) cell_sum[cell_sum$cell == id, col]
bc_diff <- cs("B", "mean_Mb") - cs("C", "mean_Mb")
bc_se   <- sqrt(cs("B", "se_Mb")^2 + cs("C", "se_Mb")^2)
print(cell_sum[, c("cell", "draws", "sr_M0", "sr_Mb", "sr_Mbk", "pick_Mbk")], digits = 3,
      row.names = FALSE)
 cell draws sr_M0 sr_Mb sr_Mbk pick_Mbk
    A    40 0.952 0.953  1.020     0.95
    B    40 0.823 0.824  0.988     1.00
    C    40 0.781 0.787  1.003     1.00
    D    20 0.769 0.777  1.017     0.75
    E    20 1.000 0.996  1.040     0.00
    F    10 1.039 1.038  0.991     1.00
sig_long <- do.call(rbind, lapply(c("M0", "Mb", "Mbk"), function(m)
  data.frame(cell = cells$cell, model = m, ratio = cells[[paste0("sr_", m)]])))
sig_long <- sig_long[sig_long$cell %in% c("A", "B", "F", "E"), ]
cell_lab <- c(A = "trap-specific, twofold", B = "trap-specific, fourfold",
              F = "trap-specific, trap-shy (half)", E = "control: global, fourfold")
sig_long$panel <- factor(cell_lab[sig_long$cell], levels = cell_lab)
sig_long$model <- factor(sig_long$model, levels = c("M0", "Mb", "Mbk"))
sig_med <- aggregate(ratio ~ panel + model, data = sig_long, FUN = median)
set.seed(11)
ggplot(sig_long, aes(model, ratio, colour = model)) +
  geom_hline(yintercept = 1, linetype = "dashed", colour = te_ink, linewidth = 0.5) +
  geom_jitter(width = 0.15, height = 0, size = 1.3, alpha = 0.6) +
  geom_crossbar(data = sig_med, aes(ymin = ratio, ymax = ratio), width = 0.5,
                linewidth = 0.6, colour = te_ink) +
  scale_colour_manual(values = c(M0 = te_gold, Mb = te_forest, Mbk = te_rust), guide = "none") +
  facet_wrap(~ panel, ncol = 2) +
  labs(x = "model fitted", y = "estimated sigma / true sigma",
       title = "Only the trap-specific model follows the address") +
  theme_datasheet() +
  theme(strip.text = element_text(colour = te_ink, face = "bold"))
Four panels of jittered points on warm off-white paper headed Only the trap-specific model follows the address, each with gold points for M0, green for Mb and red for Mbk and a black bar at each median; the vertical axis is estimated over true sigma from 0.7 to about 1.15, with a dashed line at 1. Trap-specific twofold: M0 and Mb medians both near 0.95, Mbk near 1.02. Trap-specific fourfold: M0 and Mb medians near 0.82 with points from about 0.71 to 0.96, Mbk near 0.99 with points from about 0.85 to 1.14. Trap-specific trap-shy: M0 and Mb near 1.04, Mbk just below 1. Control with a global fourfold response: M0 and Mb on the line at 1, Mbk a little above it at about 1.04.
Figure 2: Estimated sigma over true sigma for the three fitted models at a trap spacing of 1.25 sigma, one panel per truth. Small points are single simulated data sets, the bar is the median, and the dashed line marks the true value.

At the fourfold trap-happy response the median sigma is 0.82 of the truth under M0 and 0.82 under the global Mb, with Mb ranging from 0.71 to 0.96 over the 40 data sets. The global term leaves sigma where M0 put it. The trap-specific model gives 0.99, ranging from 0.85 to 1.14, and the mean paired difference between it and Mb on the same data is 0.161 (Monte Carlo standard error 0.005). AIC picks the trap-specific model in 100 per cent of the data sets.

The twofold response, close to the rise that Schmidt and colleagues took from their own bear data, does much less damage. M0 and Mb both give a median of 0.95 and the trap-specific model 1.02. The paired difference is 0.077 (standard error 0.005) over 40 data sets, so the direction is the same as at fourfold, but the shortfall under Mb is small against the spread of single estimates, which runs from 0.75 to 1.06. AIC still prefers the trap-specific model in 95 per cent of them.

The trap-shy cell runs the other way, as the mechanism says it should: halving detection at the learned trap pushes later captures off it, and M0 and Mb read the wider spread as a sigma of 1.04, while the trap-specific model gives 0.99. It is based on 10 data sets.

The control settles what the cause is. When the fourfold response is global, M0, which ignores the response altogether, still gets sigma right at 1.00, the correct Mb gives 1.00, and AIC picks Mb in 100 per cent of data sets. A response on its own does not bias sigma. A response with an address does. The trap-specific model fitted to the global truth gives 1.04, so it is not a free insurance policy either: fitting the wrong address pushes sigma the other way, although by less.

Wider spacing, larger damage

SCR sampling design and precision found its precision optimum near two sigma and quotes the standard guidance “that traps should sit no more than about two sigma apart (Sun, Fuller and Royle 2014)”. Two more cells run the fourfold trap-specific response at a spacing of 2.0 sigma, which is that guidance, and at 2.5 sigma, just past it, with density held at the base value so that the state space holds 250 and 330 animals.

sp_cells <- cell_sum[cell_sum$cell %in% c("B", "C", "D"), ]
sp_cells$spacing <- c(B = 1.25, C = 2.0, D = 2.5)[sp_cells$cell]
sp_long <- do.call(rbind, lapply(c("M0", "Mb", "Mbk"), function(m) {
  d <- cells[cells$cell %in% c("B", "C", "D"), ]
  r <- d[[paste0("sr_", m)]]
  data.frame(spacing = tapply(d$spacing_sigma, d$cell, `[`, 1), model = m,
             med = tapply(r, d$cell, median), lo = tapply(r, d$cell, min),
             hi = tapply(r, d$cell, max))
}))
sp_long$model <- factor(sp_long$model, levels = c("M0", "Mb", "Mbk"))
pos <- position_dodge(width = 0.18)
p_sig <- ggplot(sp_long, aes(spacing, med, colour = model, shape = model)) +
  geom_hline(yintercept = 1, linetype = "dashed", colour = te_ink, linewidth = 0.5) +
  geom_errorbar(aes(ymin = lo, ymax = hi), width = 0, linewidth = 0.6, position = pos) +
  geom_point(size = 2.6, position = pos) +
  scale_colour_manual(values = c(M0 = te_gold, Mb = te_forest, Mbk = te_rust), name = NULL) +
  scale_shape_manual(values = c(M0 = 16, Mb = 17, Mbk = 15), name = NULL) +
  scale_x_continuous(breaks = c(1.25, 2, 2.5)) +
  labs(x = "trap spacing (sigma units)", y = "estimated sigma / true sigma",
       title = "Spacing does not rescue Mb") +
  theme_datasheet() + theme(legend.position = "top")
p_tr <- ggplot(sp_cells, aes(spacing, traps)) +
  geom_line(colour = te_ink, linewidth = 0.6) +
  geom_point(colour = te_ink, size = 2.6) +
  scale_x_continuous(breaks = c(1.25, 2, 2.5)) +
  coord_cartesian(ylim = c(1, NA)) +
  labs(x = "trap spacing (sigma units)", y = "distinct traps per animal",
       title = "Spatial recaptures") +
  theme_datasheet()
(p_sig | p_tr) + plot_layout(widths = c(2, 1)) +
  plot_annotation(theme = theme_datasheet())
Two panels on warm off-white paper. The left panel, headed Spacing does not rescue Mb, shows points with vertical range bars at trap spacings of 1.25, 2 and 2.5 sigma, with a dashed line at 1. Gold circles for M0 and green triangles for Mb sit side by side at about 0.82, 0.79 and 0.78; red squares for Mbk sit at about 0.99, 1.00 and 1.02, with range bars from about 0.85 to 1.14 at 1.25 sigma that widen to about 0.8 to 1.36 at 2 sigma and 0.75 to 1.33 at 2.5 sigma. The right panel, headed Spatial recaptures, shows a black line falling from about 1.87 distinct traps per animal at 1.25 sigma to 1.29 at 2 sigma and 1.13 at 2.5 sigma.
Figure 3: Left: median estimated sigma over true sigma, with the range over the simulated data sets (40 at 1.25 and 2.0 sigma, 20 at 2.5 sigma), for the fourfold trap-specific response at three trap spacings. Right: mean number of distinct traps at which a detected animal was caught.

At 2.0 sigma the median sigma under Mb is 0.79, against 0.82 at 1.25 sigma, and at 2.5 sigma it is 0.78. The means tell the same story with a standard error attached: 0.824 (standard error 0.009) at 1.25 sigma and 0.790 (0.009) at 2.0 sigma, a difference of 0.035 with a standard error of 0.013. So the damage does grow from the base spacing to the recommended one, but modestly, and just past the recommendation it holds level at 0.787. The paired difference between the trap-specific model and Mb on the same data says the same: 0.161 (standard error 0.005) at 1.25 sigma, 0.219 (0.011) at 2.0 sigma and 0.210 (0.022) at 2.5 sigma. The trap-specific model gives medians of 1.00 and 1.02 at the two wider spacings.

The right panel suggests why. A detected animal is caught at 1.87 distinct traps on average at 1.25 sigma, 1.29 at 2.0 sigma and 1.13 at 2.5 sigma. At wide spacing the information about sigma rests on the few captures away from the first trap, and those are exactly the captures that a trap-happy response draws back to the first trap. The same spatial recaptures that the sampling-design post calls the currency are what the local response spends.

AIC picks the trap-specific model in 100 per cent of data sets at the base spacing, 100 per cent at 2.0 sigma and 75 per cent at 2.5 sigma, where it lost to the global Mb in 20 per cent. At the recommended spacing the address stays visible; just past it, the response begins to slip past model selection in a minority of data sets. Density is held fixed across the three spacings, so the wider grids are not sparser: they catch more animals (a median of 88 at 2.5 sigma against 55 at 1.25 sigma), and what changes with spacing is how many traps each animal meets.

Abundance, in a table

Sigma is the headline because it is where the address acts. Abundance moves too, but with 10 to 40 data sets per cell the medians below are noisy, so the table is given for completeness and most of its numbers are not interpreted further. The ratio is the Horvitz-Thompson estimate over the true number of activity centres in the state space, not over the number caught.

n_tab <- data.frame(
  setting = c("twofold, 1.25 sigma", "fourfold, 1.25 sigma", "trap-shy, 1.25 sigma",
              "global control, 1.25 sigma", "fourfold, 2.0 sigma", "fourfold, 2.5 sigma"),
  draws = cell_sum$draws[match(c("A", "B", "F", "E", "C", "D"), cell_sum$cell)])
for (m in c("M0", "Mb", "Mbk"))
  n_tab[[m]] <- sprintf("%.2f", cell_sum[[paste0("nr_", m)]][match(c("A", "B", "F", "E", "C", "D"),
                                                                  cell_sum$cell)])
knitr::kable(n_tab, col.names = c("truth and spacing", "data sets", "M0", "Mb", "Mbk"),
             caption = "Median estimated abundance over true abundance in the state space.")
Median estimated abundance over true abundance in the state space.
truth and spacing data sets M0 Mb Mbk
twofold, 1.25 sigma 40 0.98 0.99 1.02
fourfold, 1.25 sigma 40 0.92 1.06 1.00
trap-shy, 1.25 sigma 10 1.04 1.00 1.03
global control, 1.25 sigma 20 0.77 0.98 0.77
fourfold, 2.0 sigma 40 0.72 0.94 0.99
fourfold, 2.5 sigma 20 0.62 0.97 0.91

Two things in it are safe to read. M0 is low at every fourfold setting: 0.92 at the trap-specific response on the base grid, 0.77 in the global control, where its sigma is right, 0.72 at 2.0 sigma and 0.62 at 2.5 sigma. And single estimates become unstable just past the recommended spacing: at 2.5 sigma 4 of the 20 global Mb fits returned more than twice the true abundance, one of them 170 times the truth, and even the trap-specific model runs from 0.73 to 1.90 times the truth there. An abundance headline would need at least 50 data sets per cell, and none of the cells here has that many.

What to report

State whether the traps were baited or lured and whether the same trap positions were used on every occasion. That is the condition under which a trap-specific response can exist, and a reader cannot judge the sigma estimate without it.

Fit the trap-specific model alongside M0 and the global Mb and report the AIC comparison. Whether a lure at every station makes the learned response local to one station or global to the grid is an empirical question, and this comparison is how to answer it: here it picked the trap-specific model in 100 per cent of the fourfold trap-specific data sets on the base grid and in 95 per cent even at twofold, and it picked Mb in 100 per cent when the truth was global, so the address is visible in data of this size. Reporting a global Mb as “the behavioural response model” is not enough, because it cannot correct the quantity that the address biases.

Report the share of later captures that fall in the animal’s own first-capture trap next to the number of distinct traps per animal. It needs no model; compare it with the share in data simulated from the fitted M0, and a share well above that is the first sign of a learned trap. Report sigma with its estimated detection before and after capture, so that a reader can see how strong the response is that the sigma estimate depends on.

If sigma is used to derive a home-range size or to plan the next survey’s spacing, say which model it came from. A sigma from M0 or Mb under a trap-happy local response is short, and a spacing planned from it will be tighter than it needs to be.

Honest limits

Detection here is Bernoulli per trap and occasion with a half-normal shape. A hazard formulation (p = 1 - exp(-lambda0 h)) changes only the shape, and the pile-up mechanism does not depend on it. A detector where an animal can be caught at only one trap per occasion (live traps) makes traps compete, and whether that competition enlarges or shrinks the pile-up was not measured.

The response here is permanent and all or nothing: once caught at a trap, the animal keeps the raised detection there for every later occasion. Real responses fade, and secr’s transient Bk version, which depends only on the previous occasion, is a separate model that was not fitted. A fading response should produce a smaller pile-up and a smaller sigma shortfall than the permanent version here, and fitting a permanent Mbk to it would be a misspecification of its own.

Every animal responds identically. A population where some animals learn a trap and others ignore the bait mixes the trap-specific response with individual heterogeneity, and nothing above says what that mixture does to sigma.

The trap-specific model is not neutral when the truth is global: it gave a sigma of 1.04 in the control cell. AIC chose it in 0 of the 20 control data sets, so model selection protected the estimate in this design; with fewer animals or occasions that protection could be weaker, and that case was not run.

Replication is modest: 40 data sets in the twofold cell and in the fourfold cells at 1.25 and 2.0 sigma, 20 at 2.5 sigma and in the global control, and 10 in the trap-shy cell. The fourfold contrasts between models are large against the draw-to-draw spread, the twofold contrast and the growth from 1.25 to 2.0 sigma are not, which is why both carry a standard error. The two fourfold cells at 1.25 and 2.0 sigma were first run with 20 data sets each; that run left the growth between them unresolved, so both were raised to 40, with no design constant changed. The abundance table is below the replication needed to interpret it. The mask cell of about 0.6 units was chosen for speed, and the whole set of fits took 76 seconds of processor time on the machine that built this page.

There is no small-mammal anchor for the size of the response. The twofold ratio is close to the one Schmidt and colleagues estimated for bears (they simulated detection of 0.07 before and 0.15 after first capture, values taken from their own trap-specific fit at the scent-lured corrals); the fourfold ratio is a design choice.

References

Borchers DL, Efford MG 2008 Biometrics 64(2):377-385 (10.1111/j.1541-0420.2007.00927.x)

Efford MG 2004 Oikos 106(3):598-610 (10.1111/j.0030-1299.2004.13043.x)

Schmidt GM, Graves TA, Pederson JC, Carroll SL 2022 Ecological Applications 32(5):e2618 (10.1002/eap.2618)

Sun CC, Fuller AK, Royle JA 2014 PLoS ONE 9(2):e88025 (10.1371/journal.pone.0088025)

Royle JA, Chandler RB, Sollmann R, Gardner B 2014 Spatial Capture-Recapture (ISBN 978-0-12-405939-9)

Newsletter

Get new tutorials by email

New R and QGIS tutorials for ecologists, straight to your inbox. No spam; unsubscribe anytime.

By subscribing you agree to receive these emails and confirm your address once. See the privacy policy.