---
title: "Checking a foraging analysis"
description: "Three checks for a foraging study in R: prey depletion in functional-response trials, telling type II from type III, and which currency the forager maximises."
date: "2026-07-30 09:00"
categories: [ecology tutorial, R, foraging, model diagnostics, behaviour]
image: thumbnail.png
image-alt: "Estimated attack rate from a disc-equation fit and a depletion-corrected fit, with the disc equation falling well short of the true value."
---
```{r setup}
#| include: false
knitr::opts_chunk$set(echo = TRUE, message = FALSE, warning = FALSE,
dev = "png", dpi = 120, fig.path = "figures/",
fig.width = 7, fig.height = 4.4)
library(ggplot2)
theme_te <- theme_minimal(base_size = 12) +
theme(panel.background = element_rect(fill = "white", colour = NA),
plot.background = element_rect(fill = "white", colour = NA),
panel.grid.minor = element_blank(),
plot.title = element_text(face = "bold", size = 12))
theme_set(theme_te)
col_true <- "#555555"; col_disc <- "#b5651d"; col_rog <- "#3a6b52"
## (a) depletion
a0 <- 1.0; h0 <- 0.03; Tt <- 1
rogers_Ne <- function(N0) uniroot(function(Ne) Ne - N0*(1 - exp(-a0*(Tt - h0*Ne))), c(0, N0 - 1e-9))$root
set.seed(398)
N0 <- rep(c(5, 10, 20, 40, 60, 80, 100, 150), each = 6)
truth <- sapply(N0, rogers_Ne)
Ne <- pmin(rbinom(length(N0), N0, truth/N0), N0)
fdisc <- nls(Ne ~ aa*N0/(1 + aa*hh*N0), start = list(aa = 0.5, hh = 0.02), control = nls.control(warnOnly = TRUE))
rog_pred <- function(N0, aa, hh) sapply(N0, function(n) uniroot(function(Ne) Ne - n*(1 - exp(-aa*(Tt - hh*Ne))), c(0, n - 1e-9))$root)
frog <- nls(Ne ~ rog_pred(N0, aa, hh), start = list(aa = 0.6, hh = 0.02), control = nls.control(warnOnly = TRUE))
a_disc <- coef(fdisc)["aa"]; a_rog <- coef(frog)["aa"]
## (b) type II vs III proportion eaten
propII <- function(N) 0.9/(1 + 0.9*0.045*N)
propIII <- function(N) 0.05*N/(1 + 0.05*0.045*N^2)
Ng <- seq(1, 120, 1); peakIII <- Ng[which.max(propIII(Ng))]
## (c) currency
Gmax <- 30; k <- 0.18; g <- function(t) Gmax*(1 - exp(-k*t)); gp <- function(t) Gmax*k*exp(-k*t)
travel <- 6; mf <- 0.6
t_rate <- uniroot(function(t) gp(t) - g(t)/(t + travel), c(1e-6, 200))$root
tt_grid <- seq(0.01, 60, 0.01)
t_eff <- tt_grid[which.max((g(tt_grid) - mf*tt_grid)/(tt_grid + travel))]
cur_diff <- 100 * abs(t_rate - t_eff)/t_rate
```
Foraging theory makes sharp predictions, and sharp predictions are easy to test against data that were not collected to test them. Three checks catch the common ways a foraging analysis goes wrong: an estimate biased by the experiment itself, two models that the data cannot separate, and a prediction that depends on a choice the analyst never stated.
## Check 1: did the prey deplete while you measured?
Holling's disc equation assumes prey density is constant through a feeding trial. It is not: as the predator eats, prey run out, so the density it experiences falls below the density you set. Fit the disc equation to trial counts anyway and the attack rate comes out too low, because the model blames slow late-trial feeding on a low attack rate rather than on missing prey. Rogers 1972 The Journal of Animal Ecology 41(2):369-383 wrote down the random predator equation, which accounts for depletion, and it is solvable in base R with `uniroot`.
```{r depletion}
a0 <- 1.0; h0 <- 0.03 # the truth
rogers_Ne <- function(N0) uniroot(function(Ne) # prey eaten with depletion
Ne - N0*(1 - exp(-a0*(1 - h0*Ne))), c(0, N0 - 1e-9))$root
c(disc_attack_rate = unname(a_disc), rogers_attack_rate = unname(a_rog), true = a0)
```
Fitting the disc equation recovers an attack rate of `r sprintf("%.2f", a_disc)`, only `r sprintf("%.0f", 100*a_disc/a0)` per cent of the true `r sprintf("%.1f", a0)`. The depletion-aware fit recovers `r sprintf("%.2f", a_rog)`, `r sprintf("%.0f", 100*a_rog/a0)` per cent. The bias is not small and it is one-directional: any functional response fitted to trials where prey were not replaced will understate how fast the predator attacks. The check is to use the random predator equation, or to top prey up during the trial so density really is constant.
```{r fig-depletion}
#| fig-cap: "Estimated attack rate from the two fits. The disc equation, ignoring that prey deplete during a trial, lands well below the truth; the random predator equation recovers it."
#| fig-alt: "Bar chart of attack rate: a true value, a much lower disc-equation estimate, and a random-predator estimate close to the truth."
db <- data.frame(method = factor(c("true","disc equation","random predator"),
levels = c("true","disc equation","random predator")),
a = c(a0, a_disc, a_rog))
ggplot(db, aes(method, a, fill = method)) +
geom_col(width = 0.6) +
geom_hline(yintercept = a0, colour = col_true, linetype = "dashed") +
scale_fill_manual(values = c("true" = col_true, "disc equation" = col_disc, "random predator" = col_rog), guide = "none") +
labs(x = NULL, y = "Estimated attack rate", title = "Depletion biases the disc-equation attack rate down")
```
## Check 2: can the data tell type II from type III?
The [difference between type II and type III](../functional-responses-holling/) lives at low prey density, and so does the ability to tell them apart. The clean diagnostic is not intake but the *proportion* of prey eaten, intake divided by density. Under type II that proportion is highest at the lowest density and falls throughout. Under type III it rises to a hump and only then falls.
```{r proportion}
propII <- function(N) 0.9/(1 + 0.9*0.045*N) # Ne/N, type II
propIII <- function(N) 0.05*N/(1 + 0.05*0.045*N^2) # Ne/N, type III
rbind(type_II = round(propII(c(2, 20, 60)), 3),
type_III = round(propIII(c(2, 20, 60)), 3))
```
At a density of 2 the two responses predict proportions of `r sprintf("%.2f", propII(2))` and `r sprintf("%.2f", propIII(2))`, a large gap; by a density of 60 they have converged. If your feeding trials start at moderate densities, both curves fit and the proportion-eaten plot cannot separate them, yet they imply opposite things for whether the predator can regulate its prey. The check is to sample the low-density end deliberately, and to plot proportion eaten, not intake, where the two models actually diverge.
```{r fig-proportion}
#| fig-cap: "Proportion of prey eaten against density. Type II (ochre) falls from the start; type III (green) rises to a hump then falls. The models separate only at low density, shaded."
#| fig-alt: "Two curves of proportion eaten against prey density; the ochre type II curve declines throughout, the green type III curve rises then declines, differing most at low density."
dp <- rbind(data.frame(N = Ng, p = propII(Ng), type = "type II"),
data.frame(N = Ng, p = propIII(Ng), type = "type III"))
ggplot(dp, aes(N, p, colour = type)) +
annotate("rect", xmin = 0, xmax = 15, ymin = -Inf, ymax = Inf, fill = "#8aa29e", alpha = 0.15) +
geom_line(linewidth = 1) +
scale_colour_manual(values = c("type II" = col_disc, "type III" = col_rog)) +
labs(x = "Prey density", y = "Proportion of prey eaten", colour = NULL,
title = "The two responses separate only where prey are scarce")
```
## Check 3: which currency is the forager counting?
The [marginal value theorem](../the-marginal-value-theorem/) and the diet model both assume the forager maximises the long-run rate of energy intake. That is a choice, not a fact. An animal saving for a lean season may maximise net energy after paying its own foraging metabolism; one at risk of predation may minimise exposure time. Different currencies give different predictions from the same gain curve.
```{r currency}
g <- function(t) 30*(1 - exp(-0.18*t)); gp <- function(t) 30*0.18*exp(-0.18*t)
t_rate <- uniroot(function(t) gp(t) - g(t)/(t + 6), c(1e-6, 200))$root # maximise rate
tt <- seq(0.01, 60, 0.01)
t_eff <- tt[which.max((g(tt) - 0.6*tt)/(tt + 6))] # net of metabolism
round(c(rate_currency = t_rate, net_energy_currency = t_eff), 2)
```
A rate-maximiser leaves the patch at `r sprintf("%.1f", t_rate)`; a forager counting net energy after metabolism leaves at `r sprintf("%.1f", t_eff)`, a `r sprintf("%.0f", cur_diff)` per cent difference in predicted residence, with no change to the environment. When observed behaviour misses a rate-maximising prediction, the first question is not whether the theory is wrong but whether you guessed the currency (Pyke 1984 Annual Review of Ecology and Systematics 15(1):523-575). State the currency, and where you can, test more than one.
## Deviations are data
Foraging theory is a set of predictions about an optimiser, and animals are not optimisers so much as satisficers working with partial information and real risk. That is why these models earn their keep even when animals depart from them: a clean prediction turns every departure into a measurement. A predator that eats a poor prey it should ignore is telling you about recognition error or nutrient needs; a forager that stays in a patch too long is telling you about uncertainty or safety. Run the three checks so the numbers are sound, then read the residuals as biology, not as failure.
## Related tutorials
- [Functional responses in R](../functional-responses-holling/)
- [The marginal value theorem](../the-marginal-value-theorem/)
- [Diet choice and the zero-one rule](../diet-choice-and-the-zero-one-rule/)
- [Nonlinear regression with nls](../nonlinear-regression-with-nls/)
## References
- Holling CS 1959. The Canadian Entomologist 91(7):385-398 (10.4039/Ent91385-7).
- Pyke GH 1984. Annual Review of Ecology and Systematics 15(1):523-575 (10.1146/annurev.es.15.110184.002515).
- Rogers D 1972. The Journal of Animal Ecology 41(2):369-383 (10.2307/3474).