Ecological and survey data often exhibit excess zeros beyond what standard distributions predict. tulpaRatio provides several families to handle this:
Zero-inflated models assume zeros come from two sources: 1. Structural zeros (species truly absent, observer non-response) 2. Sampling zeros (from the count/binomial process)
Hurdle models assume all zeros come from a single “hurdle” process, with positive counts from a truncated distribution.
| Family | Base | ZI Type | Use Case |
|---|---|---|---|
ratiod_zinegbin() |
Negative binomial | Mixture | Overdispersed counts with excess zeros |
ratiod_zipois() |
Poisson | Mixture | Counts where overdispersion is from zeros only |
ratiod_hurdle_negbin() |
Negative binomial | Hurdle | Presence/absence + abundance |
ratiod_hurdle_pois() |
Poisson | Hurdle | Presence/absence + Poisson counts |
| Family | Base | ZI Type | Use Case |
|---|---|---|---|
ratiod_zibinomial() |
Binomial | Zero-inflated | Excess zero success rates |
ratiod_oibinomial() |
Binomial | One-inflated | Excess 100% success rates |
ratiod_zoibinomial() |
Binomial | Zero-and-one | Excess at both boundaries |
ratiod_hurdle_binomial() |
Binomial | Hurdle | Zero vs positive successes |
Use ratiod_zibinomial() when modeling proportions with
excess zeros beyond what binomial predicts.
library(tulpaRatio)
ctl <- list(seed = 1)
# Simulate detection data with structural zeros
set.seed(123)
n <- 60
habitat <- factor(rep(c("forest", "grassland"), each = n/2))
site <- factor(rep(1:10, each = n/10))
site_eff <- rnorm(10, 0, 0.5)[site]
# Detection probability rises in grassland and varies by site
p <- plogis(-0.4 + 0.5 * (habitat == "grassland") + site_eff)
# Structural zeros: 15% in forest, 40% in grassland
zi_prob <- ifelse(habitat == "forest", 0.15, 0.4)
detection <- data.frame(
detected = ifelse(
runif(n) < zi_prob,
0, # Structural zero
rbinom(n, size = 10, prob = p) # Binomial detections
),
trials = 10,
habitat = habitat,
site = site
)
# Fit zero-inflated binomial
fit_zi <- tratio(
detected | trials ~ habitat + (1 | site),
zi = zi_binomial(~ habitat), # Predictors for zero-inflation
data = detection,
family = ratiod_zibinomial(),
control = ctl
)
print(coef(summary(fit_zi)), digits = 3)The zi argument takes a specification such as
zi_binomial(), whose formula is the linear predictor for
the zero-inflation probability (beta_zi above, on the logit
scale). Use zi_binomial(~ 1) for intercept-only or include
covariates that predict structural zeros.
Use ratiod_oibinomial() when data has excess ones (100%
success rates).
# Survey completion data with some "always complete" respondents
set.seed(456)
n <- 60
oi_prob <- 0.2 # 20% always complete all items
survey <- data.frame(
completed = ifelse(
runif(n) < oi_prob,
20, # Structural one (all items completed)
rbinom(n, size = 20, prob = 0.7)
),
total_items = 20,
age_group = factor(sample(c("young", "middle", "old"), n, replace = TRUE)),
education = rnorm(n)
)
# Fit one-inflated binomial
fit_oi <- tratio(
completed | total_items ~ age_group + education,
data = survey,
family = ratiod_oibinomial(),
control = ctl
)
print(coef(summary(fit_oi)), digits = 3)Use ratiod_zoibinomial() when data has excess at both
boundaries.
Vegetation cover data often has structural zeros (no vegetation) and structural ones (complete cover).
set.seed(789)
n <- 60
zi_prob <- 0.15 # 15% bare ground
oi_prob <- 0.10 # 10% complete cover (given not bare)
cover <- data.frame(
cover_points = sapply(1:n, function(i) {
if (runif(1) < zi_prob) {
0 # Structural zero
} else if (runif(1) < oi_prob) {
100 # Structural one
} else {
rbinom(1, size = 100, prob = 0.5)
}
}),
total_points = 100,
elevation = rnorm(n), # standardized elevation
aspect = factor(sample(c("N", "S", "E", "W"), n, replace = TRUE))
)
# Fit ZOIB model
fit_zoib <- tratio(
cover_points | total_points ~ elevation + aspect,
zi = zoib(
zi_formula = ~ elevation, # Zero-inflation predictors
oi_formula = ~ elevation # One-inflation predictors
),
data = cover,
family = ratiod_zoibinomial(),
control = ctl
)
print(coef(summary(fit_zoib)), digits = 3)Use ratiod_hurdle_binomial() when zeros represent a
distinct process from positive counts.
set.seed(321)
n <- 60
occupancy <- data.frame(
visits = 5L,
habitat_quality = rnorm(n),
site = factor(rep(1:12, each = 5))
)
# Presence rises with habitat quality; where present, the detections out of
# five visits are a binomial count given at least one detection, with a
# site effect on the detection probability
present <- runif(n) < plogis(0.8 + 0.8 * occupancy$habitat_quality)
p_detect <- plogis(0.3 + 0.4 * occupancy$habitat_quality +
rnorm(12, 0, 0.5)[occupancy$site])
detect_positive <- function(p) {
repeat {
y <- rbinom(1, 5, p)
if (y > 0) return(y)
}
}
occupancy$detections <- ifelse(present, vapply(p_detect, detect_positive, numeric(1)), 0)
# Fit hurdle binomial
fit_hurdle <- tratio(
detections | visits ~ habitat_quality + (1 | site),
zi = hurdle_binomial(~ habitat_quality), # Predictors for hurdle (presence)
data = occupancy,
family = ratiod_hurdle_binomial(),
control = ctl
)
print(coef(summary(fit_hurdle)), digits = 3)For count numerators with continuous denominators (CPUE-type data):
# Zero-inflated CPUE data
set.seed(111)
n <- 60
season <- factor(rep(c("spring", "summer"), each = n/2))
vessel <- factor(rep(1:12, each = n/12))
# A vessel effect shared by catch and effort
vessel_eff <- rnorm(12, 0, 0.5)[vessel]
depth <- rnorm(n)
# Structural zeros: 15% in spring, 35% in summer
zi_prob <- ifelse(season == "spring", 0.15, 0.35)
cpue <- data.frame(
catch = ifelse(
runif(n) < zi_prob,
0,
rpois(n, exp(2 - 0.3 * depth + vessel_eff))
),
effort = rgamma(n, shape = 4, rate = exp(-vessel_eff)),
depth = depth,
season = season,
vessel = vessel
)
# Zero-inflated Poisson with gamma denominator
fit_zi_cpue <- tratio(
catch | effort ~ depth + season + (1 | vessel),
zi = zi_poisson(~ season),
data = cpue,
family = ratiod_zipois(denom_family = "gamma"),
control = ctl
)
print(coef(summary(fit_zi_cpue)), digits = 3)| Scenario | Recommended Family |
|---|---|
| Zeros from mixed sources (structural + sampling) | ratiod_zi*() |
| All zeros from single process (presence/absence) | ratiod_hurdle_*() |
| Excess at 0 only | ratiod_zibinomial() |
| Excess at 1 (100%) only | ratiod_oibinomial() |
| Excess at both 0 and 1 | ratiod_zoibinomial() |
| Overdispersion primarily from zeros | ratiod_zipois() |
| Overdispersion in zeros AND positive counts | ratiod_zinegbin() |
Compare ZI and non-ZI models using LOO-CV:
# Fit standard model
fit_standard <- tratio(
detected | trials ~ habitat + (1 | site),
data = detection,
family = ratiod_binomial(),
control = ctl
)
# Fit zero-inflated model
fit_zi <- tratio(
detected | trials ~ habitat + (1 | site),
zi = zi_binomial(~ 1),
data = detection,
family = ratiod_zibinomial(),
control = ctl
)
# Compare
ratiod_compare(fit_standard, fit_zi)The Pareto \(k\) warning marks observations whose site effect moves too far when they are left out for importance sampling to follow; their contributions to the comparison are less reliable than the rest.
Validate ZI models with posterior predictive checks:
vignette("getting-started") - Basic usagevignette("spatial-temporal") - Spatial and temporal
models?ratiod_zibinomial, ?ratiod_zoibinomial -
Function documentation