How long a calibration overlap do you need?

R
monitoring
survey design
time series
ecology tutorial
A monitoring scheme is about to change method. Measuring how many years of dual running buy back the trend, and when a free era indicator is the better spend.
Author

Tidy Ecology

Published

2026-07-31

The same moth recording network as the previous post, at an earlier moment. Twenty-four light traps, one per site, emptied every morning through the flight season, with the season’s catch of a common noctuid summed into one annual figure per trap. The series is fourteen years old and the scheme intends to run it for at least as long again. This winter a letter arrived from the supplier: the mercury vapour bulbs are being discontinued, and from next season the network will have to run low-wattage actinic tubes instead.

The scheme has a small budget for the changeover, and one obvious thing to spend it on. Some traps can run both lamps for a few seasons before the old bulbs are retired, and the pairs of catches would fix the conversion between the two instruments. Whether that is worth doing, and for how many seasons at how many traps, is a design question with a numerical answer, and it has to be answered before the last mercury vapour bulb burns out. What is being bought is not obvious either. A light trap does not sample a fixed volume of air, and the two lamps differ across the spectrum in ways that different moths respond to differently: Merckx and Slade (2014) recovered attraction radii differing by more than a factor of two between macro-moth families at one lamp, Infusino et al (2017) found UV LED traps comparable with mercury vapour for some assemblages and not others, and van Grunsven et al (2014) showed that the available spectral response models predict the differences between lamp types poorly. So the conversion is real, it is not a physical constant, and the trial is the only way to see it. Networks of exactly this shape produced the evidence for steep declines in common and widespread British moths (Conrad et al 2006), and the difficulty of telling a change in a population from a change in how it was recorded runs through Isaac et al (2014).

This post measures four things: what a year of dual running is worth in trend precision, how that compares with spending the same observer-days on more survey years, how much the composition of the trial matters as against its length, and what happens to the reported interval when the conversion is treated as known. It is the design half of a pair. Splicing a monitoring series is the diagnosis: it takes the same network after an unplanned changeover, measures the trend the seam manufactures, and shows that an era indicator in the model recovers the trend with no calibration data at all. That result is the benchmark here, because a trial has to beat a free dummy variable before it is worth a single observer-night. Checking a monitoring design names a calibration subset in one clause and carries none of it out. Errors-in-variables and Deming regression supplies the two-method calibration tool on a cross-sectional scatter, with no time axis and no trend to protect. And power to detect a population trend asks whether a fixed budget is better spent on sites or on years; the calibration overlap is a third thing to spend it on, and it is compared against the other two below.

library(ggplot2)

te_pal <- list(forest = "#275139", green = "#2f8f63", sage = "#93a87f",
               clay = "#b5534e", gold = "#cda23f", line = "#dad9ca",
               ink = "#16241d", paper = "#f5f4ee")

theme_te <- function() {
  theme_minimal(base_size = 12) +
    theme(panel.grid.minor = element_blank(),
          panel.grid.major = element_line(colour = "#e7e6dc"),
          plot.background = element_rect(fill = "#f5f4ee", colour = NA),
          panel.background = element_rect(fill = "#f5f4ee", colour = NA),
          plot.title = element_text(face = "bold", colour = te_pal$ink),
          axis.title = element_text(colour = "#2c3a31"),
          axis.text = element_text(colour = "#2c3a31"),
          legend.position = "bottom")
}

The design and the quantity the trial has to estimate

The generating model is the one from the companion post with one addition. Each trap has its own catchability, every year has a weather effect shared across the network, each trap-year has its own departure, and the catch is Poisson around the result. The population declines at a set rate. The new lamp multiplies the catch by a factor, and that factor is not the same at every trap: it depends on how rich the trap is, because a richer trap loses relatively more when the lamp changes.

n_year <- 28
yr <- 1994:2021
t_c <- yr - mean(yr)
n_trap <- 24
sw_year <- 2008
new_era <- as.integer(yr >= sw_year)
k_old <- sum(new_era == 0)
beta_true <- -0.01
sd_trap <- 0.45
sd_year <- 0.22
sd_ty <- 0.35
mu_log <- log(60)
f_meth <- 0.72
step_log <- log(f_meth)
gam_c <- -0.30
ov_max <- 12
ov_cols <- (k_old - ov_max + 1):k_old
xr_yr <- 6 * k_old * (n_year - k_old) / (n_year * (n_year^2 - 1))
n_ext <- 2
n_long <- n_year + n_ext
yr_l <- 1994:(1993 + n_long)
t_l <- yr_l - mean(yr_l)
era_l <- as.integer(yr_l >= sw_year)
budget <- 48
as_pct <- function(x) 100 * (1 - exp(-x))

print(c(traps = n_trap, years = n_year, changeover = sw_year,
        old_lamp_years = k_old, true_trend_per_decade = 10 * beta_true))
                traps                 years            changeover 
                 24.0                  28.0                2008.0 
       old_lamp_years true_trend_per_decade 
                 14.0                  -0.1 
print(round(c(method_factor = f_meth, step_log = step_log,
              conversion_sd_across_traps = abs(gam_c) * sd_trap,
              exchange_rate_per_decade = 10 * xr_yr), 4))
             method_factor                   step_log 
                    0.7200                    -0.3285 
conversion_sd_across_traps   exchange_rate_per_decade 
                    0.1350                     0.5364 

The quantity that decides everything is the exchange rate between a residual step and an apparent trend, derived in the companion post: for a seam at year \(k\) of an \(n\) year series, an uncorrected step of size \(s\) in the index moves the fitted trend by \(6sk(n-k)/(n(n^2-1))\). Here that is 0.5364 per decade for every unit of log step left behind. A calibration does not have to be perfect, then; it has to leave a residue small enough that 0.5364 times it is small next to the precision the series already has. That single multiplier is what turns a field budget into a statistical answer.

The trial estimates a network-level conversion, and that is a slightly different thing from the per-trap conversion. The network index is the log of the mean catch per trap, so what has to be removed from it is the log ratio of the network total under the two lamps, which weights each trap by its catch.

sim_net <- function(seed, beta = beta_true, gam = gam_c, n_yv = n_long) {
  set.seed(seed)
  tv <- (1:n_yv) - mean(1:n_yv)
  era_v <- as.integer(seq_len(n_yv) > k_old)
  a_i <- rnorm(n_trap, mu_log, sd_trap)                 # trap catchability
  c_i <- gam * (a_i - mu_log)                           # conversion varies with it
  u_t <- rnorm(n_yv, 0, sd_year)                        # weather, shared
  base <- outer(a_i, rep(1, n_yv)) + outer(rep(1, n_trap), beta * tv + u_t)
  cnt <- matrix(rpois(n_trap * n_yv,
                      exp(base + matrix(rnorm(n_trap * n_yv, 0, sd_ty),
                                        n_trap, n_yv) +
                            outer(c_i + step_log, era_v))), n_trap, n_yv)
  # the dual-run catches: same trap, same year, own trap-year departure
  new_ov <- matrix(rpois(n_trap * ov_max,
                         exp(base[, ov_cols] +
                               matrix(rnorm(n_trap * ov_max, 0, sd_ty),
                                      n_trap, ov_max) +
                               (c_i + step_log))), n_trap, ov_max)
  list(idx = log(colMeans(cnt))[seq_len(n_year)],
       idx_long = log(colMeans(cnt)), u_t = u_t, a_i = a_i,
       d_star = step_log + log(sum(exp(a_i + c_i))) - log(sum(exp(a_i))),
       new_ov = new_ov, old_ov = cnt[, ov_cols, drop = FALSE])
}

conv_est <- function(z, traps, k) {                     # ratio of the two totals
  cc <- (ov_max - k + 1):ov_max
  log(sum(z$new_ov[traps, cc])) - log(sum(z$old_ov[traps, cc]))
}
tr_slope <- function(y) unname(coef(lm(y ~ t_c))[2])
rmse_dec <- function(x) 10 * sqrt(mean((x - beta_true)^2))

z1 <- sim_net(20260801)
print(round(c(true_network_step = z1$d_star, nominal_step = step_log,
              gap = z1$d_star - step_log), 4))
true_network_step      nominal_step               gap 
          -0.3766           -0.3285           -0.0481 

In this network the true network step is -0.3766 against a nominal per-trap factor of -0.3285, a gap of -0.0481 produced entirely by the richest traps losing the most. That gap is worth 0.0258 per decade on its own, which is the first sign that what the trial is asked to estimate needs saying precisely before it is estimated at all.

The trial itself runs in the last few seasons before the changeover: a subset of traps is worked with both lamps, on separate nights, so the two catches share the trap and the year but not the night-to-night departure. Running the old lamp on for a few seasons after the changeover instead gives the same information and the same arithmetic.

What a year of dual running buys

Six of the twenty-four traps run both lamps, for between one and twelve seasons. For each replicate network the loop fits five things: the naive splice, the era indicator, the calibrated splice at each overlap length, and, as a floor, the splice corrected by the true network step. It records the trend error and the error in a level question, defined as the change in the index between the first and last year of the series, which is what a report means by asking how the network compares with when it started.

n_rep <- 2000
ov_len <- 0:12
n_dual <- 6
sw_tr <- matrix(NA_real_, n_rep, length(ov_len))
sw_lv <- sw_tr
sw_dd <- sw_tr
era_tr <- nai_tr <- perf_tr <- era_lv <- nai_lv <- numeric(n_rep)
for (b in seq_len(n_rep)) {
  z <- sim_net(600000 + b)
  tru_lv <- beta_true * (t_c[n_year] - t_c[1]) + z$u_t[n_year] - z$u_t[1]
  obs_lv <- z$idx[n_year] - z$idx[1]
  nai_tr[b] <- tr_slope(z$idx)
  nai_lv[b] <- obs_lv - tru_lv
  perf_tr[b] <- tr_slope(z$idx - z$d_star * new_era)
  m_era <- lm(z$idx ~ t_c + new_era)
  era_tr[b] <- unname(coef(m_era)[2])
  era_lv[b] <- obs_lv - unname(coef(m_era)[3]) - tru_lv
  for (j in seq_along(ov_len)) {
    if (ov_len[j] == 0) {
      sw_tr[b, j] <- nai_tr[b]
      sw_lv[b, j] <- nai_lv[b]
      next
    }
    dh <- conv_est(z, seq_len(n_dual), ov_len[j])
    sw_dd[b, j] <- dh - z$d_star
    sw_tr[b, j] <- tr_slope(z$idx - dh * new_era)
    sw_lv[b, j] <- obs_lv - dh - tru_lv
  }
}

sweep_tab <- data.frame(
  overlap_years = ov_len,
  dual_trap_years = ov_len * n_dual,
  conv_sd = apply(sw_dd, 2, sd),
  trend_mse = apply(sw_tr, 2, function(x) mean((x - beta_true)^2)),
  trend_rmse_dec = apply(sw_tr, 2, rmse_dec),
  pct_dec = as_pct(apply(sw_tr, 2, rmse_dec)),
  level_rmse = sqrt(colMeans(sw_lv^2)))
print(round(sweep_tab, 5))
   overlap_years dual_trap_years conv_sd trend_mse trend_rmse_dec  pct_dec
1              0               0      NA   0.00044        0.20942 18.89423
2              1               6 0.23686   0.00019        0.13949 13.02011
3              2              12 0.17304   0.00012        0.10936 10.35910
4              3              18 0.14553   0.00009        0.09663  9.21084
5              4              24 0.12883   0.00008        0.08861  8.47987
6              5              30 0.11828   0.00007        0.08444  8.09758
7              6              36 0.11090   0.00007        0.08164  7.83941
8              7              42 0.10466   0.00006        0.07882  7.57948
9              8              48 0.09906   0.00006        0.07670  7.38306
10             9              54 0.09479   0.00006        0.07492  7.21805
11            10              60 0.09184   0.00005        0.07376  7.11080
12            11              66 0.08926   0.00005        0.07276  7.01792
13            12              72 0.08675   0.00005        0.07173  6.92170
   level_rmse
1     0.39837
2     0.26779
3     0.20881
4     0.18763
5     0.17333
6     0.16568
7     0.16049
8     0.15599
9     0.15264
10    0.14964
11    0.14786
12    0.14651
13    0.14530
era_rmse <- rmse_dec(era_tr)
perf_rmse <- rmse_dec(perf_tr)
print(round(c(era_trend_rmse_dec = era_rmse,
              era_level_rmse = sqrt(mean(era_lv^2)),
              naive_trend_rmse_dec = rmse_dec(nai_tr),
              naive_level_rmse = sqrt(mean(nai_lv^2)),
              conversion_known_rmse_dec = perf_rmse), 5))
       era_trend_rmse_dec            era_level_rmse      naive_trend_rmse_dec 
                  0.11343                   0.22623                   0.20942 
         naive_level_rmse conversion_known_rmse_dec 
                  0.39837                   0.05533 

Three reference points before the curve. The naive splice, which subtracts nothing, has a trend error of 18.89 per cent per decade, almost all of it bias. The era indicator, which costs nothing, has 10.72. And a splice corrected by the true conversion, which no trial can achieve, has 5.38. Every overlap length lives between the second and the third of those, and the first is the reason anyone is having this conversation.

pred_rmse <- sqrt(perf_rmse^2 + (10 * xr_yr * sweep_tab$conv_sd)^2)
print(round(cbind(overlap_years = ov_len,
                  observed = sweep_tab$trend_rmse_dec,
                  predicted = pred_rmse,
                  conv_sd = sweep_tab$conv_sd)[-1, ], 5))
      overlap_years observed predicted conv_sd
 [1,]             1  0.13949   0.13858 0.23686
 [2,]             2  0.10936   0.10806 0.17304
 [3,]             3  0.09663   0.09568 0.14553
 [4,]             4  0.08861   0.08853 0.12883
 [5,]             5  0.08444   0.08418 0.11828
 [6,]             6  0.08164   0.08124 0.11090
 [7,]             7  0.07882   0.07882 0.10466
 [8,]             8  0.07670   0.07672 0.09906
 [9,]             9  0.07492   0.07515 0.09479
[10,]            10  0.07376   0.07409 0.09184
[11,]            11  0.07276   0.07317 0.08926
[12,]            12  0.07173   0.07230 0.08675
marg <- -diff(sweep_tab$pct_dec)
names(marg) <- paste0("year_", ov_len[-1])
print(round(marg, 4))
 year_1  year_2  year_3  year_4  year_5  year_6  year_7  year_8  year_9 year_10 
 5.8741  2.6610  1.1483  0.7310  0.3823  0.2582  0.2599  0.1964  0.1650  0.1072 
year_11 year_12 
 0.0929  0.0962 
knee <- max(ov_len[-1][marg >= 1])
bev <- ov_len[which(sweep_tab$trend_rmse_dec < era_rmse)[1]]
print(c(knee_years = knee, knee_dual_trap_years = knee * n_dual,
        breakeven_years = bev, breakeven_dual_trap_years = bev * n_dual))
               knee_years      knee_dual_trap_years           breakeven_years 
                        3                        18                         2 
breakeven_dual_trap_years 
                       12 
print(round(c(dual_years_per_point_at_knee = 1 / marg[knee],
              dual_years_per_point_at_12 = 1 / marg[12],
              price_ratio = marg[knee] / marg[12]), 3))
dual_years_per_point_at_knee.year_3  dual_years_per_point_at_12.year_12 
                              0.871                              10.393 
                 price_ratio.year_3 
                             11.934 

The curve is not mysterious. The trend error at overlap length \(k\) is the floor and the conversion error added in quadrature, the second term scaled by the exchange rate, and the predicted column tracks the observed one across the whole sweep. Everything about the shape of the curve therefore comes from how fast the standard deviation of the conversion estimate falls, which is as one over the square root of the number of dual trap-years until the variation between traps takes over.

Read the marginal column as a price list. The first season of dual running removes 5.87 percentage points per decade from the trend error, the second 2.66, the third 1.15, and the twelfth 0.096. The last season that buys a full point per decade is season 3, which is 18 dual trap-years, and that is the knee. In the units a scheme manager can act on: at the knee one season of dual running at six traps costs 0.87 seasons per percentage point per decade of trend precision; by the twelfth season the same point costs 10.39 seasons, 11.9 times as much. A trial planned for three seasons and a trial planned for twelve are not two versions of the same decision.

The other number in that block is the one that decides whether to run a trial at all. The calibrated splice does not beat the free era indicator until season 2, at 12 dual trap-years. A single season of dual running at six traps gives a trend error of 13.02 per cent per decade against the era indicator’s 10.72: worse, and it cost 6 trap-seasons of fieldwork. A calibration is an estimate with a variance, and a short one injects more noise into the trend than the collinearity penalty of a dummy variable does.

A line chart. The horizontal axis runs from one to twelve years of dual running at six traps; the vertical axis is the error in the trend estimate in per cent per decade, from about five to nineteen. A dark green curve marked with black dots starts at thirteen at one year, falls to ten and a third at two years and nine and a fifth at three, then flattens and ends just below seven at twelve years. A red dashed line runs across the top of the panel near nineteen, labelled no overlap, naive splice. A gold dashed line near ten and three quarters is labelled no overlap, era indicator, and the green curve passes below it between the first and second year. A pale green solid line near five and a third is labelled conversion known exactly. A vertical grey line stands at three years.
Figure 1: Error in the estimated trend against the length of the dual-running period, at six of the twenty-four traps, over two thousand simulated networks. The two dashed lines are the options that cost nothing: a naive splice at the top and an era indicator in the middle. The solid line at the bottom is the floor reached if the conversion were known exactly. The curve crosses the era indicator between the first and second season and is nearly flat after the third.

Forty-eight trap-years, spent three ways

Dual running is not free, and the observer-nights it consumes could have gone somewhere else. The trade between more sites and more years has a closed form for trend monitoring (Gerrodette 1987) and the previous post in that series works it through; a calibration overlap is a third axis, and it is not in the formula. Fix the budget at 48 extra trap-years, which is two extra seasons of the whole network, and put three uses of it side by side: the whole network dual-running for two seasons, six traps dual-running for eight, and two extra survey years added to the end of the series with an era indicator and no trial at all. The two free options stay in the table for scale.

n_rep2 <- 2000
opt <- matrix(NA_real_, n_rep2, 10)
for (b in seq_len(n_rep2)) {
  z <- sim_net(740000 + b)
  tru_lv <- beta_true * (t_c[n_year] - t_c[1]) + z$u_t[n_year] - z$u_t[1]
  obs_lv <- z$idx[n_year] - z$idx[1]
  d_wide <- conv_est(z, seq_len(n_trap), budget %/% n_trap)
  d_deep <- conv_est(z, seq_len(6), budget %/% 6)
  m_short <- lm(z$idx ~ t_c + new_era)
  m_long <- lm(z$idx_long ~ t_l + era_l)
  opt[b, ] <- c(tr_slope(z$idx - d_wide * new_era),
                tr_slope(z$idx - d_deep * new_era),
                unname(coef(m_long)[2]), unname(coef(m_short)[2]),
                tr_slope(z$idx),
                obs_lv - d_wide - tru_lv, obs_lv - d_deep - tru_lv,
                obs_lv - unname(coef(m_long)[3]) - tru_lv,
                obs_lv - unname(coef(m_short)[3]) - tru_lv,
                obs_lv - tru_lv)
}

opt_lab <- c("overlap, 24 traps x 2 seasons", "overlap, 6 traps x 8 seasons",
             "2 extra survey years, era term", "era term only, no spend",
             "naive splice, no spend")
budget_tab <- data.frame(
  option = opt_lab,
  spend = c(budget, budget, budget, 0, 0),
  trend_bias_dec = 10 * (colMeans(opt[, 1:5]) - beta_true),
  trend_rmse_dec = 10 * sqrt(colMeans((opt[, 1:5] - beta_true)^2)),
  level_rmse = sqrt(colMeans(opt[, 6:10]^2)))
budget_tab$trend_mse_rel <-
  (budget_tab$trend_rmse_dec / budget_tab$trend_rmse_dec[4])^2
budget_tab$level_mse_rel <-
  (budget_tab$level_rmse / budget_tab$level_rmse[4])^2
print(budget_tab, digits = 4, row.names = FALSE)
                         option spend trend_bias_dec trend_rmse_dec level_rmse
  overlap, 24 traps x 2 seasons    48      1.439e-03        0.07146     0.1476
   overlap, 6 traps x 8 seasons    48     -1.161e-03        0.07719     0.1603
 2 extra survey years, era term    48     -2.212e-03        0.09925     0.2183
        era term only, no spend     0     -4.617e-05        0.11184     0.2261
         naive splice, no spend     0     -2.003e-01        0.20887     0.3988
 trend_mse_rel level_mse_rel
        0.4082        0.4265
        0.4763        0.5030
        0.7875        0.9324
        1.0000        1.0000
        3.4880        3.1116

Against the free era indicator, 48 trap-years of dual running across the whole network cut the trend mean squared error to 0.408 of it, and the same 48 trap-years bought as two extra survey years cut it only to 0.788. For a scheme facing a method change, the calibration is the better buy, and it is not close. The reason is in the exchange rate again: two extra years at the end lengthen the series and dilute a step by a little, but they leave the conversion exactly as unknown as it was, whereas the trial attacks the term that dominates.

One prediction did not come out. Going in, the expectation was an asymmetry: that a calibration buys the level and the era indicator handles the trend, so that the two options would separate sharply depending on the question. They do not. The whole-network trial takes the trend error to 0.408 of the era indicator’s and the level error to 0.427, which is the same factor to within Monte Carlo noise, and the six-trap trial gives 0.476 and 0.503. The reason is visible once stated: both questions are damaged by the same quantity, the error in the conversion, and both are damaged in proportion to it. The trend pays 0.5364 per decade per unit of conversion error, the level pays one unit per unit, and relative to the precision each question already had those two prices happen to be close. The genuine asymmetry is elsewhere in the table: the naive splice is 3.488 times worse than the era indicator for the trend and 3.112 times for the level, and it is the only option in the table that is biased rather than merely imprecise.

Buying survey years is a poor third for a different reason at the level question. Two extra years take the level error only to 0.932 of the era indicator’s, which is nothing at all: extra years at the end of a series barely improve an era term whose problem is its collinearity with time, not its sample size.

A horizontal bar chart with two side by side panels, one headed trend question and one headed level question. Five bars in each. From top to bottom: overlap with twenty-four traps for two seasons, a short dark green bar at about four tenths in both panels; overlap with six traps for eight seasons, dark green at about half; two extra survey years with an era term, dark green at eight tenths in the trend panel and nine tenths in the level panel; era term only with no spend, dark green reaching exactly one in both; and naive splice with no spend, a long red bar reaching about three and a half in the trend panel and three in the level panel. A vertical grey reference line stands at one.
Figure 2: Mean squared error of five ways of handling the changeover, relative to the free era indicator, for a trend question and for a level question. The top three options each spend forty-eight trap-years; the bottom two spend nothing. Bars shorter than the reference line at one are better than doing nothing beyond adding a dummy variable.

Where the trial runs matters more than how long it runs

Two trials of the same total size can be laid out differently: a few traps for many seasons, or many traps for a few. The total number of dual trap-years is held at 48 and the shape is varied.

shape <- data.frame(traps = c(4, 6, 8, 12, 24), years = c(12, 8, 6, 4, 2))
n_rep3 <- 2000
sh_out <- matrix(NA_real_, nrow(shape), 3)
for (r in seq_len(nrow(shape))) {
  dv <- tvals <- numeric(n_rep3)
  for (b in seq_len(n_rep3)) {
    z <- sim_net(750000 + b)
    dh <- conv_est(z, seq_len(shape$traps[r]), shape$years[r])
    dv[b] <- dh - z$d_star
    tvals[b] <- tr_slope(z$idx - dh * new_era)
  }
  sh_out[r, ] <- c(mean(dv), sd(dv), rmse_dec(tvals))
}
shape$conv_bias <- sh_out[, 1]
shape$conv_sd <- sh_out[, 2]
shape$trend_rmse_dec <- sh_out[, 3]
print(round(shape, 5))
  traps years conv_bias conv_sd trend_rmse_dec
1     4    12   0.01117 0.10550        0.07861
2     6     8   0.00598 0.09987        0.07691
3     8     6   0.00297 0.09547        0.07457
4    12     4   0.00083 0.08933        0.07222
5    24     2  -0.00148 0.08386        0.07045

Spreading the trial thinly is better, and by less than the section heading promises. Four traps for twelve seasons gives a trend error of 7.56 per cent per decade and the whole network for two seasons gives 6.8, a difference of 24.5 per cent in mean squared error. The mechanism is the between-trap part of the conversion variance, which no number of seasons at four traps can reduce, and at this level of trap-to-trap variation that floor is a modest part of the total. The practical reading is that a scheme with an awkward field constraint, two observers willing to run two lamps for a decade, is not throwing its money away.

What does matter is which traps. A trial has to go somewhere, and the sites it goes to are rarely drawn at random: they are the ones with keen operators, easy access and catches big enough to compare, which in this network means the richest traps, which are exactly the ones whose conversion is furthest from the network average. The estimator matters too. Averaging the per-night log ratios answers a different question from taking the ratio of the two totals, because the network index is built from totals.

conv_mlog <- function(z, traps, k) {
  cc <- (ov_max - k + 1):ov_max
  mean(log((z$new_ov[traps, cc, drop = FALSE] + 0.5) /
             (z$old_ov[traps, cc, drop = FALSE] + 0.5)))
}

n_rep4 <- 2000
pick <- matrix(NA_real_, n_rep4, 6)
for (b in seq_len(n_rep4)) {
  z <- sim_net(760000 + b)
  top <- order(z$a_i, decreasing = TRUE)[seq_len(6)]
  d_rand <- conv_est(z, seq_len(6), 8)
  d_top <- conv_est(z, top, 8)
  d_ml <- conv_mlog(z, seq_len(6), 8)
  pick[b, ] <- c(d_rand - z$d_star, d_top - z$d_star, d_ml - z$d_star,
                 tr_slope(z$idx - d_rand * new_era),
                 tr_slope(z$idx - d_top * new_era),
                 tr_slope(z$idx - d_ml * new_era))
}
pick_tab <- data.frame(
  overlap = c("six traps at random", "the six biggest catches",
              "six at random, mean of log ratios"),
  conv_bias = colMeans(pick[, 1:3]),
  conv_rmse = sqrt(colMeans(pick[, 1:3]^2)),
  trend_bias_dec = 10 * (colMeans(pick[, 4:6]) - beta_true),
  trend_rmse_dec = 10 * sqrt(colMeans((pick[, 4:6] - beta_true)^2)))
print(pick_tab, digits = 4, row.names = FALSE)
                           overlap conv_bias conv_rmse trend_bias_dec
               six traps at random  0.008659   0.09745       -0.00502
           the six biggest catches -0.125718   0.15055        0.06706
 six at random, mean of log ratios  0.051685   0.10470       -0.02810
 trend_rmse_dec
        0.07559
        0.09609
        0.07874

Eight seasons at six traps chosen at random recovers the conversion with a bias of 0.0087 and leaves the trend with a bias of -0.005 per decade. The same eight seasons at the six biggest catches recovers a conversion 0.1257 too low, which is 33.4 per cent of the whole step, and turns the trend bias into 0.0671 per decade: the sign has flipped, and a declining population is now being read as declining less. The trend error rises from 7.28 to 9.16 per cent per decade. Averaging log ratios instead of taking the ratio of totals is a smaller version of the same mistake in the opposite direction: it targets the unweighted mean conversion rather than the catch-weighted one, biasing the conversion by 0.0517 and the trend by -0.0281 per decade, with the trial itself perfectly well run.

How far can the trap selection go before the trial is a liability? That depends on how much the conversion actually varies from trap to trap, which is a fact about the two lamps rather than about the design, so it is worth sweeping.

gam_grid <- c(0, -0.15, -0.3, -0.45, -0.6)
n_rep5 <- 800
gam_tab <- t(vapply(gam_grid, function(g) {
  vt <- vr <- ve <- numeric(n_rep5)
  for (b in seq_len(n_rep5)) {
    z <- sim_net(770000 + b, gam = g)
    top <- order(z$a_i, decreasing = TRUE)[seq_len(6)]
    vt[b] <- tr_slope(z$idx - conv_est(z, top, 8) * new_era)
    vr[b] <- tr_slope(z$idx - conv_est(z, seq_len(6), 8) * new_era)
    ve[b] <- unname(coef(lm(z$idx ~ t_c + new_era))[2])
  }
  c(conv_sd_across_traps = abs(g) * sd_trap, top_rmse_dec = rmse_dec(vt),
    rand_rmse_dec = rmse_dec(vr), era_rmse_dec = rmse_dec(ve))
}, numeric(4)))
print(round(gam_tab, 5))
     conv_sd_across_traps top_rmse_dec rand_rmse_dec era_rmse_dec
