This tutorial simulates how a population might grow or decline over time when faced with varying environmental conditions — “good years” and “bad years”. It uses matrix population models (MPMs) and introduces the concept of a stochastic growth rate.
We’ll walk through each step of the code and explain what it’s doing in plain language. Don’t worry if some parts of the code seem confusing — just focus on the big ideas, try running the code, and ask your instructors for help if you get stuck!
We begin by defining two different projection matrices, representing a good year and a bad year for the population. Each matrix describes how individuals move between life stages and how many new individuals are produced.
# Set seed so results are the same each time we run
set.seed(42)
# Good year matrix
A_good <- matrix(c(
0.0, 1.2, 1.5, # Fecundity (new individuals from stage 2 and 3)
0.5, 0.3, 0.0, # Survival and growth between stages
0.0, 0.6, 0.7
), nrow = 3, byrow = TRUE)
# Bad year matrix
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)
Each matrix describes transitions between three life stages (say, juveniles, subadults, adults).
Now we define the starting population and simulation settings.
# Starting population vector: 50 juveniles, 30 subadults, 20 adults
n0 <- c(50, 30, 20)
# Set parameters
timesteps <- 100 # Number of years to simulate
n_sim <- 1000 # Number of repeated simulations
p_good <- 0.5 # Probability that a given year is a good year
We’ll track total population size and stochastic growth rates
(lambda_s) for each simulation.
# Vector to store growth rate estimates
lambda_s <- numeric(n_sim)
# Matrix to store population size over time for each simulation
trajectory_data <- matrix(NA, nrow = timesteps + 1, ncol = n_sim)
Here’s the heart of the analysis! We simulate population dynamics over time, choosing randomly between good and bad years.
for (i in 1:n_sim) {
pop <- n0 # Start with initial population
total_N <- numeric(timesteps + 1)
total_N[1] <- sum(pop)
for (t in 1:timesteps) {
# Randomly choose either a good year or a bad year
A_t <- if (runif(1) < p_good) A_good else A_bad
pop <- A_t %*% pop # Project population forward one year
total_N[t + 1] <- sum(pop) # Store total population size
}
# Estimate stochastic growth rate (slope of log population size over time)
time <- 0:timesteps
lambda_s[i] <- coef(lm(log(total_N) ~ time))[2]
# Save trajectory
trajectory_data[, i] <- total_N
}
💡 Note: Don’t worry if you don’t understand all of this code — try copying and running it, and focus on understanding what it’s modeling!
Let’s plot the population trajectories on a log scale so we can compare them more easily.
# Log-transform the data
trajectory_data <- log(trajectory_data)
# Plot all trajectories
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 a red line showing the 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")
Now we calculate and visualize the stochastic growth
rate (λs) — how fast the population grows on
average across all simulations.
# Convert from log scale to multiplicative growth rate
lambda_s_exp <- exp(lambda_s)
# Histogram of growth rates
hist(lambda_s_exp, breaks = 30, col = "skyblue", border = "white",
main = "Distribution of Stochastic Growth Rate (λs)",
xlab = "λs (per time step)")
# Add mean and 95% confidence interval
mean_lambda_s <- mean(lambda_s_exp)
ci_lambda_s <- quantile(lambda_s_exp, probs = c(0.025, 0.975))
abline(v = mean_lambda_s, col = "red", lwd = 2)
abline(v = ci_lambda_s, col = "darkgreen", lty = 2)
legend("topright", legend = c("Mean", "95% CI"),
col = c("red", "darkgreen"), lty = c(1, 2), lwd = 2)
# Print results
cat("Estimated stochastic growth rate (λs):\n")
## Estimated stochastic growth rate (λs):
cat("Mean λs:", round(mean_lambda_s, 4), "\n")
## Mean λs: 1.0418
cat("95% CI:", round(ci_lambda_s[1], 4), "-", round(ci_lambda_s[2], 4), "\n")
## 95% CI: 0.992 - 1.0944
We can now assess the risk of decline. If λs < 1, the
population is shrinking.
# How many runs are showing decline?
table(lambda_s_exp < 1)
##
## FALSE TRUE
## 951 49
Let’s compare the stochastic growth rate to the deterministic growth rates of the good and bad years (i.e., assuming only good or only bad years).
# Compare with deterministic growth rate from each matrix
(lambda_bad <- popdemo::eigs(A_bad, what = "lambda"))
## [1] 0.8255641
(lambda_good <- popdemo::eigs(A_good, what = "lambda"))
## [1] 1.314143
# Mean of the deterministic rates
mean(c(lambda_bad, lambda_good))
## [1] 1.069853
📌 You should notice that the stochastic growth rate is lower than the average of the good and bad year deterministic rates. This is a key insight: variation reduces growth!
Try changing these parameters and see what happens:
p_good <
0.5)λs) helps us
understand the long-term viability of a population under variable
conditions.