Revisit designs for long-term monitoring

R
survey design
monitoring
ecology tutorial
Four revisit schedules on one annual budget in R: re-using sites sharpens the trend and blurs the regional mean, and the design enters through one matrix.
Author

Tidy Ecology

Published

2026-07-25

A monitoring programme can afford a fixed number of site visits per year. The number is set by staff and money, and it rarely changes. What does change, and what is nearly free to change, is which sites those visits go to: the same ones every year, a fresh set every year, or something in between.

That choice is the revisit design. It is usually made once, at the start, without measurement. This post spends the same annual budget four different ways over the same twelve years, and works out exactly what each one delivers.

Four ways to spend the same budget

Thirty site visits a year, twelve years, two counts per visit. Every design below costs exactly the same.

n_visit <- 30      # site visits per year
n_year  <- 12
n_rep   <- 2       # counts per site visit

# a design maps each year to the site identifiers visited that year
visits <- function(design) {
  switch(design,
    always = lapply(1:n_year, function(j) 1:n_visit),
    rotate = lapply(1:n_year, function(j) ((j - 1) %% 4) * n_visit + 1:n_visit),
    split  = lapply(1:n_year, function(j) c(1:15, 15 + ((j - 1) %% 4) * 15 + 1:15)),
    fresh  = lapply(1:n_year, function(j) (j - 1) * n_visit + 1:n_visit))
}
designs <- c("always", "rotate", "split", "fresh")
distinct <- sapply(designs, function(d) length(unique(unlist(visits(d)))))
data.frame(design = designs, distinct_sites = unname(distinct),
           visits_per_site = round(n_visit * n_year / unname(distinct), 1))
  design distinct_sites visits_per_site
1 always             30            12.0
2 rotate            120             3.0
3  split             75             4.8
4  fresh            360             1.0

Always revisit keeps 30 permanent sites and returns to all of them every year. Rotate holds 120 sites in four panels of 30 and works through them on a four year cycle, so each site is seen three times in twelve years. Split keeps 15 permanent sites and rotates the other 15 visits through four panels of 15. Fresh draws 30 new sites every year and never returns.

Four small grids side by side showing which site is visited in which year: a solid block for always revisit, a repeating diagonal for rotate, a solid strip plus a diagonal for split, and a single staircase for fresh sites.
Figure 1: Visit schedules for the four designs. Each row is a site in the pool and each column a year; a filled cell is a visit. Only the first 60 site slots are drawn, so the fresh design shows just its first two annual blocks. Every panel has the same number of filled cells in every column.

The design enters through one matrix

The data model is the one from the previous post: a persistent site level, a year effect shared by every site, a site by year interaction, and counting noise on each visit. Both headline numbers come from the annual means, so the whole comparison reduces to the covariance matrix of those 12 numbers.

