11  Aerial, Camera-Assisted, and Remote Count Designs

Author

Christopher Chizinski

Keywords

creel survey, fisheries, design-based inference, R, tidycreel, Quarto

11.1 Why remote counts?

Standard creel survey designs — access-point (Section 9.1) and roving (Section 10.1) — assume a field crew can stand at an access point or travel a route and count anglers directly. That assumption fails when the waterbody is too large for crew coverage, when weather prevents reliable manual counts, when budget limits staffing to only a few days per week, or when the survey requires higher temporal resolution than a crew can provide.

Remote and technology-assisted count methods solve the coverage problem by replacing or supplementing the human observer with a sensor — a camera at a boat ramp, an aircraft flying a systematic route, a vehicle counter at a road crossing. Coverage goes up, field labor goes down, and the record becomes persistent: a camera image can be reviewed; a flight log can be audited.

The estimation logic does not change. Effort is still the product of a count of anglers and the hours those anglers were available to fish. What changes is how the count is collected and how timing uncertainty is handled. An aerial flight covers the entire waterbody in thirty minutes but captures only a snapshot at the flight time. A boat-ramp camera records every ingress event but produces a vehicle count, not a direct angler-hour measurement. Both require a link between the count and total effort — through a model, through a calibration ratio, or through mean trip length from interviews.

tidycreel provides dedicated design types and estimators for each major remote-count approach: design_type = "aerial" with estimate_effort_aerial_glmm() for time-of-day-adjusted aerial surveys, and design_type = "camera" with est_effort_camera() for ingress/egress counter designs. The same creel_design()add_counts()add_interviews() → estimate pipeline applies to both, with a few additional required arguments that capture the characteristics of the observation system.

The common thread is simple: the remote sensor changes how the count is observed, but not what the analyst ultimately needs. The target is still angler-hours, so each design has to translate the sensor output back into that unit.

11.2 Aerial surveys

The observation problem

An aerial survey produces one count per flight, and the count reflects the number of anglers visible at a single moment in time. Angler density on a lake changes continuously across the fishing day: low at first light, building through midmorning as anglers arrive, peaking around midday or early afternoon, then declining as anglers leave and daylight wanes. A single flight at 10:00 captures a different density than a flight at 14:00 on the same day.

The naive expansion — count × hours_open — assumes the observed density is representative of the full day, which it is only if the flight happens exactly at the time when mean daily density occurs. If flights are consistently timed early or late relative to peak use, the effort estimate is biased in a predictable direction.

The standard fix is a time-of-day model that predicts how angler density varies across the fishing day. Multiple flights on the same days, spaced at different times, provide the data to fit that model. The fitted curve is then integrated across the full open-water period to recover total daily effort. estimate_effort_aerial_glmm() fits this model as a generalized linear mixed model (GLMM) with a quadratic time-of-day effect and a day-level random intercept:

\text{count}_{d,t} = \exp\bigl(\beta_0 + \beta_1 t + \beta_2 t^2 + u_d\bigr)

where t is flight time, u_d \sim N(0, \sigma^2) is a day-level random effect that absorbs day-to-day variation in total use, and the exponential link keeps predictions non-negative. Integrating the fitted curve over the h_{\text{open}} open hours of the fishing day recovers a day-specific total angler-hours estimate, which is then expanded to the full season using the stratified ratio estimator.

Data structure

An aerial survey dataset contains one row per flight observation — not one row per day. Multiple flights on the same day appear as separate rows, linked by date and distinguished by time_of_flight. The tidycreel bundled dataset example_aerial_glmm_counts illustrates this structure:

Code
example_aerial_glmm_counts |>
  arrange(date, time_of_flight) |>
  head(12)
         date day_type n_anglers time_of_flight
1  2024-06-03  weekday         3              7
2  2024-06-03  weekday        30             10
3  2024-06-03  weekday        65             13
4  2024-06-03  weekday        50             16
5  2024-06-06  weekday         5              7
6  2024-06-06  weekday        15             10
7  2024-06-06  weekday        40             13
8  2024-06-06  weekday        23             16
9  2024-06-09  weekend        10              7
10 2024-06-09  weekend        21             10
11 2024-06-09  weekend        49             13
12 2024-06-09  weekend        43             16

