---
title: "Plot-Level Analysis: Invasive Species Effects"
subtitle: "Evaluating Impacts on Native Flora Biodiversity and Composition"
author: "Felipe Melo and Nduah Nderi"
date: today
format:
html:
toc: true
toc-depth: 3
theme: cosmo
code-fold: true
code-tools: true
---
```{r, include=FALSE}
knitr::opts_chunk$set(echo = TRUE, warning = FALSE, message = FALSE)
```
## Introduction
This document performs a plot-level analysis examining the effects of invasive species cover on native flora. The analysis aggregates data collected across 12 quadrats per plot by averaging cover values, resulting in a dataset representing **100 distinct plots**.
## 1. Environment Setup & Libraries
We begin by loading all necessary packages required for data manipulation, diversity metrics, regression modeling, and ecological community analysis.
```{r libraries}
library(dplyr)
library(tidyr)
library(readxl)
library(vegan)
library(hillR)
library(segmented)
library(indicspecies)
library(ggplot2)
library(corrplot)
library(betapart)
library(lme4)
library(car)
library(emmeans)
library(scatterplot3d)
library(cowplot)
library(knitr)
library(viridis)
library(gt)
```
## 2. Data Loading & Preprocessing
### Load Dataset
First, we load the raw quadrat-level dataset.
::: {.callout-note}
Ensure your `invasive_cover.xlsx` file is placed directly inside your active R project directory to support reproducible parsing paths.
:::
```{r load-data}
setwd("~/Google Drive/nduah_analyses")
invasive_cover <- read_excel("invasive_cover.xlsx")
```
### Format Transformation & Mid-Point Conversion
We convert the ordinal cover rankings to numeric mid-point percentages using a lookup vector. Then, we transform the community data into a long format for easier aggregation.
```{r preprocessing}
ordinal_to_midpoint <- c(
"1" = 0.5, # <1%
"2" = 2.5, # 1‑5%
"3" = 7.5, # 5‑10%
"4" = 17.5, # 10‑25%
"5" = 32.5, # 25‑50%
"6" = 62.5, # 50‑75%
"7" = 87.5 # 75‑100%
)
quadrat_cols <- colnames(invasive_cover)[5:ncol(invasive_cover)]
df_long <- invasive_cover %>%
pivot_longer(cols = all_of(quadrat_cols),
names_to = "quadrat_id",
values_to = "cover_ord") %>%
mutate(
cover_pct = ifelse(cover_ord == 0, 0, ordinal_to_midpoint[as.character(cover_ord)]),
plot_id = sub("Q.*$", "", quadrat_id)
)
head(df_long)
```
### Data Aggregation
We average quadrat cover levels up to the Plot $\times$ Species level.
```{r aggregate-plots}
plot_species <- df_long %>%
group_by(plot_id, species_name, type, form, family) %>%
summarise(cover_pct = mean(cover_pct, na.rm = TRUE), .groups = "drop")
head(plot_species)
```
## 3. Species per life form
```{r lifeform-distribution, cache=FALSE}
# ------------------------------------------------------------
# UNIQUE SPECIES PER LIFE FORM × ORIGIN
# (count distinct species names, not plot occurrences)
# ------------------------------------------------------------
lifeform_richness <- plot_species %>%
mutate(origin = ifelse(type == "Native", "Native", "Exotic")) %>%
group_by(form, origin) %>%
summarise(n_species = n_distinct(species_name), .groups = "drop") %>%
pivot_wider(names_from = origin, values_from = n_species, values_fill = 0) %>%
dplyr::select(form, Native, Exotic) %>% # <-- guarantees column order
mutate(Total = Native + Exotic,
Exotic_pct = round(100 * Exotic / Total, 1)) %>%
arrange(desc(Total))
lifeform_richness %>%
gt() %>%
tab_caption("Species richness of native and exotic flora by life form")
# ------------------------------------------------------------
# MEAN COVER PER LIFE FORM × ORIGIN (averaged across plots)
# ------------------------------------------------------------
lifeform_cover <- plot_species %>%
mutate(origin = ifelse(type == "Native", "Native", "Exotic")) %>%
group_by(form, origin) %>%
summarise(
mean_cover = round(mean(cover_pct, na.rm = TRUE), 2),
sd_cover = round(sd(cover_pct, na.rm = TRUE), 2),
.groups = "drop"
) %>%
mutate(cover_label = paste0(mean_cover, " ± ", sd_cover)) %>%
dplyr::select(form, origin, cover_label) %>%
pivot_wider(names_from = origin, values_from = cover_label,
values_fill = "—") %>%
# Rename AND reorder before kable touches it — no col.names argument needed
dplyr::transmute(
`Life Form` = form,
`Native (mean ± SD %)` = Native,
`Exotic (mean ± SD %)` = Exotic
) %>%
arrange(`Life Form`)
lifeform_cover %>%
gt() %>%
tab_caption("Mean cover (%) of native and exotic flora by life form (across all plot × species records)")
```
### Full table of species per plot
This table tells us how frequent each species of native and exotic species were and help to show who is who in this grassland community.
```{r}
species_frequency <- plot_species %>%
filter(cover_pct > 0) %>% # presence only for frequency count
group_by(species_name, type, form, family) %>%
summarise(
n_plots = n_distinct(plot_id),
mean_cover = round(mean(cover_pct, na.rm = TRUE), 2),
sd_cover = round(sd(cover_pct, na.rm = TRUE), 2),
.groups = "drop"
) %>%
mutate(
cover_label = paste0(mean_cover, " ± ", sd_cover),
freq_pct = round(100 * n_plots / n_distinct(plot_species$plot_id), 1),
origin = ifelse(type == "Native", "Native", "Exotic")
) %>%
dplyr::transmute(
`Species` = species_name,
`Family` = family,
`Life Form` = form,
`Origin` = origin,
`N Plots` = n_plots,
`Frequency (%)` = freq_pct,
`Mean Cover ± SD` = cover_label
) %>%
arrange(desc(`N Plots`), `Species`)
species_frequency %>%
gt() %>%
tab_caption("All species ordered by plot frequency (n = 100 plots)") %>%
tab_style(
style = cell_text(style = "italic"),
locations = cells_body(columns = `Species`)
) %>%
tab_style(
style = cell_fill(color = "#fff3e0"),
locations = cells_body(rows = `Origin` == "Exotic")
) %>%
data_color(
columns = `Frequency (%)`,
method = "numeric",
palette = "Blues"
)
```
> **Interpretation**: Many of the most widespread species are exotic ones, specially herbs/forbs... tye must have a really important role in the ecosystem fucntioning
## 3. Metric Calculations
## Native and Exotic Diversity (Hill Numbers)
We restructure native and exotic species records into parallel wide format community matrices to calculate Hill numbers ($q = 0, 1, 2$).
```{r diversity-calculations}
native_wide <- plot_species %>%
filter(type == "Native") %>%
dplyr::select(plot_id, species_name, cover_pct) %>%
pivot_wider(names_from = species_name, values_from = cover_pct, values_fill = 0) %>%
as.data.frame()
rownames(native_wide) <- native_wide$plot_id
native_wide_matrix <- native_wide %>% dplyr::select(-plot_id)
exotic_wide <- plot_species %>%
filter(type != "Native") %>%
dplyr::select(plot_id, species_name, cover_pct) %>%
pivot_wider(names_from = species_name, values_from = cover_pct, values_fill = 0) %>%
as.data.frame()
rownames(exotic_wide) <- exotic_wide$plot_id
exotic_wide_matrix <- exotic_wide %>% dplyr::select(-plot_id)
native_div <- data.frame(
plot_id = rownames(native_wide_matrix),
q0 = hill_taxa(native_wide_matrix, q = 0),
q1 = hill_taxa(native_wide_matrix, q = 1),
q2 = hill_taxa(native_wide_matrix, q = 2)
)
exotic_div <- data.frame(
plot_id = rownames(exotic_wide_matrix),
q0 = hill_taxa(exotic_wide_matrix, q = 0),
q1 = hill_taxa(exotic_wide_matrix, q = 1),
q2 = hill_taxa(exotic_wide_matrix, q = 2)
)
```
## 4. Visualizations & Distributions
### Histograms of True Diversity
``` {r }
combined_wide <- bind_rows(
native_div %>% mutate(origin = "Native"),
exotic_div %>% mutate(origin = "Exotic")
)
div_long <- combined_wide %>%
pivot_longer(
cols = c(q0, q1, q2),
names_to = "q_level",
values_to = "diversity_value"
) %>%
mutate(
origin = factor(origin, levels = c("Native", "Exotic")),
q_level = case_when(
q_level == "q0" ~ "q = 0 (Rare species)",
q_level == "q1" ~ "q = 1 (Common species)",
q_level == "q2" ~ "q = 2 (Dominant species)"
)
)
ggplot(div_long, aes(x = diversity_value, fill = origin)) +
geom_histogram(bins = 10, color = "white", alpha = 0.85) +
facet_grid(origin ~ q_level, scales = "free") +
scale_fill_manual(values = c("Native" = "#2e7d32", "Exotic" = "#c62828")) +
labs(
title = "True Diversity Value Distributions",
x = "Diversity Metric Value",
y = "Number of Plots (Frequency)"
) +
theme_minimal(base_size = 12) +
theme(
legend.position = "none",
strip.text = element_text(face = "bold", size = 11),
strip.background = element_rect(fill = "gray95", color = "gray80"),
panel.spacing = unit(1.2, "lines"),
plot.title = element_text(face = "bold", size = 13)
) -> hist.all
hist.all
```
> **Interpretation**: Most plots with few species (left-skewed) means that highly diverse patches are uncommon and probably patchy.
### Diversity Profiles
By summarizing mean tendencies alongside 95% Confidence Intervals
```{r visual-profiles, fig.width = 8, fig.height = 5}
profile_stats <- div_long %>%
group_by(origin, q_level) %>%
summarise(
n = n(),
mean_val = mean(diversity_value, na.rm = TRUE),
sd_val = sd(diversity_value, na.rm = TRUE),
.groups = "drop"
) %>%
mutate(
se_val = sd_val / sqrt(n),
ci_lower = mean_val - (1.96 * se_val),
ci_upper = mean_val + (1.96 * se_val)
)
pd <- position_dodge(width = 0.25)
ggplot(profile_stats, aes(x = q_level, y = mean_val, color = origin, group = origin)) +
geom_line(size = 1.2, position = pd) +
geom_errorbar(aes(ymin = ci_lower, ymax = ci_upper), width = 0.15, size = 0.8, position = pd) +
geom_point(size = 4, stroke = 1.5, fill = "white", shape = 21, position = pd) +
scale_color_manual(values = c("Native" = "#2e7d32", "Exotic" = "#c62828")) +
labs(
title = "Taxonomic Diversity Scaling Profiles",
x = "Order of Diversity (Hill Numbers)",
y = "Effective Number of Species (Mean)",
color = "Origin of Species"
) +
theme_minimal(base_size = 12) +
theme(
legend.position = "bottom",
panel.grid.minor = element_blank(),
plot.title = element_text(face = "bold", size = 13)
) -> profile.all
profile.all
```
> **Intrpretation:** Rare species (q=0) comprise the most of the diversiy of native species but not that exotic repond fo up to 30-40% of all species. However, common and dominant species are exotic, suggesting that ecosystem fucntioning might be heavily driven by exotic species.
## 5. Regression Modeling & Intercept Strengths
We map native versus exotic variables horizontally using `facet_grid()` to observe shifting interaction trends directly.
```{r regressions, fig.width = 12, fig.height = 4}
regression_data <- combined_wide %>%
pivot_longer(
cols = c(q0, q1, q2),
names_to = "q_level",
values_to = "diversity_value"
) %>%
pivot_wider(
names_from = origin,
values_from = diversity_value
) %>%
mutate(
q_level = factor(q_level, levels = c("q0", "q1", "q2"),
labels = c("q0 (Rare species)", "q1 (Common species)", "q2 (Dominant species)"))
)
ggplot(regression_data, aes(x = Exotic, y = Native)) +
geom_jitter(alpha = 0.6, color = "#2b5c8f", size = 2.5) +
geom_smooth(method = "lm", formula = y ~ x, color = "#c62828", fill = "gray85", size = 1.2) +
facet_grid(. ~ q_level, scales = "free") +
labs(
x = "Exotic Diversity",
y = "Native Diversity"
) +
theme_minimal(base_size = 12) +
theme(
strip.text = element_text(face = "bold", size = 11),
strip.background = element_rect(fill = "gray95", color = "gray80"),
panel.spacing = unit(1.5, "lines"),
plot.title = element_text(face = "bold", size = 13)
)
```
### Mixed-Effects Slope Comparison
To determine if regression slopes differ significantly across Hill values while controlling for repeated measures within plots, we implement a linear mixed-effects framework.
```{r mixed-model}
regression_data_model <- combined_wide %>%
pivot_longer(cols = c(q0, q1, q2), names_to = "q_level", values_to = "diversity_value") %>%
pivot_wider(names_from = origin, values_from = diversity_value) %>%
mutate(q_level = factor(q_level, levels = c("q0", "q1", "q2")))
mixed_model <- lmer(Native ~ Exotic * q_level + (1 | plot_id), data = regression_data_model)
cat(" Type III ANOVA Results \n")
Anova(mixed_model, type = "III")
cat("\n Estimated Slopes (Beta Coefficients) per q-level \n")
slopes_output <- emtrends(mixed_model, ~ q_level, var = "Exotic")
print(slopes_output)
cat("\n Pairwise Comparisons of Slopes (Beta Strength) \n")
pairs(slopes_output)
```
| Contrast | Estimate ($\beta$ Difference) | Standard Error (SE) | Degrees of Freedom (df) | t-ratio | p-value | Signif. |
| :--- | :---: | :---: | :---: | :---: | :---: | :---: |
| **q0 - q1** | 0.0842 | 0.0899 | 202 | 0.936 | 0.6179 | ns |
| **q0 - q2** | 0.2350 | 0.0928 | 201 | 2.533 | 0.0322 | * |
| **q1 - q2** | 0.1509 | 0.0928 | 196 | 1.627 | 0.2368 | ns |
> **Conclusion:** Across rare and common species metrics ($q_0$ and $q_1$), exotic richness follows native baseline capacities without clear signifiers of exclusion. However, the strength of this relationship changes significantly when comparing species richness ($q_0$) against dominant profiles ($q_2$), where exotic dominance tracks distinctly higher.
## 6. Multivariate Cross-Boundary Species Correlations
### Community Wide Matrix Preparation
We build a flat wide table tracking species across all unique plots for ecological validation.
```{r wide-matrix}
matrix_data <- plot_species %>%
mutate(origin_status = ifelse(type == "Native", "Native", "Exotic")) %>%
dplyr::select(species_name, origin_status, plot_id, cover_pct)
master_wide_matrix <- matrix_data %>%
pivot_wider(names_from = plot_id, values_from = cover_pct, values_fill = 0) %>%
arrange(origin_status, species_name)
print(master_wide_matrix[1:5, 1:5])
```
### Multiple Cross-Boundary Significance Mapping
We test every individual Native-to-Exotic species coverage intersection using Spearman Rank calculations due to zero-inflated tracking dimensions.
```{r multi-correlations, fig.width = 12, fig.height = 10}
community_wide <- plot_species %>%
mutate(origin_status = ifelse(type == "Native", "Native", "Exotic")) %>%
dplyr::select(plot_id, species_name, cover_pct) %>%
pivot_wider(names_from = species_name, values_from = cover_pct, values_fill = 0) %>%
as.data.frame()
rownames(community_wide) <- community_wide$plot_id
community_wide$plot_id <- NULL
native_spp <- intersect(unique(plot_species$species_name[plot_species$type == "Native"]), colnames(community_wide))
exotic_spp <- intersect(unique(plot_species$species_name[plot_species$type != "Native"]), colnames(community_wide))
test_res <- cor.mtest(community_wide, method = "spearman", conf.level = 0.95)
full_r <- cor(community_wide, method = "spearman")
full_p <- test_res$p
rownames(full_p) <- colnames(community_wide)
colnames(full_p) <- colnames(community_wide)
sub_r <- full_r[native_spp, exotic_spp, drop = FALSE]
sub_p <- full_p[native_spp, exotic_spp, drop = FALSE]
valid_rows <- apply(sub_r, 1, function(x) !all(is.na(x)))
valid_cols <- apply(sub_r, 2, function(x) !all(is.na(x)))
sub_r <- sub_r[valid_rows, valid_cols, drop = FALSE]
sub_p <- sub_p[valid_rows, valid_cols, drop = FALSE]
corrplot(
sub_r,
method = "color",
p.mat = sub_p,
sig.level = 0.05,
insig = "label_sig",
pch.col = "black",
pch.cex = 0.8,
tl.col = "black",
tl.srt = 45,
tl.cex = 0.7,
mar = c(0, 0, 2, 0),
title = "Significant Interactions (Alpha = 0.05)"
)
```
### Quantifying Significant Relationships
We summarize the total volume of positive, negative, and neutral species-to-species interactions.
```{r summary-table-gen}
summary_table <- data.frame(
r_value = as.vector(sub_r),
p_value = as.vector(sub_p)
) %>%
filter(!is.na(r_value) & !is.na(p_value)) %>%
mutate(
interaction_type = case_when(
p_value <= 0.05 & r_value < 0 ~ "Significant Negative (Competitive)",
p_value <= 0.05 & r_value > 0 ~ "Significant Positive (Facilitative)",
TRUE ~ "Non-Significant"
)
)
interaction_summary <- summary_table %>%
group_by(interaction_type) %>%
summarise(Count = n(), .groups = "drop") %>%
mutate(Percentage = round((Count / sum(Count)) * 100, 2))
kable(interaction_summary, col.names = c("Interaction Category", "Count", "Percentage (%)"),
caption = "Summary Metrics of Evaluated Inter-Species Relationships")
```
## 7. Individual Exotic Species Interaction Profiles
We evaluate which particular exotic entities hold the widest tracking footprint of significant correlations across native plants.
```{r profiling-jitter, fig.width = 11, fig.height = 8}
interaction_df <- as.data.frame(as.table(sub_r)) %>% rename(Native_Species = Var1, Exotic_Species = Var2, r_value = Freq)
p_values_df <- as.data.frame(as.table(sub_p)) %>% rename(Native_Species = Var1, Exotic_Species = Var2, p_value = Freq)
sig_interactions <- interaction_df %>%
left_join(p_values_df, by = c("Native_Species", "Exotic_Species")) %>%
filter(p_value <= 0.05) %>%
mutate(correlation_direction = ifelse(r_value < 0, "Negative (Competitive)", "Positive (Facilitative)"))
exotic_order <- sig_interactions %>%
group_by(Exotic_Species) %>%
summarise(total_sig = n(), .groups = "drop")
sig_interactions <- sig_interactions %>%
left_join(exotic_order, by = "Exotic_Species") %>%
mutate(Exotic_Species = reorder(Exotic_Species, total_sig))
ggplot(sig_interactions, aes(x = r_value, y = Exotic_Species, color = correlation_direction)) +
geom_vline(xintercept = 0, linetype = "dashed", color = "gray50", size = 0.8) +
geom_jitter(width = 0, height = 0.2, size = 3, alpha = 0.75) +
scale_color_manual(values = c("Negative (Competitive)" = "#d32f2f", "Positive (Facilitative)" = "#1976d2")) +
xlim(-0.5, 0.5) +
labs(
x = "Spearman Correlation Coefficient (r)",
y = "Exotic Species",
color = "Correlation Direction"
) +
theme_minimal(base_size = 12) +
theme(
panel.grid.minor = element_blank(),
axis.text.y = element_text(face = "italic"),
plot.title = element_text(face = "bold", size = 13),
legend.position = "bottom"
)
```
> **Ecological Insight:** The positive interaction frequencies distinctly outweigh competitive exclusions. This indicates that during early natural regeneration, shared environmental niches and facilitation play a larger role in shaping community structure than direct competition.
## 8. Multiplicative Regional Beta Diversity Partitioning
To determine if exotic species are driving community **homogenization** (reducing variation among plots) or **differentiation** (increasing variation among plots), we partition diversity metrics into Alpha, Beta, and Gamma components.
### Partitioning Matrix Executions
```{r beta-partition}
native_matrix <- plot_species %>%
filter(type == "Native") %>%
dplyr::select(plot_id, species_name, cover_pct) %>%
pivot_wider(names_from = species_name, values_from = cover_pct, values_fill = 0) %>%
as.data.frame()
rownames(native_matrix) <- native_matrix$plot_id
native_matrix$plot_id <- NULL
combined_matrix <- plot_species %>%
dplyr::select(plot_id, species_name, cover_pct) %>%
pivot_wider(names_from = species_name, values_from = cover_pct, values_fill = 0) %>%
as.data.frame()
rownames(combined_matrix) <- combined_matrix$plot_id
combined_matrix$plot_id <- NULL
get_hill_partition <- function(matrix_input) {
q0_part <- hill_taxa_parti(matrix_input, q = 0)
q1_part <- hill_taxa_parti(matrix_input, q = 1)
q2_part <- hill_taxa_parti(matrix_input, q = 2)
data.frame(
Order = c("q = 0", "q = 1", "q = 2"),
Alpha = c(q0_part$TD_alpha, q1_part$TD_alpha, q2_part$TD_alpha),
Gamma = c(q0_part$TD_gamma, q1_part$TD_gamma, q2_part$TD_gamma),
Beta = c(q0_part$TD_beta, q1_part$TD_beta, q2_part$TD_beta)
)
}
native_partition <- get_hill_partition(native_matrix) %>% mutate(Community = "Native Only")
combined_partition <- get_hill_partition(combined_matrix) %>% mutate(Community = "Combined (Nat + Exo)")
comparison_table <- bind_rows(native_partition, combined_partition) %>%
dplyr::select(Community, Order, Alpha, Gamma, Beta) %>%
arrange(Order, Community)
comparison_table %>%
rename(
`Community Profile` = Community,
`Diversity Order` = Order,
`Mean Alpha (α)` = Alpha,
`Regional Gamma (γ)` = Gamma,
`Beta Diversity (β)` = Beta
) %>%
kable(format = "markdown", digits = 3, caption = "Multiplicative Hill Diversity Partitioning Summary")
```
Here, it is clear that exotic adds to alpha and gamma-diversity at all levels (0,1 and 2) and reduces beta-diversity at all levels, too... more prominently to rare species (q=0)
## Beta-diversity
or presence/absence data, betapart uses the Baselga decomposition:
βsor=βsim+βsne
where:
βsim = turnover component
βsne = nestedness-resultant component
βsor = total Sørensen beta diversity
```{r}
## Presence-absence matrices
native_pa <- native_matrix
native_pa[native_pa > 0] <- 1
combined_pa <- combined_matrix
combined_pa[combined_pa > 0] <- 1
## Beta partitioning
native_beta <- beta.multi(native_pa, index.family = "sorensen")
combined_beta <- beta.multi(combined_pa, index.family = "sorensen")
## Summary table
beta_partition_summary <- data.frame(
Community = c("Native only",
"Native + Exotic"),
Total_Beta = c(native_beta$beta.SOR,
combined_beta$beta.SOR),
Turnover = c(native_beta$beta.SIM,
combined_beta$beta.SIM),
Nestedness = c(native_beta$beta.SNE,
combined_beta$beta.SNE)
)
knitr::kable(
beta_partition_summary,
digits = 3,
caption = "Beta diversity partitioning into turnover and nestedness components"
)
```
Species turnover is high, even accounting for exotic species
## NMDS
```{r}
#| message: false
#| warning: false
library(dplyr)
library(tidyr)
library(vegan)
library(ggplot2)
library(viridis)
# ------------------------------------------------------------
# COMMUNITY MATRIX (COVER ABUNDANCE), ALL SPECIES
# combined_matrix was already built in Section 8 (plot x species cover_pct, native + exotic)
# ------------------------------------------------------------
# ------------------------------------------------------------
# NMDS (BRAY-CURTIS, ABUNDANCE-WEIGHTED)
# ------------------------------------------------------------
set.seed(123)
nmds_combined <- metaMDS(
combined_matrix,
distance = "bray",
k = 2,
trymax = 200
)
cat("\nNMDS Stress =", round(nmds_combined$stress, 3), "\n")
# ------------------------------------------------------------
# EXOTIC CONTRIBUTION TO TOTAL COVER, PER PLOT
# ------------------------------------------------------------
plot_contribution <- plot_species %>%
mutate(origin_status = ifelse(type == "Native", "Native", "Exotic")) %>%
group_by(plot_id) %>%
summarise(
# NOTE: sum(), not mean() — this is RELATIVE COVER (share of cumulative
# plot cover attributable to exotics), the standard approach for cover-class
# vegetation data where summed cover across species can exceed 100% due to
# overlapping layers/patchy co-occurring species. mean() would not be
# bounded 0-100% and would shift with species count, not actual dominance.
total_cover = sum(cover_pct, na.rm = TRUE),
exotic_cover = sum(cover_pct[origin_status == "Exotic"], na.rm = TRUE),
.groups = "drop"
) %>%
mutate(exotic_contrib_pct = 100 * exotic_cover / total_cover)
# ------------------------------------------------------------
# MERGE WITH NMDS SITE SCORES
# ------------------------------------------------------------
nmds_scores_combined <- as.data.frame(scores(nmds_combined, display = "sites"))
nmds_scores_combined$plot_id <- rownames(nmds_scores_combined)
nmds_scores_combined <- nmds_scores_combined %>%
left_join(plot_contribution, by = "plot_id")
# ------------------------------------------------------------
# ENVFIT VECTOR OVERLAY (OPTIONAL BUT KEEPS CONSISTENCY WITH SECTION 8)
# ------------------------------------------------------------
fit_contrib <- envfit(
nmds_combined,
plot_contribution["exotic_contrib_pct"],
permutations = 999
)
print(fit_contrib)
vec_contrib <- as.data.frame(scores(fit_contrib, display = "vectors"))
vec_contrib$label <- paste0(
"Exotic contribution\nR\u00b2=", round(fit_contrib$vectors$r, 2),
"\np=", signif(fit_contrib$vectors$pvals, 2)
)
# ------------------------------------------------------------
# PLOT: SIZE + COLOR BOTH ENCODE EXOTIC COVER CONTRIBUTION
# ------------------------------------------------------------
ggplot(nmds_scores_combined,
aes(NMDS1, NMDS2,
size = exotic_contrib_pct,
color = exotic_contrib_pct)) +
geom_point(alpha = 0.85) +
geom_segment(
data = vec_contrib,
aes(x = 0, y = 0, xend = NMDS1, yend = NMDS2),
inherit.aes = FALSE,
arrow = arrow(length = unit(0.25, "cm")),
linewidth = 1.1,
colour = "black"
) +
geom_text(
data = vec_contrib,
aes(x = NMDS1, y = NMDS2, label = label),
inherit.aes = FALSE,
hjust = -0.1, vjust = -0.2, size = 4
) +
scale_color_viridis_c(option = "plasma", name = "Exotic cover\ncontribution (%)") +
scale_size_continuous(range = c(2, 9), name = "Exotic cover\ncontribution (%)") +
labs(
title = "NMDS of Plant Community Composition (All Species)",
subtitle = paste0(
"Bray-Curtis on cover abundance | Stress = ",
round(nmds_combined$stress, 3)
),
x = "NMDS1",
y = "NMDS2"
) +
theme_minimal(base_size = 13) +
theme(
panel.grid.minor = element_blank(),
plot.title = element_text(face = "bold"),
legend.position = "right"
)
```
> **Interpretation**: NMDS stress is too high, so it should be interpreted cautiously but it suggests that exotic species drives plant community to somewhere the upper-left corner of the graph.
##