Prof. Marc Los Huertos • Pomona College
envfit().
adonis2()).
What this module covers. A typical vegetation survey records 30 species at 20 sites. You cannot run 30 separate ANOVAs — you would inflate the Type I error rate, lose the correlations among species, and miss the whole point, which is to describe community structure as an integrated whole. Ordination compresses multivariate species or environmental data into a low-dimensional space where patterns of similarity and difference become visible. This module covers the three methods you will encounter most often in ecological literature and EA030 field projects.
Ordination places sites (rows) in a reduced-dimension space such that sites with similar species composition end up close together, and dissimilar sites end up far apart. Species (columns) can be projected into the same space as arrows or points to show which species drive the separation.
The result is always a biplot — a single figure showing both sites and species (or environmental vectors) simultaneously.
| Method | Full name | Best input | Distance metric | Core question |
|---|---|---|---|---|
| PCA | Principal Components Analysis | Continuous variables (env. data) | Euclidean (linear) | Which env. variables co-vary? |
| NMDS | Non-metric Multidimensional Scaling | Community abundance/presence-absence | Any (Bray-Curtis recommended) | How do communities differ? |
| RDA | Redundancy Analysis | Species matrix + environmental predictors | Euclidean on fitted values | How much community variation do env. predictors explain? |
For species data, Euclidean distance is inappropriate. Two sites that both lack a rare species are scored as “similar” on that species — but shared absence carries no ecological information. Bray-Curtis dissimilarity ignores double-zeros and is the standard for community ecology:
\[BC_{ij} = 1 - \frac{2 \sum_k \min(y_{ik},\, y_{jk})}{\sum_k y_{ik} + \sum_k y_{jk}}\]
where \(y_{ik}\) is the abundance of species \(k\) at site \(i\).
⚠️ Do not run PCA on species abundance data PCA uses Euclidean distance, which is dominated by the many shared-absence zeros in sparse community matrices. Use NMDS or RDA for species data. PCA is appropriate for continuous, roughly normally distributed environmental variables with few zeros.
We use two classic datasets built into the vegan
package:
dune — cover-abundance of 30 plant
species at 20 dune meadow sites (the Netherlands). Values 0–9 on a
modified Braun-Blanquet scale.dune.env — five site-level
environmental variables: A1 (thickness of the A1 soil
horizon, cm), Moisture (1–5 ordinal scale),
Management (4 management types), Use (three
land-use types), Manure (0–4 ordinal).varespec — percentage cover of 44
lichen/bryophyte species at 24 boreal forest sites in Finland.varechem — 14 soil chemistry variables
at the same 24 sites.library(vegan)
library(ggplot2)
library(dplyr)
library(knitr)
data(dune); data(dune.env)
data(varespec); data(varechem)
cat("dune: ", nrow(dune), "sites x", ncol(dune), "species\n")
## dune: 20 sites x 30 species
cat("dune.env: ", nrow(dune.env),"sites x", ncol(dune.env),"variables\n")
## dune.env: 20 sites x 5 variables
cat("varespec: ", nrow(varespec),"sites x", ncol(varespec),"species\n")
## varespec: 24 sites x 44 species
cat("varechem: ", nrow(varechem),"sites x", ncol(varechem),"variables\n")
## varechem: 24 sites x 14 variables
| A1 | Moisture | Management | Use | Manure |
|---|---|---|---|---|
| 2.8 | 1 | SF | Haypastu | 4 |
| 3.5 | 1 | BF | Haypastu | 2 |
| 4.3 | 2 | SF | Haypastu | 4 |
| 4.2 | 2 | SF | Haypastu | 4 |
| 6.3 | 1 | HF | Hayfield | 2 |
| 4.3 | 1 | HF | Haypastu | 2 |
PCA is appropriate for the continuous environmental
variables in varechem — soil nitrogen, phosphorus,
pH, etc. All are numeric, and the question is: which variables co-vary,
and can we reduce 14 dimensions to a handful of interpretable axes?
In vegan, rda() with no right-hand-side formula runs an
unconstrained PCA. We scale the variables (z-scores) because they are on
very different units (mg/kg vs. percent vs. pH units).
# Scale to unit variance before PCA (important when units differ)
pca <- rda(varechem, scale = TRUE)
summary(pca, display = NULL) # eigenvalues only — suppress long output
##
## Call:
## rda(X = varechem, scale = TRUE)
##
## Partitioning of correlations:
## Inertia Proportion
## Total 14 1
## Unconstrained 14 1
##
## Eigenvalues, and their contribution to the correlations
##
## Importance of components:
## PC1 PC2 PC3 PC4 PC5 PC6 PC7
## Eigenvalue 5.1916 3.1928 1.6855 1.06899 0.81599 0.70581 0.43641
## Proportion Explained 0.3708 0.2281 0.1204 0.07636 0.05829 0.05042 0.03117
## Cumulative Proportion 0.3708 0.5989 0.7193 0.79564 0.85392 0.90434 0.93551
## PC8 PC9 PC10 PC11 PC12 PC13 PC14
## Eigenvalue 0.36884 0.1707 0.14953 0.08526 0.06986 0.035057 0.023615
## Proportion Explained 0.02635 0.0122 0.01068 0.00609 0.00499 0.002504 0.001687
## Cumulative Proportion 0.96185 0.9740 0.98473 0.99082 0.99581 0.998313 1.000000
# Variance explained by each axis
eig <- eigenvals(pca)
pct_var <- round(100 * eig / sum(eig), 1)
cum_var <- cumsum(pct_var)
cat("PC1:", pct_var[1], "%\n")
## PC1: 37.1 %
cat("PC2:", pct_var[2], "%\n")
## PC2: 22.8 %
cat("PC3:", pct_var[3], "%\n")
## PC3: 12 %
cat("Cumulative (PC1-PC3):", cum_var[3], "%\n")
## Cumulative (PC1-PC3): 71.9 %
barplot(pct_var[1:8],
names.arg = paste0("PC", 1:8),
col = "#4a7c5e", border = "white",
ylab = "Variance explained (%)",
main = "Scree Plot: Soil Chemistry PCA",
las = 1)
abline(h = 100 / ncol(varechem), lty = 2,
col = "#c8832a", lwd = 1.5)
legend("topright", legend = "Equal-contribution threshold",
lty = 2, col = "#c8832a", bty = "n", cex = 0.85)
Scree plot for the soil chemistry PCA. The elbow suggests retaining two or three principal components, which together explain the majority of variance in the 14-variable dataset.
A biplot shows sites as points and variables as arrows. Arrows pointing in the same direction indicate positively correlated variables; opposite directions indicate negative correlation. Arrow length reflects how strongly each variable loads on the displayed axes.
# Site scores and species (variable) loadings
site_sc <- scores(pca, display = "sites", choices = 1:2)
var_sc <- scores(pca, display = "species", choices = 1:2)
# Scale arrows for readability
arrow_scale <- 0.8 * max(abs(site_sc)) / max(abs(var_sc))
var_sc_sc <- var_sc * arrow_scale
# Plot
par(mar = c(4, 4, 3, 1), las = 1)
plot(site_sc,
xlab = paste0("PC1 (", pct_var[1], "%)"),
ylab = paste0("PC2 (", pct_var[2], "%)"),
pch = 21, bg = "#4a7c5e", col = "#1e3a2f", cex = 1.3,
main = "PCA Biplot: Boreal Forest Soil Chemistry")
abline(h = 0, v = 0, col = "#ede8df", lty = 1)
arrows(0, 0, var_sc_sc[, 1], var_sc_sc[, 2],
length = 0.1, col = "#c8832a", lwd = 1.5)
text(var_sc_sc * 1.12, labels = rownames(var_sc_sc),
cex = 0.72, col = "#7a5200", font = 2)
text(site_sc, labels = rownames(site_sc), cex = 0.6,
pos = 3, col = "#1e3a2f")
PCA biplot for boreal forest soil chemistry (varespec sites). Sites are points; environmental variables are arrows. Sites in the direction of an arrow have higher values for that variable.
📌 Reading a biplot Sites in the upper-right quadrant have high values for variables whose arrows point upper-right. The angle between two arrows approximates the correlation between those variables (small angle = high positive correlation; right angle = uncorrelated; obtuse angle = negative correlation). Project a site point perpendicularly onto an arrow to read off its approximate value on that variable.
Check which variables load most strongly on each axis:
loadings <- scores(pca, display = "species", choices = 1:2)
# Top contributors to PC1
cat("Strongest PC1 loadings:\n")
## Strongest PC1 loadings:
print(sort(abs(loadings[, 1]), decreasing = TRUE)[1:5])
## K Ca P Mg Zn
## 0.9368729 0.9280154 0.9180524 0.9087391 0.8926155
cat("\nStrongest PC2 loadings:\n")
##
## Strongest PC2 loadings:
print(sort(abs(loadings[, 2]), decreasing = TRUE)[1:5])
## Al Fe Humdepth pH Baresoil
## 0.9719998 0.7748245 0.6824708 0.6517311 0.6386581
NMDS does not try to explain variance — it finds a low-dimensional configuration of sites that best preserves the rank order of dissimilarities. Two sites that are highly dissimilar in species composition will end up far apart; similar sites end up close together. Because it works on ranks (not raw distances), it tolerates non-normality, zeros, and skewed abundance distributions.
The stress value measures how faithfully the 2D plot represents the true dissimilarity structure. Lower is better:
| Stress value | Ordination quality |
|---|---|
| < 0.05 | Excellent |
| 0.05 - 0.10 | Good |
| 0.10 - 0.20 | Acceptable — minor distortion |
| > 0.20 | Poor — interpret with caution |
set.seed(42) # NMDS uses random starts — set seed for reproducibility
nmds <- metaMDS(dune,
distance = "bray", # Bray-Curtis dissimilarity
k = 2, # 2 dimensions
trymax = 100, # max random starts
trace = FALSE)
cat("Stress:", round(nmds$stress, 4), "\n")
## Stress: 0.1183
cat("Convergent solution:", nmds$converged, "\n")
## Convergent solution: 4
The Shepard diagram plots observed dissimilarities against ordination distances. A good ordination shows a monotone relationship with little scatter.
stressplot(nmds,
main = paste0("Shepard Diagram (stress = ",
round(nmds$stress, 3), ")"))
Shepard diagram for the dune meadow NMDS. Points represent site pairs; the stepped line is the non-metric fit. Scatter around the fit line contributes to stress.
# Site and species scores
site_nm <- scores(nmds, display = "sites")
spec_nm <- scores(nmds, display = "species")
# Colour by Management type
mgmt_col <- c("BF" = "#1e3a2f", "HF" = "#4a7c5e",
"NM" = "#c8832a", "SF" = "#8aad96")
pt_cols <- mgmt_col[as.character(dune.env$Management)]
par(mar = c(4, 4, 3, 8), xpd = FALSE)
plot(site_nm,
xlab = "NMDS1", ylab = "NMDS2",
pch = 21, bg = pt_cols, col = "#1e3a2f", cex = 1.6,
main = paste0("Dune Meadow NMDS (stress = ",
round(nmds$stress, 3), ")"))
abline(h = 0, v = 0, col = "#ede8df")
# Add species names (less frequent ones suppressed for readability)
spec_df <- as.data.frame(spec_nm)
spec_df$label <- rownames(spec_df)
top_sp <- spec_df[order(rowSums(spec_df[, 1:2]^2), decreasing = TRUE)[1:15], ]
text(top_sp$NMDS1, top_sp$NMDS2, labels = top_sp$label,
cex = 0.6, col = "#7a5200", font = 3)
# Legend
par(xpd = TRUE)
legend(par("usr")[2] + 0.05, par("usr")[4],
legend = names(mgmt_col),
pch = 21,
pt.bg = mgmt_col, col = "#1e3a2f",
title = "Management", bty = "n", cex = 0.85, pt.cex = 1.4)
NMDS ordination of dune meadow sites coloured by Management type. Sites with similar plant communities cluster together. Management type appears to separate along the ordination axes.
par(xpd = FALSE)
envfit() projects environmental variables onto the NMDS
as arrows, fitting them by correlation with the ordination axes. The
\(r^2\) value and permutation p-value
indicate how strongly each variable correlates with community
composition.
set.seed(42)
ef <- envfit(nmds, dune.env[, c("A1", "Moisture", "Manure")],
permutations = 999, na.rm = TRUE)
print(ef)
##
## ***VECTORS
##
## NMDS1 NMDS2 r2 Pr(>r)
## A1 0.96474 0.26321 0.3649 0.023 *
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## Permutation: free
## Number of permutations: 999
##
## ***FACTORS:
##
## Centroids:
## NMDS1 NMDS2
## Moisture1 -0.5101 -0.0403
## Moisture2 -0.3938 0.0139
## Moisture4 0.2765 -0.4033
## Moisture5 0.6561 0.1476
## Manure0 0.2958 0.5790
## Manure1 -0.2482 -0.0215
## Manure2 -0.3079 -0.1866
## Manure3 0.3101 -0.2470
## Manure4 -0.3463 -0.5583
##
## Goodness of fit:
## r2 Pr(>r)
## Moisture 0.5014 0.002 **
## Manure 0.4247 0.022 *
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## Permutation: free
## Number of permutations: 999
par(mar = c(4, 4, 3, 8), xpd = FALSE)
plot(site_nm,
xlab = "NMDS1", ylab = "NMDS2",
pch = 21, bg = pt_cols, col = "#1e3a2f", cex = 1.6,
main = "NMDS with Environmental Vectors")
abline(h = 0, v = 0, col = "#ede8df")
plot(ef, col = "#c8832a", lwd = 2, add = TRUE)
par(xpd = TRUE)
legend(par("usr")[2] + 0.05, par("usr")[4],
legend = names(mgmt_col),
pch = 21,
pt.bg = mgmt_col, col = "#1e3a2f",
title = "Management", bty = "n", cex = 0.85, pt.cex = 1.4)
NMDS ordination of dune meadow sites with significant environmental vectors overlaid. Moisture and A1 horizon depth emerge as strong gradients.
par(xpd = FALSE)
NMDS shows visual separation by Management — but is it statistically
significant? PERMANOVA (Permutational MANOVA, adonis2())
tests whether group centroids in multivariate space differ more than
expected by chance, using permutation rather than distributional
assumptions.
set.seed(42)
perm_result <- adonis2(
dune ~ Management,
data = dune.env,
method = "bray",
permutations = 999
)
print(perm_result)
## Permutation test for adonis under reduced model
## Permutation: free
## Number of permutations: 999
##
## adonis2(formula = dune ~ Management, data = dune.env, permutations = 999, method = "bray")
## Df SumOfSqs R2 F Pr(>F)
## Model 3 1.4686 0.34161 2.7672 0.003 **
## Residual 16 2.8304 0.65839
## Total 19 4.2990 1.00000
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## Management explains 34.2% of community variation (R2 = 0.342, p = 0.003).
📌 PERMANOVA vs ANOSIM Both test group differences
in community composition. PERMANOVA (adonis2()) is
preferred because it returns an R² (proportion of variance explained),
handles unbalanced designs, and is less sensitive to within-group
dispersion differences. ANOSIM (anosim()) only tests rank
differences in among- vs within-group dissimilarities.
PERMANOVA assumes similar within-group dispersion across groups
(analogous to homogeneity of variance in ANOVA).
betadisper() tests this.
dist_dune <- vegdist(dune, method = "bray")
bd <- betadisper(dist_dune, dune.env$Management)
anova(bd) # p > 0.05 = groups have similar dispersion (assumption met)
## Analysis of Variance Table
##
## Response: Distances
## Df Sum Sq Mean Sq F value Pr(>F)
## Groups 3 0.13831 0.046104 1.9506 0.1622
## Residuals 16 0.37816 0.023635
RDA is a constrained ordination: the axes are linear combinations of environmental predictors, not free to explain any community variation. This directly answers: how much of the community variation is explained by the measured environmental variables?
The variance decomposition is: - Constrained inertia — variation explained by the environmental model - Unconstrained inertia — residual variation not explained by predictors
# Hellinger transformation reduces the influence of rare species
# and makes Euclidean (RDA) distances ecologically more meaningful
dune_hel <- decostand(dune, method = "hellinger")
# RDA: species ~ environmental predictors
rda_fit <- rda(dune_hel ~ A1 + Moisture + Manure,
data = dune.env)
summary(rda_fit, display = NULL)
##
## Call:
## rda(formula = dune_hel ~ A1 + Moisture + Manure, data = dune.env)
##
## Partitioning of variance:
## Inertia Proportion
## Total 0.5559 1.000
## Constrained 0.3513 0.632
## Unconstrained 0.2046 0.368
##
## Eigenvalues, and their contribution to the variance
##
## Importance of components:
## RDA1 RDA2 RDA3 RDA4 RDA5 RDA6 RDA7
## Eigenvalue 0.1476 0.09674 0.03638 0.02452 0.01935 0.01222 0.008914
## Proportion Explained 0.2655 0.17402 0.06544 0.04410 0.03481 0.02198 0.016035
## Cumulative Proportion 0.2655 0.43951 0.50494 0.54904 0.58385 0.60583 0.621866
## RDA8 PC1 PC2 PC3 PC4 PC5 PC6
## Eigenvalue 0.005648 0.05115 0.03971 0.03461 0.0189 0.01792 0.01219
## Proportion Explained 0.010161 0.09202 0.07143 0.06225 0.0340 0.03224 0.02193
## Cumulative Proportion 0.632027 0.72405 0.79548 0.85773 0.8917 0.92397 0.94591
## PC7 PC8 PC9 PC10 PC11
## Eigenvalue 0.01067 0.008518 0.006583 0.003232 0.001066
## Proportion Explained 0.01920 0.015323 0.011842 0.005814 0.001917
## Cumulative Proportion 0.96510 0.980427 0.992269 0.998083 1.000000
##
## Accumulated constrained eigenvalues
## Importance of components:
## RDA1 RDA2 RDA3 RDA4 RDA5 RDA6 RDA7
## Eigenvalue 0.1476 0.09674 0.03638 0.02452 0.01935 0.01222 0.008914
## Proportion Explained 0.4201 0.27533 0.10354 0.06978 0.05508 0.03477 0.025371
## Cumulative Proportion 0.4201 0.69539 0.79893 0.86870 0.92378 0.95855 0.983924
## RDA8
## Eigenvalue 0.005648
## Proportion Explained 0.016076
## Cumulative Proportion 1.000000
# Proportion of variance constrained by the environmental model
inertia <- summary(rda_fit, display = NULL)$cont$importance
cat("Total inertia:", round(rda_fit$tot.chi, 4), "\n")
## Total inertia: 0.5559
cat("Constrained: ",
round(rda_fit$CCA$tot.chi, 4),
sprintf("(%.1f%%)\n", 100 * rda_fit$CCA$tot.chi / rda_fit$tot.chi))
## Constrained: 0.3513 (63.2%)
cat("Unconstrained:",
round(rda_fit$CA$tot.chi, 4),
sprintf("(%.1f%%)\n", 100 * rda_fit$CA$tot.chi / rda_fit$tot.chi))
## Unconstrained: 0.2046 (36.8%)
# Global permutation test for the full model
set.seed(42)
anova(rda_fit, permutations = 999)
## Permutation test for rda under reduced model
## Permutation: free
## Number of permutations: 999
##
## Model: rda(formula = dune_hel ~ A1 + Moisture + Manure, data = dune.env)
## Df Variance F Pr(>F)
## Model 8 0.35135 2.3617 0.001 ***
## Residual 11 0.20456
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
# Test each RDA axis individually
set.seed(42)
anova(rda_fit, by = "axis", permutations = 999)
## Permutation test for rda under reduced model
## Forward tests for axes
## Permutation: free
## Number of permutations: 999
##
## Model: rda(formula = dune_hel ~ A1 + Moisture + Manure, data = dune.env)
## Df Variance F Pr(>F)
## RDA1 1 0.147585 7.9363 0.001 ***
## RDA2 1 0.096737 5.6750 0.001 ***
## RDA3 1 0.036377 2.3118 0.305
## RDA4 1 0.024516 1.6779 0.709
## RDA5 1 0.019351 1.4190 0.793
## RDA6 1 0.012217 0.9556 0.958
## RDA7 1 0.008914 0.7408 0.958
## RDA8 1 0.005648 0.4970 0.958
## Residual 11 0.204557
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
rda_sites <- scores(rda_fit, display = "sites", choices = 1:2)
rda_spec <- scores(rda_fit, display = "species", choices = 1:2)
rda_env <- scores(rda_fit, display = "bp", choices = 1:2)
# Axis labels: proportion of total inertia each RDA axis explains
ax_pct <- round(100 * eigenvals(rda_fit)[1:2] / rda_fit$tot.chi, 1)
# Scale species and env arrows
sp_scale <- 0.6 * max(abs(rda_sites)) / max(abs(rda_spec))
env_scale <- 0.8 * max(abs(rda_sites)) / max(abs(rda_env))
par(mar = c(4, 4, 3, 8), xpd = FALSE)
plot(rda_sites,
xlab = paste0("RDA1 (", ax_pct[1], "% of total)"),
ylab = paste0("RDA2 (", ax_pct[2], "% of total)"),
pch = 21, bg = pt_cols, col = "#1e3a2f", cex = 1.5,
main = "RDA: Dune Vegetation ~ A1 + Moisture + Manure")
abline(h = 0, v = 0, col = "#ede8df")
# Species scores
text(rda_spec * sp_scale,
labels = rownames(rda_spec),
cex = 0.55, col = "#2d5a3d", font = 3)
# Environmental arrows
arrows(0, 0, rda_env[, 1] * env_scale, rda_env[, 2] * env_scale,
length = 0.12, col = "#c8832a", lwd = 2)
text(rda_env * env_scale * 1.15, labels = rownames(rda_env),
cex = 0.8, col = "#7a5200", font = 2)
par(xpd = TRUE)
legend(par("usr")[2] + 0.05, par("usr")[4],
legend = names(mgmt_col), pch = 21,
pt.bg = mgmt_col, col = "#1e3a2f",
title = "Management", bty = "n", cex = 0.85, pt.cex = 1.4)
RDA biplot of dune meadow vegetation constrained by soil horizon depth (A1), moisture, and manure input. Site points are coloured by management type. Environmental arrows show the direction of each predictor’s effect on community composition.
par(xpd = FALSE)
💡 Hellinger transformation for RDA Raw abundance
data has many zeros and is dominated by common species. The Hellinger
transformation (decostand(x, “hellinger”)) divides each
value by the site total, then takes the square root. This gives
appropriate weight to both common and rare species and makes Euclidean
distance (used internally by RDA) ecologically meaningful.
| Scenario | Recommended approach |
|---|---|
| Explore co-variation among continuous env. variables | PCA (scale = TRUE if variables are in different units) |
| Visualise community similarity among sites, no env. data | NMDS with Bray-Curtis |
| Test whether groups differ in community composition | PERMANOVA via adonis2() |
| Relate community composition to env. predictors | RDA (after Hellinger transformation) |
| Data with many zeros and rare species | NMDS or RDA with Hellinger — avoid raw PCA |
| Presence-absence data | NMDS with Jaccard distance; or RDA with decostand(x, ‘pa’) |
rda(x, scale=TRUE) in vegan.
adonis2()) tests group
differences statistically; check dispersion with
betadisper() first.
The varechem dataset has 14 soil chemistry variables at 24
boreal forest sites. (a) Run PCA with rda(varechem, scale =
TRUE). (b) How many axes are needed to explain 70% of variance?
(c) Make a biplot. Which soil variables are most strongly positively
correlated with each other on PC1? Which point in opposite directions
(negatively correlated)?
library(vegan)
data(varechem)
pca_vc <- rda(varechem, scale = TRUE)
# Variance explained
eig <- eigenvals(pca_vc)
cumsum(100 * eig / sum(eig)) # find where cumulative exceeds 70%
# Biplot
biplot(pca_vc, scaling = 2,
main = "varechem PCA biplot",
col = c("#1e3a2f", "#c8832a"))
Typically 3-4 axes are needed for 70%. On PC1, N and Al often load positively together (nutrient-poor acidic soils), while Ca, Mg, and pH load negatively (richer, less acidic soils) — consistent with a soil fertility gradient.
Run NMDS on the varespec lichen data with Bray-Curtis
distance. (a) Report the stress value and assess quality. (b) Fit the
varechem variables onto the ordination with
envfit() (permutations = 999, set.seed(42)). (c) Which soil
variables are most significantly correlated with lichen community
composition? Plot the ordination with significant vectors overlaid.
library(vegan)
data(varespec); data(varechem)
set.seed(42)
nmds_v <- metaMDS(varespec, distance = "bray", k = 2,
trymax = 100, trace = FALSE)
cat("Stress:", nmds_v$stress, "\n")
set.seed(42)
ef_v <- envfit(nmds_v, varechem, permutations = 999)
print(ef_v)
plot(nmds_v, display = "sites",
main = "varespec NMDS with soil chemistry vectors")
plot(ef_v, p.max = 0.05, col = "#c8832a", add = TRUE)
Stress is typically around 0.18 — acceptable. Al, Fe, and humus thickness often emerge as the strongest correlates of lichen community composition, reflecting the importance of soil acidity and organic matter in boreal forest ground-layer communities.
Test whether dune meadow communities differ by Use type
(Hayfield, Haypastu, Pasture) as well as by Management. (a)
Run adonis2(dune ~ Use, data = dune.env, method = “bray”).
(b) Check dispersion with betadisper(). (c) Run a model
with both predictors: dune ~ Management + Use. Which
explains more variance?
library(vegan)
data(dune); data(dune.env)
set.seed(42)
# Use alone
adonis2(dune ~ Use, data = dune.env, method = "bray", permutations = 999)
# Dispersion check
dist_d <- vegdist(dune, method = "bray")
anova(betadisper(dist_d, dune.env$Use))
# Combined model
set.seed(42)
adonis2(dune ~ Management + Use, data = dune.env,
method = "bray", permutations = 999, by = "margin")
Management typically explains more variance than Use. The combined model shows each predictor’s marginal contribution; compare R2 values to rank importance. Check that betadisper p > 0.05 before trusting PERMANOVA.
Run an RDA of varespec (Hellinger-transformed) constrained
by Al, N, and pH from
varechem. (a) What percentage of community variation is
explained by these three soil variables? (b) Test the model significance
with anova(). (c) Make a biplot and describe the ecological
gradient along RDA1.
library(vegan)
data(varespec); data(varechem)
vs_hel <- decostand(varespec, "hellinger")
rda_vs <- rda(vs_hel ~ Al + N + pH, data = varechem)
# Variance explained
cat("Constrained:", round(100 * rda_vs$CCA$tot.chi / rda_vs$tot.chi, 1), "%\n")
# Significance
set.seed(42)
anova(rda_vs, permutations = 999)
# Biplot
plot(rda_vs, scaling = 2,
main = "RDA: Lichen ~ Al + N + pH",
col = c("#1e3a2f", "#c8832a"))
Al and N typically explain 20-35% of lichen community variation. RDA1 often separates acidic, aluminium-rich, nutrient-poor sites (with ericaceous and lichen species) from sites with higher pH and N (supporting more productive mosses and grasses).