Four flights per day at hours 7, 10, 13, and 16. The counts rise from early morning to a midday peak, then fall — exactly the pattern the GLMM is designed to capture. With four time points per day, the quadratic curve is well constrained.

Building the aerial design

An aerial design requires three additions to the standard creel_design() call:

  • design_type = "aerial" — routes count data to the GLMM estimator
  • h_open — total fishing hours per day (daylight hours accessible to anglers)
  • Raw, unaggregated counts passed to add_counts() without count_time_col, so time_of_flight is preserved in design$counts for the model
Code
aerial_calendar <- tibble(
  date     = seq(min(example_aerial_glmm_counts$date),
                 max(example_aerial_glmm_counts$date), by = "day"),
  day_type = if_else(wday(date) %in% c(1L, 7L), "weekend", "weekday")
)

design_aerial <- creel_design(
  calendar    = aerial_calendar,
  date        = "date",
  strata      = "day_type",
  design_type = "aerial",
  h_open      = 14
) |>
  add_counts(example_aerial_glmm_counts) |>
  add_interviews(
    interviews    = example_aerial_interviews,
    catch         = "walleye_catch",
    harvest       = "walleye_kept",
    effort        = "hours_fished",
    trip_status   = "trip_status",
    trip_duration = "hours_fished"
  )