[1,]               0.0000      0.06838       0.06995      0.10935
[2,]               0.0675      0.07701       0.07228      0.10899
[3,]               0.1350      0.10021       0.07467      0.10913
[4,]               0.2025      0.13044       0.08265      0.10900
[5,]               0.2700      0.16543       0.08963      0.10870
cross <- approx(gam_tab[, "top_rmse_dec"] - gam_tab[, "era_rmse_dec"],
                gam_tab[, "conv_sd_across_traps"], xout = 0)$y
print(round(c(crossover_conversion_sd = cross,
              this_post_conversion_sd = abs(gam_c) * sd_trap), 4))
crossover_conversion_sd this_post_conversion_sd 
                 0.1548                  0.1350 

The crossing is at a conversion spread of 0.1548 on the log scale across traps. Below that, a trial at the six best traps is still better than no trial; above it, eight seasons of fieldwork produce an answer worse than a dummy variable. The network in this post sits at 0.135, just under the line, which is the uncomfortable place to be: the trial is worth running, but only just, and nothing in its output would tell the scheme how close to the line it was. The trial at six random traps stays clear of the era indicator across the whole sweep, so the fix is in the sampling of traps rather than in the length of the trial.

A line chart. The horizontal axis is the spread of the true conversion across traps on the log scale, from zero to about twenty-seven hundredths. The vertical axis is the error in the trend estimate in per cent per decade, from about six to sixteen. A red line of triangles labelled overlap at the six biggest catches starts near six and a half at the left and rises almost straight to fifteen at the right. A dark green line of circles labelled overlap at six traps at random starts at the same place and rises gently to about eight and a half. A gold line of squares labelled no overlap, era indicator is flat at about ten and a third across the whole panel. A dashed vertical line marks where the red line crosses the gold one, at about fifteen hundredths.
Figure 3: Trend error against how much the true conversion varies from trap to trap, for two ways of choosing which traps run both lamps and for the option of not running a trial. Eight seasons at six traps in every case. The trial at the six richest traps is a straight line rising through the era indicator; the trial at six random traps rises slowly and stays below it.

A calibration is an estimate, and the interval has to say so

Once the conversion is subtracted, the corrected series goes into lm() like any other series, and the standard error that comes back is the standard error of a regression on data treated as given. It knows nothing about where the correction came from. The fix is the delta method: the trend is a function of the index and of the conversion estimate, its derivative with respect to the conversion is minus the exchange rate, and the two variances add.

\[\operatorname{Var}(\hat\beta) \;\approx\; \operatorname{Var}_{\text{lm}}(\hat\beta) \;+\; \left(\frac{6k(n-k)}{n(n^2-1)}\right)^{2} \operatorname{Var}(\hat\delta)\]

The remaining question is where \(\operatorname{Var}(\hat\delta)\) comes from, and the honest answer is not the within-trap noise, because the trap-to-trap variation in the conversion is part of the error too. A jackknife over traps carries both.

jack_var <- function(z, traps, k) {
  m <- length(traps)
  ps <- vapply(seq_len(m), function(j) conv_est(z, traps[-j], k), numeric(1))
  (m - 1) / m * sum((ps - mean(ps))^2)
}

n_rep6 <- 2000
prop <- matrix(NA_real_, n_rep6, 6)
tq <- qt(0.975, n_year - 2)
for (b in seq_len(n_rep6)) {
  z <- sim_net(780000 + b)
  dh <- conv_est(z, seq_len(6), 5)
  m <- lm((z$idx - dh * new_era) ~ t_c)
  se_lm <- summary(m)$coefficients[2, 2]
  vj <- jack_var(z, seq_len(6), 5)
  se_pr <- sqrt(se_lm^2 + xr_yr^2 * vj)
  bh <- unname(coef(m)[2])
  prop[b, ] <- c(bh, se_lm, se_pr, sqrt(vj),
                 abs(bh - beta_true) < tq * se_lm,
                 abs(bh - beta_true) < tq * se_pr)
}
print(round(c(true_sd_dec = 10 * sd(prop[, 1]),
              reported_se_dec = 10 * mean(prop[, 2]),
              propagated_se_dec = 10 * mean(prop[, 3]),
              understatement = sd(prop[, 1]) / mean(prop[, 2]),
              jackknife_conv_sd = mean(prop[, 4]),
              true_conv_sd = sd(sw_dd[, ov_len == 5])), 4))
      true_sd_dec   reported_se_dec propagated_se_dec    understatement 
           0.0810            0.0548            0.0840            1.4781 
jackknife_conv_sd      true_conv_sd 
           0.1143            0.1183 
print(round(c(coverage_as_reported = mean(prop[, 5]),
              coverage_propagated = mean(prop[, 6]), nominal = 0.95), 4))
coverage_as_reported  coverage_propagated              nominal 
              0.8310               0.9515               0.9500 

With a five-season trial at six traps, the spread of the trend estimate across networks is 0.081 per decade and the standard error the regression reports is 0.0548. The reported figure understates the real one by a factor of 1.478, and the interval built on it covers the true trend 83.1 per cent of the time instead of ninety-five. Adding the jackknife term takes the average standard error to 0.084 per decade and coverage to 95.15 per cent. The jackknife itself is honest about the conversion: it returns 0.1143 against a true spread of 0.1183, from six traps.

That understatement is worth holding next to the earlier result. A trial of this size takes the trend error below the era indicator’s, but a scheme that then reports the uncorrected interval is claiming a precision it has not got, and the amount it is overclaiming, 47.8 per cent on the standard error, is larger than the 25.6 per cent by which the trial cut the real error below the era indicator’s. Bland and Altman (1986) made the same point about clinical method comparison: the agreement between two methods is itself estimated, with limits, and quoting the conversion without them is where the trouble starts.

The limit: one number, two quantities

Everything so far assumed that the two lamps measure the same thing up to a constant. Suppose they do not. The annual figure is the catch of what the recording card calls one species, and suppose it is really two forms with different sizes and different flight periods: group A, which is stable and which both lamps take alike, and group B, which is declining fast and which the actinic tube takes much less well.

