## ----setup, include=FALSE----------------------------------------------------- knitr::opts_chunk$set( collapse = TRUE, comment = "#>", fig.width = 7, fig.height = 5 ) ## ----packages----------------------------------------------------------------- library(mpmaggregate) library(knitr) library(kableExtra) ## ----fetch-comadre------------------------------------------------------------ # Example matrices used in the general-to-general MPM aggregation examples below # Population projection matrix for Rourea induta # Not run: requires Rcompadre and internet access # MatrixID = 246781 # Matrices can be retrieved with: # library(Rcompadre) # compadre <- cdb_fetch("compadre") # mpm <- compadre[compadre$MatrixID == 246781, ] # matA <- matA(mpm)[[1]] # matU <- matU(mpm)[[1]] # matF <- matF(mpm)[[1]] # matC <- matC(mpm)[[1]] # The matrices are defined locally so that Rcompadre and internet access # are not required # Population projection matrix matA <- matrix(c( 0.000,0.000,0.000,0.000,0.002,0.007,0.021,0.055,0.102,0.139,0.176,0.191,0.203,0.214, 0.000,0.000,0.000,0.001,0.002,0.004,0.007,0.011,0.016,0.021,0.036,0.052,0.072,0.096, 0.833,0.043,0.883,0.151,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000, 0.000,0.391,0.043,0.791,0.079,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000, 0.000,0.348,0.000,0.047,0.810,0.139,0.024,0.024,0.056,0.000,0.000,0.000,0.000,0.000, 0.000,0.000,0.000,0.000,0.087,0.733,0.082,0.071,0.000,0.000,0.000,0.000,0.000,0.000, 0.000,0.087,0.000,0.000,0.000,0.129,0.765,0.024,0.000,0.013,0.000,0.000,0.000,0.000, 0.000,0.000,0.000,0.000,0.000,0.000,0.106,0.762,0.028,0.000,0.000,0.000,0.000,0.000, 0.000,0.000,0.000,0.000,0.000,0.000,0.012,0.119,0.778,0.000,0.000,0.000,0.000,0.000, 0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.083,0.861,0.000,0.000,0.000,0.000, 0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.127,0.891,0.000,0.000,0.000, 0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.091,0.909,0.000,0.000, 0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.091,0.918,0.000, 0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.082,0.967 ), nrow = 14, byrow = TRUE) # Survival/transition matrix matU <- matrix(c( 0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000, 0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000, 0.833,0.043,0.883,0.151,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000, 0.000,0.391,0.043,0.791,0.079,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000, 0.000,0.348,0.000,0.047,0.810,0.139,0.024,0.024,0.056,0.000,0.000,0.000,0.000,0.000, 0.000,0.000,0.000,0.000,0.087,0.733,0.082,0.071,0.000,0.000,0.000,0.000,0.000,0.000, 0.000,0.087,0.000,0.000,0.000,0.129,0.765,0.024,0.000,0.013,0.000,0.000,0.000,0.000, 0.000,0.000,0.000,0.000,0.000,0.000,0.106,0.762,0.028,0.000,0.000,0.000,0.000,0.000, 0.000,0.000,0.000,0.000,0.000,0.000,0.012,0.119,0.778,0.000,0.000,0.000,0.000,0.000, 0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.083,0.861,0.000,0.000,0.000,0.000, 0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.127,0.891,0.000,0.000,0.000, 0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.091,0.909,0.000,0.000, 0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.091,0.918,0.000, 0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.082,0.967 ), nrow = 14, byrow = TRUE) # Sexual reproduction matrix matF <- matrix(c( 0,0,0,0,0.002,0.007,0.021,0.055,0.102,0.139,0.176,0.191,0.203,0.214, 0,0,0,0,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000, 0,0,0,0,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000, 0,0,0,0,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000, 0,0,0,0,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000, 0,0,0,0,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000, 0,0,0,0,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000, 0,0,0,0,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000, 0,0,0,0,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000, 0,0,0,0,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000, 0,0,0,0,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000, 0,0,0,0,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000, 0,0,0,0,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000, 0,0,0,0,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000 ),nrow=14,byrow=TRUE) #Clonal reproduction matrix matC <- matrix(c( 0,0,0,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000, 0,0,0,0.001,0.002,0.004,0.007,0.011,0.016,0.021,0.036,0.052,0.072,0.096, 0,0,0,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000, 0,0,0,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000, 0,0,0,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000, 0,0,0,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000, 0,0,0,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000, 0,0,0,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000, 0,0,0,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000, 0,0,0,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000, 0,0,0,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000, 0,0,0,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000, 0,0,0,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000, 0,0,0,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000 ), nrow = 14, byrow = TRUE) #redefined matF so it includes sexual + clonal reproduction matF <- matF + matC #sanity check stopifnot(all.equal(unname(matA), unname(matU + matF))) #1 active Seedling #2 active Sucker #3 active 1-1 .9 mm #4 active 2-3 .9 mm #5 active 4-5 .9 mm #6 active 6-7 .9 mm #7 active 8-.9 .9 mm #8 active 10-14 .9 mm #9 active 15-19.9 mm #10 active 20-29 mm #11 active 30-39 mm #12 active 40-49 mm #13 active 50-69 mm #14 active 70+ mm # Stage aggregation used in later examples: # Form the aggregated groups, leaving Seedling and Sucker stages alone groups <- list( c(1), #Seedling c(2), #Sucker c(3, 4, 5, 6), #1-7.9 mm c(7, 8, 9, 10), #8-29 mm c( 11, 12, 13, 14) #30+ mm ) ## ----helpers------------------------------------------------------------------ `%||%` <- function(x, y) if (!is.null(x)) x else y get_R0 <- function(U, F) { I <- diag(nrow(U)) N <- solve(I - U) K <- F %*% N spectral_radius(K) } get_Ta <- function(U, F) { generation_time(F, U, framework = "lambda")$generation_time } get_Tc <- function(U, F) { generation_time(F, U, framework = "R0")$generation_time } ## ----aggregate---------------------------------------------------------------- agg1 <- mpm_aggregate( matU = matU, matF = matF, groups = groups, framework = "lambda", criterion = "standard" ) agg2 <- mpm_aggregate( matU = matU, matF = matF, groups = groups, framework = "lambda", criterion = "elasticity" ) agg3 <- mpm_aggregate( matU = matU, matF = matF, groups = groups, framework = "R0", criterion = "standard" ) agg4 <- mpm_aggregate( matU = matU, matF = matF, groups = groups, framework = "R0", criterion = "elasticity" ) # Extract aggregated U and F robustly (in case object field names differ by version) extract_U <- function(x) x$matUk_agg %||% x$matU_agg %||% x$matUagg %||% x$U extract_F <- function(x) x$matFk_agg %||% x$matF_agg %||% x$matFagg %||% x$F extract_A <- function(x) x$matAk_agg %||% x$matA_agg %||% x$matAagg %||% x$A as_ufA <- function(x) { U <- extract_U(x) F <- extract_F(x) A <- extract_A(x) if (is.null(A) && !is.null(U) && !is.null(F)) A <- U + F list(U = U, F = F, A = A) } m0 <- list(U = matU, F = matF, A = matA) m1 <- as_ufA(agg1) m2 <- as_ufA(agg2) m3 <- as_ufA(agg3) m4 <- as_ufA(agg4) eff <- c(NA,agg1$effectiveness,agg2$effectiveness,agg3$effectiveness,agg4$effectiveness) models <- list( "Original" = m0, "Agg lambda + standard" = m1, "Agg lambda + elasticity" = m2, "Agg R0 + standard" = m3, "Agg R0 + elasticity" = m4 ) model_labels <- c( Original = "Original", Agg_lambda_std = "Agg λ + standard", Agg_lambda_elast = "Agg λ + elasticity", Agg_R0_std = "Agg R0 + standard", Agg_R0_elast = "Agg R0 + elasticity" ) names(models) <- c( "Original", "Agg_lambda_std", "Agg_lambda_elast", "Agg_R0_std", "Agg_R0_elast" ) # Basic checks stopifnot(all(sapply(models, function(m) !is.null(m$A)))) #stopifnot(all(sapply(models, function(m) nrow(m$A) == length(groups)))) #show aggregated matrices print("Agg λ + standard") m1$A print("Agg λ + elasticity") m2$A print("Agg R0 + standard") m3$A print("Agg R0 + elasticity") m4$A ## ----results-table------------------------------------------------------------ results <- data.frame( Model = unname(model_labels[names(models)]), lambda = sapply(models, function(m) spectral_radius(m$A)), R0 = sapply(models, function(m) get_R0(m$U, m$F)), Ta = sapply(models, function(m) get_Ta(m$U, m$F)), Tc = sapply(models, function(m) get_Tc(m$U, m$F)), Effectiveness = eff, row.names = NULL, stringsAsFactors = FALSE ) n_rows <- nrow(results) knitr::kable( results, col.names = c( "Model", "λ", "R0", "Ta (years)", "Tc (years)", "ρ2" ), digits = 3, caption = paste0( "Table 1. Comparison of demographic properties for the original ", "stage-structured matrix population model of Rourea induta ", "(COMPADRE MatrixID 246781) and four aggregated versions. The original 14 stages ", "are collapsed to 5 while leaving the Seedling and Sucker stages unchanged. ", "“Standard” denotes use of a standard aggregator and ", "“elasticity” denotes elasticity-consistent aggregation. ", "Ta is generation time (years) in the λ framework and ", "Tc is cohort generation time (years) in the ", "R0 framework. ", "ρ2 quantifies aggregation effectiveness, with values closer to 1 ", "indicating closer agreement between the aggregated and reference models." ), format = "html", escape = FALSE ) |> kable_styling(full_width = TRUE) |> row_spec(0, extra_css = "border-bottom: 2px solid black;") |> row_spec(nrow(results), extra_css = "border-bottom: 2px solid black;") |> footnote( general_title = "", general = paste0( "", "Note. The projection interval associated with the population ", "growth rate (λ) is 1 year." ), footnote_as_chunk = TRUE, escape = FALSE ) ## ----elasticities------------------------------------------------------------- # Partitioning matrix induced by the stage groups # This matrix is constructed internally by mpm_aggregate(), # but we reproduce it here because it can be used to map quantities # from the original stage space to the aggregated space. P <- mpm_partition(groups=groups, n=nrow(matA)) # Elasticity matrices in aggregated space (k x k) for each model E_A <- mpm_elasticity(matA=matA,framework="lambda")$elasticity E_list <- list( "Original" = P %*% E_A %*% t(P), "Agg lambda + standard" = mpm_elasticity(matA=m1$A,framework="lambda")$elasticity, "Agg lambda + elasticity" = mpm_elasticity(matA=m2$A,framework="lambda")$elasticity, "Agg R0 + standard" = mpm_elasticity(matA=m3$A,framework="lambda")$elasticity, "Agg R0 + elasticity" = mpm_elasticity(matA=m4$A,framework="lambda")$elasticity ) # Build a long data.frame for all nonzero entries (row-major order) to_long <- function(M, model_name) { idx <- which(M != 0, arr.ind = TRUE) data.frame( model = model_name, row = idx[, 1], col = idx[, 2], entry = paste(idx[, 1], idx[, 2], sep = ","), elasticity = M[idx], stringsAsFactors = FALSE ) } elast_df <- do.call(rbind, Map(to_long, E_list, names(E_list))) # Keep positive values for log scale elast_df <- elast_df[elast_df$elasticity > 0, ] # Row-major ordering: row 1 entries first, row 2 second, rows 3+ third, etc. elast_df <- elast_df[order(elast_df$row, elast_df$col), ] models_order <- names(E_list) entries <- unique(elast_df$entry) # Create matrix for barplot: rows = models (5), cols = entries elast_mat <- sapply(models_order, function(m) { elast_df$elasticity[elast_df$model == m] }) elast_mat <- t(elast_mat) rownames(elast_mat) <- models_order colnames(elast_mat) <- entries ## ----lambda-elasticity-plot, fig.width=12------------------------------------- # Build biologically meaningful entry labels # Build (row, col) vectors in the exact column order of elast_mat rc <- do.call(rbind, strsplit(colnames(elast_mat), ",")) r_vec <- as.integer(rc[, 1]) c_vec <- as.integer(rc[, 2]) make_entry_label <- function(r, c) { if (r == 1) { bquote(F[1, .(c)]) } else if (r == 2) { bquote(C[2, .(c)]) } else { bquote(U[.(r), .(c)]) } } entry_labels <- mapply(make_entry_label, r_vec, c_vec, SIMPLIFY = FALSE) # Colors by model # Same colors as the vital-rate plot for consistency cols <- c("#1b9e77", "#d95f02", "#7570b3", "#e7298a", "#66a61e") # Leave room for left legend and long x labels op <- par(mar = c(7, 6, 4, 2)) bp <- barplot( elast_mat, beside = TRUE, log = "y", col = cols, border = NA, ylab = expression("Elasticity of " * lambda * " (log scale)"), xaxt = "n", space = c(0.2, 1) # ← this is the key line ) axis( side = 1, at = colMeans(bp), labels = entry_labels, las = 2, cex.axis = 0.9 ) legend( "topleft", legend = c( "Original", expression(paste("Agg ", lambda, " + standard")), expression(paste("Agg ", lambda, " + elasticity")), expression(paste("Agg ", R[0], " + standard")), expression(paste("Agg ", R[0], " + elasticity")) ), fill = cols, bty = "n", inset = 0.02 ) par(op) ## ----elasticities of R0------------------------------------------------------- # Partitioning matrix induced by the stage group # Reintroduced here so this section can be read independently P <- mpm_partition(groups=groups, n=nrow(matA)) # Elasticity matrices in aggregated space (k x k) for each model # The elasticity matrix of the original model is presented in its aggregated # form using the partitioning matrix P. This is the true aggregated form of the elasticity matrix # to which elasticities derived from aggregated models are compared. E_A <- mpm_elasticity(matF=matF,matU=matU,framework="R0", normalize=TRUE)$elasticity E_list <- list( "Original" = P %*% E_A %*% t(P), "Agg lambda + standard" = mpm_elasticity(matF=m1$F, matU=m1$U,framework="R0", normalize=TRUE)$elasticity, "Agg lambda + elasticity" = mpm_elasticity(matF=m2$F, matU=m2$U,framework="R0", normalize=TRUE)$elasticity, "Agg R0 + standard" = mpm_elasticity(matF=m3$F, matU=m3$U,framework="R0", normalize=TRUE)$elasticity, "Agg R0 + elasticity" = mpm_elasticity(matF=m4$F, matU=m4$U,framework="R0", normalize=TRUE)$elasticity ) # Build a long data.frame for all nonzero entries (row-major order) to_long <- function(M, model_name) { idx <- which(M != 0, arr.ind = TRUE) data.frame( model = model_name, row = idx[, 1], col = idx[, 2], entry = paste(idx[, 1], idx[, 2], sep = ","), elasticity = M[idx], stringsAsFactors = FALSE ) } elast_df <- do.call(rbind, Map(to_long, E_list, names(E_list))) # Keep positive values for log scale elast_df <- elast_df[elast_df$elasticity > 0, ] # Row-major ordering: row 1 entries first, row 2 second, rows 3+ third, etc. elast_df <- elast_df[order(elast_df$row, elast_df$col), ] models_order <- names(E_list) entries <- unique(elast_df$entry) # Create matrix for barplot: rows = models (5), cols = entries elast_mat2 <- sapply(models_order, function(m) { elast_df$elasticity[elast_df$model == m] }) elast_mat2 <- t(elast_mat2) rownames(elast_mat2) <- models_order colnames(elast_mat2) <- entries ## ----elasticity-R0-plot, fig.width=12, fig.height=6--------------------------- # Build biologically meaningful entry labels # Build (row, col) vectors in the exact column order of elast_mat rc <- do.call(rbind, strsplit(colnames(elast_mat2), ",")) r_vec <- as.integer(rc[, 1]) c_vec <- as.integer(rc[, 2]) make_entry_label <- function(r, c) { if (r == 1) { bquote(F[1, .(c)]) } else if (r == 2) { bquote(C[2, .(c)]) } else { bquote(U[.(r), .(c)]) } } entry_labels <- mapply(make_entry_label, r_vec, c_vec, SIMPLIFY = FALSE) # Colors by model # Same colors as the vital-rate plot for consistency cols <- c("#1b9e77", "#d95f02", "#7570b3", "#e7298a", "#66a61e") # Leave room for left legend and long x labels op <- par(mar = c(7, 6, 4, 2)) bp <- barplot( elast_mat2, beside = TRUE, log = "y", col = cols, border = NA, ylab = expression("Normalized elasticity of " * R[0] * " (log scale)"), xaxt = "n", space = c(0.2, 1) # ← this is the key line ) axis( side = 1, at = colMeans(bp), labels = entry_labels, las = 2, cex.axis = 0.9 ) legend( "topleft", legend = c( "Original", expression(paste("Agg ", lambda, " + standard")), expression(paste("Agg ", lambda, " + elasticity")), expression(paste("Agg ", R[0], " + standard")), expression(paste("Agg ", R[0], " + elasticity")) ), fill = cols, bty = "n", inset = 0.02 ) par(op) ## ----lambda-vs.-R0-elasticities----------------------------------------------- #compare elasticities of top 3 vital rates #top three elasticities in lambda framework sum(elast_mat[,c(9,13,15)])/5 #top three elasticities in R0 framework sum(elast_mat2[,c(9,13,15)])/5