
GLMM-based aerial effort estimation with diurnal correction
Source:R/creel-estimates-aerial-glmm.R
estimate_effort_aerial_glmm.RdEstimates total angler effort from aerial creel surveys using a generalized linear mixed model (GLMM), following the approach of Askey et al. (2018). When flights occur at non-random times of day, simple scaling of instantaneous counts can over- or under-estimate daily effort. This function fits a negative-binomial GLMM (or user-specified family) to model how angler counts change through the day, then integrates the fitted diurnal curve over the fishing day to obtain a bias-corrected effort estimate.
The default model is the quadratic temporal model from Askey (2018):
count ~ poly(time_col, 2) + (1 | date), fitted via
lme4::glmer.nb(). Variance is propagated via the delta method (default) or
parametric bootstrap (lme4::bootMer()).
Usage
estimate_effort_aerial_glmm(
design,
time_col,
formula = NULL,
family = NULL,
boot = FALSE,
nboot = 500L,
conf_level = 0.95,
target = c("sampled_days", "mean_day"),
by = NULL
)Arguments
- design
A
creel_design()object withdesign_type == "aerial"and counts attached viaadd_counts(). The counts data must contain the time-of-flight column specified bytime_col.- time_col
Unquoted name of the numeric column in
design$countsrecording the hour of each aerial overflight (e.g.,time_of_flight).- formula
Optional. A formula for the GLMM, passed directly to
lme4::glmer.nb()orlme4::glmer(). IfNULL(default), the Askey (2018) quadratic formula is used:count ~ poly(time_col, 2) + (1 | date).- family
Optional. A family object or character string specifying the GLM family. If
NULLor"negbin"(default),lme4::glmer.nb()is used. Otherwise,lme4::glmer()is called with the specified family.- boot
Logical. If
TRUE, uselme4::bootMer()for parametric bootstrap confidence intervals instead of the delta method. DefaultFALSE.- nboot
Integer. Number of bootstrap replicates when
boot = TRUE. Default500L.- conf_level
Numeric confidence level for the CI. Default
0.95.- target
Character string giving the temporal basis of the returned estimate.
"sampled_days"(default) expands the fitted day to every day the design sampled, matching whatestimate_effort()returns for the same design so the two are comparable."mean_day"reports a single average day, the basis this function reported before tidycreel 8.0.0. It is not identical to the old value: it now carries the retransformation factor the old code omitted, so it is higher byexp(sigma^2 / 2).Both are expectations, so both carry the retransformation factor described under Details. Neither expands beyond the sampled days: this model-based estimator has no calendar expansion. The design-based aerial estimate in
estimate_effort()does, through itstargetargument (#519).- by
Optional tidy selection of columns in
design$countsto estimate within, typically the design strata (for exampleby = day_type). The strata enter the default model as additive fixed effects in ONE model –count ~ poly(time_col, 2) + day_type + (1 | date)– so they share the diurnal curve's shape and differ in its level; see Details. Each stratum expands by its own sampled days. A count with a missing value in abycolumn belongs to an unknown stratum, which is its own level in the model and its own row (NA), so the rows account for every count. Stratum columns keep their source type. Supported on the delta path only (boot = FALSE). Whenformulais supplied it is used as given, and it must contain everybycolumn as a fixed effect; otherwise every stratum would get the same curve, so the call is refused. Abycolumn with only one observed level is left out of the model (its level is the intercept) and still reported as a row.
Value
A creel_estimates object with:
estimate: total angler effort integrated over the fishing dayse: standard error (delta method or bootstrap SD)se_between: same asse(fixed-effect SE component)se_within: alwaysNA_real_— no Rasmussen within-day decomposition is performed for GLMM estimatesci_lower,ci_upper: confidence interval bounds, andNA_real_wheneverseis, on both the delta and bootstrap paths. If the visibility correction or the angler-to-people ratio was declared unknown, the total's uncertainty was never fully propagated, so no unconditional interval exists to report. Reporting the remaining spread would be an interval conditional on the unknown multiplier being exact – indistinguishable from declaring it known with zero uncertainty, which is precisely the confusionNAexists to prevent.n: number of count observations used to fit the model (withby, the observations in that stratum)method:"aerial_glmm_total"
With by, there is one row per stratum, headed by the by columns, and
the object carries strata_vcov: the covariance matrix of the stratum
estimates, rows and columns in the order of the estimate rows. Its
diagonal is se^2. The off-diagonal is not zero: every stratum is
predicted from the same fixed effects, and the visibility correction and
angler-to-people ratio are single estimates that multiply every stratum.
Combine strata with it – sqrt(sum(strata_vcov)) is the SE of the
summed total – and never by adding the rows' SEs in quadrature, which
understates it.
Details
The fitted curve is a fixed-effects prediction: the day whose random
intercept is zero. On a log link that is the median day rather than the
mean one, so summing it across days would understate the total. Both targets
therefore carry a factor of exp(sigma^2 / 2), where sigma^2 is the
day-level intercept variance — 4% on the package's own fixture, and larger
where days vary more.
That factor treats sigma^2 as known. The reported standard error scales
with the expansion but does not carry the uncertainty in the variance
component itself, so it is mildly optimistic; quantifying that would need a
variance method neither the delta nor the bootstrap path offers today.
Estimating within strata
With by, the strata are additive: they shift the level of one shared
diurnal curve and do not change its shape. That follows Askey et al.
(2018), where day type is additive in every model structure compared; the
one interaction they tested (month x hour) was preferred by AIC, rejected by
BIC, and bought no predictive gain in cross-validation. A single model also
shares strength across strata, which matters when a stratum has few flights.
The assumption can fail. Smucker et al. (2010, Table 1) report weekday and
weekend diurnal effort that differ in shape, most clearly for shore anglers.
So the default grouped fit also fits the time x stratum interaction and
reports the BIC difference as a message. A negative difference favours
separate shapes; fit one by passing formula, for example
n_anglers ~ poly(time_of_flight, 2) * day_type + (1 | date). The estimate
is never switched automatically, because a choice made from the data is not
reflected in the reported standard error.
Askey et al. also found that with many randomly timed counts (about 60 or more) a model-based estimator offered no advantage over expanding the mean count, which was the only unbiased estimator in their comparison. The GLMM earns its place when flights are few or their timing is not random.
References
Askey, P.J., Ward, H., Godin, T., Boucher, M., and Northrup, S. (2018). Angler effort estimates from instantaneous aerial counts: use of high-frequency time-lapse camera data to inform model-based estimators. North American Journal of Fisheries Management, 38, 194-209. doi:10.1002/nafm.10010
Smucker, B.J., Lorantas, R.M., and Rosenberger, J.L. (2010). Correcting bias introduced by aerial counts in angler effort estimation. North American Journal of Fisheries Management, 30, 1051-1061. doi:10.1577/M09-193.1
See also
Other "Estimation":
compare_cpue_estimators(),
est_age_distribution(),
est_biomass(),
est_compliance(),
est_effort_camera_mi(),
est_length_distribution(),
est_mean_age(),
est_mean_length(),
estimate_catch_rate(),
estimate_effort(),
estimate_harvest_rate(),
estimate_release_rate(),
estimate_total_catch(),
estimate_total_harvest(),
estimate_total_release()
Examples
data(example_aerial_glmm_counts)
aerial_cal <- unique(example_aerial_glmm_counts[, c("date", "day_type")])
aerial_cal <- aerial_cal[order(aerial_cal$date), ]
design <- creel_design(
aerial_cal,
date = date,
strata = day_type,
survey_type = "aerial",
visibility_correction = "none",
angler_ratio = 1,
angler_ratio_se = 0,
h_open = 14
)
design <- add_counts(design, example_aerial_glmm_counts, count_col = n_anglers)
#> Warning: `counts` has 36 repeated sampling units, with no count time to tell them apart.
#> ℹ The repeated rows are keyed on date and day_type.
#> ℹ Estimators that sum these rows refuse them; supply `count_time_col` if they
#> are repeat counts, or `unit_cols` if they are distinct units.
# Default Askey quadratic model with delta-method SE
result <- estimate_effort_aerial_glmm(design, time_col = time_of_flight)
#> Warning: iteration limit reached
#> ℹ Integration window start derived from data: 6.5 h (earliest flight - 0.5 h).
#> Specify `open_start` in `creel_design()` for a fixed fishery opening time.
print(result)
#>
#> ── Creel Survey Estimates ──────────────────────────────────────────────────────
#> Method: aerial_glmm_total
#> Variance: delta
#> Confidence level: 95%
#> Effort target: sampled_days
#> model: 412 (known, but se is `NA`)
#> visibility: NA (unknown, so se is `NA`)
#> angler_ratio: 0 (known, but se is `NA`)
#>
#> # A tibble: 1 × 7
#> estimate se se_between se_within ci_lower ci_upper n
#> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <int>
#> 1 4729. NA NA NA NA NA 48
# One row per day type, from one model with day type as an additive term.
# `visibility_correction = "none"` above leaves the SEs unknown (NA), so this
# design declares a measured detection probability and its SE.
design_v <- creel_design(
aerial_cal,
date = date,
strata = day_type,
survey_type = "aerial",
visibility_correction = 0.85,
visibility_se = 0.05,
angler_ratio = 1,
angler_ratio_se = 0,
h_open = 14
)
design_v <- add_counts(design_v, example_aerial_glmm_counts, count_col = n_anglers)
#> Warning: `counts` has 36 repeated sampling units, with no count time to tell them apart.
#> ℹ The repeated rows are keyed on date and day_type.
#> ℹ Estimators that sum these rows refuse them; supply `count_time_col` if they
#> are repeat counts, or `unit_cols` if they are distinct units.
by_day <- estimate_effort_aerial_glmm(design_v, time_col = time_of_flight, by = day_type)
#> Warning: iteration limit reached
#> ℹ BIC(interaction) - BIC(additive) = 5.9: favours the shared shape used for
#> this estimate.
#> The interaction fit raised 1 warning; treat the comparison with caution.
#> ℹ Integration window start derived from data: 6.5 h (earliest flight - 0.5 h).
#> Specify `open_start` in `creel_design()` for a fixed fishery opening time.
print(by_day)
#>
#> ── Creel Survey Estimates ──────────────────────────────────────────────────────
#> Method: aerial_glmm_total
#> Variance: delta
#> Confidence level: 95%
#> Grouped by: day_type
#> Effort target: sampled_days
#> model: 380.6 and 290.4 (included in se)
#> visibility: 212.0 and 115.3 (included in se)
#> angler_ratio: 0 and 0 (included in se)
#>
#> # A tibble: 2 × 8
#> day_type estimate se se_between se_within ci_lower ci_upper n
#> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <int>
#> 1 weekday 3603. 436. 436. NA 2750. 4457. 32
#> 2 weekend 1960. 312. 312. NA 1347. 2572. 16
# SE of the summed total: use the joint covariance, not quadrature
sqrt(sum(by_day$strata_vcov))
#> [1] 581.6951
# Bootstrap CIs. `nboot` is held low here so the example stays fast on a
# check machine; use at least 1000 replicates for real inference. The block
# is wrapped in \donttest{} for runtime alone -- it needs no resource the
# example cannot reach.
# \donttest{
result_boot <- estimate_effort_aerial_glmm(
design,
time_col = time_of_flight,
boot = TRUE,
nboot = 25L
)
#> Warning: iteration limit reached
#> ℹ Integration window start derived from data: 6.5 h (earliest flight - 0.5 h).
#> Specify `open_start` in `creel_design()` for a fixed fishery opening time.
#> Running 25 bootstrap replicates via lme4::bootMer...
#> Warning: ! Bootstrap SE ignores design strata ("day_type").
#> ℹ The default GLMM formula has no stratum term; bootstrap resamples from a
#> single pooled model. Include strata in `formula` for stratified inference.
print(result_boot)
#>
#> ── Creel Survey Estimates ──────────────────────────────────────────────────────
#> Method: aerial_glmm_total
#> Variance: Bootstrap
#> Confidence level: 95%
#> Effort target: sampled_days
#> model: NA (unknown, so se is `NA`)
#>
#> # A tibble: 1 × 7
#> estimate se se_between se_within ci_lower ci_upper n
#> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <int>
#> 1 4729. NA NA NA NA NA 48
# }