model_vars <- attr(rf_final$terms, "term.labels")
cat("Model expects these predictors:\n")
## Model expects these predictors:
print(model_vars)
## [1] "Cogongrass_Cover" "elevation" "slope" "pr"
## [5] "srad" "TreeCanopy_NLCD" "SoilType" "ndvi"
## [9] "avg_pop_den_1km"
env_vars <- model_vars
cat(sprintf("\nEnvironmental predictors : %s\n", paste(env_vars, collapse = ", ")))
##
## Environmental predictors : Cogongrass_Cover, elevation, slope, pr, srad, TreeCanopy_NLCD, SoilType, ndvi, avg_pop_den_1km
env_vars <- env_vars[env_vars != "Cogongrass_Cover"]
raster_paths <- list(
pr = "C:/Users/alanivory34428/Desktop/03_Biodiversity/02_Data/5_Year_Average_Precipitation.tif",
sph = "C:/Users/alanivory34428/Desktop/03_Biodiversity/02_Data/5_Year_Average_Specific_Humidity.tif",
srad = "C:/Users/alanivory34428/Desktop/03_Biodiversity/02_Data/5_Year_Average_Radiation.tif",
TreeCanopy_NLCD = "C:/Users/alanivory34428/Desktop/03_Biodiversity/02_Data/Tree_Cover_SE.tif",
ndvi = "C:/Users/alanivory34428/Desktop/03_Biodiversity/02_Data/CombinedMedianNDVI.tif",
tmmn = "C:/Users/alanivory34428/Desktop/03_Biodiversity/02_Data/5_Year_Average_Min_Temperature.tif",
elevation = "C:/Users/alanivory34428/Desktop/03_Biodiversity/02_Data/Elevation_CONUS.tif",
SoilType = "C:/Users/alanivory34428/Desktop/03_Biodiversity/02_Data/taxorder_CONUS_30m.tif",
avg_pop_den_1km = "C:/Users/alanivory34428/Desktop/03_Biodiversity/02_Data/US_PopDensity_2020.tif",
nlcd = "C:/Users/alanivory34428/Desktop/03_Biodiversity/02_Data/Annual_NLCD_LndCov_2024_CU_C1V1.tif",
slope = "C:/Users/alanivory34428/Desktop/03_Biodiversity/02_Data/Slope_30m_Merged.tif"
)
missing_rasts <- setdiff(env_vars, names(raster_paths))
if (length(missing_rasts) > 0) {
stop(sprintf("Missing raster paths for: %s", paste(missing_rasts, collapse = ", ")))
}
## Load only the rasters needed by the model
cat("Loading rasters...\n")
## Loading rasters...
rast_list <- lapply(env_vars, function(v) {
cat(sprintf(" Loading: %s\n", v))
rast(raster_paths[[v]])
})
## Loading: elevation
## Loading: slope
## Loading: pr
## Loading: srad
## Loading: TreeCanopy_NLCD
## Loading: SoilType
## Loading: ndvi
## Loading: avg_pop_den_1km
names(rast_list) <- env_vars
# Loop through and plot each raster
for (name in names(raster_paths)) {
r <- rast(raster_paths[[name]])
plot(r, main = name)
}
cat("CRS of each raster:\n")
## CRS of each raster:
for (v in names(rast_list)) {
cat(sprintf(" %-20s %s\n", v, crs(rast_list[[v]], describe = TRUE)$code))
}
## elevation 4326
## slope 5070
## pr 4326
## srad 4326
## TreeCanopy_NLCD 5070
## SoilType 5070
## ndvi 4326
## avg_pop_den_1km 4326
# Use NDVI raster extent as the boundary
cat("\nExtracting extent from NDVI raster...\n")
##
## Extracting extent from NDVI raster...
ndvi_rast <- rast(raster_paths[["ndvi"]])
ndvi_crs <- crs(ndvi_rast)
ndvi_extent <- ext(ndvi_rast)
cat(sprintf("NDVI CRS: %s\n", crs(ndvi_rast, describe = TRUE)$code))
## NDVI CRS: 4326
cat("NDVI Extent:\n")
## NDVI Extent:
print(ndvi_extent)
## SpatExtent : -131.325070711546, -59.42391537062, 19.9520316179366, 55.2019233667867 (xmin, xmax, ymin, ymax)
# Load, crop each raster to NDVI extent
local_dir <- "C:/Users/alanivory34428/Desktop/03_Biodiversity/02_Data/local_cache"
dir.create(local_dir, showWarnings = FALSE, recursive = TRUE)
rast_list <- lapply(names(raster_paths), function(v) {
src_path <- raster_paths[[v]]
local_path <- file.path(local_dir, paste0(v, ".tif"))
if (!file.exists(local_path)) {
cat(sprintf(" Copying %-20s to local cache...\n", v))
r_src <- rast(src_path)
writeRaster(r_src, local_path, overwrite = TRUE)
}
r <- rast(local_path)
# Reproject the NDVI extent into this raster's CRS for cropping
if (!same.crs(r, ndvi_rast)) {
ndvi_ext_reproj <- ext(project(vect(ndvi_extent, crs = ndvi_crs), crs(r)))
} else {
ndvi_ext_reproj <- ndvi_extent
}
r_crop <- crop(r, ndvi_ext_reproj)
cat(sprintf(" %-20s done | dims: %d x %d | CRS: %s\n",
v, nrow(r_crop), ncol(r_crop),
crs(r_crop, describe = TRUE)$code))
r_crop
})
## pr done | dims: 3924 x 8004 | CRS: 4326
## sph done | dims: 3924 x 8004 | CRS: 4326
## srad done | dims: 3924 x 8004 | CRS: 4326
## |---------|---------|---------|---------|========================================= TreeCanopy_NLCD done | dims: 93792 x 154328 | CRS: 5070
## ndvi done | dims: 3924 x 8004 | CRS: 4326
## tmmn done | dims: 3924 x 8004 | CRS: 4326
## elevation done | dims: 1252 x 2841 | CRS: 4326
## |---------|---------|---------|---------|========================================= SoilType done | dims: 93376 x 153996 | CRS: 5070
## avg_pop_den_1km done | dims: 2766 x 6433 | CRS: 4326
## |---------|---------|---------|---------|========================================= nlcd done | dims: 98117 x 160000 | CRS: NA
## |---------|---------|---------|---------|========================================= slope done | dims: 54490 x 109186 | CRS: 5070
names(rast_list) <- names(raster_paths)
# Use the NDVI raster as the reference grid for alignment
ref_rast <- rast_list[["ndvi"]]
cat("Aligning all rasters to the NDVI 30m reference grid...\n")
## Aligning all rasters to the NDVI 30m reference grid...
cat("\nReprojecting and resampling TreeCanopy and SoilType to match ref grid...\n")
##
## Reprojecting and resampling TreeCanopy and SoilType to match ref grid...
rast_aligned <- lapply(names(rast_list), function(v) {
r <- rast_list[[v]]
# Define categorical layers that MUST use Nearest Neighbor
categorical_layers <- c("SoilType", "nlcd")
method <- if (v %in% categorical_layers) "near" else "bilinear"
if (!same.crs(r, ref_rast)) {
cat(sprintf(" Reprojecting: %s to 30m...\n", v))
r <- project(r, ref_rast, method = method)
} else if (!compareGeom(r, ref_rast, stopOnError = FALSE)) {
cat(sprintf(" Resampling: %s to 30m...\n", v))
r <- resample(r, ref_rast, method = method)
} else {
cat(sprintf(" Already Aligned: %s\n", v))
}
r
})
## Already Aligned: pr
## Already Aligned: sph
## Already Aligned: srad
## Reprojecting: TreeCanopy_NLCD to 30m...
## |---------|---------|---------|---------|========================================= Already Aligned: ndvi
## Already Aligned: tmmn
## Resampling: elevation to 30m...
## Reprojecting: SoilType to 30m...
## |---------|---------|---------|---------|========================================= Resampling: avg_pop_den_1km to 30m...
## Reprojecting: nlcd to 30m...
## |---------|---------|---------|---------|========================================= Reprojecting: slope to 30m...
## |---------|---------|---------|---------|=========================================
names(rast_aligned) <- names(rast_list)
env_stack <- rast(rast_aligned)
names(env_stack) <- names(rast_aligned)
cat(sprintf("\nenv_stack: %d layers, %d x %d cells\n",
nlyr(env_stack), nrow(env_stack), ncol(env_stack)))
##
## env_stack: 11 layers, 3924 x 8004 cells
# Check for NaN/NA in each layer
cat("\nNon-NA cell counts per layer:\n")
##
## Non-NA cell counts per layer:
for (v in names(env_stack)) {
n <- global(env_stack[[v]], fun = "notNA")[1,1]
cat(sprintf(" %-20s %d\n", v, n))
}
## pr 10408368
## sph 10408368
## srad 10408368
## TreeCanopy_NLCD 16872041
## ndvi 6217899
## tmmn 10408368
## elevation 18998331
## SoilType 9512108
## avg_pop_den_1km 10160873
## nlcd 10514534
## slope 4124352
Only focus on forest, shrubland, or heraceous areas NLCD Classes: 41, 42, 43 (Forests); 52 (Shrub); 71 (Herbaceous); 90 (Woody Wetland); 95 (Emergent Wetland)
mask_habitats <- function(env_stack, nlcd_raster) {
cat("Masking landscape to Forest, Shrubland, and Herbaceous cover...\n")
# Align NLCD to the stack
if(!compareGeom(nlcd_raster, env_stack, stopOnError = FALSE)) {
nlcd_raster <- project(nlcd_raster, env_stack, method = "near")
}
# Target classes
target_classes <- c(41, 42, 43, 52, 71, 90, 95)
mask_layer <- nlcd_raster %in% target_classes
mask_layer[mask_layer == 0] <- NA
# Apply mask to the entire stack
env_stack_masked <- mask(env_stack, mask_layer)
return(env_stack_masked)
}
env_stack <- mask_habitats(env_stack, rast_list[["nlcd"]])
## Masking landscape to Forest, Shrubland, and Herbaceous cover...
## |---------|---------|---------|---------|=========================================
# Path to the longleaf pine range shapefile.
# NOTE: st_read needs the companion files (.shx, .dbf, .prj) in the same folder.
longleaf_path <- "C:/Users/alanivory34428/Desktop/03_Biodiversity/02_Data/pinupalu.shp"
cat("Loading longleaf pine historic range...\n")
## Loading longleaf pine historic range...
longleaf_range <- sf::st_read(longleaf_path, quiet = TRUE)
# The Little's range shapefiles often ship without a .prj, so the CRS comes in
# undefined. They are in geographic coordinates (lon/lat, decimal degrees), so we
# assign that here before reprojecting. Change assumed_crs if you know the file's
# true CRS (the documented original datum for these maps is NAD27 = EPSG:4267).
assumed_crs <- 4326 # WGS84 lon/lat
if (is.na(sf::st_crs(longleaf_range))) {
cat(sprintf(" Shapefile has no CRS; assigning EPSG:%d (assumed).\n", assumed_crs))
sf::st_crs(longleaf_range) <- assumed_crs
}
## Shapefile has no CRS; assigning EPSG:4326 (assumed).
# Match the CRS of the environmental stack, then convert to a terra vector
longleaf_range <- sf::st_transform(longleaf_range, crs(env_stack))
longleaf_vect <- terra::vect(longleaf_range)
# Crop to the range's bounding box (large speed-up on the 101 scenarios),
# then mask so that cells outside the range polygon(s) become NA.
cat("Cropping and masking env_stack to the longleaf pine range...\n")
## Cropping and masking env_stack to the longleaf pine range...
env_stack <- terra::crop(env_stack, longleaf_vect)
env_stack <- terra::mask(env_stack, longleaf_vect)
cat(sprintf("env_stack restricted to longleaf range: %d x %d cells\n",
nrow(env_stack), ncol(env_stack)))
## env_stack restricted to longleaf range: 1139 x 2162 cells
# Quick visual check of the masked extent
plot(env_stack[[1]], main = "env_stack masked to longleaf pine range")
plot(sf::st_geometry(longleaf_range), add = TRUE, border = "black")
# Builds predictor dataframe, inserts cogongrass scenario, predicts, returns raster
predict_diversity <- function(cogongrass_value, env_stack, rf_model) {
cat(sprintf(" Predicting at Cogongrass_Cover = %d%%...\n",
cogongrass_value))
# Convert stack to data frame
pred_df <- as.data.frame(env_stack, xy = FALSE, na.rm = FALSE)
# Only predict on valid cells
valid_cells <- complete.cases(pred_df)
if(sum(valid_cells) == 0)
stop("No valid cells found for prediction.")
# Add scenario variable
pred_df$Cogongrass_Cover <- cogongrass_value
# Match model variables
all_vars <- attr(rf_model$terms, "term.labels")
pred_df_valid <- pred_df[valid_cells,
all_vars, drop = FALSE]
# Predict
predicted_vals <- predict(rf_model,
newdata = pred_df_valid)
# Map back to raster
predicted_full <- rep(NA_real_,
nrow(pred_df))
predicted_full[valid_cells] <- predicted_vals
result_rast <- env_stack[[1]]
values(result_rast) <- predicted_full
names(result_rast) <- paste0("Shannon_",
cogongrass_value)
return(result_rast)
}
scenario_levels <- seq(0, 100, by = 1)
prediction_list <- list()
cat("\n========== Running All Cogongrass Scenarios ==========\n")
##
## ========== Running All Cogongrass Scenarios ==========
for(level in scenario_levels) {
prediction_list[[paste0("p", level)]] <- predict_diversity(level,
env_stack, rf_final)
}
## Predicting at Cogongrass_Cover = 0%...
## Predicting at Cogongrass_Cover = 1%...
## Predicting at Cogongrass_Cover = 2%...
## Predicting at Cogongrass_Cover = 3%...
## Predicting at Cogongrass_Cover = 4%...
## Predicting at Cogongrass_Cover = 5%...
## Predicting at Cogongrass_Cover = 6%...
## Predicting at Cogongrass_Cover = 7%...
## Predicting at Cogongrass_Cover = 8%...
## Predicting at Cogongrass_Cover = 9%...
## Predicting at Cogongrass_Cover = 10%...
## Predicting at Cogongrass_Cover = 11%...
## Predicting at Cogongrass_Cover = 12%...
## Predicting at Cogongrass_Cover = 13%...
## Predicting at Cogongrass_Cover = 14%...
## Predicting at Cogongrass_Cover = 15%...
## Predicting at Cogongrass_Cover = 16%...
## Predicting at Cogongrass_Cover = 17%...
## Predicting at Cogongrass_Cover = 18%...
## Predicting at Cogongrass_Cover = 19%...
## Predicting at Cogongrass_Cover = 20%...
## Predicting at Cogongrass_Cover = 21%...
## Predicting at Cogongrass_Cover = 22%...
## Predicting at Cogongrass_Cover = 23%...
## Predicting at Cogongrass_Cover = 24%...
## Predicting at Cogongrass_Cover = 25%...
## Predicting at Cogongrass_Cover = 26%...
## Predicting at Cogongrass_Cover = 27%...
## Predicting at Cogongrass_Cover = 28%...
## Predicting at Cogongrass_Cover = 29%...
## Predicting at Cogongrass_Cover = 30%...
## Predicting at Cogongrass_Cover = 31%...
## Predicting at Cogongrass_Cover = 32%...
## Predicting at Cogongrass_Cover = 33%...
## Predicting at Cogongrass_Cover = 34%...
## Predicting at Cogongrass_Cover = 35%...
## Predicting at Cogongrass_Cover = 36%...
## Predicting at Cogongrass_Cover = 37%...
## Predicting at Cogongrass_Cover = 38%...
## Predicting at Cogongrass_Cover = 39%...
## Predicting at Cogongrass_Cover = 40%...
## Predicting at Cogongrass_Cover = 41%...
## Predicting at Cogongrass_Cover = 42%...
## Predicting at Cogongrass_Cover = 43%...
## Predicting at Cogongrass_Cover = 44%...
## Predicting at Cogongrass_Cover = 45%...
## Predicting at Cogongrass_Cover = 46%...
## Predicting at Cogongrass_Cover = 47%...
## Predicting at Cogongrass_Cover = 48%...
## Predicting at Cogongrass_Cover = 49%...
## Predicting at Cogongrass_Cover = 50%...
## Predicting at Cogongrass_Cover = 51%...
## Predicting at Cogongrass_Cover = 52%...
## Predicting at Cogongrass_Cover = 53%...
## Predicting at Cogongrass_Cover = 54%...
## Predicting at Cogongrass_Cover = 55%...
## Predicting at Cogongrass_Cover = 56%...
## Predicting at Cogongrass_Cover = 57%...
## Predicting at Cogongrass_Cover = 58%...
## Predicting at Cogongrass_Cover = 59%...
## Predicting at Cogongrass_Cover = 60%...
## Predicting at Cogongrass_Cover = 61%...
## Predicting at Cogongrass_Cover = 62%...
## Predicting at Cogongrass_Cover = 63%...
## Predicting at Cogongrass_Cover = 64%...
## Predicting at Cogongrass_Cover = 65%...
## Predicting at Cogongrass_Cover = 66%...
## Predicting at Cogongrass_Cover = 67%...
## Predicting at Cogongrass_Cover = 68%...
## Predicting at Cogongrass_Cover = 69%...
## Predicting at Cogongrass_Cover = 70%...
## Predicting at Cogongrass_Cover = 71%...
## Predicting at Cogongrass_Cover = 72%...
## Predicting at Cogongrass_Cover = 73%...
## Predicting at Cogongrass_Cover = 74%...
## Predicting at Cogongrass_Cover = 75%...
## Predicting at Cogongrass_Cover = 76%...
## Predicting at Cogongrass_Cover = 77%...
## Predicting at Cogongrass_Cover = 78%...
## Predicting at Cogongrass_Cover = 79%...
## Predicting at Cogongrass_Cover = 80%...
## Predicting at Cogongrass_Cover = 81%...
## Predicting at Cogongrass_Cover = 82%...
## Predicting at Cogongrass_Cover = 83%...
## Predicting at Cogongrass_Cover = 84%...
## Predicting at Cogongrass_Cover = 85%...
## Predicting at Cogongrass_Cover = 86%...
## Predicting at Cogongrass_Cover = 87%...
## Predicting at Cogongrass_Cover = 88%...
## Predicting at Cogongrass_Cover = 89%...
## Predicting at Cogongrass_Cover = 90%...
## Predicting at Cogongrass_Cover = 91%...
## Predicting at Cogongrass_Cover = 92%...
## Predicting at Cogongrass_Cover = 93%...
## Predicting at Cogongrass_Cover = 94%...
## Predicting at Cogongrass_Cover = 95%...
## Predicting at Cogongrass_Cover = 96%...
## Predicting at Cogongrass_Cover = 97%...
## Predicting at Cogongrass_Cover = 98%...
## Predicting at Cogongrass_Cover = 99%...
## Predicting at Cogongrass_Cover = 100%...
baseline <- prediction_list[["p0"]]
diff_list <- list()
cat("\nCalculating differences relative to 0% baseline...\n")
##
## Calculating differences relative to 0% baseline...
for(level in scenario_levels[-1]) {
# Skip 0
diff_name <- paste0("Diff_", level,
"_vs_0")
diff_list[[diff_name]] <- prediction_list[[paste0("p", level)]] -
baseline
# Save result
writeRaster(diff_list[[diff_name]], paste0(diff_name, ".tif"), overwrite = TRUE)
}
# Create the summary table
stats_summary <- lapply(names(diff_list), function(nm) {
v <- values(diff_list[[nm]], na.rm = TRUE)
data.frame(
Scenario = nm,
Mean_Change = mean(v),
SD_Change = sd(v),
Max_Loss = min(v),
Max_Gain = max(v)
)
}) %>%
bind_rows() %>%
mutate(across(where(is.numeric), ~ round(.x, 3)))
# Create formatted table
ft <- flextable(stats_summary)
ft <- autofit(ft)
# Create Word document
doc <- read_docx()
doc <- body_add_par(
doc,
"Scenario Summary Statistics (Relative to Baseline)",
style = "heading 1"
)
doc <- body_add_flextable(doc, ft)
# Save to the output directory
output_file <- file.path(output_dir, "Scenario_Summary_Statistics.docx")
print(doc, target = output_file)
cat("Table saved to:\n", output_file, "\n")
## Table saved to:
## C:/Users/alanivory34428/Desktop/03_Biodiversity/03_Outputs/Scenario_Summary_Statistics.docx
plot_raster <- function(rast_layer, title, subtitle = NULL,
palette = "viridis", midpoint = NULL,
low = NULL, high = NULL, mid = NULL,
limits = NULL) {
# Convert SpatRaster
df <- as.data.frame(rast_layer, xy = TRUE)
colnames(df)[3] <- "value"
df <- df[!is.na(df$value), ]
p <- ggplot() +
geom_raster(data = df, aes(x = x, y = y, fill = value)) +
geom_sf(data = states_sf, fill = NA, colour = "grey30", linewidth = 0.3) +
coord_sf(xlim = c(-92, -75), ylim = c(24, 37)) +
labs(title = title, subtitle = subtitle, x = NULL, y = NULL) +
theme_bw(base_size = 12) +
theme(legend.position = "right",
axis.text = element_text(size = 8))
# Logic for diverging (diff) vs sequential (absolute) scales
if (!is.null(midpoint)) {
p <- p + scale_fill_gradient2(
low = low, mid = mid, high = high,
midpoint = midpoint,
limits = limits,
name = "ΔShannon",
na.value = "transparent"
)
} else {
p <- p + scale_fill_viridis_c(
option = palette,
limits = limits,
name = "Shannon\nDiversity",
na.value = "transparent"
)
}
return(p)
}
se_states <- c("Florida", "Georgia", "Alabama", "Mississippi", "South Carolina",
"North Carolina", "Tennessee", "Arkansas", "Louisiana", "Virginia")
states_sf <- maps::map("state", regions = tolower(se_states), fill = TRUE, plot = FALSE) %>%
st_as_sf()
p_final <- plot_raster(diff_list[["Diff_100_vs_0"]],
title = "Total Predicted Biodiversity Impact",
subtitle = "100% Cogongrass vs. 0% Baseline",
midpoint = 0,
low = "#d73027", mid = "white", high = "#1a9850")
print(p_final)
## Warning: Raster pixels are placed at uneven horizontal intervals and will be shifted
## ℹ Consider using `geom_tile()` instead.
thresholds <- c(0.5, 0.6, 0.7)
threshold_rasters <- lapply(thresholds, function(thresh) {
diff_list[["Diff_100_vs_0"]] < -thresh
})
names(threshold_rasters) <- paste0("Loss > ", thresholds)
par(mfrow = c(1, 3), mar = c(3, 3, 3, 4))
for (i in seq_along(thresholds)) {
plot(
threshold_rasters[[i]],
main = paste0("Diversity Loss > ", thresholds[i]),
legend = FALSE,
col = c("grey90", "#d73027"),
axes = FALSE,
box = FALSE
)
v <- values(threshold_rasters[[i]], na.rm = TRUE)
mtext(
sprintf("%.1f%% of cells affected", mean(v, na.rm = TRUE) * 100),
side = 1, line = 1, cex = 0.8
)
}
ggsave(
plot = last_plot(),
filename = "threshold_analysis.png",
width = 10,
height = 4,
units = "in",
dpi = 300
)
## Warning: Raster pixels are placed at uneven horizontal intervals and will be shifted
## ℹ Consider using `geom_tile()` instead.
par(mfrow = c(1, 1)) # reset layout
plot_threshold_maps <- function(diff_raster,
diff_name,
thresholds = c(0.5, 0.6, 0.7),
out_dir = ".") {
library(terra)
# Extract cover percentage from the raster name
cover <- as.numeric(sub("Diff_([0-9]+)_vs_0", "\\1", diff_name))
# Create threshold rasters
threshold_rasters <- lapply(thresholds, function(thresh) {
diff_raster < -thresh
})
letters <- LETTERS[seq_along(thresholds)]
## Save PNG
png(
file.path(out_dir,
paste0(diff_name, "_SHDI_loss_thresholds.png")),
width = 12,
height = 4,
units = "in",
res = 300
)
par(mfrow = c(1, length(thresholds)),
mar = c(3, 3, 4, 2))
for (i in seq_along(thresholds)) {
plot(
threshold_rasters[[i]],
main = sprintf(
"%s) %d%% Cover \u2014 SHDI Loss > %.2f",
letters[i],
cover,
thresholds[i]
),
legend = FALSE,
col = c("grey90", "#d73027"),
axes = FALSE,
box = FALSE
)
}
dev.off()
## Save PDF
pdf(
file.path(out_dir,
paste0(diff_name, "_SHDI_loss_thresholds.pdf")),
width = 12,
height = 4
)
par(mfrow = c(1, length(thresholds)),
mar = c(3, 3, 4, 2))
for (i in seq_along(thresholds)) {
plot(
threshold_rasters[[i]],
main = sprintf(
"%s) %d%% Cover \u2014 SHDI Loss > %.2f",
letters[i],
cover,
thresholds[i]
),
legend = FALSE,
col = c("grey90", "#d73027"),
axes = FALSE,
box = FALSE
)
}
dev.off()
}
thresholds <- c(0.25, 0.5, 0.75)
diff_layers <- c("Diff_50_vs_0", "Diff_75_vs_0", "Diff_100_vs_0")
cover_labels <- c(
"Diff_50_vs_0" = "50% Cover",
"Diff_75_vs_0" = "75% Cover",
"Diff_100_vs_0" = "100% Cover"
)
# --- State outlines (same approach as your other script) ---
se_states <- c("Florida", "Georgia", "Alabama", "Mississippi", "South Carolina",
"North Carolina", "Tennessee", "Arkansas", "Louisiana", "Virginia")
states_sf <- maps::map("state", regions = tolower(se_states), fill = TRUE, plot = FALSE) %>%
st_as_sf()
# --- Build a threshold raster + subplot for every cover% x threshold combo ---
# NOTE: letters are assigned in the same nested-loop order as the original
# script (diff_layers outer, thresholds inner), so panel A-I map to:
# A) 50% Cover: D 0.25 SHDI D) 75% Cover: D 0.25 SHDI G) 100% Cover: D 0.25 SHDI
# B) 50% Cover: D 0.5 SHDI E) 75% Cover: D 0.5 SHDI H) 100% Cover: D 0.5 SHDI
# C) 50% Cover: D 0.75 SHDI F) 75% Cover: D 0.75 SHDI I) 100% Cover: D 0.75 SHDI
# Swap the order of the two loops below if you need a different pairing.
panel_letters <- LETTERS[seq_len(length(diff_layers) * length(thresholds))]
threshold_rasters <- list()
plot_list <- list()
cell_stats <- data.frame(
panel = character(),
cover = character(),
threshold = numeric(),
n_affected = integer(),
n_total = integer(),
pct_affected = numeric(),
stringsAsFactors = FALSE
)
i <- 1
for (dl in diff_layers) {
for (thresh in thresholds) {
key <- paste0(dl, "_gt_", thresh)
r <- diff_list[[dl]] < -thresh
threshold_rasters[[key]] <- r
# count + percent of cells affected -> now goes into the table, not onto the plot
v <- terra::values(r, na.rm = TRUE)
n_total <- length(v)
n_aff <- sum(v, na.rm = TRUE)
pct <- mean(v, na.rm = TRUE) * 100
letter <- panel_letters[i]
panel_title <- sprintf("%s) %s: \u0394 %.2g SHDI", letter, cover_labels[[dl]], thresh)
cell_stats <- rbind(cell_stats, data.frame(
panel = letter,
cover = cover_labels[[dl]],
threshold = thresh,
n_affected = n_aff,
n_total = n_total,
pct_affected = round(pct, 1)
))
df <- as.data.frame(r, xy = TRUE)
colnames(df)[3] <- "value"
df <- df[!is.na(df$value), ]
df$value <- factor(df$value, levels = c(FALSE, TRUE))
p <- ggplot() +
geom_raster(data = df, aes(x = x, y = y, fill = value)) +
geom_sf(data = states_sf, fill = NA, colour = "grey30", linewidth = 0.3) +
coord_sf(xlim = c(-91, -74), ylim = c(24, 37), datum = NA) +
scale_fill_manual(values = c("FALSE" = "grey90", "TRUE" = "#FA4616"), guide = "none") +
labs(title = panel_title, x = NULL, y = NULL) +
theme_bw(base_size = 10) +
theme(
axis.text = element_blank(),
axis.ticks = element_blank(),
panel.grid = element_blank(),
plot.title = element_text(size = 10, face = "bold")
)
plot_list[[key]] <- p
i <- i + 1
}
}
# --- Combine the 9 panels into one 3x3 figure ---
final_plot <- wrap_plots(plot_list, ncol = 3, nrow = 3)
ggsave(
filename = file.path(output_dir, "threshold_raster_analysis.png"),
plot = final_plot,
width = 12,
height = 12,
units = "in",
dpi = 300
)
## Warning: Raster pixels are placed at uneven horizontal intervals and will be shifted
## ℹ Consider using `geom_tile()` instead.
## Raster pixels are placed at uneven horizontal intervals and will be shifted
## ℹ Consider using `geom_tile()` instead.
## Raster pixels are placed at uneven horizontal intervals and will be shifted
## ℹ Consider using `geom_tile()` instead.
## Raster pixels are placed at uneven horizontal intervals and will be shifted
## ℹ Consider using `geom_tile()` instead.
## Raster pixels are placed at uneven horizontal intervals and will be shifted
## ℹ Consider using `geom_tile()` instead.
## Raster pixels are placed at uneven horizontal intervals and will be shifted
## ℹ Consider using `geom_tile()` instead.
## Raster pixels are placed at uneven horizontal intervals and will be shifted
## ℹ Consider using `geom_tile()` instead.
## Raster pixels are placed at uneven horizontal intervals and will be shifted
## ℹ Consider using `geom_tile()` instead.
## Raster pixels are placed at uneven horizontal intervals and will be shifted
## ℹ Consider using `geom_tile()` instead.
# --- Separate table of % cells affected (no longer printed on the plot) ---
print(cell_stats)
## panel cover threshold n_affected n_total pct_affected
## 1 A 50% Cover 0.25 0 411926 0.0
## 2 B 50% Cover 0.50 0 411926 0.0
## 3 C 50% Cover 0.75 0 411926 0.0
## 4 D 75% Cover 0.25 2 411926 0.0
## 5 E 75% Cover 0.50 0 411926 0.0
## 6 F 75% Cover 0.75 0 411926 0.0
## 7 G 100% Cover 0.25 407258 411926 98.9
## 8 H 100% Cover 0.50 209885 411926 51.0
## 9 I 100% Cover 0.75 158 411926 0.0
write.csv(
cell_stats,
file = file.path(output_dir, "threshold_cell_stats.csv"),
row.names = FALSE
)
eco_l2 <- st_read("C:/Users/alanivory34428/Desktop/03_Biodiversity/02_Data/NA_CEC_Eco_Level2.shp") %>% st_transform(crs(diff_list[[1]]))
## Reading layer `NA_CEC_Eco_Level2' from data source
## `C:\Users\alanivory34428\Desktop\03_Biodiversity\02_Data\NA_CEC_Eco_Level2.shp'
## using driver `ESRI Shapefile'
## Simple feature collection with 2261 features and 8 fields
## Geometry type: POLYGON
## Dimension: XY
## Bounding box: xmin: -4334052 ymin: -3313739 xmax: 3324076 ymax: 4267265
## Projected CRS: Sphere_ARC_INFO_Lambert_Azimuthal_Equal_Area
eco_l3 <- st_read("C:/Users/alanivory34428/Desktop/03_Biodiversity/02_Data/NA_CEC_Eco_Level3.shp") %>% st_transform(crs(diff_list[[1]]))
## Reading layer `NA_CEC_Eco_Level3' from data source
## `C:\Users\alanivory34428\Desktop\03_Biodiversity\02_Data\NA_CEC_Eco_Level3.shp'
## using driver `ESRI Shapefile'
## Simple feature collection with 2548 features and 11 fields
## Geometry type: POLYGON
## Dimension: XY
## Bounding box: xmin: -4334052 ymin: -3313739 xmax: 3324076 ymax: 4267265
## Projected CRS: Sphere_ARC_INFO_Lambert_Azimuthal_Equal_Area
# Convert to SpatVector for terra::extract
eco_l2_vect <- terra::vect(eco_l2)
eco_l3_vect <- terra::vect(eco_l3)
summarize_by_ecoregion <- function(diff_list, eco_vect, name_col) {
# Rasterize polygons
template_raster <- diff_list[[1]]
eco_rast <- terra::rasterize(eco_vect, template_raster, field = name_col)
lapply(names(diff_list), function(nm) {
r <- diff_list[[nm]]
z <- terra::zonal(r, eco_rast, fun = "mean", na.rm = TRUE)
colnames(z) <- c("Ecoregion", "Mean_Change")
z$Scenario <- nm
z
}) %>%
bind_rows() %>%
filter(!is.na(Mean_Change)) %>%
mutate(
Scenario_num = as.numeric(gsub("Diff_|_vs_0", "", Scenario))
)
}
# names(as.data.frame(eco_l2_vect))
# names(as.data.frame(eco_l3_vect))
# Common EPA shapefile name columns:
# Level 2 -> "NA_L2NAME"
# Level 3 -> "US_L3NAME"
eco_l2_summary <- summarize_by_ecoregion(diff_list, eco_l2_vect, name_col = "NA_L2NAME")
eco_l3_summary <- summarize_by_ecoregion(diff_list, eco_l3_vect, name_col = "NA_L3NAME")
# L3 Ecoregion
eco_l3_100 <- eco_l3_summary %>%
dplyr::filter(Scenario_num == 100)
eco_l3_map <- eco_l3 %>%
dplyr::left_join(eco_l3_100, by = c("NA_L3NAME" = "Ecoregion"))
eco_l3_map_vect <- terra::vect(eco_l3_map)
template_raster <- diff_list[[1]]
eco_l3_map_vect <- terra::crop(eco_l3_map_vect, template_raster)
terra::plot(
eco_l3_map_vect,
"Mean_Change",
col = hcl.colors(50, "RdYlGn", rev = TRUE),
main = "Δ Shannon Diversity (100% Cogongrass)"
)
ggplot(eco_l3_map) +
geom_sf(aes(fill = Mean_Change), color = NA) +
scale_fill_gradient2(
low = "#1a9850",
mid = "white",
high = "#d73027",
midpoint = 0
) +
theme_bw() +
labs(
title = "Δ Shannon Diversity (100% Cogongrass)",
fill = "Change"
)
#L2 Ecoregion
eco_l2_100 <- eco_l2_summary %>%
dplyr::filter(Scenario_num == 100)
eco_l2_map <- eco_l2 %>%
dplyr::left_join(eco_l2_100, by = c("NA_L2NAME" = "Ecoregion"))
eco_l2_map_vect <- terra::vect(eco_l2_map)
template_raster <- diff_list[[1]]
eco_l2_map_vect <- terra::crop(eco_l2_map_vect, template_raster)
terra::plot(
eco_l2_map_vect,
"Mean_Change",
col = hcl.colors(50, "RdYlGn", rev = TRUE),
main = "Δ Shannon Diversity (Level 2 Ecoregions, 100% Cogongrass)"
)
ggplot(eco_l2_map) +
geom_sf(aes(fill = Mean_Change), color = "black", linewidth = 0.2) +
scale_fill_gradient2(
low = "#1a9850",
mid = "white",
high = "#d73027",
midpoint = 0
) +
theme_bw() +
labs(
title = "Δ Shannon Diversity (Level 2 Ecoregions, 100% Cogongrass)",
fill = "Change"
)
## two panel figure
# Common color limits so both maps are directly comparable
lims <- range(
c(eco_l2_map$Mean_Change, eco_l3_map$Mean_Change),
na.rm = TRUE
)
# -----------------------------
# Level III Ecoregions
# -----------------------------
p_l3 <- ggplot(eco_l3_map) +
geom_sf(aes(fill = Mean_Change), color = "black", linewidth = 0.1) +
scale_fill_gradient2(
low = "#1a9850",
mid = "white",
high = "#d73027",
midpoint = 0,
limits = lims,
name = expression(Delta*" Shannon")
) +
coord_sf(xlim = c(-92, -75), ylim = c(24, 37), expand = FALSE) +
labs(title = "Level III Ecoregions") +
theme_bw(base_size = 12) +
theme(
panel.grid = element_blank(),
axis.title = element_blank(),
plot.title = element_text(face = "bold", hjust = 0.5)
)
# -----------------------------
# Level II Ecoregions
# -----------------------------
p_l2 <- ggplot(eco_l2_map) +
geom_sf(aes(fill = Mean_Change), color = "black", linewidth = 0.2) +
scale_fill_gradient2(
low = "#1a9850",
mid = "white",
high = "#d73027",
midpoint = 0,
limits = lims,
name = expression(Delta*" Shannon")
) +
coord_sf(xlim = c(-92, -75), ylim = c(24, 37), expand = FALSE) +
labs(title = "Level II Ecoregions") +
theme_bw(base_size = 12) +
theme(
panel.grid = element_blank(),
axis.title = element_blank(),
plot.title = element_text(face = "bold", hjust = 0.5)
)
eco_panel <-
p_l3 + p_l2 +
plot_layout(guides = "collect") +
plot_annotation(
title = "Predicted Change in Shannon Diversity Under 100% Cogongrass",
tag_levels = "A"
) &
theme(
legend.position = "right",
plot.tag = element_text(face = "bold", size = 14)
)
eco_panel
# Save
ggsave(
filename = "Ecoregion_Shannon_Change_100Cogongrass.png",
plot = eco_panel,
width = 12,
height = 6,
dpi = 600,
bg = "white"
)
thresholds <- c(0.5, 0.6, 0.7)
threshold_rasters <- lapply(thresholds, function(thresh) {
diff_list[["Diff_100_vs_0"]] < -thresh
})
names(threshold_rasters) <- paste0("Loss > ", thresholds)
## Quick on-screen panel + saved PNG (base graphics -> capture with png(), NOT ggsave)
png(file.path(output_dir, "threshold_analysis.png"),
width = 10, height = 4, units = "in", res = 300)
par(mfrow = c(1, 3), mar = c(3, 3, 3, 4))
for (i in seq_along(thresholds)) {
plot(
threshold_rasters[[i]],
main = paste0("Diversity Loss > ", thresholds[i]),
legend = FALSE,
col = c("grey90", "#FA4616"),
axes = FALSE,
box = FALSE
)
v <- values(threshold_rasters[[i]], na.rm = TRUE)
mtext(
sprintf("%.1f%% of cells affected", mean(v, na.rm = TRUE) * 100),
side = 1, line = 1, cex = 0.8
)
}
dev.off()
## png
## 2
par(mfrow = c(1, 1)) # reset layout
## Reusable function: saves a PNG + PDF panel of loss thresholds for one diff raster.
## out_dir defaults to the global output_dir defined in the setup chunk.
plot_threshold_maps <- function(diff_raster,
diff_name,
thresholds = c(0.5, 0.6, 0.7),
out_dir = output_dir) {
library(terra)
# Extract cover percentage from the raster name
cover <- as.numeric(sub("Diff_([0-9]+)_vs_0", "\\1", diff_name))
# Create threshold rasters
threshold_rasters <- lapply(thresholds, function(thresh) {
diff_raster < -thresh
})
panel_letters <- LETTERS[seq_along(thresholds)]
draw_panel <- function() {
par(mfrow = c(1, length(thresholds)), mar = c(3, 3, 4, 2))
for (i in seq_along(thresholds)) {
plot(
threshold_rasters[[i]],
main = sprintf(
"%s) %d%% Cover \u2014 SHDI Loss > %.2f",
panel_letters[i], cover, thresholds[i]
),
legend = FALSE,
col = c("grey90", "#FA4616"),
axes = FALSE,
box = FALSE
)
}
}
## Save PNG
png(file.path(out_dir, paste0(diff_name, "_SHDI_loss_thresholds.png")),
width = 12, height = 4, units = "in", res = 300)
draw_panel()
dev.off()
## Save PDF
pdf(file.path(out_dir, paste0(diff_name, "_SHDI_loss_thresholds.pdf")),
width = 12, height = 4)
draw_panel()
dev.off()
}
## Example: plot_threshold_maps(diff_list[["Diff_100_vs_0"]], "Diff_100_vs_0")
## ============================================================
## Objective 3 figures, rebuilt around what the scenarios actually show
##
## The 3x3 threshold panel has six empty panels because nothing crosses
## 0.25 SHDI until ~87% cover. The scenario series locates the change as a
## step between 86% and 87%: mean goes from -0.234 to -0.486, five times
## larger than any other one-percent increment. That is the finding, and
## the figure should show it.
##
## Requires: diff_list, states_sf, output_dir (from the prediction Rmd)
## ============================================================
pacman::p_load(terra, sf, tidyverse, patchwork, maps)
gator_orange <- "#FA4616"
gator_blue <- "#0021A5"
## ---- 0. Correct cell area -----------------------------------------------
## The reference grid is the NDVI raster: ~0.009 degrees, i.e. roughly 1 km,
## NOT 30 m as the alignment comments say. In a lon/lat CRS cell area also
## varies with latitude, so use cellSize() rather than a constant.
cell_km2 <- terra::cellSize(diff_list[[1]], unit = "km")
total_km2 <- as.numeric(terra::global(
terra::mask(cell_km2, diff_list[[1]]), "sum", na.rm = TRUE))
cat(sprintf("Study extent: %s cells, %.0f km2 (%.1f million ha)\n",
format(as.numeric(terra::global(!is.na(diff_list[[1]]), "sum",
na.rm = TRUE)), big.mark = ","),
total_km2, total_km2 * 100 / 1e6))
## Study extent: 411,926 cells, 348177 km2 (34.8 million ha)
## ---- 1. The scenario curve: this is the key panel ------------------------
scen <- map_dfr(names(diff_list), function(nm) {
v <- terra::values(diff_list[[nm]], na.rm = TRUE)
tibble(cover = as.numeric(gsub("Diff_|_vs_0", "", nm)),
mean = mean(v), sd = sd(v),
min = min(v), max = max(v))
}) %>% arrange(cover)
## Locate the transition: the single largest one-percent drop
steps <- scen %>% mutate(delta = mean - lag(mean)) %>% filter(!is.na(delta))
brk <- steps$cover[which.min(steps$delta)]
cat(sprintf("Largest one-percent drop at %d%% cover: %.3f units\n",
brk, min(steps$delta, na.rm = TRUE)))
## Largest one-percent drop at 87% cover: -0.251 units
cat(sprintf(" next largest is %.3f, so the step is %.1f times larger\n",
sort(steps$delta)[2], min(steps$delta) / sort(steps$delta)[2]))
## next largest is -0.051, so the step is 4.9 times larger
## Cover above which predictions stop changing = the observed data ceiling
flat <- scen %>% filter(abs(mean - last(scen$mean)) < 0.001) %>% pull(cover) %>% min()
cat(sprintf("Predictions flat from %d%% cover onward (RF cannot extrapolate)\n", flat))
## Predictions flat from 92% cover onward (RF cannot extrapolate)
p_curve <- ggplot(scen, aes(cover, mean)) +
geom_hline(yintercept = 0, colour = "grey60", linewidth = 0.3) +
geom_ribbon(aes(ymin = mean - sd, ymax = mean + sd),
fill = gator_orange, alpha = 0.25) +
geom_line(colour = gator_blue, linewidth = 1.1) +
geom_vline(xintercept = brk, linetype = "dashed", colour = "grey30") +
annotate("text", x = brk - 2, y = min(scen$mean) * 0.55,
label = sprintf("%d%%", brk), hjust = 1, size = 3.5, fontface = "bold") +
annotate("rect", xmin = flat, xmax = 100, ymin = -Inf, ymax = Inf,
fill = "grey50", alpha = 0.12) +
annotate("text", x = (flat + 100) / 2, y = max(scen$mean + scen$sd),
label = "beyond\nobserved\nrange", size = 2.7, colour = "grey40", vjust = 1) +
labs(x = "Cogongrass cover (%)",
y = expression(Delta*" Shannon diversity"),
title = "A) Range-wide mean change") +
theme_classic(base_size = 11) +
theme(plot.title = element_text(face = "bold", size = 10))
## ---- 2. Maps that are not empty ------------------------------------------
## Bracket the transition instead of showing 50/75/100, where two rows are blank.
map_panel <- function(diff_name, thresh, title) {
r <- diff_list[[diff_name]] < -thresh
df <- as.data.frame(r, xy = TRUE)
colnames(df)[3] <- "value"
df <- df[!is.na(df$value), ]
df$value <- factor(df$value, levels = c(FALSE, TRUE))
pct <- 100 * mean(terra::values(r, na.rm = TRUE), na.rm = TRUE)
ggplot() +
geom_raster(data = df, aes(x, y, fill = value)) +
geom_sf(data = states_sf, fill = NA, colour = "grey30", linewidth = 0.25) +
coord_sf(xlim = c(-95, -75), ylim = c(24, 37), datum = NA) +
scale_fill_manual(values = c("FALSE" = "grey90", "TRUE" = gator_orange),
guide = "none") +
labs(title = sprintf("%s (%.1f%%)", title, pct), x = NULL, y = NULL) +
theme_bw(base_size = 10) +
theme(axis.text = element_blank(), axis.ticks = element_blank(),
panel.grid = element_blank(),
plot.title = element_text(size = 9, face = "bold"))
}
## Row 1: same threshold, cover levels that bracket the step
row1 <- list(
map_panel("Diff_80_vs_0", 0.25, "B) 80% cover, loss > 0.25"),
map_panel(paste0("Diff_", brk, "_vs_0"), 0.25, sprintf("C) %d%% cover, loss > 0.25", brk)),
map_panel(paste0("Diff_", flat, "_vs_0"), 0.25, sprintf("D) %d%% cover, loss > 0.25", flat))
)
## Row 2: at the observed ceiling, vary the threshold - this is where the
## spatial signal lives, since almost everything crosses 0.25
row2 <- list(
map_panel(paste0("Diff_", flat, "_vs_0"), 0.25, sprintf("E) %d%% cover, loss > 0.25", flat)),
map_panel(paste0("Diff_", flat, "_vs_0"), 0.50, sprintf("F) %d%% cover, loss > 0.50", flat)),
map_panel(paste0("Diff_", flat, "_vs_0"), 0.75, sprintf("G) %d%% cover, loss > 0.75", flat))
)
fig6 <- p_curve / wrap_plots(row1, nrow = 1) / wrap_plots(row2, nrow = 1) +
plot_layout(heights = c(1, 1, 1))
ggsave(file.path(output_dir, "Figure6_threshold_and_maps.png"),
fig6, width = 10, height = 11, units = "in", dpi = 300, bg = "white")
## Warning: Raster pixels are placed at uneven horizontal intervals and will be shifted
## ℹ Consider using `geom_tile()` instead.
## Raster pixels are placed at uneven horizontal intervals and will be shifted
## ℹ Consider using `geom_tile()` instead.
## Raster pixels are placed at uneven horizontal intervals and will be shifted
## ℹ Consider using `geom_tile()` instead.
## Raster pixels are placed at uneven horizontal intervals and will be shifted
## ℹ Consider using `geom_tile()` instead.
## Raster pixels are placed at uneven horizontal intervals and will be shifted
## ℹ Consider using `geom_tile()` instead.
## Raster pixels are placed at uneven horizontal intervals and will be shifted
## ℹ Consider using `geom_tile()` instead.
## ---- 3. Onset map: the cover at which each cell first crosses -0.25 ------
## This is the direct answer to Victoria's comments 111, 113, 114 and 116 -
## it shows spatial variation in a single map instead of asking the reader to
## compare panels. Cells that never cross stay NA.
cat("\nBuilding onset map (this loops over all scenarios)...\n")
##
## Building onset map (this loops over all scenarios)...
onset <- diff_list[[1]] * NA
for (nm in names(diff_list)) {
cov_i <- as.numeric(gsub("Diff_|_vs_0", "", nm))
crossed <- diff_list[[nm]] <= -0.25
onset <- terra::ifel(is.na(onset) & crossed, cov_i, onset)
}
names(onset) <- "onset_cover"
onset_df <- as.data.frame(onset, xy = TRUE)
colnames(onset_df)[3] <- "cover"
onset_df <- onset_df[!is.na(onset_df$cover), ]
p_onset <- ggplot() +
geom_raster(data = onset_df, aes(x, y, fill = cover)) +
geom_sf(data = states_sf, fill = NA, colour = "grey30", linewidth = 0.25) +
coord_sf(xlim = c(-95, -75), ylim = c(24, 37), datum = NA) +
scale_fill_viridis_c(option = "magma", direction = -1,
name = "Cogongrass\ncover (%)") +
labs(title = "Cover at which predicted diversity first falls by 0.25 units",
x = NULL, y = NULL) +
theme_bw(base_size = 11) +
theme(axis.text = element_blank(), axis.ticks = element_blank(),
panel.grid = element_blank())
ggsave(file.path(output_dir, "Figure_onset_cover.png"),
p_onset, width = 8, height = 6, units = "in", dpi = 300, bg = "white")
## Warning: Raster pixels are placed at uneven horizontal intervals and will be shifted
## ℹ Consider using `geom_tile()` instead.
cat("\nOnset cover summary:\n")
##
## Onset cover summary:
print(summary(onset_df$cover))
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 72.00 84.00 87.00 85.36 87.00 90.00
## ---- 4. Ecoregion table for the Results text ----------------------------
## eco_l3_summary is built earlier in the prediction Rmd.
if (exists("eco_l3_summary")) {
eco_top <- eco_l3_summary %>%
filter(Scenario_num == flat) %>%
arrange(Mean_Change) %>%
mutate(Mean_Change = round(Mean_Change, 3))
cat(sprintf("\n===== Ecoregions ranked by mean loss at %d%% cover =====\n", flat))
print(as.data.frame(head(eco_top, 10)))
cat("\n--- least affected ---\n")
print(as.data.frame(tail(eco_top, 5)))
}
##
## ===== Ecoregions ranked by mean loss at 92% cover =====
## Ecoregion Mean_Change Scenario Scenario_num
## 1 Ridge and Valley -0.559 Diff_92_vs_0 92
## 2 South Central Plains -0.532 Diff_92_vs_0 92
## 3 Southwestern Appalachians -0.526 Diff_92_vs_0 92
## 4 Mississippi Alluvial Plain -0.525 Diff_92_vs_0 92
## 5 Mississippi Valley Loess Plains -0.510 Diff_92_vs_0 92
## 6 Southern Coastal Plain -0.496 Diff_92_vs_0 92
## 7 Southeastern Plains -0.486 Diff_92_vs_0 92
## 8 Western Gulf Coastal Plain -0.482 Diff_92_vs_0 92
## 9 Piedmont -0.478 Diff_92_vs_0 92
## 10 Blue Ridge -0.468 Diff_92_vs_0 92
##
## --- least affected ---
## Ecoregion Mean_Change Scenario Scenario_num
## 7 Southeastern Plains -0.486 Diff_92_vs_0 92
## 8 Western Gulf Coastal Plain -0.482 Diff_92_vs_0 92
## 9 Piedmont -0.478 Diff_92_vs_0 92
## 10 Blue Ridge -0.468 Diff_92_vs_0 92
## 11 Middle Atlantic Coastal Plain -0.442 Diff_92_vs_0 92