11.2 Zero-inflated outcomes
Zero-inflated outcomes arise when the data contain more zeros than a simple count model predicts. Mixture models combine a process that generates zeros with a process that generates counts.
11.2.1 The zero-inflated mixture
Zero-inflated data come from two processes:
- With probability , the observation is always zero (structural zero).
- With probability , the observation comes from a count distribution (e.g., Poisson).
# Zero-inflated Poisson: simulate and fit
set.seed(42)
n <- 300
x <- runif(n, 0, 10)
# Process 1: structural zeros (probability depends on x)
logit_pi <- 0.5 - 0.2 * x
pi <- 1 / (1 + exp(-logit_pi))
is_structural <- rbinom(n, 1, pi)
# Process 2: Poisson counts
lambda <- exp(0.3 + 0.15 * x)
y_pois <- rpois(n, lambda)
# Combine
y <- ifelse(is_structural, 0, y_pois)
cat("Zero-inflated data:\n")
cat(" Total zeros:", sum(y == 0), "/", n, "\n")
cat(" Proportion zeros:", round(mean(y == 0), 3), "\n")
cat(" Mean of non-zeros:", round(mean(y[y > 0]), 2), "\n")
# Fit zero-inflated Poisson using pscl package (if available)
# Here we demonstrate the concept with a two-part model
# Part 1: logistic for y > 0
m_zero <- glm(I(y > 0) ~ x, family = binomial)
# Part 2: Poisson for y given y > 0
m_count <- glm(y[y > 0] ~ x[y > 0], family = poisson)
cat("\nTwo-part (hurdle) approximation:\n")
cat(" Zero process: intercept =", round(coef(m_zero)[1], 3),
" slope =", round(coef(m_zero)[2], 3), "\n")
cat(" Count process: intercept =", round(coef(m_count)[1], 3),
" slope =", round(coef(m_count)[2], 3), "\n")
# Plot
par(mfrow = c(1, 2))
hist(y, breaks = max(y) + 1, col = "steelblue",
main = "Zero-inflated counts", xlab = "y")
plot(x, y, pch = 16, col = adjustcolor("steelblue", 0.5),
main = "y vs x", xlab = "x", ylab = "y")
par(mfrow = c(1, 1))11.2.2 ZIP vs hurdle
Both ZIP and hurdle models handle excess zeros, but they differ:
- ZIP: Zeros can come from either the structural process or the Poisson. The Poisson can also produce zeros.
- Hurdle: All zeros come from the hurdle. The count part is a truncated Poisson (no zeros).
# Compare ZIP and hurdle concepts
set.seed(42)
n <- 500
x <- runif(n, 0, 10)
# ZIP: zeros from structural + Poisson
pi_zip <- 0.3
is_struct <- rbinom(n, 1, pi_zip)
lambda <- exp(0.5 + 0.1 * x)
y_zip <- ifelse(is_struct, 0, rpois(n, lambda))
# Hurdle: zeros only from hurdle, counts are truncated Poisson
p_nonzero <- 1 / (1 + exp(-(0 + 0.2 * x)))
is_nonzero <- rbinom(n, 1, p_nonzero)
y_hurdle <- ifelse(is_nonzero, rpois(n, lambda) + 1, 0) # +1 to avoid 0
cat("ZIP data:\n")
cat(" Zeros:", sum(y_zip == 0), " Mean (y>0):", round(mean(y_zip[y_zip > 0]), 2), "\n")
cat("\nHurdle data:\n")
cat(" Zeros:", sum(y_hurdle == 0), " Mean (y>0):", round(mean(y_hurdle[y_hurdle > 0]), 2), "\n")
cat("\nKey difference:\n")
cat(" ZIP: Poisson can produce 0 (so some zeros are 'count zeros')\n")
cat(" Hurdle: count part starts at 1 (all zeros are 'hurdle zeros')\n")Zeros are not all the same. In a ZIP model, a zero can mean "this observation cannot produce a count" (structural zero) or "this observation could produce a count but happened to get zero" (Poisson zero). This distinction matters for interpretation: structural zeros tell us about suitability or participation, while Poisson zeros tell us about chance. In a hurdle model, all zeros are the same — they all mean "the hurdle was not crossed." The choice between ZIP and hurdle depends on the scientific question: are there two kinds of zeros, or just one?
Summary of Section 11.2
- Zero-inflated data have more zeros than a count model predicts.
- ZIP: mixture of structural zeros and Poisson — zeros from two sources.
- Hurdle: all zeros from the hurdle; count part is truncated.
- Both have two linear predictors: one for the zero process, one for the count.
- Choose based on whether zeros come from one or two processes.