## ----setup, include=FALSE----------------------------------------------------- knitr::opts_chunk$set( collapse = TRUE, comment = "#>", fig.width = 7, fig.height = 5 ) ## ----packages----------------------------------------------------------------- library(mpmaggregate) library(kableExtra) library(rphylopic) library(expm) ## ----------------------------------------------------------------------------- # Leslie population projection matrix for Marmota flaviventris # MatrixID = 249455 # Not run: requires Rcompadre and internet access # Matrices can be retrieved with: # library(Rcompadre) # comadre <- cdb_fetch("comadre",version = "4.23.3.1") # mpm <- comadre[comadre$MatrixID == 249455, ] # matA <- matA(mpm)[[1]] # matU <- matU(mpm)[[1]] # matF <- matF(mpm)[[1]] # Matrices are defined locally to avoid requiring the use of the internet and Rcompadre # Leslie population projection matrix matA <- matrix(c( 0.000000, 0.258040, 0.627254, 0.700037, 0.700037, 0.700037, 0.700037, 0.700037, 0.700037, 0.700037, 0.545125, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.498625, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.592875, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.643500, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.643500, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.643500, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.643500, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.643500, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.643500, 0.000000 ), nrow = 10, byrow = TRUE) # Reproduction matrix matF <- matrix(c( 0, 0.25804, 0.627254, 0.700037, 0.700037, 0.700037, 0.700037, 0.700037, 0.700037, 0.700037, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ), nrow = 10, byrow = TRUE) # Survival/transition matrix matU <- matrix(c( 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.545125,0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.498625,0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.592875,0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.6435, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.6435, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.6435, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.6435, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.6435, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.6435, 0 ), nrow = 10, byrow = TRUE) # Sanity check: A = U + F (after folding any C into F) stopifnot(all.equal(unname(matA), unname(matU + matF))) ## ----helpers------------------------------------------------------------------ `%||%` <- function(x, y) if (!is.null(x)) x else y # Return R0, the net reproductive rate get_R0 <- function(U, F) { I <- diag(nrow(U)) N <- solve(I - U) K <- F %*% N spectral_radius(K) } # Return generation time get_Ta <- function(U, F) { generation_time(F, U, framework = "lambda")$generation_time } # Return cohort generation time get_Tc <- function(U, F) { generation_time(F, U, framework = "R0")$generation_time } ## ----aggregate---------------------------------------------------------------- # Aggregate into 5 x 5 matrices (m = 5) ngroups <- 5 agg1 <- leslie_aggregate( matA = matA, ngroups = ngroups, framework = "lambda", criterion = "standard" ) agg2 <- leslie_aggregate( matA = matA, ngroups = ngroups, framework = "lambda", criterion = "elasticity" ) agg3 <- leslie_aggregate( matA = matA, ngroups = ngroups, framework = "R0", criterion = "standard" ) agg4 <- leslie_aggregate( matA = matA, ngroups = ngroups, framework = "R0", criterion = "elasticity" ) # Split a Leslie matrix into U and F components (Leslie form: fertility in row 1) split_matA <- function(A) { n <- nrow(A) U <- A U[1, ] <- 0 F <- matrix(0, n, n) F[1, ] <- A[1, ] list(U = U, F = F, A = A) } m0 <- list(U = matU, F = matF, A = matA) m1 <- split_matA(agg1$matAk_agg) m2 <- split_matA(agg2$matAk_agg) m3 <- split_matA(agg3$matAk_agg) m4 <- split_matA(agg4$matAk_agg) eff <- c( NA, agg1$effectiveness, agg2$effectiveness, agg3$effectiveness, agg4$effectiveness ) models <- list( Original = m0, Agg_lambda_std = m1, Agg_lambda_elast = m2, Agg_R0_std = m3, Agg_R0_elast = 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" ) # Basic checks stopifnot(all(sapply(models, function(m) !is.null(m$A)))) # 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 alt-------------------------------------------------------- # Adjust lambdas so that they are all per capita growth rate over a year # Adjust generation times so they all use units of 1 year adj = c(1,2,2,2,2) lambda = sapply(models, function(m) spectral_radius(m$A)) lambda = lambda^(1/adj) R0 = sapply(models, function(m) get_R0(m$U, m$F)) Ta = sapply(models, function(m) get_Ta(m$U, m$F))*adj Tc = sapply(models, function(m) get_Tc(m$U, m$F))*adj results <- data.frame( Model = unname(model_labels[names(models)]), lambda = lambda, R0 = R0, Ta = Ta, Tc = Tc, 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 ", "yellow-bellied marmot Marmota flaviventris (COMADRE MatrixID 249455) ", "using the original 10 × 10 Leslie matrix and four aggregated Leslie matrices ", "collapsed to 5 age groups. Metrics include population growth rate ", "(λ), net reproductive rate (R0), ", "generation time (years; Ta), cohort generation time ", "(years; Tc), and aggregation effectiveness ", "(ρ2). “Standard” denotes use of a standard aggregator ", "and “elasticity” denotes elasticity-consistent aggregation." ), 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. Population growth rates (λ) are all transformed to ", "represent growth over 1 year. Generation times have all been transformed to ", "represent generation time measured in years." ), footnote_as_chunk = TRUE, escape = FALSE ) ## ----elasticities-lambda^k---------------------------------------------------- # Age groups used for aggregation in example groups <- list(c(1,2), c(3,4), c(5,6), c(7,8), c(9,10)) # Partitioning matrix induced by the age groups. # This matrix is constructed internally by leslie_aggregate(), # but we construct it explicitly here so it can be used to # map quantities from the original age-class space to the # aggregated space. P <- mpm_partition(groups = groups, n = nrow(matA)) # Dimensions of the original and aggregated matrices m <- ngroups n <- nrow(matA) k <- n / m matAk <- matA %^% k # Elasticity of lambda^k with respect to entries of A^k E_A <- mpm_elasticity(matA = matAk, framework = "lambda")$elasticity E_list <- list( "Original" = P %*% E_A %*% t(P), "Agg λ + standard" = mpm_elasticity(matA = m1$A, framework = "lambda")$elasticity, "Agg λ + 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 ) 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))) elast_df <- elast_df[elast_df$elasticity > 0, ] elast_df <- elast_df[order(elast_df$row, elast_df$col), ] models_order <- names(E_list) entries <- unique(elast_df$entry) 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 ## ----elasticity-plot, fig.width=12, fig.height=6------------------------------ # Not run: requires internet access # Marmot 2012 Feb 2 by T. Michael Keesey, CC0 1.0, Marmota monax (Linnaeus 1758) # uuid <- "eee50efb-40dc-47d0-b2cb-52a14a5e0e51" # img <- get_phylopic(uuid = uuid, format = "raster", height = 869) # Write image # png::writePNG(unclass(img), "inst/extdata/marmot.png") # Load image locally img_file <- system.file("extdata", "marmot.png", package = "mpmaggregate") if (!nzchar(img_file)) { img_file <- "../inst/extdata/marmot.png" } img <- png::readPNG(img_file) 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 { bquote(U[.(r), .(c)]) } } entry_labels <- mapply(make_entry_label, r_vec, c_vec, SIMPLIFY = FALSE) cols <- c("#1b9e77", "#d95f02", "#7570b3", "#e7298a", "#66a61e") op <- par(mar = c(7, 6, 4, 2)) bp <- barplot( elast_mat, beside = TRUE, ylim = c(0, .35), col = cols, border = NA, ylab = expression("Elasticity of " * lambda^k), xaxt = "n", space = c(0.2, 1) ) axis(side = 1, at = colMeans(bp), labels = entry_labels, las = 2, cex.axis = 0.9) legend("topleft", legend = rownames(elast_mat), fill = cols, bty = "n", inset = 0.02) lims <- par("usr") mydiff <- lims[4] - lims[3] add_phylopic_base( img = img, x = NULL, y = mydiff * .9, height = .2 * mydiff * dim(img)[1] / 1024 ) par(op) ## ----elasticities-r0---------------------------------------------------------- # Age groupings used for aggregation (reintroduced here so this section # can be read independently) groups <- list(c(1,2), c(3,4), c(5,6), c(7,8), c(9,10)) # Partitioning matrix induced by the age groups. # Constructed internally by leslie_aggregate(), but reproduced here # because it is used to map quantities to aggregated space. P <- mpm_partition(groups = groups, n = nrow(matA)) # Dimensions of the original and aggregated matrices # Reintroduced here for clarity m <- ngroups n <- nrow(matA) k <- n / m R0 <- spectral_radius(matF %*% solve(diag(n) - matU)) # Reference operator for aggregation eigenstructure in the R0 framework # Divide by R0 so that it has Leslie matrix form A_R0ref <- (matF + R0 * matU) / R0 Uk <- expm::`%^%`(matU, k) Rk <- (expm::`%^%`(A_R0ref, k) - Uk) * R0 Ak <- Uk + Rk # Elasticity of R0 with respect to entries of Ak (closest to what aggregated matrices yield) E_A <- mpm_elasticity(matF = Rk, matU = Uk, framework = "R0", normalize = TRUE)$elasticity E_list2 <- list( "Original" = P %*% E_A %*% t(P), "Agg λ + standard" = mpm_elasticity(matF = m1$F, matU = m1$U, framework = "R0", normalize = TRUE)$elasticity, "Agg λ + 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 ) elast_df2 <- do.call(rbind, Map(to_long, E_list2, names(E_list2))) elast_df2 <- elast_df2[elast_df2$elasticity > 0, ] elast_df2 <- elast_df2[order(elast_df2$row, elast_df2$col), ] models_order2 <- names(E_list2) entries2 <- unique(elast_df2$entry) elast_mat2 <- sapply(models_order2, function(m) elast_df2$elasticity[elast_df2$model == m]) elast_mat2 <- t(elast_mat2) rownames(elast_mat2) <- models_order2 colnames(elast_mat2) <- entries2 ## ----elasticity-r0-plot, fig.width=12, fig.height=6--------------------------- # Not run: requires internet access # Marmot 2012 Feb 2 by T. Michael Keesey, CC0 1.0, Marmota monax (Linnaeus 1758) # uuid <- "eee50efb-40dc-47d0-b2cb-52a14a5e0e51" # img <- get_phylopic(uuid = uuid, format = "raster", height = 869) # Write image # png::writePNG(unclass(img), "inst/extdata/marmot.png") # Load the image file locally img_file <- system.file("extdata", "marmot.png", package = "mpmaggregate") #For development if (!nzchar(img_file)) { img_file <- "../inst/extdata/marmot.png" } img <- png::readPNG(img_file) rc <- do.call(rbind, strsplit(colnames(elast_mat2), ",")) r_vec <- as.integer(rc[, 1]) c_vec <- as.integer(rc[, 2]) entry_labels <- mapply(make_entry_label, r_vec, c_vec, SIMPLIFY = FALSE) cols <- c("#1b9e77", "#d95f02", "#7570b3", "#e7298a", "#66a61e") op <- par(mar = c(7, 6, 4, 2)) bp <- barplot( elast_mat2, beside = TRUE, ylim = c(0, .35), col = cols, border = NA, ylab = expression("Normalized elasticity of " * R[0]), xaxt = "n", space = c(0.2, 1) ) axis(side = 1, at = colMeans(bp), labels = entry_labels, las = 2, cex.axis = 0.9) legend("topleft", legend = rownames(elast_mat2), fill = cols, bty = "n", inset = 0.02) lims <- par("usr") mydiff <- lims[4] - lims[3] add_phylopic_base( img = img, x = NULL, y = mydiff * .9, height = .2 * mydiff * dim(img)[1] / 1024 ) par(op) ## ----fertility-elasticity-mass------------------------------------------------ # Mass for elasticity of R0 w.r.t. fertilities sum(elast_mat2[, 1:5]) / 5 # Mass for elasticity of lambda^k w.r.t. fertilities sum(elast_mat[, 1:5]) / 5