w_b <- 0.30
b_a <- 0
b_b <- -0.05
rho <- 0.35
eff_a <- f_meth / ((1 - w_b) + rho * w_b)
grp_b <- function(tt) w_b * exp(b_b * tt)
tot_old <- function(tt) (1 - w_b) * exp(b_a * tt) + grp_b(tt)
tot_new <- function(tt) eff_a * ((1 - w_b) * exp(b_a * tt) + rho * grp_b(tt))
true_mix <- unname(coef(lm(log(tot_old(t_c)) ~ t_c))[2])
conv_curve <- log(tot_new(t_c) / tot_old(t_c))

mix_tab <- t(vapply(1:12, function(kk) {
  cc <- (k_old - kk + 1):k_old
  d_k <- log(sum(tot_new(t_c[cc])) / sum(tot_old(t_c[cc])))
  ser <- log(tot_old(t_c)) * (1 - new_era) +
    (log(tot_new(t_c)) - d_k) * new_era
  sl <- unname(coef(lm(ser ~ t_c))[2])
  c(overlap_years = kk, conversion_used = d_k, recovered_dec = 10 * sl,
    truth_dec = 10 * true_mix, shortfall_pct = 100 * (1 - sl / true_mix))
}, numeric(5)))
print(round(mix_tab, 5))
      overlap_years conversion_used recovered_dec truth_dec shortfall_pct
 [1,]             1        -0.33277      -0.11351    -0.152      25.32452
 [2,]             2        -0.33716      -0.11115    -0.152      26.87167
 [3,]             3        -0.34164      -0.10875    -0.152      28.45363
 [4,]             4        -0.34622      -0.10629    -0.152      30.07089
 [5,]             5        -0.35091      -0.10378    -0.152      31.72395
 [6,]             6        -0.35569      -0.10121    -0.152      33.41327
 [7,]             7        -0.36059      -0.09859    -0.152      35.13929
 [8,]             8        -0.36558      -0.09591    -0.152      36.90244
 [9,]             9        -0.37068      -0.09317    -0.152      38.70312
[10,]            10        -0.37589      -0.09038    -0.152      40.54170
[11,]            11        -0.38121      -0.08752    -0.152      42.41852
[12,]            12        -0.38664      -0.08461    -0.152      44.33390
print(round(c(share_b_first = grp_b(t_c[1]) / tot_old(t_c[1]),
              share_b_last = grp_b(t_c[n_year]) / tot_old(t_c[n_year]),
              conv_first = conv_curve[1], conv_swap = conv_curve[k_old],
              conv_last = conv_curve[n_year],
              efficiency_group_a = eff_a, efficiency_group_b = eff_a * rho), 4))
     share_b_first       share_b_last         conv_first          conv_swap 
            0.4570             0.1791            -0.4641            -0.3328 
         conv_last efficiency_group_a efficiency_group_b 
           -0.2354             0.8944             0.3130 

Group B is 30 per cent of the catch at the midpoint of the series, the new lamp takes group A at 0.8944 of the old lamp’s rate and group B at 0.313, and the two together happen to give the same overall factor of 0.72 at the midpoint that the rest of the post used. So a trial run at the changeover would report a familiar number, and it would still be the wrong kind of number.

There is no constant to estimate. The conversion is -0.4641 in the first year of the series, -0.3328 in the year of the changeover and -0.2354 in the last, because group B’s share of the catch falls from 45.7 per cent to 17.9 per cent while the trial reports one number. Correcting by that number recovers a trend of -0.1135 per decade from a single-season trial against a truth of -0.152, a shortfall of 25.3 per cent of the real decline. A twelve-season trial recovers -0.0846, a shortfall of 44.3 per cent. The longer trial is further from the truth, because a longer trial reaches back into earlier seasons when group B was commoner and the conversion was steeper, so the one number it reports is a better estimate of a period the scheme does not care about.

ser_raw <- log(tot_old(t_c)) * (1 - new_era) + log(tot_new(t_c)) * new_era
era_mix <- 10 * unname(coef(lm(ser_raw ~ t_c + new_era))[2])
ser_cls <- log(tot_old(t_c)) * (1 - new_era) +
  log((1 - w_b) * exp(b_a * t_c) + grp_b(t_c)) * new_era
cls_mix <- 10 * unname(coef(lm(ser_cls ~ t_c))[2])
print(round(c(truth_dec = 10 * true_mix, era_term_dec = era_mix,
              one_season_trial_dec = mix_tab[1, "recovered_dec"],
              per_class_conversion_dec = cls_mix), 5))
                         truth_dec                       era_term_dec 
                          -0.15200                           -0.11883 
one_season_trial_dec.recovered_dec           per_class_conversion_dec 
                          -0.11351                           -0.15200 
ov5 <- (k_old - 4):k_old
df_dr <- 6 * length(ov5) - 2
dr_slope <- unname(coef(lm(conv_curve[ov5] ~ I(t_c[ov5])))[2])
se_dr <- sqrt(2 * sd_ty^2 + 2 / exp(mu_log)) /
  sqrt(6 * sum((t_c[ov5] - mean(t_c[ov5]))^2))
pw_dr <- 1 - pt(qt(0.975, df_dr), df_dr, dr_slope / se_dr) +
  pt(-qt(0.975, df_dr), df_dr, dr_slope / se_dr)
print(round(c(drift_within_5_seasons = conv_curve[k_old] - conv_curve[k_old - 4],
              drift_after_the_swap = conv_curve[n_year] - conv_curve[k_old],
              drift_slope_per_year = dr_slope, se_of_that_slope = se_dr,
              power_to_detect_drift = pw_dr), 5))
drift_within_5_seasons   drift_after_the_swap   drift_slope_per_year 
               0.03632                0.09740                0.00908 
      se_of_that_slope  power_to_detect_drift 
               0.06811                0.05190 

The trial cannot see this from inside itself. Across the five seasons of a five-season overlap the true conversion moves by 0.0363, against 0.0974 across the years that follow the changeover. A regression of the trial’s log ratio on year within the trial has a true slope of 0.00908 per year and a standard error of 0.0681, so the test for a drifting conversion has power 0.0519, which is its own false-positive rate. The trial reports a tight conversion, a flat log ratio and no warning of any kind, and it is measuring something the scheme did not ask about.