Warning in add_counts(creel_design(calendar = aerial_calendar, date = "date", : Duplicate PSU values detected in count data.
ℹ Found 36 duplicate value(s) in column date.
ℹ If multiple counts were taken per day, specify `count_time_col`.
ℹ Example: `add_counts(design, counts, count_time_col = count_time)`
Warning in svydesign.default(ids = psu_formula, strata = strata_formula, : No
weights or probabilities supplied, assuming equal probability
ℹ No `n_anglers` provided — assuming 1 angler per interview.
ℹ Pass `n_anglers = <column>` to use actual party sizes for angler-hour
  normalization.
Warning: 15 interviews have zero catch.
ℹ Zero catch may be valid (skunked) or indicate missing data.
ℹ Added 48 interviews: 48 complete (100%), 0 incomplete (0%)

The contrast with a standard instantaneous design is in design_type and h_open. Everything else — add_interviews(), estimation calls — follows the same pattern. The GLMM handles the time-of-day adjustment internally; the analyst provides the data and the open-water window.

Time-of-day pattern

Before estimating effort, it is worth visualizing what the GLMM is fitting. Figure 11.1 shows mean count by flight time across both day types, with the quadratic pattern that drives the model.

Code
example_aerial_glmm_counts |>
  group_by(day_type, time_of_flight) |>
  summarise(mean_count = mean(n_anglers), .groups = "drop") |>
  ggplot(aes(time_of_flight, mean_count, colour = day_type, group = day_type)) +
  geom_point(size = 3) +
  geom_smooth(method = "loess", se = FALSE, linewidth = 1) +
  scale_colour_manual(
    values = c(weekday = "#2171b5", weekend = "#e6550d"),
    labels = c(weekday = "Weekday", weekend = "Weekend"),
    name   = NULL
  ) +
  labs(
    x = "Time of flight (hour of day)",
    y = "Mean angler count"
  ) +
  theme_bw(base_size = 12) +
  theme(legend.position = "bottom")
`geom_smooth()` using formula = 'y ~ x'
Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
: span too small.  fewer data values than degrees of freedom.
Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
: pseudoinverse used at 6.955
Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
: neighborhood radius 6.045
Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
: reciprocal condition number 0
Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
: There are other near singularities as well. 36.542
Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
: span too small.  fewer data values than degrees of freedom.
Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
: pseudoinverse used at 6.955
Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
: neighborhood radius 6.045
Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
: reciprocal condition number 0
Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
: There are other near singularities as well. 36.542
Scatter plot with time of flight (hours) on the x axis from 7 to 16, mean angler count on the y axis. Two smooth curves, one per day type, both arching upward then downward with weekends higher than weekdays.
Figure 11.1: Mean aerial count by time of flight and day type. Points show stratum means across sampled days; the curve is a loess smoother that approximates the quadratic GLMM fit. Counts peak in early afternoon and are consistently higher on weekends, reflecting higher recreational use.

The inverted-U shape confirms that a single flight at 07:00 would underestimate total daily effort, while a flight at 13:00 would overestimate it. The GLMM integrates the full curve rather than multiplying one snapshot count by hours_open.

That is the reason the model is doing real work here. It is not just smoothing noise; it is replacing a one-time observation with a full-day representation of use.

Estimating effort

estimate_effort_aerial_glmm() fits the negative-binomial GLMM, integrates the fitted curve across h_open hours, and expands to the full season using the stratified frame. The result is a total angler-hours estimate for the survey period with a delta-method standard error.

Code
effort_aerial <- estimate_effort_aerial_glmm(design_aerial, time_col = time_of_flight)
Warning in theta.ml(Y, mu, weights = object@resp$weights, limit = limit, :
iteration limit reached
Code
effort_aerial

── Creel Survey Estimates ──────────────────────────────────────────────────────
Method: aerial_glmm_total
Variance: delta
Confidence level: 95%

# A tibble: 1 × 7
  estimate    se se_between se_within ci_lower ci_upper     n
     <dbl> <dbl>      <dbl>     <dbl>    <dbl>    <dbl> <int>
1     379.  33.0       33.0        NA     314.     443.    48

The GLMM estimates 379 total angler-hours (95% CI: 314–443). The total and its standard error reflect two sources of variance: between-day variation in total use (absorbed by the GLMM day-level random effect) and between-stratum variation in sampling intensity (handled by the stratified expansion). The ratio of se_between to total se indicates how much of the uncertainty comes from which-days-were-sampled versus which-time-flights-were- conducted.

The GLMM requires lme4. If the package is not installed, tidycreel will prompt installation before the call proceeds.

Naive estimate vs GLMM

To see what the time-of-day correction buys, it helps to compare the GLMM result against the naive single-flight expansion. example_aerial_counts contains one count per day — a midmorning flight — that can be treated as a standard instantaneous-count design using estimate_effort():

Code
# Standard instantaneous design with one flight per day
# h_open goes into creel_design(), not estimate_effort()
design_naive <- creel_design(
  calendar = aerial_calendar,
  date     = "date",
  strata   = "day_type",
  h_open   = 14
) |>
  add_counts(example_aerial_counts) |>
  add_interviews(
    interviews    = example_aerial_interviews,
    catch         = "walleye_catch",
    harvest       = "walleye_kept",
    effort        = "hours_fished",
    trip_status   = "trip_status",
    trip_duration = "hours_fished"
  )

effort_naive <- estimate_effort(design_naive)
effort_naive$estimates
# A tibble: 1 × 7
  estimate    se se_between se_within ci_lower ci_upper     n
     <dbl> <dbl>      <dbl>     <dbl>    <dbl>    <dbl> <int>
1      637  47.0       47.0         0     536.     738.    16
Code
bind_rows(
  effort_naive$estimates  |> mutate(method = "Naive (single flight × h_open)"),
  effort_aerial$estimates |> mutate(method = "GLMM (multi-flight, time-of-day model)")
) |>
  ggplot(aes(estimate, method)) +
  geom_pointrange(aes(xmin = ci_lower, xmax = ci_upper), size = 0.8) +
  labs(
    x = "Total angler-hours (season estimate)",
    y = NULL
  ) +
  theme_bw(base_size = 12)
Point-and-range plot with two rows, one per method. Both show total angler-hours with 95% confidence intervals. The naive estimate is higher than the GLMM estimate, indicating upward bias from the midmorning flight timing.
Figure 11.2: Aerial effort estimates from a naive single-flight expansion versus the GLMM time-of-day model. The naive estimate treats a midmorning count as representative of the full day; the GLMM integrates a fitted density curve. The difference illustrates the timing bias that motivates multi-flight designs.

The midmorning flight catches anglers near the daily peak, so the naive expansion inflates the estimate relative to the GLMM, which integrates the full day including early-morning and late-afternoon periods of lower activity. The confidence intervals also differ: the naive estimate has wider uncertainty because it does not leverage the within-day temporal structure to constrain the daily total.

Catch rate from an aerial design

Remote count designs change how effort is estimated but leave the catch-rate pipeline unchanged. estimate_catch_rate() on an aerial design object behaves identically to a standard access-point design (Chapter 4) and the catch estimation pipeline in Chapter 13:

Code
estimate_catch_rate(design_aerial)
ℹ Using complete trips for CPUE estimation
  (n=48, 100% of 48 interviews) [default]

── Creel Survey Estimates ──────────────────────────────────────────────────────
Method: Ratio-of-Means CPUE
Variance: Taylor linearization
Confidence level: 95%

# A tibble: 1 × 5
  estimate     se ci_lower ci_upper     n
     <dbl>  <dbl>    <dbl>    <dbl> <int>
1    0.413 0.0601    0.295    0.531    48

The CPUE estimate uses completed-trip interviews only, exactly as in a standard access-point design. The design_type = "aerial" flag affects only effort estimation; catch rate, harvest, and release estimation are design-type agnostic.

That asymmetry is deliberate: aerial data revise the effort side of the estimate, while completed-trip interviews still supply the catch-rate side. Only the effort side is sensor-specific.

11.3 Camera-assisted designs

Access-point counters

Boat-ramp cameras and vehicle counters take a different approach to remote counting: instead of a snapshot of who is on the water, they record every arrival (ingress) and, when configured to capture both directions, every departure (egress). The count is not a density at a moment in time — it is a cumulative total of use-events for the day.

The challenge this introduces is downtime: cameras fail. Batteries die, memory cards fill, lenses ice over, and power outages interrupt recording. A day with camera downtime is a day where the count is missing or underrepresented. If those days are disproportionately high-use days — weekends, good-weather days — excluding them biases the effort estimate downward.

In other words, the camera can fail precisely when use is highest. That makes missingness part of the design problem, not just a maintenance issue.

impute_camera_counts() handles missing counts using a stratum-specific generalized linear model fitted to the operational days. The imputed values carry a flag (.imputed = TRUE) that allows downstream diagnostics to distinguish observed from modeled counts.

Camera data and downtime

The bundled example_camera_counts dataset has one day with NA ingress count due to battery failure:

Code
example_camera_counts
         date day_type ingress_count   camera_status
1  2024-06-03  weekday            48     operational
2  2024-06-04  weekday            55     operational
3  2024-06-05  weekday            43     operational
4  2024-06-07  weekend            91     operational
5  2024-06-08  weekend            85     operational
6  2024-06-10  weekday            50     operational
7  2024-06-11  weekday            NA battery_failure
8  2024-06-12  weekday            61     operational
9  2024-06-14  weekend            98     operational
10 2024-06-15  weekend            82     operational

impute_camera_counts() fills the missing value using a within-stratum GLM fitted to the operational rows. The imputed count is flagged in the .imputed column:

Code
camera_counts_clean <- impute_camera_counts(
  example_camera_counts,
  count_col  = "ingress_count",
  strata_col = "day_type",
  status_col = "camera_status"
)
camera_counts_clean
         date day_type ingress_count   camera_status .imputed
1  2024-06-03  weekday            48     operational    FALSE
2  2024-06-04  weekday            55     operational    FALSE
3  2024-06-05  weekday            43     operational    FALSE
4  2024-06-10  weekday            50     operational    FALSE
5  2024-06-11  weekday            51 battery_failure     TRUE
6  2024-06-12  weekday            61     operational    FALSE
7  2024-06-07  weekend            91     operational    FALSE
8  2024-06-08  weekend            85     operational    FALSE
9  2024-06-14  weekend            98     operational    FALSE
10 2024-06-15  weekend            82     operational    FALSE

Weekday mean ingress on operational days is 51 vehicles; the imputed value for June 11 (weekday, battery failure) is 51 — the stratum mean. With more operational weekdays in the training set, the GLM would use day-of-week or environmental covariates if they were present in the data.

Figure 11.3 shows the ingress counts across the survey period, with the imputed observation visually distinguished.

Code
camera_counts_clean |>
  mutate(date = as.Date(date)) |>
  ggplot(aes(date, ingress_count, colour = day_type, shape = .imputed)) +
  geom_point(size = 3.5) +
  scale_colour_manual(
    values = c(weekday = "#2171b5", weekend = "#e6550d"),
    labels = c(weekday = "Weekday", weekend = "Weekend"),
    name   = NULL
  ) +
  scale_shape_manual(
    values = c(`FALSE` = 16, `TRUE` = 2),
    labels = c(`FALSE` = "Observed", `TRUE` = "Imputed (battery failure)"),
    name   = NULL
  ) +
  labs(
    x = NULL,
    y = "Ingress count (vehicles)"
  ) +
  theme_bw(base_size = 12) +
  theme(legend.position = "bottom")
Scatter plot with date on the x axis and ingress count on the y axis. Points are coloured by day type (weekday blue, weekend orange) and shaped by imputed status (solid circles for observed, open triangle for imputed). Weekend points cluster around 80-100; weekday points cluster around 45-60.
Figure 11.3: Daily boat-ramp camera ingress counts over the survey period. Observed counts (solid points) are distinguished from the imputed value on June 11 (open triangle, battery failure). Weekend counts are consistently higher than weekday counts, reflecting the typical recreational fishing pattern.

Building the camera design

Camera designs use design_type = "camera" and require camera_mode to specify what the counter records. The current supported modes are:

  • "counter" — a simple ingress counter (vehicles or anglers entering)
  • "ingress_egress" — paired ingress and egress timestamps, from which preprocess_camera_timestamps() computes trip-level effort

The counter mode is the most common and requires only the imputed count table passed to add_counts(). This is the simplest camera case: the sensor gives a daily traffic total, and the analyst reconstructs angler-hours from counts and mean trip length.

Code
camera_calendar <- tibble(
  date     = seq(min(example_camera_counts$date),
                 max(example_camera_counts$date), by = "day"),
  day_type = if_else(wday(date) %in% c(1L, 7L), "weekend", "weekday")
)

design_camera <- creel_design(
  calendar    = camera_calendar,
  date        = "date",
  strata      = "day_type",
  design_type = "camera",
  camera_mode = "counter",
  h_open      = 14
) |>
  add_counts(camera_counts_clean) |>
  add_interviews(
    interviews    = example_camera_interviews,
    catch         = "walleye",
    harvest       = "walleye_kept",
    effort        = "hours_fished",
    trip_status   = "trip_status",
    trip_duration = "hours_fished"
  )
Warning in svydesign.default(ids = psu_formula, strata = strata_formula, : No
weights or probabilities supplied, assuming equal probability
ℹ No `n_anglers` provided — assuming 1 angler per interview.
ℹ Pass `n_anglers = <column>` to use actual party sizes for angler-hour
  normalization.
Warning: 14 interviews have zero catch.
ℹ Zero catch may be valid (skunked) or indicate missing data.
ℹ Added 40 interviews: 40 complete (100%), 0 incomplete (0%)

Ratio-calibrated effort estimation

est_effort_camera() uses the ratio of mean interview trip duration to the camera count — a ratio estimator that converts ingress events to angler-hours. The intuition: if the camera records 60 vehicles entering and the mean trip length from interviews is 3.5 hours, the day-level effort is approximately 60 × 3.5 = 210 angler-hours. The ratio is estimated within strata and expanded to the full survey period:

Code
effort_camera <- est_effort_camera(
  design_camera,
  interviews = example_camera_interviews,
  effort_col = "hours_fished"
)
effort_camera

── Creel Survey Estimates ──────────────────────────────────────────────────────
Method: camera_ratio
Variance: Taylor linearization
Confidence level: 95%

# A tibble: 1 × 7
  estimate    se se_between se_within ci_lower ci_upper     n
     <dbl> <dbl>      <dbl>     <dbl>    <dbl>    <dbl> <int>
1     123.  10.9       10.9         0     97.9     148.    10

The result is a total angler-hours estimate with a Taylor-linearization standard error. For the example data, 123.1 angler-hours (95% CI: 97.9–148.3) across the 10-day window. The se_between component reflects between-day variation in the ingress × trip-length product; se_within reflects within-day interview variance in trip duration.

That split tells you where the next precision gain is likely to come from: more camera-days if between-day variation dominates, more interviews if trip-length variation dominates.

As with the aerial design, catch rate estimation is unchanged:

Code
estimate_catch_rate(design_camera)
ℹ Using complete trips for CPUE estimation
  (n=40, 100% of 40 interviews) [default]

── Creel Survey Estimates ──────────────────────────────────────────────────────
Method: Ratio-of-Means CPUE
Variance: Taylor linearization
Confidence level: 95%

# A tibble: 1 × 5
  estimate     se ci_lower ci_upper     n
     <dbl>  <dbl>    <dbl>    <dbl> <int>
1    0.453 0.0755    0.305    0.601    40

The design_type affects only how effort is estimated. From estimate_catch_rate() onward, camera and access-point designs use identical code paths.

Ingress-egress timestamps

When cameras record both arrival and departure times for individual vehicles, preprocess_camera_timestamps() converts the raw timestamp pairs into daily effort totals. This requires a data frame with one row per vehicle event and ingress_time and egress_time columns as POSIXct values:

Code
example_camera_timestamps
         date day_type        ingress_time         egress_time
1  2024-06-03  weekday 2024-06-03 06:30:00 2024-06-03 09:45:00
2  2024-06-03  weekday 2024-06-03 07:15:00 2024-06-03 12:15:00
3  2024-06-03  weekday 2024-06-03 08:00:00 2024-06-03 10:45:00
4  2024-06-04  weekday 2024-06-04 05:45:00 2024-06-04 10:00:00
5  2024-06-04  weekday 2024-06-04 06:30:00 2024-06-04 10:00:00
6  2024-06-04  weekday 2024-06-04 07:00:00 2024-06-04 12:30:00
7  2024-06-04  weekday 2024-06-04 08:30:00 2024-06-04 10:00:00
8  2024-06-08  weekend 2024-06-08 05:30:00 2024-06-08 14:30:00
9  2024-06-08  weekend 2024-06-08 06:00:00 2024-06-08 10:30:00
10 2024-06-08  weekend 2024-06-08 06:30:00 2024-06-08 10:00:00
11 2024-06-08  weekend 2024-06-08 07:00:00 2024-06-08 09:30:00
12 2024-06-09  weekend 2024-06-09 06:15:00 2024-06-09 11:30:00
13 2024-06-09  weekend 2024-06-09 07:00:00 2024-06-09 11:00:00
14 2024-06-09  weekend 2024-06-09 07:30:00 2024-06-09 11:00:00
Code
daily_effort <- preprocess_camera_timestamps(
  example_camera_timestamps,
  date_col    = "date",
  ingress_col = "ingress_time",
  egress_col  = "egress_time"
)
daily_effort
        date daily_effort_hours
1 2024-06-03              11.00
2 2024-06-04              14.75
3 2024-06-08              19.50
4 2024-06-09              12.75

The output is a daily summary of total recorded angler-hours from timestamp differences. This can substitute for or supplement the interview-based mean trip duration estimate — the camera itself measures effort directly, rather than inferring it from a count and a mean duration.

This is the cleanest camera case because effort is observed directly from the start and end times rather than inferred from counts.

11.4 Sample size for camera designs

Once a pilot camera season establishes the stratum-level mean and variance of the ingress × trip-length product, creel_n_camera() returns the number of sampled days needed to achieve a target coefficient of variation. The required inputs mirror those of creel_n_effort(): target CV, total days in the stratum, and stratum-level mean and variance from the pilot:

Code
# Compute stratum summary from operational days only
cam_summary <- camera_counts_clean |>
  filter(!.imputed) |>
  group_by(day_type) |>
  summarise(
    N_h    = n(),
    ybar_h = mean(ingress_count),
    s2_h   = var(ingress_count),
    .groups = "drop"
  )
cam_summary
# A tibble: 2 × 4
  day_type   N_h ybar_h  s2_h
  <chr>    <int>  <dbl> <dbl>
1 weekday      5   51.4  47.3
2 weekend      4   89    50  
Code
# Days needed per stratum to achieve CV = 0.15
# creel_n_camera() requires named numeric vectors
creel_n_camera(
  cv_target = 0.15,
  N_h    = setNames(cam_summary$N_h,    cam_summary$day_type),
  ybar_h = setNames(cam_summary$ybar_h, cam_summary$day_type),
  s2_h   = setNames(cam_summary$s2_h,   cam_summary$day_type)
)
weekday weekend   total 
      1       1       1 

With only five operational weekday observations and four weekend observations, the variance estimate is too thin to say much, so both strata bottom out at n = 1. That is a toy-data artifact: real pilot seasons with 20–30 operational days per stratum usually provide enough spread in ingress counts to make the calculation informative. Treat the output as a minimum sample-size target for each stratum, not as a precise optimum. creel_n_camera() also emits a warning when the pilot falls below the empirical minimum recommended by Feltz and Middaugh (2025); that warning is suppressed here because the example data intentionally fall below it.

11.5 Proxy counts and validation

Vehicle counters and gate sensors

Aerial counts and camera-monitored ramps are the most thoroughly supported remote-count methods in tidycreel, but other proxy systems appear in practice: pneumatic road tubes at boat launch entrances, infrared gate sensors at trailheads, and acoustic hydrophone arrays that detect boat motors in restricted channels. Each records a correlated signal of fishing activity rather than a direct observation of anglers.

The key question for any proxy is how strongly it tracks what the survey domain requires. A pneumatic tube at a single-access boat ramp on a lake with no bank fishing likely correlates nearly 1:1 with launched boats. A road-tube counter at a multi-access reservoir where some anglers walk in from informal paths captures only the trailered-boat fraction. The correlation structure — and therefore the calibration factor — depends on access geometry, angler behavior, and whether the proxy integrates the full reporting domain.

Calibration against ground truth

Paired sampling is the standard validation approach: on a subset of days, both the proxy count and a direct ground count are collected on the same time window. The ratio of ground count to proxy count on paired days becomes the calibration factor applied to proxy-only days.

A calibration regression extends this: ground = a + b × proxy with the slope b estimated from paired days. The intercept accounts for baseline angler activity not captured by the proxy (walk-in anglers, for example), and the slope converts proxy units to ground-count equivalents.

Code
# Paired calibration: road-tube counter vs direct creel-crew count on the same days
# Simulated paired data: 15 days where both methods were deployed simultaneously
set.seed(42)
paired_days <- tibble(
  date        = seq.Date(as.Date("2024-06-03"), by = "2 days", length.out = 15),
  tube_count  = as.integer(rpois(15, lambda = 55)),
  crew_count  = as.integer(round(0.82 * tube_count + rnorm(15, 0, 4)))
)

cal_fit <- lm(crew_count ~ tube_count, data = paired_days)
coef(cal_fit)
(Intercept)  tube_count 
  2.1682712   0.8162411 

The intercept (2.2) captures angler activity not counted by the road tube — walk-in anglers, kayak launches from an unmonitored site. The slope (0.816) converts tube-count units to crew-count equivalents. On proxy-only days, multiply the tube count by this slope and add the intercept to recover the calibrated angler count before applying mean trip length.

A slope near 1.0 indicates the proxy and direct count are on the same scale; a slope below 1 indicates systematic undercounting — the tube misses some fraction of the angler population — that the calibration corrects. The regression diagnostic (R^2, residual plot) should be checked before trusting the calibration: if the relationship is noisy or nonlinear, a simple ratio calibration will not transfer reliably to proxy-only days.

Documenting downtime and coverage gaps

No remote monitoring system has perfect uptime. Effective survey documentation requires a record of:

  • Days when the system was fully operational
  • Days with partial downtime (camera down for X of Y hours)
  • Days with complete failure (no data)
  • Known failure modes (battery, connectivity, vandalism, weather)

The camera_status column in example_camera_counts illustrates the minimum required record. In practice, richer downtime records allow the imputation model to condition on failure type — battery failures cluster by temperature and season; connectivity failures cluster by storm events.

Code
example_camera_counts |>
  count(camera_status, day_type) |>
  arrange(desc(n))
    camera_status day_type n
1     operational  weekday 5
2     operational  weekend 4
3 battery_failure  weekday 1

A camera with 90% uptime over a 30-day survey loses roughly 3 operational days. If those failures are random relative to angler activity, imputation adds noise but not much bias. If failures correlate with weather — cameras fail in cold rain, which also suppresses fishing — the imputed values reflect low-use conditions, and the estimate is biased downward. Understanding the failure mechanism matters as much as the imputation method.

11.6 tidycreel support summary

The three major remote-count workflows and their tidycreel entry points:

Design design_type Count function Estimate function
Aerial GLMM "aerial" add_counts() (raw flights) estimate_effort_aerial_glmm()
Camera counter "camera" add_counts() (imputed) est_effort_camera()
Camera ingress/egress "camera" + timestamps preprocess_camera_timestamps() est_effort_camera()
Proxy count (manual calibration) any calibration regression ratio applied to standard estimate

All four share the same downstream pipeline: design object → add_interviews()estimate_catch_rate() / estimate_total_harvest(). The remote-count method changes how effort is estimated; catch rate and harvest estimation proceed identically to access-point and roving designs.

11.7 Key considerations

Frame alignment. The camera or aircraft must cover the same spatial domain as the reporting unit. A ramp camera that misses an informal carry-in launch is not counting the full angler population. Aerial coverage that excludes a productive back bay leaves effort in that bay unrecorded. Frame gaps in remote designs are harder to detect than in creel-crew designs because there is no field crew to notice unexpected activity outside the monitored area.

Timing alignment. For aerial designs, h_open must match what the GLMM integrates over. If the model integrates from 06:00 to 20:00 (14 hours) but fishing in the reporting domain occurs only between 07:00 and 18:00, the integration window overestimates effort. Setting h_open conservatively from local sunrise-to-sunset tables avoids this bias.

Sensor resolution. A camera that counts vehicles counts launched boats (typically one to four anglers per boat). Converting vehicle counts to angler-hours requires an estimate of mean party size from interviews — an additional ratio estimator step with its own variance. The standard error from est_effort_camera() propagates this uncertainty automatically when party-size information is included in add_interviews().

Uptime documentation. Effort estimates from camera designs are only as reliable as the imputation model for failed days. More operational days in the calibration sample → better imputation. Effort estimates should report the fraction of days that were imputed alongside the estimate and standard error, so users can assess how much of the result rests on modeled versus observed counts.

Remote count designs extend the creel survey toolkit substantially: larger waterbodies become tractable, high-frequency temporal monitoring becomes affordable, and record-keeping becomes auditable. What they do not change is the need for a defined frame, a defensible sampling design, and explicit assumptions about what the sensor measures and what it misses.

Feltz, N. G., and C. R. Middaugh. 2025. Improving efficiency of estimating angler effort using low-frequency time-lapse camera data. North American Journal of Fisheries Management 45(2):322–332.