Two years share a year effect only if they are the same year. They share site levels in proportion to how many sites they have in common. Write that overlap count as O[j, j'], and the covariance of the annual means is

sd_site <- 0.55; sd_year <- 0.20; sd_sy <- 0.30; sd_res <- 0.35
beta_true <- -0.03
tc  <- (1:n_year) - mean(1:n_year)
Sxx <- sum(tc^2)

overlap <- function(v) {
  outer(1:n_year, 1:n_year, Vectorize(function(i, j) length(intersect(v[[i]], v[[j]]))))
}
cov_annual <- function(v) {
  within_year <- sd_year^2 + (sd_sy^2 + sd_res^2 / n_rep) / n_visit
  diag(n_year) * within_year + sd_site^2 * overlap(v) / n_visit^2
}
overlap(visits("split"))[1:5, 1:5]
     [,1] [,2] [,3] [,4] [,5]
[1,]   30   15   15   15   30
[2,]   15   30   15   15   15
[3,]   15   15   30   15   15
[4,]   15   15   15   30   15
[5,]   30   15   15   15   30

The split panel shares 15 sites between any two years and all 30 when the years are four apart. That single matrix carries the entire design.

The slope from a regression of the annual means on year is a fixed linear combination of them, with weights w = (t - tbar) / Sxx that sum to zero. The period mean uses equal weights. So both variances are quadratic forms, and no simulation is needed.

w_trend  <- tc / Sxx
w_status <- rep(1 / n_year, n_year)

exact <- t(sapply(designs, function(d) {
  S <- cov_annual(visits(d))
  c(se_trend  = sqrt(drop(t(w_trend)  %*% S %*% w_trend)),
    se_status = sqrt(drop(t(w_status) %*% S %*% w_status)))
}))
exact <- data.frame(design = designs, distinct = unname(distinct), exact,
                    trend_rel  = exact[, "se_trend"]  / exact[1, "se_trend"],
                    status_rel = exact[, "se_status"] / exact[1, "se_status"])
print(exact, digits = 4, row.names = FALSE)
 design distinct se_trend se_status trend_rel status_rel
 always       30  0.01775   0.11763     1.000     1.0000
 rotate      120  0.01836   0.07921     1.035     0.6734
  split       75  0.01806   0.10028     1.017     0.8525
  fresh      360  0.01963   0.06778     1.106     0.5762

Every design gives an unbiased slope, so the table is a comparison of precision alone, and the two columns move in opposite directions.

set.seed(368)
run_design <- function(design, B = 3000) {
  v <- visits(design); npool <- length(unique(unlist(v)))
  t(replicate(B, {
    s <- rnorm(npool, 0, sd_site)
    a <- rnorm(n_year, 0, sd_year)
    annual <- vapply(1:n_year, function(j) {
      ids <- v[[j]]
      mean(3 + beta_true * tc[j] + s[ids] + a[j] +
           rnorm(length(ids), 0, sd_sy) + rnorm(length(ids), 0, sd_res / sqrt(n_rep)))
    }, numeric(1))
    c(slope = unname(coef(lm(annual ~ tc))[2]), status = mean(annual))
  }))
}
mc <- t(sapply(designs, function(d) {
  m <- run_design(d)
  c(mc_trend = sd(m[, "slope"]), mc_status = sd(m[, "status"]),
    bias = mean(m[, "slope"]) - beta_true)
}))
round(cbind(mc, exact_trend = exact$se_trend, exact_status = exact$se_status), 5)
       mc_trend mc_status     bias exact_trend exact_status
always  0.01756   0.11513  0.00050     0.01775      0.11763
rotate  0.01830   0.07973  0.00074     0.01836      0.07921
split   0.01818   0.10039  0.00005     0.01806      0.10028
fresh   0.01992   0.06692 -0.00008     0.01963      0.06778

Simulation reproduces the exact values, and the largest bias across 12,000 simulated programmes is 0.00074 against a true slope of -0.03. From here the exact numbers are used, because they carry no Monte Carlo error at all.

Scatter plot with four labelled points forming an arc from the top left to the bottom right, showing the trade-off between precision of the regional mean and precision of the trend.
Figure 2: Each design plotted by what it delivers. Left is a sharper regional mean, down is a sharper trend. Nothing sits in the bottom left corner: re-using sites moves a programme down and to the right, and the split panel comes closest to both goals at once.

Why re-use helps one and hurts the other

Both effects come out of the quadratic forms, and they land on the overlap matrix from opposite sides.

The trend weights sum to zero. If two years share the same number of sites no matter which two years they are, the site term is the same constant everywhere in the matrix, and a constant added to every entry contributes nothing to a contrast. That is exactly the always-revisit design, and its site contribution is 8e-34, zero to machine precision. Any design in which the overlap depends on the year gap leaves a residue, and that residue is a cost.

The period mean weights are all positive, so the site term enters through the total of the overlap matrix. That total is what an effective number of sites means.

eff_sites <- sapply(designs, function(d) {
  O <- overlap(visits(d))
  n_visit^2 * n_year^2 / sum(O)
})
data.frame(design = designs, distinct_sites = unname(distinct),
           effective_sites = round(unname(eff_sites), 1))
  design distinct_sites effective_sites
1 always             30              30
2 rotate            120             120
3  split             75              48
4  fresh            360             360

The split panel visits 75 distinct sites but is worth only 48 of them for the regional mean, because its permanent half is counted twelve times and its rotating half three times. Distinct sites are not the currency; the total of the overlap matrix is.

How large is the penalty

The trend cost of never returning depends on how many sites are visited each year, because the site component enters the annual mean divided by that number.

penalty <- function(n) {
  base <- sd_year^2 + (sd_sy^2 + sd_res^2 / n_rep) / n
  sqrt((base + sd_site^2 / n) / base)
}
data.frame(sites_per_year = c(5, 10, 20, 30, 60, 100),
           trend_se_penalty = round(penalty(c(5, 10, 20, 30, 60, 100)), 3))
  sites_per_year trend_se_penalty
1              5            1.364
2             10            1.244
3             20            1.148
4             30            1.106
5             60            1.058
6            100            1.036
Decreasing curve starting near 1.36 at five sites per year and flattening towards 1.04 by one hundred sites per year, with a dashed reference line at one.
Figure 3: Cost of never returning to a site, as a function of how many sites are visited each year. The penalty is the ratio of trend standard errors, fresh sites against permanent sites. It is large for a small programme and close to nothing for a large one.

At five sites a year the penalty for never returning is 1.36, and at one hundred it is 1.04. The rule that permanent sites protect a trend is real, and it matters most to the smallest programmes.

Rotating is far cheaper than never returning, and the reason is visible in the exact table: a four year cycle costs 3.5 per cent on the trend against the 10.6 per cent of a fresh sample, because every site does come back twice more and part of the cancellation is recovered. The split panel costs 1.7 per cent on the trend and saves 15 per cent on the regional mean, which is why it is the standard recommendation in the monitoring literature.

The honest limit is that every number here came out of one set of variance components. Change the ratio of site variance to year variance and the middle two designs can swap; only the two extremes are fixed by the algebra. A programme should estimate its own components first and re-run the table with them, which means changing four numbers in the code above.

There is also a question this post has not asked: whether the sites in the pool represent the region at all. A design can be excellent for trend and describe nothing, if the sites were chosen because they were easy to reach.

Where this leaves the series

The design fixes the standard error. Whether that standard error is small enough to answer the question is a separate calculation, and it is the one most programmes skip. The next post does it.

References

Urquhart, Paulsen and Larsen 1998 Ecological Applications 8:246-257 (bracketed publisher DOI, cited without it)

Urquhart and Kincaid 1999 Journal of Agricultural, Biological, and Environmental Statistics 4(4):404-414 (no publisher DOI located)

Larsen, Kincaid, Jacobs and Urquhart 2001 BioScience 51(12):1069-1078 (bracketed publisher DOI, cited without it)

Rhodes and Jonzen 2011 Ecography 34(6):1040-1048 (10.1111/j.1600-0587.2011.06370.x)

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.