Ecologists routinely analyse ratios: catch per unit effort (CPUE), species per unit area, biomass per trawl hour, detections per camera-night. The standard approach is to compute the ratio and model it directly:
# Simulated trawls: site quality raises both catch and effort,
# while the true CPUE is 3 everywhere
set.seed(1)
n <- 200
site_quality <- rnorm(20, 0, 0.5)
data <- data.frame(
site = factor(rep(1:20, each = 10)),
depth = rnorm(n),
season = factor(sample(c("spring", "summer", "fall"), n, replace = TRUE))
)
data$effort <- rgamma(n, shape = 5, rate = 5 / exp(1 + site_quality[data$site]))
data$catch <- rpois(n, lambda = 3 * data$effort)
# The WRONG approach (don't do this)
data$cpue <- data$catch / data$effort
lm(cpue ~ depth + season, data = data)This approach is fundamentally flawed. Here’s why.
A common “fix” is to use an offset:
This assumes the ratio is the quantity of interest and that effort affects catch proportionally with coefficient exactly 1. Neither assumption is generally true:
Effort has its own structure: Effort varies systematically: more effort in accessible sites, during good weather, when funding permits. These patterns create confounding.
The proportionality assumption fails: A 10% increase in effort rarely yields exactly 10% more catch. Saturation effects, interference between sampling units, and diminishing returns are common.
Uncertainty in effort is ignored: Effort measurements have error. Offset models treat effort as fixed and known.
tulpaRatio takes a different approach. Instead of modelling the ratio, we jointly model the two processes that generate it:
\[ Y_i \sim \text{Distribution}_1(\mu_Y, \phi_Y) \quad \text{(numerator)} \] \[ N_i \sim \text{Distribution}_2(\mu_N, \phi_N) \quad \text{(denominator)} \]
with linked linear predictors:
\[ \log(\mu_Y) = X\beta_Y + \underbrace{b_{\text{shared}}}_{\text{enters both}} + \eta_i \] \[ \log(\mu_N) = Z\beta_N + \underbrace{b_{\text{shared}}}_{\text{enters both}} + \xi_i \]
The ratio is then a derived quantity, computed in the posterior:
\[ r_i = \frac{\mathbb{E}[Y_i \mid \cdot]}{\mathbb{E}[N_i \mid \cdot]} = \exp(\eta_i - \xi_i) \]
tulpaRatio provides three model families for different data types:
ratiod_negbin_negbin()Both numerator and denominator are overdispersed counts:
\[ Y_i \sim \text{NegBin}(\mu_Y, \phi_Y), \quad N_i \sim \text{NegBin}(\mu_N, \phi_N) \]
Use cases: Species count / total count, events / opportunities
This is the family for the “hard case”: what people cannot do correctly with binomial models.
ratiod_binomial()Successes out of known trials:
\[ Y_i \sim \text{Binomial}(N_i, p_i) \]
Use cases: Detections / availability, successes / attempts
Here the denominator is treated as known (though it can be modeled if uncertain).
ratiod_poisson_gamma()Count numerator, continuous positive denominator:
\[ Y_i \sim \text{Poisson}(\mu_Y), \quad E_i \sim \text{Gamma}(\alpha, \beta) \]
Use cases: CPUE, observations per hour, catch per trawl
Because ratios are computed from the full posterior, every draw reflects uncertainty in:
This is exact posterior inference on the ratio, not an approximation or delta-method estimate.
There are cases where shared structure is not needed:
Designed experiments: If effort is controlled and randomized, it may be independent of the outcome process.
Known denominators: Population counts from a census, predetermined sample sizes.
Different observation processes: If numerator and denominator are measured by completely independent methods with no common drivers.
In these cases, use shared = ~ 0:
set.seed(2)
experiment_data <- data.frame(
treatment = factor(rep(c("control", "treated"), each = 20)),
trials = rpois(40, 30) + 1
)
experiment_data$successes <- rbinom(
40, experiment_data$trials,
ifelse(experiment_data$treatment == "treated", 0.6, 0.4)
)
fit_indep <- tratio(
successes | trials ~ treatment,
shared = ~ 0, # Explicit independence (triggers warning)
family = ratiod_binomial(),
data = experiment_data,
control = list(seed = 1)
)The warning reminds you that this is a strong assumption.
| Approach | Problem |
|---|---|
| Model ratio directly | Heteroscedasticity, ignores denominator uncertainty |
| Use offset | Assumes proportionality, ignores effort structure |
| Independent joint model | Spurious ratio effects from shared unmeasured drivers |
| tulpaRatio (shared structure) | Correct propagation of uncertainty, shared drivers cancel |
tulpaRatio enforces the correct approach by design: