In this exercise, we’ll use matrix population models (MPMs) again to simulate how a population behaves under fluctuating environmental conditions. This time, we’ll focus on quasi-extinction risk — the chance that a population falls below a critical threshold, even if it hasn’t gone extinct.
👉 Quasi-extinction doesn’t mean the population has hit zero — just that it has dropped so low that recovery might be unlikely.
We’re going to use the same setup as in Part 1. If you’re running this code fresh, you’ll need to load everything again.
# Set seed for reproducibility
set.seed(42)
# Define projection matrices for Good and Bad years
A_good <- matrix(c(
0.0, 1.2, 1.5,
0.5, 0.3, 0.0,
0.0, 0.6, 0.7
), nrow = 3, byrow = TRUE)
A_bad <- matrix(c(
0.0, 0.8, 0.75,
0.3, 0.2, 0.0,
0.0, 0.4, 0.5
), nrow = 3, byrow = TRUE)
# Initial population: 50 juveniles, 30 subadults, 20 adults
n0 <- c(50, 30, 20)
# Simulation parameters
timesteps <- 100 # Years to simulate
n_sim <- 1000 # Number of simulations
p_good <- 0.5 # Probability that any year is a good year
# Set up storage for results
lambda_s <- numeric(n_sim)
trajectory_data <- matrix(NA, nrow = timesteps + 1, ncol = n_sim)
We’ll simulate 1000 possible futures for the population, switching randomly between good and bad years.
for (i in 1:n_sim) {
pop <- n0
total_N <- numeric(timesteps + 1)
total_N[1] <- sum(pop)
for (t in 1:timesteps) {
A_t <- if (runif(1) < p_good) A_good else A_bad
pop <- A_t %*% pop
total_N[t + 1] <- sum(pop)
}
time <- 0:timesteps
lambda_s[i] <- coef(lm(log(total_N) ~ time))[2]
trajectory_data[, i] <- total_N
}
Let’s plot all the simulation runs on a log scale to show variation in population size over time.
# Log transform the data
trajectory_data <- log(trajectory_data)
# Plot all runs
matplot(0:timesteps, trajectory_data, type = "l", lty = 1,
col = adjustcolor("darkblue", alpha.f = 0.3),
xlab = "Time (years)", ylab = "Total Population Size (log)",
main = "Variation in Stochastic Trajectories (1000 Runs)")
# Add mean trajectory
lines(0:timesteps, rowMeans(trajectory_data, na.rm = TRUE),
col = "red", lwd = 2)
legend("topright", legend = c("Individual runs", "Mean"),
col = c("darkblue", "red"), lty = c(1, 1), lwd = c(1, 2), bty = "n")
Here’s the new part! We’re going to look at how many simulations drop below a critical population size — our quasi-extinction threshold.
We’ll check for quasi-extinction within the first 50 years.
timeLimit <- 50
trajectory_data_cut <- trajectory_data[1:timeLimit, ]
exp_data <- exp(trajectory_data_cut)
Let’s say a population size below 10 individuals counts as quasi-extinct.
threshold <- 10
We’ll look across all 50 years for each simulation to see if the population ever dropped below 10.
quasiExtinct <- apply(exp_data, 2, function(x) any(x < threshold))
# Count how many simulations show quasi-extinction
table(quasiExtinct)
## quasiExtinct
## FALSE TRUE
## 990 10
# As a proportion
table(quasiExtinct)[2] / sum(table(quasiExtinct))
## TRUE
## 0.01
The proportion of simulations that dip below the threshold gives us a risk estimate. If many of them do, it may suggest a significant risk of collapse, even if the mean population looks stable.
Here are some what-if questions to try:
p_good <- 0.3 instead of
0.5.threshold <- 40 or even
60.timeLimit <- 30 or
timeLimit <- 100.Use the following code, above:
time_to_extinction <- apply(exp_data, 2, function(x) {
w <- which(x < threshold)
if (length(w) == 0) return(NA)
return(min(w))
})
hist(time_to_extinction, main = "Time to Quasi-Extinction", xlab = "Years")
total_N[t + 1] <- sum(pop) #find this line of code in your script
if(total_N[t + 1] < 100){pop <- pop + c(0,0,10)} #add this line
Rcompadre and modify the code above to take
an arbitrary number of matrices.