Nothing statistical repairs this. The era indicator is not a fix either: it returns -0.1188 per decade against the truth of -0.152, less wrong than the long trial and wrong all the same, because after the changeover the network is simply counting a different thing. What does repair it is a field decision taken at the same time as the trial. If the trial’s catch is identified and counted at the level at which the two methods could plausibly differ, by species, by size class, by night of the flight period, then a conversion can be estimated for each class and applied separately. The same overlap data, sorted, recovers -0.152 per decade, which is the truth exactly, because correcting each class by its own factor reconstructs the old-lamp total rather than approximating it. Sorting the trial catch costs bench time in the winter and nothing in the field, and it is the difference between a calibration that is about the index and one that is about a number.

The general form of the rule is that a trial must record what the two methods disagree about, not just how much they disagree in total. Double sampling in bird surveys has the same structure: a cheap method is calibrated against an intensive one on a subset, and the calibration is only transportable to the extent that the ratio between them is stable across the conditions it will be applied to (Bart and Earnst 2002). A ratio measured on last decade’s community and applied to this decade’s is a transport that nobody checked.

Two stacked panels sharing a horizontal axis of year from 1994 to 2021, with a dashed vertical line just before 2008 marking the changeover. The upper panel, headed the conversion the two lamps really have, shows a red curve rising steadily from about minus forty-six hundredths at the left to about minus twenty-four hundredths at the right, flattening as it goes, with a short flat dark green segment at about minus thirty-five hundredths drawn across the five years before the changeover and a dark green caption at the lower right reading what a five-season trial reports. The lower panel, headed trend recovered after correcting by one number, shows a red line of twelve black dots running from about minus eighty-five thousandths at 1996 down to about minus one hundred and fourteen thousandths at 2007, all of them far above a dark green dashed horizontal line at minus one hundred and fifty-two thousandths labelled the true trend.
Figure 4: Top: the true log ratio between the two lamps over the twenty-eight years, rising as the poorly caught group declines out of the catch, with the flat single number that a five-season trial ending at the changeover would report. Bottom: the trend recovered after correcting by one number, plotted against the first season of the dual-running window, so that longer trials sit further left. The dashed line is the truth. Every trial understates the decline, and the longer ones understate it more.

What to take away

A scheme planning a method change has three options, and the free one is better than most people expect. An era indicator costs a degree of freedom and no fieldwork, and it takes the trend error from 18.89 per cent per decade to 10.72. That is the number a trial has to beat, and a trial of one season at six traps does not beat it. Two seasons at six traps, 12 dual trap-years, is where the calibration starts paying, and season 3 is the last one that buys a full percentage point per decade. After that a point costs 10.4 seasons instead of 0.87.

At a matched budget of 48 trap-years the calibration is the better spend, by a wide margin over buying survey years: mean squared error 0.408 of the era indicator’s against 0.788 for two extra years. The asymmetry I went in expecting, that calibration buys the level and the era indicator handles the trend, is not there: the factors are 0.408 and 0.427, near enough the same, because both questions are damaged by the same conversion error in the same proportion. The real division in the table is between the naive splice, which is biased, and everything else, which is not.

Two things about the shape of the trial went the other way from what the design intuition suggested. Concentrating the same effort on four traps rather than twenty-four costs only 24.5 per cent in mean squared error, so the arrangement is a weaker lever than expected. Choosing those traps badly is a much stronger one: eight seasons at the six richest traps biases the conversion by -0.1257 and the trend by 0.0671 per decade towards zero, and once the conversion varies across traps by more than 0.155 on the log scale, that trial is worse than no trial. Neither the length of the trial nor its own output gives any hint of this.

Two smaller results carry further than the moth network. The conversion estimator has to match the index: the ratio of the two totals is right for an index built from totals, and averaging log ratios biases the conversion by 0.0517 here. And the conversion is an estimate, so the trend interval has to carry its variance; the delta method with a jackknife over traps moved coverage from 83.1 to 95.15 per cent, and the correction to the standard error, 47.8 per cent, was larger than the whole gain the trial bought.

The honest limit is that the overlap identifies the conversion only if the two methods track the same quantity up to that conversion. When they do not, a longer trial makes the answer worse rather than better, the trial’s internal check for a drifting conversion has power 0.052, and the report is a confident number about the wrong thing. There is no statistical repair, and there is a cheap field one: sort and identify the trial’s catch at the level at which the two methods could differ, and estimate a conversion for each class. In the mixture above that recovers -0.152 per decade against a truth of -0.152, from exactly the same nights of fieldwork.

References

Merckx T, Slade EM 2014 Insect Conservation and Diversity 7(5):453-461 (10.1111/icad.12068)

Infusino M, Brehm G, Di Marco C, Scalercio S 2017 European Journal of Entomology 114:25-33 (10.14411/eje.2017.004)

van Grunsven RHA, Donners M, Boekee K, Tichelaar I, van Geffen KG, Groenendijk D, Berendse F, Veenendaal EM 2014 Journal of Insect Conservation 18(2):225-231 (10.1007/s10841-014-9633-9)

Conrad KF, Warren MS, Fox R, Parsons MS, Woiwod IP 2006 Biological Conservation 132(3):279-291 (10.1016/j.biocon.2006.04.020)

Isaac NJB, van Strien AJ, August TA, de Zeeuw MP, Roy DB 2014 Methods in Ecology and Evolution 5(10):1052-1060 (10.1111/2041-210X.12254)

Bart J, Earnst S 2002 The Auk 119(1):36-45 (10.1093/auk/119.1.36)

Bland JM, Altman DG 1986 The Lancet 327(8476):307-310 (10.1016/S0140-6736(86)90837-8)

Gerrodette T 1987 Ecology 68(5):1364-1372 (10.2307/1939220)

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.