The aquats package provides a robust, streamlined
statistical and visualization toolkit tailored specifically for
fisheries science, stock assessment workflows, and aquatic ecology. It
bridges standard parametric models (ANOVA, ANCOVA, MANOVA) with advanced
ecological tools, including non-linear morphometric regressions,
generalized linear models (GLMs) for count data, customizable
correlation heatmaps, and integrated Mantel test network linkages.
Load the package and required dependencies to begin:
Comparing continuous ecological, biometric, or water-quality
parameters between two distinct sample groups (e.g., Reference
vs. Impacted sites, Upstream vs. Downstream) is a foundational
statistical task. While Student’s classical \(t\)-test assumes equal population
variances, ecological field data frequently violate this condition. The
compare_two_groups() function automates variance screening
via Fisher’s \(F\)-test, executes
either Student’s or Welch’s \(t\)-test
with Satterthwaite degrees of freedom, and packages diagnostics, effect
sizes (Cohen’s \(d\)), and graphics
into a single call.
The procedure simultaneously tests: 1. Variance Homogeneity
(Fisher’s \(F\)-test): \[H_0: \sigma_1^2 = \sigma_2^2 \quad \text{vs.}
\quad H_1: \sigma_1^2 \neq \sigma_2^2\] 2. Mean Equality
(\(t\)-test): \[H_0: \mu_1 = \mu_2 \quad \text{vs.} \quad H_1:
\mu_1 \neq \mu_2\] When var_equal = "auto", Welch’s
correction is adopted automatically if the \(F\)-test rejects homoscedasticity (\(p < 0.05\)).
We simulate dissolved oxygen (DO, \(\text{mg}\cdot\text{L}^{-1}\)) monitoring records across two river stations exhibiting unequal variances:
Default publication aesthetic displaying distribution medians, interquartile ranges, raw observations as jittered points, and an annotated significance bracket:
res_box <- compare_two_groups(
data = river_survey,
response_var = "DO_mgL",
group_var = "Station",
var_equal = "auto",
plot_type = "box",
palette = "Dark2",
title = "Style A: Two-Sample Boxplot with Significance Bracket"
)
print(res_box$Plot)Displays underlying data density distributions combined with an internal box-whisker geometry:
res_violin <- compare_two_groups(
data = river_survey,
response_var = "DO_mgL",
group_var = "Station",
var_equal = "auto",
plot_type = "violin",
palette = "Set2",
title = "Style B: Density Violin with Internal Quartiles"
)
print(res_violin$Plot)A clean point-and-interval layout emphasizing group means and 95% confidence intervals against jittered points:
res_jitter <- compare_two_groups(
data = river_survey,
response_var = "DO_mgL",
group_var = "Station",
var_equal = "auto",
plot_type = "jitter",
palette = "Set1",
title = "Style C: Group Means and 95% CI Error Bars"
)
print(res_jitter$Plot)Review the automated assumption checks, formal test statistics, and effect sizes:
# 1. Distribution Normality per group (Shapiro-Wilk)
knitr::kable(res_box$Normality_Test, caption = "Shapiro-Wilk Normality Evaluation")| Group | Statistic_W | p_value | Normal_05 |
|---|---|---|---|
| Downstream | 0.9716 | 0.6866 | Yes |
| Upstream | 0.9499 | 0.2495 | Yes |
# 2. Fisher's F-test for Variance Equality
knitr::kable(res_box$F_Test, caption = "Fisher's F-Test for Homogeneity of Variances")| Ratio_Var | F_Statistic | df_num | df_denom | p_value | Equal_Var_05 |
|---|---|---|---|---|---|
| 2.297 | 2.297 | 24 | 24 | 0.0468 | No |
| Test_Type | Alternative | t_Statistic | df | p_value | CI_Lower | CI_Upper |
|---|---|---|---|---|---|---|
| Welch Two Sample t-test | two.sided | -7.6683 | 41.57 | 0 | -2.5276 | -1.4741 |
# 4. Group Descriptives and Cohen's d
knitr::kable(res_box$Summary_Table, caption = "Group Statistics and Standardized Effect Sizes")| Group | N | Mean | SD | SE | CI_Lower | CI_Upper | Mean_Diff | Cohen_d |
|---|---|---|---|---|---|---|---|---|
| Downstream | 25 | 5.902 | 1.089 | 0.218 | 5.453 | 6.352 | -2.001 | 2.169 |
| Upstream | 25 | 7.903 | 0.719 | 0.144 | 7.607 | 8.200 | 2.001 | 2.169 |
While analysis of variance and \(t\)-tests evaluate continuous measurements
across groups, ecological and aquatic assessments frequently involve
categorical observations (e.g., habitat substrate types, feeding guilds,
species maturity status, or pollution compliance tiers). The
test_chisq() function tests for significant associations
between categorical factors (Test of Independence) or evaluates whether
an observed frequency distribution conforms to theoretical expectations
(Goodness-of-Fit).
For a two-way contingency table (\(r \times c\)): 1. Hypothesis of Independence: \[H_0: \text{Variable } X \text{ and Variable } Y \text{ are independent}\] \[H_1: \text{Variable } X \text{ and Variable } Y \text{ are associated}\] 2. Standardized Residuals: To determine which specific combinations drive a significant test result, standardized Pearson residuals (\(r_{ij}\)) are computed: \[r_{ij} = \frac{O_{ij} - E_{ij}}{\sqrt{E_{ij} (1 - p_{i\cdot}) (1 - p_{\cdot j})}}\] Cells with \(\vert{}r_{ij}\vert{} > 2.0\) indicate deviations significant at \(\alpha = 0.05\). 3. Strength of Association: Quantified using Cramér’s \(V\): \[V = \sqrt{\frac{\chi^2}{N \cdot \min(r - 1, c - 1)}}\]
We simulate fish maturity status across three sampling river reaches:
set.seed(42)
fish_maturity <- data.frame(
Reach = factor(rep(c("Upstream", "Midstream", "Estuary"), times = c(60, 70, 70))),
Maturity = factor(c(
sample(c("Immature", "Maturing", "Spent"), 60, replace = TRUE, prob = c(0.60, 0.30, 0.10)),
sample(c("Immature", "Maturing", "Spent"), 70, replace = TRUE, prob = c(0.35, 0.45, 0.20)),
sample(c("Immature", "Maturing", "Spent"), 70, replace = TRUE, prob = c(0.15, 0.35, 0.50))
))
)Displays the relative conditional percentage breakdown of categories across groups:
res_stacked <- test_chisq(
data = fish_maturity,
x = "Reach",
y = "Maturity",
plot_type = "stacked",
palette = "Set2",
title = "Style A: Maturity Proportions Across River Reaches"
)
print(res_stacked$Plot)Review test statistics, expected cell frequencies, and standardized residuals:
# 1. Statistical Test Summary and Effect Size (Cramér's V)
knitr::kable(res_stacked$Chi_Square_Test, caption = "Chi-Square Test Summary and Association Strength")| Test_Type | Chi_Square | df | p_value | Cramers_V | Phi |
|---|---|---|---|---|---|
| Pearson’s Chi-squared test | 30.9278 | 4 | 0 | 0.2781 | NA |
# 2. Observed Contingency Table
knitr::kable(res_stacked$Observed_Counts, caption = "Observed Cell Frequencies")| Reach | Immature | Maturing | Spent | Total |
|---|---|---|---|---|
| Estuary | 8 | 29 | 33 | 70 |
| Midstream | 30 | 29 | 11 | 70 |
| Upstream | 29 | 18 | 13 | 60 |
| Total | 67 | 76 | 57 | 200 |
# 3. Expected Cell Frequencies
knitr::kable(res_stacked$Expected_Counts, caption = "Expected Frequencies Under Independence")| Reach | Immature | Maturing | Spent |
|---|---|---|---|
| Estuary | 23.45 | 26.6 | 19.95 |
| Midstream | 23.45 | 26.6 | 19.95 |
| Upstream | 20.10 | 22.8 | 17.10 |
# 4. Standardized Pearson Residuals (|r| > 2 denotes significant deviation)
knitr::kable(res_stacked$Standardized_Residuals, caption = "Standardized Pearson Residuals")| Reach | Var1 | Var2 | Freq |
|---|---|---|---|
| 1 | Estuary | Immature | -4.853 |
| 2 | Midstream | Immature | 2.057 |
| 3 | Upstream | Immature | 2.910 |
| 4 | Estuary | Maturing | 0.733 |
| 5 | Midstream | Maturing | 0.733 |
| 6 | Upstream | Maturing | -1.526 |
| 7 | Estuary | Spent | 4.286 |
| 8 | Midstream | Spent | -2.939 |
| 9 | Upstream | Spent | -1.401 |
While the Chi-Square test evaluates associations among discrete frequency classes, comparing continuous metric responses across three or more treatment groups requires partitioning continuous variance, addressed in the next section.
The one_way_anova() function provides an end-to-end
analytical and graphical pipeline for evaluating differences in a
continuous response variable across levels of a single categorical
factor. In aquatic and fisheries research, this is commonly employed to
compare water quality parameters across sampling stations, fish
morphometric indices across river reaches, or nutrient concentrations
across seasons.
The function handles defensive data cleaning, model fitting, computation of global F-tests, extraction of Estimated Marginal Means (EMMs), pairwise post-hoc contrasts with compact letter displays (CLD), and automated generation of publication-ready ggplot2 figures.
A classical fixed-effects One-Way ANOVA partitions the total variance in the response variable into between-group (explained) variance and within-group (unexplained error) variance:
\[y_{ij} = \mu + \alpha_i + \varepsilon_{ij}\]
where: - \(y_{ij}\) represents the \(j\)-th observation from the \(i\)-th treatment or habitat group (\(i = 1, 2, \dots, k\); \(j = 1, 2, \dots, n_i\)), - \(\mu\) represents the grand population mean, - \(\alpha_i = \mu_i - \mu\) is the differential effect associated with group \(i\), subject to the constraint \(\sum n_i \alpha_i = 0\), - \(\varepsilon_{ij} \overset{\text{i.i.d.}}{\sim} \mathcal{N}(0, \sigma^2)\) represents random residual error.
The omnibus null hypothesis states that all group means are identical:
\[H_0: \mu_1 = \mu_2 = \dots = \mu_k\]
\[H_1: \text{At least one group mean differs from the others.}\]
The hypothesis is evaluated using the F-statistic:
\[F = \frac{\text{MS}_{\text{between}}}{\text{MS}_{\text{within}}} = \frac{\text{SS}_{\text{between}} / (k - 1)}{\text{SS}_{\text{within}} / (N - k)}\]
When \(H_0\) is rejected (\(p < 0.05\)), pairwise differences are resolved using Tukey’s Honest Significant Difference (HSD) test:
\[q = \frac{\bar{y}_i - \bar{y}_j}{\sqrt{\frac{\text{MS}_{\text{within}}}{2} \left(\frac{1}{n_i} + \frac{1}{n_j}\right)}}\]
Groups sharing the same letter in the Compact Letter Display (CLD) are not statistically distinguishable at the specified significance threshold (\(\alpha = 0.05\)).
To illustrate the functionality, we simulate a standard riverine
biomonitoring dataset evaluating Dissolved Oxygen (DO, mg/L) across
three river zones: Upper Basin (forested reference),
Mid Reach (agricultural run-off), and
Lower Estuary (downstream industrial/tidal influence).
set.seed(101)
n_obs <- 30
do_survey <- data.frame(
Zone = factor(
rep(c("Upper Basin", "Mid Reach", "Lower Estuary"), each = n_obs),
levels = c("Upper Basin", "Mid Reach", "Lower Estuary")
),
DO_mgL = c(
rnorm(n_obs, mean = 8.50, sd = 0.45),
rnorm(n_obs, mean = 6.80, sd = 0.55),
rnorm(n_obs, mean = 4.90, sd = 0.60)
)
)
head(do_survey, 6)
#> Zone DO_mgL
#> 1 Upper Basin 8.353284
#> 2 Upper Basin 8.748608
#> 3 Upper Basin 8.196275
#> 4 Upper Basin 8.596462
#> 5 Upper Basin 8.639846
#> 6 Upper Basin 9.028285The one_way_anova() function supports multiple layout
types depending on reporting requirements.
This style shows median lines, interquartile ranges, raw sample
distributions via jittered points, and group means displayed as diamonds
(show_mean = TRUE). Tukey post-hoc letters are
automatically placed above each group distribution:
res_box <- one_way_anova(
data = do_survey,
factor_var = "Zone",
numeric_var = "DO_mgL",
plot_type = "boxplot",
add_jitter = TRUE,
show_mean = TRUE,
mean_color = "darkred",
sig_display = "letters",
color_palette = "Blues"
)
res_box$PlotFor aquaculture trials or fisheries management reports,
plot_type = "barplot" aggregates observations to their mean
and attaches directional error bars (error_type = "se" or
error_type = "sd"):
res_bar <- one_way_anova(
data = do_survey,
factor_var = "Zone",
numeric_var = "DO_mgL",
plot_type = "barplot",
error_type = "se",
sig_display = "letters",
color_palette = "Blues"
)
res_bar$Plotplot_type = "pointrange" provides a clean, modern
alternative for comparing group means without column bars:
res_point <- one_way_anova(
data = do_survey,
factor_var = "Zone",
numeric_var = "DO_mgL",
plot_type = "pointrange",
error_type = "se",
sig_display = "letters",
color_palette = "Dark2"
)
res_point$PlotThe function returns a comprehensive list containing the underlying ANOVA model, post-hoc pairwise comparisons, Estimated Marginal Means, and a summary statistics table:
# Global ANOVA Table (F-statistic and p-value)
res_box$ANOVA_Summary
#> Df Sum Sq Mean Sq F value Pr(>F)
#> Zone 2 184.71 92.36 339.9 <2e-16 ***
#> Residuals 87 23.64 0.27
#> ---
#> Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
# Pairwise Tukey HSD Differences
res_box$TukeyHSD
#> Tukey multiple comparisons of means
#> 95% family-wise confidence level
#>
#> Fit: stats::aov(formula = formula_str, data = clean_data)
#>
#> $Zone
#> diff lwr upr p adj
#> Mid Reach-Upper Basin -1.740017 -2.060938 -1.419097 0
#> Lower Estuary-Upper Basin -3.509114 -3.830034 -3.188193 0
#> Lower Estuary-Mid Reach -1.769096 -2.090017 -1.448176 0
# Estimated Marginal Means (EMMs)
res_box$EMMs
#> Zone emmean SE df lower.CL upper.CL
#> Upper Basin 8.46 0.0952 87 8.27 8.65
#> Mid Reach 6.72 0.0952 87 6.53 6.91
#> Lower Estuary 4.95 0.0952 87 4.76 5.14
#>
#> Confidence level used: 0.95
# Computed Summary Statistics Table
res_box$Summary_Table
#> # A tibble: 3 × 6
#> Zone mean_val sd_val se_val error_margin upper_limit
#> <fct> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 Upper Basin 8.46 0.376 0.0686 0.0686 8.53
#> 2 Mid Reach 6.72 0.564 0.103 0.103 6.83
#> 3 Lower Estuary 4.95 0.596 0.109 0.109 5.06The two_way_anova() function performs factorial analysis
of variance to simultaneously test the main effects of two categorical
factors as well as their multiplicative interaction on a continuous
aquatic response variable. In fisheries biology and aquatic ecology,
factorial designs are standard practice for assessing seasonal variation
across river basins, depth profiles across salinity gradients, or growth
performance across diet and temperature treatments.
The function handles automated missing value filtering, model parameter estimation, computation of interaction sum-of-squares, post-hoc contrast grouping (via Estimated Marginal Means and Tukey’s HSD), and renders both grouped interaction plots and faceted sub-panel displays.
A classical fixed-effects balanced or unbalanced Two-Way ANOVA models the continuous response \(y_{ijk}\) as:
\[y_{ijk} = \mu + \alpha_i + \beta_j + (\alpha\beta)_{ij} + \varepsilon_{ijk}\]
where: - \(y_{ijk}\) is the \(k\)-th replicate observation within level \(i\) of Factor A and level \(j\) of Factor B (\(i = 1, \dots, a\); \(j = 1, \dots, b\); \(k = 1, \dots, n_{ij}\)), - \(\mu\) is the overall population grand mean, - \(\alpha_i\) is the main effect of Factor A, subject to the identifiability constraint \(\sum n_i \alpha_i = 0\), - \(\beta_j\) is the main effect of Factor B, subject to the identifiability constraint \(\sum n_j \beta_j = 0\), - \((\alpha\beta)_{ij}\) represents the interaction term capturing non-additive synergies or antagonisms between factors, - \(\varepsilon_{ijk} \overset{\text{i.i.d.}}{\sim} \mathcal{N}(0, \sigma^2)\) is the random error term.
The test partitions the total sum of squares (\(\text{SS}_{\text{Total}}\)) into orthogonal components:
\[\text{SS}_{\text{Total}} = \text{SS}_{A} + \text{SS}_{B} + \text{SS}_{AB} + \text{SS}_{\text{Residual}}\]
The three core hypotheses evaluated by the model are:
Main Effect of Factor A: \[H_0^{(A)}: \alpha_1 = \dots = \alpha_a = 0 \quad \text{vs.} \quad F_A = \frac{\text{MS}_A}{\text{MS}_{\text{Residual}}}\]
Main Effect of Factor B: \[H_0^{(B)}: \beta_1 = \dots = \beta_b = 0 \quad \text{vs.} \quad F_B = \frac{\text{MS}_B}{\text{MS}_{\text{Residual}}}\]
Interaction Effect (\(A \times B\)): \[H_0^{(AB)}: (\alpha\beta)_{11} = \dots = (\alpha\beta)_{ab} = 0 \quad \text{vs.} \quad F_{AB} = \frac{\text{MS}_{AB}}{\text{MS}_{\text{Residual}}}\]
If the interaction effect is statistically significant (\(p < 0.05\)), main effects cannot be interpreted in isolation, and post-hoc pairwise contrasts are computed across the joint interaction levels using Estimated Marginal Means (EMMs).
We simulate a biomonitoring dataset evaluating fish condition factor
(\(K\)) across three river basins
(Upper, Central, Lower) sampled
over two distinct hydrological periods (Wet Season vs
Dry Season):
set.seed(42)
n_reps <- 15
basins <- c("Upper Reach", "Central Basin", "Lower Estuary")
seasons <- c("Wet Season", "Dry Season")
fish_condition <- data.frame(
Basin = factor(rep(rep(basins, each = n_reps), times = 2), levels = basins),
Season = factor(rep(seasons, each = n_reps * length(basins)), levels = seasons),
Condition_Factor = c(
# Wet Season: Upper, Central, Lower
rnorm(n_reps, mean = 1.35, sd = 0.08),
rnorm(n_reps, mean = 1.18, sd = 0.09),
rnorm(n_reps, mean = 1.05, sd = 0.10),
# Dry Season: Upper, Central, Lower
rnorm(n_reps, mean = 1.20, sd = 0.07),
rnorm(n_reps, mean = 0.98, sd = 0.08),
rnorm(n_reps, mean = 0.82, sd = 0.09)
)
)
head(fish_condition, 6)
#> Basin Season Condition_Factor
#> 1 Upper Reach Wet Season 1.459677
#> 2 Upper Reach Wet Season 1.304824
#> 3 Upper Reach Wet Season 1.379050
#> 4 Upper Reach Wet Season 1.400629
#> 5 Upper Reach Wet Season 1.382341
#> 6 Upper Reach Wet Season 1.341510The two_way_anova() function renders both grouped
interaction plots (dodged) and faceted sub-panel displays across all
standard geometries.
This style groups the primary factor along the x-axis, using color fill to contrast the secondary factor side-by-side with raw sample jitters and mean indicators:
res_two_box <- two_way_anova(
data = fish_condition,
factor1_var = "Basin",
factor2_var = "Season",
numeric_var = "Condition_Factor",
plot_type = "boxplot",
add_jitter = TRUE,
show_mean = TRUE,
mean_color = "darkred",
color_palette = "Set2"
)
res_two_box$PlotBy specifying facet_var = "Season", the visualization
cleanly segments the interaction into independent panels, preventing
crowded error bars and overlapping categories:
res_two_bar <- two_way_anova(
data = fish_condition,
factor1_var = "Basin",
factor2_var = "Season",
numeric_var = "Condition_Factor",
facet_var = "Season",
plot_type = "barplot",
error_type = "se",
color_palette = "Set2"
)
res_two_bar$PlotThe point-range display plots mean coordinates and directional standard error bars, making it straightforward to inspect interaction divergence between levels:
res_two_point <- two_way_anova(
data = fish_condition,
factor1_var = "Basin",
factor2_var = "Season",
numeric_var = "Condition_Factor",
plot_type = "pointrange",
error_type = "se",
color_palette = "Dark2"
)
res_two_point$PlotThe function returns the comprehensive ANOVA table showing degrees of freedom, sum-of-squares, mean-squares, \(F\)-values, and \(p\)-values for both main factors and their interaction:
# Factorial ANOVA Table (Main effects and Basin:Season interaction)
res_two_box$ANOVA_Summary
#> Df Sum Sq Mean Sq F value Pr(>F)
#> Basin 2 2.1146 1.0573 136.277 < 2e-16 ***
#> Season 1 0.6614 0.6614 85.244 2.02e-14 ***
#> Basin:Season 2 0.0048 0.0024 0.312 0.733
#> Residuals 84 0.6517 0.0078
#> ---
#> Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
# Post-hoc pairwise differences (Tukey HSD)
res_two_box$TukeyHSD
#> Tukey multiple comparisons of means
#> 95% family-wise confidence level
#>
#> Fit: stats::aov(formula = formula_str, data = clean_data)
#>
#> $Basin
#> diff lwr upr p adj
#> Central Basin-Upper Reach -0.2244000 -0.2786628 -0.1701371 0
#> Lower Estuary-Upper Reach -0.3728956 -0.4271585 -0.3186328 0
#> Lower Estuary-Central Basin -0.1484957 -0.2027586 -0.0942328 0
#>
#> $Season
#> diff lwr upr p adj
#> Dry Season-Wet Season -0.171446 -0.208373 -0.134519 0
#>
#> $`Basin:Season`
#> diff lwr
#> Central Basin:Wet Season-Upper Reach:Wet Season -0.23997417 -0.33377867
#> Lower Estuary:Wet Season-Upper Reach:Wet Season -0.37294488 -0.46674938
#> Upper Reach:Dry Season-Upper Reach:Wet Season -0.18186162 -0.27566612
#> Central Basin:Dry Season-Upper Reach:Wet Season -0.39068738 -0.48449188
#> Lower Estuary:Dry Season-Upper Reach:Wet Season -0.55470804 -0.64851254
#> Lower Estuary:Wet Season-Central Basin:Wet Season -0.13297070 -0.22677521
#> Upper Reach:Dry Season-Central Basin:Wet Season 0.05811255 -0.03569195
#> Central Basin:Dry Season-Central Basin:Wet Season -0.15071321 -0.24451771
#> Lower Estuary:Dry Season-Central Basin:Wet Season -0.31473387 -0.40853837
#> Upper Reach:Dry Season-Lower Estuary:Wet Season 0.19108326 0.09727876
#> Central Basin:Dry Season-Lower Estuary:Wet Season -0.01774250 -0.11154700
#> Lower Estuary:Dry Season-Lower Estuary:Wet Season -0.18176316 -0.27556766
#> Central Basin:Dry Season-Upper Reach:Dry Season -0.20882576 -0.30263026
#> Lower Estuary:Dry Season-Upper Reach:Dry Season -0.37284642 -0.46665092
#> Lower Estuary:Dry Season-Central Basin:Dry Season -0.16402066 -0.25782516
#> upr p adj
#> Central Basin:Wet Season-Upper Reach:Wet Season -0.14616967 0.0000000
#> Lower Estuary:Wet Season-Upper Reach:Wet Season -0.27914038 0.0000000
#> Upper Reach:Dry Season-Upper Reach:Wet Season -0.08805712 0.0000031
#> Central Basin:Dry Season-Upper Reach:Wet Season -0.29688288 0.0000000
#> Lower Estuary:Dry Season-Upper Reach:Wet Season -0.46090354 0.0000000
#> Lower Estuary:Wet Season-Central Basin:Wet Season -0.03916620 0.0011495
#> Upper Reach:Dry Season-Central Basin:Wet Season 0.15191705 0.4670331
#> Central Basin:Dry Season-Central Basin:Wet Season -0.05690871 0.0001515
#> Lower Estuary:Dry Season-Central Basin:Wet Season -0.22092937 0.0000000
#> Upper Reach:Dry Season-Lower Estuary:Wet Season 0.28488776 0.0000009
#> Central Basin:Dry Season-Lower Estuary:Wet Season 0.07606200 0.9937363
#> Lower Estuary:Dry Season-Lower Estuary:Wet Season -0.08795866 0.0000031
#> Central Basin:Dry Season-Upper Reach:Dry Season -0.11502126 0.0000001
#> Lower Estuary:Dry Season-Upper Reach:Dry Season -0.27904192 0.0000000
#> Lower Estuary:Dry Season-Central Basin:Dry Season -0.07021616 0.0000301
# Estimated Marginal Means with compact letter displays
res_two_box$EMMs
#> Basin Season emmean SE df lower.CL upper.CL
#> Upper Reach Wet Season 1.389 0.0227 84 1.344 1.434
#> Central Basin Wet Season 1.149 0.0227 84 1.104 1.194
#> Lower Estuary Wet Season 1.016 0.0227 84 0.971 1.061
#> Upper Reach Dry Season 1.207 0.0227 84 1.162 1.252
#> Central Basin Dry Season 0.998 0.0227 84 0.953 1.043
#> Lower Estuary Dry Season 0.834 0.0227 84 0.789 0.879
#>
#> Confidence level used: 0.95
# Summary statistics table
res_two_box$Summary_Table
#> # A tibble: 6 × 7
#> Basin Season mean_val sd_val se_val error_margin upper_limit
#> <fct> <fct> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 Upper Reach Wet Season 1.39 0.0820 0.0212 0.0212 1.41
#> 2 Upper Reach Dry Season 1.21 0.0762 0.0197 0.0197 1.23
#> 3 Central Basin Wet Season 1.15 0.122 0.0315 0.0315 1.18
#> 4 Central Basin Dry Season 0.998 0.0648 0.0167 0.0167 1.01
#> 5 Lower Estuary Wet Season 1.02 0.0997 0.0257 0.0257 1.04
#> 6 Lower Estuary Dry Season 0.834 0.0704 0.0182 0.0182 0.852The three_way_anova() function evaluates the
simultaneous influence of three categorical factors and their complete
interaction structure on a continuous aquatic response variable. In
limnology, marine biology, and ecotoxicology, three-way factorial
designs are commonly implemented to examine community or physiological
responses across spatial zones, hydrological seasons, and water column
depths simultaneously (e.g., Basin × Season × Depth).
The function automates factorial model fitting, calculation of main and interaction \(F\)-tests, derivation of Estimated Marginal Means across the complete three-way interaction lattice, post-hoc Tukey HSD letter grouping, and publication-ready faceted plotting.
The full three-way fixed-effects factorial model partitions the response variable \(y_{ijkl}\) into three main factor effects, three two-way interaction terms, and one three-way interaction term:
\[y_{ijkl} = \mu + \alpha_i + \beta_j + \gamma_k + (\alpha\beta)_{ij} + (\alpha\gamma)_{ik} + (\beta\gamma)_{jk} + (\alpha\beta\gamma)_{ijk} + \varepsilon_{ijkl}\]
where: - \(y_{ijkl}\) represents the \(l\)-th observation under treatment combination \((i, j, k)\), - \(\mu\) represents the grand population mean, - \(\alpha_i\), \(\beta_j\), and \(\gamma_k\) represent the main effects of Factor 1 (e.g., Basin), Factor 2 (e.g., Season), and Factor 3 (e.g., Depth Layer), - \((\alpha\beta)_{ij}\), \((\alpha\gamma)_{ik}\), and \((\beta\gamma)_{jk}\) represent the two-way interaction effects, - \((\alpha\beta\gamma)_{ijk}\) represents the three-way interaction capturing higher-order non-linear dependencies across all factor levels, - \(\varepsilon_{ijkl} \overset{\text{i.i.d.}}{\sim} \mathcal{N}(0, \sigma^2)\) is the random error term.
Total variation is partitioned into seven distinct explained components alongside the residual error:
\[\text{SS}_{\text{Total}} = \text{SS}_A + \text{SS}_B + \text{SS}_C + \text{SS}_{AB} + \text{SS}_{AC} + \text{SS}_{BC} + \text{SS}_{ABC} + \text{SS}_{\text{Residual}}\]
Each source of variation is tested against the residual mean square (\(\text{MS}_{\text{Residual}}\)). If the three-way interaction term \((\alpha\beta\gamma)_{ijk}\) is significant (\(p < 0.05\)), the response to Factor A depends simultaneously on the levels of both Factor B and Factor C. In this scenario, lower-order terms cannot be interpreted independently, and post-hoc contrasts are conducted across all joint treatment combinations.
We simulate a limnological biomonitoring dataset evaluating
Phytoplankton Chlorophyll-a concentrations (\(\mu\text{g/L}\)) across two Lake Basins
(North Basin, South Basin), two Seasons
(Pre-Monsoon, Post-Monsoon), and two Depth
Zones (Epilimnion, Hypolimnion):
set.seed(303)
n_reps <- 10
basins <- c("North Basin", "South Basin")
seasons <- c("Pre-Monsoon", "Post-Monsoon")
depths <- c("Epilimnion", "Hypolimnion")
design_grid <- expand.grid(
Basin = basins,
Season = seasons,
Depth = depths
)
chla_data <- do.call(rbind, lapply(seq_len(nrow(design_grid)), function(idx) {
row_info <- design_grid[idx, ]
# Baseline Chlorophyll-a dynamic
base_val <- 14.0
if (row_info$Basin == "South Basin") base_val <- base_val + 4.5
if (row_info$Season == "Post-Monsoon") base_val <- base_val + 6.0
if (row_info$Depth == "Hypolimnion") base_val <- base_val - 5.5
# Three-way synergistic interaction in productive shallow waters
if (row_info$Basin == "South Basin" && row_info$Season == "Post-Monsoon" && row_info$Depth == "Epilimnion") {
base_val <- base_val + 8.0
}
data.frame(
Basin = factor(rep(row_info$Basin, n_reps), levels = basins),
Season = factor(rep(row_info$Season, n_reps), levels = seasons),
Depth = factor(rep(row_info$Depth, n_reps), levels = depths),
Chla_ugL = rnorm(n_reps, mean = base_val, sd = 1.2)
)
}))
head(chla_data, 6)
#> Basin Season Depth Chla_ugL
#> 1 North Basin Pre-Monsoon Epilimnion 13.40367
#> 2 North Basin Pre-Monsoon Epilimnion 14.33461
#> 3 North Basin Pre-Monsoon Epilimnion 14.46136
#> 4 North Basin Pre-Monsoon Epilimnion 14.08230
#> 5 North Basin Pre-Monsoon Epilimnion 13.30615
#> 6 North Basin Pre-Monsoon Epilimnion 15.57667The three_way_anova() function maps Factor 1 to the
x-axis, Factor 2 to fill/color groupings, and uses
facet_wrap to partition panels by Factor 3.
Displays medians, IQRs, observation jitters, diamond mean markers, and three-way interaction letters:
res_three_box <- three_way_anova(
data = chla_data,
factor1_var = "Basin",
factor2_var = "Season",
factor3_var = "Depth",
numeric_var = "Chla_ugL",
plot_type = "boxplot",
add_jitter = TRUE,
show_mean = TRUE,
mean_color = "darkred",
color_palette = "Set1"
)
res_three_box$PlotDisplays aggregated group means with directional standard error bars across faceted sub-panels:
res_three_bar <- three_way_anova(
data = chla_data,
factor1_var = "Basin",
factor2_var = "Season",
factor3_var = "Depth",
numeric_var = "Chla_ugL",
plot_type = "barplot",
error_type = "se",
color_palette = "Set1"
)
res_three_bar$PlotPresents mean values and error bounds across faceted panels:
res_three_point <- three_way_anova(
data = chla_data,
factor1_var = "Basin",
factor2_var = "Season",
factor3_var = "Depth",
numeric_var = "Chla_ugL",
plot_type = "pointrange",
error_type = "se",
color_palette = "Dark2"
)
res_three_point$PlotExtract the global three-way ANOVA summary, pairwise Tukey HSD differences, and Estimated Marginal Means:
# Global Three-Way ANOVA Table (Main effects, 2-way, and 3-way interactions)
res_three_box$ANOVA_Summary
#> Df Sum Sq Mean Sq F value Pr(>F)
#> Basin 1 842.8 842.8 588.35 < 2e-16 ***
#> Season 1 1355.0 1355.0 945.99 < 2e-16 ***
#> Depth 1 1127.1 1127.1 786.88 < 2e-16 ***
#> Basin:Season 1 88.7 88.7 61.95 2.67e-11 ***
#> Basin:Depth 1 77.7 77.7 54.27 2.32e-10 ***
#> Season:Depth 1 55.3 55.3 38.58 3.03e-08 ***
#> Basin:Season:Depth 1 44.3 44.3 30.90 4.33e-07 ***
#> Residuals 72 103.1 1.4
#> ---
#> Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
# Post-hoc pairwise differences (Tukey HSD)
res_three_box$TukeyHSD
#> Tukey multiple comparisons of means
#> 95% family-wise confidence level
#>
#> Fit: stats::aov(formula = formula_str, data = clean_data)
#>
#> $Basin
#> diff lwr upr p adj
#> South Basin-North Basin 6.49138 5.95789 7.02487 0
#>
#> $Season
#> diff lwr upr p adj
#> Post-Monsoon-Pre-Monsoon 8.231143 7.697653 8.764633 0
#>
#> $Depth
#> diff lwr upr p adj
#> Hypolimnion-Epilimnion -7.507095 -8.040585 -6.973605 0
#>
#> $`Basin:Season`
#> diff lwr
#> South Basin:Pre-Monsoon-North Basin:Pre-Monsoon 4.384920 3.3895175
#> North Basin:Post-Monsoon-North Basin:Pre-Monsoon 6.124683 5.1292805
#> South Basin:Post-Monsoon-North Basin:Pre-Monsoon 14.722523 13.7271207
#> North Basin:Post-Monsoon-South Basin:Pre-Monsoon 1.739763 0.7443605
#> South Basin:Post-Monsoon-South Basin:Pre-Monsoon 10.337603 9.3422007
#> South Basin:Post-Monsoon-North Basin:Post-Monsoon 8.597840 7.6024377
#> upr p adj
#> South Basin:Pre-Monsoon-North Basin:Pre-Monsoon 5.380323 0.0000000
#> North Basin:Post-Monsoon-North Basin:Pre-Monsoon 7.120086 0.0000000
#> South Basin:Post-Monsoon-North Basin:Pre-Monsoon 15.717926 0.0000000
#> North Basin:Post-Monsoon-South Basin:Pre-Monsoon 2.735166 0.0001037
#> South Basin:Post-Monsoon-South Basin:Pre-Monsoon 11.333006 0.0000000
#> South Basin:Post-Monsoon-North Basin:Post-Monsoon 9.593243 0.0000000
#>
#> $`Basin:Depth`
#> diff lwr
#> South Basin:Epilimnion-North Basin:Epilimnion 8.462844 7.467441
#> North Basin:Hypolimnion-North Basin:Epilimnion -5.535631 -6.531034
#> South Basin:Hypolimnion-North Basin:Epilimnion -1.015715 -2.011117
#> North Basin:Hypolimnion-South Basin:Epilimnion -13.998475 -14.993877
#> South Basin:Hypolimnion-South Basin:Epilimnion -9.478558 -10.473961
#> South Basin:Hypolimnion-North Basin:Hypolimnion 4.519916 3.524514
#> upr p adj
#> South Basin:Epilimnion-North Basin:Epilimnion 9.45824641 0.0000000
#> North Basin:Hypolimnion-North Basin:Epilimnion -4.54022843 0.0000000
#> South Basin:Hypolimnion-North Basin:Epilimnion -0.02031205 0.0437494
#> North Basin:Hypolimnion-South Basin:Epilimnion -13.00307228 0.0000000
#> South Basin:Hypolimnion-South Basin:Epilimnion -8.48315590 0.0000000
#> South Basin:Hypolimnion-North Basin:Hypolimnion 5.51531894 0.0000000
#>
#> $`Season:Depth`
#> diff lwr
#> Post-Monsoon:Epilimnion-Pre-Monsoon:Epilimnion 9.8934143 8.8980118
#> Pre-Monsoon:Hypolimnion-Pre-Monsoon:Epilimnion -5.8448235 -6.8402261
#> Post-Monsoon:Hypolimnion-Pre-Monsoon:Epilimnion 0.7240484 -0.2713541
#> Pre-Monsoon:Hypolimnion-Post-Monsoon:Epilimnion -15.7382379 -16.7336404
#> Post-Monsoon:Hypolimnion-Post-Monsoon:Epilimnion -9.1693659 -10.1647685
#> Post-Monsoon:Hypolimnion-Pre-Monsoon:Hypolimnion 6.5688720 5.5734694
#> upr p adj
#> Post-Monsoon:Epilimnion-Pre-Monsoon:Epilimnion 10.888817 0.0000000
#> Pre-Monsoon:Hypolimnion-Pre-Monsoon:Epilimnion -4.849421 0.0000000
#> Post-Monsoon:Hypolimnion-Pre-Monsoon:Epilimnion 1.719451 0.2317211
#> Pre-Monsoon:Hypolimnion-Post-Monsoon:Epilimnion -14.742835 0.0000000
#> Post-Monsoon:Hypolimnion-Post-Monsoon:Epilimnion -8.173963 0.0000000
#> Post-Monsoon:Hypolimnion-Pre-Monsoon:Hypolimnion 7.564275 0.0000000
#>
#> $`Basin:Season:Depth`
#> diff
#> South Basin:Pre-Monsoon:Epilimnion-North Basin:Pre-Monsoon:Epilimnion 4.8686400
#> North Basin:Post-Monsoon:Epilimnion-North Basin:Pre-Monsoon:Epilimnion 6.2992104
#> South Basin:Post-Monsoon:Epilimnion-North Basin:Pre-Monsoon:Epilimnion 18.3562582
#> North Basin:Pre-Monsoon:Hypolimnion-North Basin:Pre-Monsoon:Epilimnion -5.3611036
#> South Basin:Pre-Monsoon:Hypolimnion-North Basin:Pre-Monsoon:Epilimnion -1.4599035
#> North Basin:Post-Monsoon:Hypolimnion-North Basin:Pre-Monsoon:Epilimnion 0.5890520
#> South Basin:Post-Monsoon:Hypolimnion-North Basin:Pre-Monsoon:Epilimnion 5.7276847
#> North Basin:Post-Monsoon:Epilimnion-South Basin:Pre-Monsoon:Epilimnion 1.4305705
#> South Basin:Post-Monsoon:Epilimnion-South Basin:Pre-Monsoon:Epilimnion 13.4876182
#> North Basin:Pre-Monsoon:Hypolimnion-South Basin:Pre-Monsoon:Epilimnion -10.2297436
#> South Basin:Pre-Monsoon:Hypolimnion-South Basin:Pre-Monsoon:Epilimnion -6.3285435
#> North Basin:Post-Monsoon:Hypolimnion-South Basin:Pre-Monsoon:Epilimnion -4.2795879
#> South Basin:Post-Monsoon:Hypolimnion-South Basin:Pre-Monsoon:Epilimnion 0.8590448
#> South Basin:Post-Monsoon:Epilimnion-North Basin:Post-Monsoon:Epilimnion 12.0570477
#> North Basin:Pre-Monsoon:Hypolimnion-North Basin:Post-Monsoon:Epilimnion -11.6603140
#> South Basin:Pre-Monsoon:Hypolimnion-North Basin:Post-Monsoon:Epilimnion -7.7591139
#> North Basin:Post-Monsoon:Hypolimnion-North Basin:Post-Monsoon:Epilimnion -5.7101584
#> South Basin:Post-Monsoon:Hypolimnion-North Basin:Post-Monsoon:Epilimnion -0.5715257
#> North Basin:Pre-Monsoon:Hypolimnion-South Basin:Post-Monsoon:Epilimnion -23.7173618
#> South Basin:Pre-Monsoon:Hypolimnion-South Basin:Post-Monsoon:Epilimnion -19.8161617
#> North Basin:Post-Monsoon:Hypolimnion-South Basin:Post-Monsoon:Epilimnion -17.7672061
#> South Basin:Post-Monsoon:Hypolimnion-South Basin:Post-Monsoon:Epilimnion -12.6285734
#> South Basin:Pre-Monsoon:Hypolimnion-North Basin:Pre-Monsoon:Hypolimnion 3.9012001
#> North Basin:Post-Monsoon:Hypolimnion-North Basin:Pre-Monsoon:Hypolimnion 5.9501557
#> South Basin:Post-Monsoon:Hypolimnion-North Basin:Pre-Monsoon:Hypolimnion 11.0887883
#> North Basin:Post-Monsoon:Hypolimnion-South Basin:Pre-Monsoon:Hypolimnion 2.0489556
#> South Basin:Post-Monsoon:Hypolimnion-South Basin:Pre-Monsoon:Hypolimnion 7.1875883
#> South Basin:Post-Monsoon:Hypolimnion-North Basin:Post-Monsoon:Hypolimnion 5.1386327
#> lwr
#> South Basin:Pre-Monsoon:Epilimnion-North Basin:Pre-Monsoon:Epilimnion 3.1977257
#> North Basin:Post-Monsoon:Epilimnion-North Basin:Pre-Monsoon:Epilimnion 4.6282962
#> South Basin:Post-Monsoon:Epilimnion-North Basin:Pre-Monsoon:Epilimnion 16.6853440
#> North Basin:Pre-Monsoon:Hypolimnion-North Basin:Pre-Monsoon:Epilimnion -7.0320178
#> South Basin:Pre-Monsoon:Hypolimnion-North Basin:Pre-Monsoon:Epilimnion -3.1308177
#> North Basin:Post-Monsoon:Hypolimnion-North Basin:Pre-Monsoon:Epilimnion -1.0818622
#> South Basin:Post-Monsoon:Hypolimnion-North Basin:Pre-Monsoon:Epilimnion 4.0567705
#> North Basin:Post-Monsoon:Epilimnion-South Basin:Pre-Monsoon:Epilimnion -0.2403437
#> South Basin:Post-Monsoon:Epilimnion-South Basin:Pre-Monsoon:Epilimnion 11.8167040
#> North Basin:Pre-Monsoon:Hypolimnion-South Basin:Pre-Monsoon:Epilimnion -11.9006578
#> South Basin:Pre-Monsoon:Hypolimnion-South Basin:Pre-Monsoon:Epilimnion -7.9994577
#> North Basin:Post-Monsoon:Hypolimnion-South Basin:Pre-Monsoon:Epilimnion -5.9505021
#> South Basin:Post-Monsoon:Hypolimnion-South Basin:Pre-Monsoon:Epilimnion -0.8118694
#> South Basin:Post-Monsoon:Epilimnion-North Basin:Post-Monsoon:Epilimnion 10.3861335
#> North Basin:Pre-Monsoon:Hypolimnion-North Basin:Post-Monsoon:Epilimnion -13.3312282
#> South Basin:Pre-Monsoon:Hypolimnion-North Basin:Post-Monsoon:Epilimnion -9.4300281
#> North Basin:Post-Monsoon:Hypolimnion-North Basin:Post-Monsoon:Epilimnion -7.3810726
#> South Basin:Post-Monsoon:Hypolimnion-North Basin:Post-Monsoon:Epilimnion -2.2424399
#> North Basin:Pre-Monsoon:Hypolimnion-South Basin:Post-Monsoon:Epilimnion -25.3882760
#> South Basin:Pre-Monsoon:Hypolimnion-South Basin:Post-Monsoon:Epilimnion -21.4870759
#> North Basin:Post-Monsoon:Hypolimnion-South Basin:Post-Monsoon:Epilimnion -19.4381203
#> South Basin:Post-Monsoon:Hypolimnion-South Basin:Post-Monsoon:Epilimnion -14.2994876
#> South Basin:Pre-Monsoon:Hypolimnion-North Basin:Pre-Monsoon:Hypolimnion 2.2302859
#> North Basin:Post-Monsoon:Hypolimnion-North Basin:Pre-Monsoon:Hypolimnion 4.2792415
#> South Basin:Post-Monsoon:Hypolimnion-North Basin:Pre-Monsoon:Hypolimnion 9.4178741
#> North Basin:Post-Monsoon:Hypolimnion-South Basin:Pre-Monsoon:Hypolimnion 0.3780414
#> South Basin:Post-Monsoon:Hypolimnion-South Basin:Pre-Monsoon:Hypolimnion 5.5166741
#> South Basin:Post-Monsoon:Hypolimnion-North Basin:Post-Monsoon:Hypolimnion 3.4677185
#> upr
#> South Basin:Pre-Monsoon:Epilimnion-North Basin:Pre-Monsoon:Epilimnion 6.5395542
#> North Basin:Post-Monsoon:Epilimnion-North Basin:Pre-Monsoon:Epilimnion 7.9701246
#> South Basin:Post-Monsoon:Epilimnion-North Basin:Pre-Monsoon:Epilimnion 20.0271724
#> North Basin:Pre-Monsoon:Hypolimnion-North Basin:Pre-Monsoon:Epilimnion -3.6901894
#> South Basin:Pre-Monsoon:Hypolimnion-North Basin:Pre-Monsoon:Epilimnion 0.2110107
#> North Basin:Post-Monsoon:Hypolimnion-North Basin:Pre-Monsoon:Epilimnion 2.2599663
#> South Basin:Post-Monsoon:Hypolimnion-North Basin:Pre-Monsoon:Epilimnion 7.3985989
#> North Basin:Post-Monsoon:Epilimnion-South Basin:Pre-Monsoon:Epilimnion 3.1014847
#> South Basin:Post-Monsoon:Epilimnion-South Basin:Pre-Monsoon:Epilimnion 15.1585324
#> North Basin:Pre-Monsoon:Hypolimnion-South Basin:Pre-Monsoon:Epilimnion -8.5588294
#> South Basin:Pre-Monsoon:Hypolimnion-South Basin:Pre-Monsoon:Epilimnion -4.6576293
#> North Basin:Post-Monsoon:Hypolimnion-South Basin:Pre-Monsoon:Epilimnion -2.6086737
#> South Basin:Post-Monsoon:Hypolimnion-South Basin:Pre-Monsoon:Epilimnion 2.5299590
#> South Basin:Post-Monsoon:Epilimnion-North Basin:Post-Monsoon:Epilimnion 13.7279620
#> North Basin:Pre-Monsoon:Hypolimnion-North Basin:Post-Monsoon:Epilimnion -9.9893998
#> South Basin:Pre-Monsoon:Hypolimnion-North Basin:Post-Monsoon:Epilimnion -6.0881997
#> North Basin:Post-Monsoon:Hypolimnion-North Basin:Post-Monsoon:Epilimnion -4.0392442
#> South Basin:Post-Monsoon:Hypolimnion-North Basin:Post-Monsoon:Epilimnion 1.0993885
#> North Basin:Pre-Monsoon:Hypolimnion-South Basin:Post-Monsoon:Epilimnion -22.0464476
#> South Basin:Pre-Monsoon:Hypolimnion-South Basin:Post-Monsoon:Epilimnion -18.1452475
#> North Basin:Post-Monsoon:Hypolimnion-South Basin:Post-Monsoon:Epilimnion -16.0962919
#> South Basin:Post-Monsoon:Hypolimnion-South Basin:Post-Monsoon:Epilimnion -10.9576592
#> South Basin:Pre-Monsoon:Hypolimnion-North Basin:Pre-Monsoon:Hypolimnion 5.5721143
#> North Basin:Post-Monsoon:Hypolimnion-North Basin:Pre-Monsoon:Hypolimnion 7.6210699
#> South Basin:Post-Monsoon:Hypolimnion-North Basin:Pre-Monsoon:Hypolimnion 12.7597026
#> North Basin:Post-Monsoon:Hypolimnion-South Basin:Pre-Monsoon:Hypolimnion 3.7198698
#> South Basin:Post-Monsoon:Hypolimnion-South Basin:Pre-Monsoon:Hypolimnion 8.8585025
#> South Basin:Post-Monsoon:Hypolimnion-North Basin:Post-Monsoon:Hypolimnion 6.8095469
#> p adj
#> South Basin:Pre-Monsoon:Epilimnion-North Basin:Pre-Monsoon:Epilimnion 0.0000000
#> North Basin:Post-Monsoon:Epilimnion-North Basin:Pre-Monsoon:Epilimnion 0.0000000
#> South Basin:Post-Monsoon:Epilimnion-North Basin:Pre-Monsoon:Epilimnion 0.0000000
#> North Basin:Pre-Monsoon:Hypolimnion-North Basin:Pre-Monsoon:Epilimnion 0.0000000
#> South Basin:Pre-Monsoon:Hypolimnion-North Basin:Pre-Monsoon:Epilimnion 0.1311252
#> North Basin:Post-Monsoon:Hypolimnion-North Basin:Pre-Monsoon:Epilimnion 0.9546834
#> South Basin:Post-Monsoon:Hypolimnion-North Basin:Pre-Monsoon:Epilimnion 0.0000000
#> North Basin:Post-Monsoon:Epilimnion-South Basin:Pre-Monsoon:Epilimnion 0.1480327
#> South Basin:Post-Monsoon:Epilimnion-South Basin:Pre-Monsoon:Epilimnion 0.0000000
#> North Basin:Pre-Monsoon:Hypolimnion-South Basin:Pre-Monsoon:Epilimnion 0.0000000
#> South Basin:Pre-Monsoon:Hypolimnion-South Basin:Pre-Monsoon:Epilimnion 0.0000000
#> North Basin:Post-Monsoon:Hypolimnion-South Basin:Pre-Monsoon:Epilimnion 0.0000000
#> South Basin:Post-Monsoon:Hypolimnion-South Basin:Pre-Monsoon:Epilimnion 0.7457880
#> South Basin:Post-Monsoon:Epilimnion-North Basin:Post-Monsoon:Epilimnion 0.0000000
#> North Basin:Pre-Monsoon:Hypolimnion-North Basin:Post-Monsoon:Epilimnion 0.0000000
#> South Basin:Pre-Monsoon:Hypolimnion-North Basin:Post-Monsoon:Epilimnion 0.0000000
#> North Basin:Post-Monsoon:Hypolimnion-North Basin:Post-Monsoon:Epilimnion 0.0000000
#> South Basin:Post-Monsoon:Hypolimnion-North Basin:Post-Monsoon:Epilimnion 0.9614041
#> North Basin:Pre-Monsoon:Hypolimnion-South Basin:Post-Monsoon:Epilimnion 0.0000000
#> South Basin:Pre-Monsoon:Hypolimnion-South Basin:Post-Monsoon:Epilimnion 0.0000000
#> North Basin:Post-Monsoon:Hypolimnion-South Basin:Post-Monsoon:Epilimnion 0.0000000
#> South Basin:Post-Monsoon:Hypolimnion-South Basin:Post-Monsoon:Epilimnion 0.0000000
#> South Basin:Pre-Monsoon:Hypolimnion-North Basin:Pre-Monsoon:Hypolimnion 0.0000000
#> North Basin:Post-Monsoon:Hypolimnion-North Basin:Pre-Monsoon:Hypolimnion 0.0000000
#> South Basin:Post-Monsoon:Hypolimnion-North Basin:Pre-Monsoon:Hypolimnion 0.0000000
#> North Basin:Post-Monsoon:Hypolimnion-South Basin:Pre-Monsoon:Hypolimnion 0.0063183
#> South Basin:Post-Monsoon:Hypolimnion-South Basin:Pre-Monsoon:Hypolimnion 0.0000000
#> South Basin:Post-Monsoon:Hypolimnion-North Basin:Post-Monsoon:Hypolimnion 0.0000000
# Estimated Marginal Means across the 3-way interaction grid
res_three_box$EMMs
#> Basin Season Depth emmean SE df lower.CL upper.CL
#> North Basin Pre-Monsoon Epilimnion 13.92 0.378 72 13.17 14.68
#> South Basin Pre-Monsoon Epilimnion 18.79 0.378 72 18.04 19.55
#> North Basin Post-Monsoon Epilimnion 20.22 0.378 72 19.47 20.98
#> South Basin Post-Monsoon Epilimnion 32.28 0.378 72 31.52 33.03
#> North Basin Pre-Monsoon Hypolimnion 8.56 0.378 72 7.81 9.32
#> South Basin Pre-Monsoon Hypolimnion 12.46 0.378 72 11.71 13.22
#> North Basin Post-Monsoon Hypolimnion 14.51 0.378 72 13.76 15.27
#> South Basin Post-Monsoon Hypolimnion 19.65 0.378 72 18.90 20.41
#>
#> Confidence level used: 0.95
# Aggregated summary statistics
res_three_box$Summary_Table
#> # A tibble: 8 × 8
#> Basin Season Depth mean_val sd_val se_val error_margin upper_limit
#> <fct> <fct> <fct> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 North Basin Pre-Monsoon Epil… 13.9 0.898 0.284 0.284 14.2
#> 2 North Basin Pre-Monsoon Hypo… 8.56 1.47 0.466 0.466 9.03
#> 3 North Basin Post-Monsoon Epil… 20.2 1.21 0.384 0.384 20.6
#> 4 North Basin Post-Monsoon Hypo… 14.5 1.11 0.352 0.352 14.9
#> 5 South Basin Pre-Monsoon Epil… 18.8 0.964 0.305 0.305 19.1
#> 6 South Basin Pre-Monsoon Hypo… 12.5 1.52 0.481 0.481 12.9
#> 7 South Basin Post-Monsoon Epil… 32.3 0.910 0.288 0.288 32.6
#> 8 South Basin Post-Monsoon Hypo… 19.7 1.31 0.413 0.413 20.1The ancova_analysis() function combines linear
regression and Analysis of Variance to test for differences in a
continuous response variable across categorical treatment groups while
statistically controlling for a continuous nuisance variable
(covariate). In fisheries stock assessment and aquatic ecology, ANCOVA
is routinely used to compare fish weight across sampling locations while
controlling for body length, or to compare metabolic excretion rates
across diet formulations while controlling for individual biomass.
The function handles missing value removal, fits an additive general linear model, extracts the omnibus ANCOVA table, computes Estimated Marginal Means (EMMs) adjusted to the mean value of the covariate, and renders publication-quality scatter plots with parallel regression trend lines.
The additive fixed-effects ANCOVA model represents the response variable \(y_{ij}\) as:
\[y_{ij} = \mu + \alpha_i + \beta (x_{ij} - \bar{x}) + \varepsilon_{ij}\]
where: - \(y_{ij}\) is the response observed for the \(j\)-th individual in group \(i\) (\(i = 1, \dots, k\); \(j = 1, \dots, n_i\)), - \(\mu\) is the overall adjusted grand mean, - \(\alpha_i\) is the categorical treatment effect of group \(i\), subject to the constraint \(\sum n_i \alpha_i = 0\), - \(\beta\) is the common pooled regression slope relating the continuous covariate \(x\) to response \(y\), - \(x_{ij}\) is the covariate value for individual \(j\) in group \(i\), centered around the grand covariate mean \(\bar{x}\), - \(\varepsilon_{ij} \overset{\text{i.i.d.}}{\sim} \mathcal{N}(0, \sigma^2)\) is the normally distributed random error term.
A core assumption of additive ANCOVA is the homogeneity of regression slopes, meaning the relationship between the covariate (\(x\)) and the response (\(y\)) does not vary across factor levels (\(\beta_1 = \beta_2 = \dots = \beta_k = \beta\)).
When this assumption holds, the categorical factor effect evaluates whether the elevations (y-intercepts) of the parallel regression lines differ significantly:
\[H_0: \alpha_1 = \alpha_2 = \dots = \alpha_k = 0\] \[H_1: \text{At least one group elevation differs.}\]
Because raw group means are confounded by variations in the
covariate, ancova_analysis() computes Estimated Marginal
Means (EMMs). The adjusted mean for group \(i\) (\(\hat{\mu}_i\)) is evaluated at the grand
mean of the covariate (\(\bar{x}_{\bullet\bullet}\)):
\[\hat{\mu}_i = \bar{y}_{i\bullet} - \hat{\beta}(\bar{x}_{i\bullet} - \bar{x}_{\bullet\bullet})\]
We simulate a fisheries length-weight dataset evaluating somatic body
weight (\(\text{Weight}\), \(\text{g}\)) across three distinct aquatic
habitats (Estuary, River, Marine)
while controlling for continuous total body length (\(\text{Length}\), \(\text{cm}\)):
set.seed(2026)
n_per_habitat <- 35
habitats <- c("Estuary", "River", "Marine")
fish_growth <- do.call(rbind, lapply(habitats, function(hab) {
# Generate continuous covariate: fish total length (cm)
len <- runif(n_per_habitat, min = 12.0, max = 38.0)
# Group elevation shifts (intercept differences)
elevation_shift <- switch(hab,
"Estuary" = -12.0,
"River" = -4.0,
"Marine" = 8.0
)
# Additive response: common slope (beta = 3.25) + habitat elevation + noise
wt <- 3.25 * len + elevation_shift + rnorm(n_per_habitat, mean = 0, sd = 4.2)
data.frame(
Habitat = factor(rep(hab, n_per_habitat), levels = habitats),
Length = len,
Weight = wt
)
}))
head(fish_growth, 6)
#> Habitat Length Weight
#> 1 Estuary 30.16551 85.25238
#> 2 Estuary 26.46979 73.78666
#> 3 Estuary 15.64364 38.28039
#> 4 Estuary 19.42881 55.97057
#> 5 Estuary 26.43959 73.29607
#> 6 Estuary 12.65341 21.49277ancova_analysis() plots the empirical observations
alongside model-fitted regression lines. This preserves the parallel
slopes of the additive model, avoiding the misleading non-parallel lines
produced by unconstrained smoothers.
Presents all groups on a shared coordinate grid
(facet = FALSE), allowing direct visual comparison of
vertical line separations (elevation differences):
res_ancova <- ancova_analysis(
data = fish_growth,
response_var = "Weight",
factor_var = "Habitat",
covariate_var = "Length",
facet = FALSE,
color_palette = "Dark2"
)
# Render publication plot
res_ancova$PlotWhen working with dense datasets or multiple overlapping factor
levels, setting facet = TRUE separates each group into its
own panel using facet_wrap, while keeping the coordinate
scales fixed for consistent comparison:
res_ancova_facet <- ancova_analysis(
data = fish_growth,
response_var = "Weight",
factor_var = "Habitat",
covariate_var = "Length",
facet = TRUE,
color_palette = "Dark2"
)
res_ancova_facet$PlotThe returned list object provides the ANOVA table testing the covariate and categorical factor effects, as well as the covariate-adjusted Estimated Marginal Means:
# 1. ANCOVA Table (Testing covariate and factor significance)
res_ancova$ANCOVA_Table
#> Analysis of Variance Table
#>
#> Response: Weight
#> Df Sum Sq Mean Sq F value Pr(>F)
#> Length 1 69814 69814 4111.69 < 2.2e-16 ***
#> Habitat 2 6473 3236 190.61 < 2.2e-16 ***
#> Residuals 101 1715 17
#> ---
#> Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
# 2. Estimated Marginal Means (EMMs adjusted to mean Length)
res_ancova$Adjusted_Means
#> Habitat emmean SE df lower.CL upper.CL
#> Estuary 61.6 0.699 101 60.2 63.0
#> River 69.2 0.698 101 67.8 70.6
#> Marine 81.0 0.703 101 79.6 82.3
#>
#> Confidence level used: 0.95The manova_analysis() function extends analysis of
variance to multi-response hypotheses, testing whether mean vectors of
several correlated continuous variables differ across levels of a
categorical grouping variable. In fisheries biology and aquatic ecology,
MANOVA is commonly applied in morphometric stock discrimination (e.g.,
comparing head length, body depth, and caudal peduncle length
simultaneously across river populations) or multi-metric water quality
assessments (e.g., evaluating simultaneous variations in nitrates,
phosphates, and ammonium across wetland sites).
The function conducts defensive data verification, fits the multivariate general linear model, evaluates omnibus significance using Pillai’s trace criterion, derives univariate ANOVA decompositions, and provides two distinct visualization geometries: faceted univariate trait boxplots and Canonical Discriminant Analysis (LDA) scatter plots with 95% confidence ellipses.
Let \(\mathbf{y}_{ij} = [y_{ij1}, y_{ij2}, \dots, y_{ijp}]^T\) represent a \(p\)-dimensional vector of continuous response measurements recorded for the \(j\)-th specimen in treatment group \(i\) (\(i = 1, \dots, k\); \(j = 1, \dots, n_i\)). The one-way multivariate linear model is defined as:
\[\mathbf{y}_{ij} = \boldsymbol{\mu} + \boldsymbol{\alpha}_i + \boldsymbol{\varepsilon}_{ij}\]
where: - \(\boldsymbol{\mu} = [\mu_1, \dots, \mu_p]^T\) represents the grand centroid vector across all \(p\) traits, - \(\boldsymbol{\alpha}_i = [\alpha_{i1}, \dots, \alpha_{ip}]^T\) represents the differential group effect vector for group \(i\), - \(\boldsymbol{\varepsilon}_{ij} \overset{\text{i.i.d.}}{\sim} \mathcal{N}_p(\mathbf{0}, \boldsymbol{\Sigma})\) represents the multivariate normal random error vector with variance-covariance matrix \(\boldsymbol{\Sigma}\).
The omnibus multivariate null hypothesis states that group centroids are identical across all \(p\) dimensions:
\[H_0: \boldsymbol{\mu}_1 = \boldsymbol{\mu}_2 = \dots = \boldsymbol{\mu}_k\] \[H_1: \text{At least two group centroid vectors differ.}\]
The total variance-covariance matrix is partitioned into the Between-Group Sum of Squares and Cross-Products matrix (\(\mathbf{H}\)) and the Within-Group (Error) Sum of Squares and Cross-Products matrix (\(\mathbf{E}\)):
\[\mathbf{H} = \sum_{i=1}^k n_i (\bar{\mathbf{y}}_{i\bullet} - \bar{\mathbf{y}}_{\bullet\bullet})(\bar{\mathbf{y}}_{i\bullet} - \bar{\mathbf{y}}_{\bullet\bullet})^T\]
\[\mathbf{E} = \sum_{i=1}^k \sum_{j=1}^{n_i} (\mathbf{y}_{ij} - \bar{\mathbf{y}}_{i\bullet})(\mathbf{y}_{ij} - \bar{\mathbf{y}}_{i\bullet})^T\]
While several test criteria exist (Wilks’ Lambda, Hotelling-Lawley
trace, Roy’s largest root), manova_analysis() defaults to
Pillai’s Trace (\(V\)) due to its
robustness against departures from multivariate normality and moderate
covariance heterogeneity:
\[V = \text{tr}\left[\mathbf{H}(\mathbf{H} + \mathbf{E})^{-1}\right] = \sum_{m=1}^s \frac{\lambda_m}{1 + \lambda_m}\]
where \(\lambda_m\) represents the eigenvalues of \(\mathbf{E}^{-1}\mathbf{H}\), and \(s = \min(p, k - 1)\). The statistic is converted to an approximate \(F\)-distribution to establish statistical significance.
We simulate a morphometric stock identification survey measuring four
anatomical truss traits (Total Length TL, Head Length
HL, Body Depth BD, and Caudal Width
CW in cm) across three wild fish populations
(River_A, River_B, and
River_C):
set.seed(505)
n_fish <- 25
populations <- c("River_A", "River_B", "River_C")
# Simulate multivariate normal morphometric data
morpho_data <- do.call(rbind, lapply(populations, function(pop) {
# Baseline centroid shifts per population
shift <- switch(pop,
"River_A" = c(TL = 24.0, HL = 5.2, BD = 6.5, CW = 2.1),
"River_B" = c(TL = 28.5, HL = 6.4, BD = 7.8, CW = 2.6),
"River_C" = c(TL = 21.0, HL = 4.8, BD = 5.9, CW = 1.9)
)
data.frame(
Population = factor(rep(pop, n_fish), levels = populations),
TL = rnorm(n_fish, mean = shift["TL"], sd = 1.5),
HL = rnorm(n_fish, mean = shift["HL"], sd = 0.4),
BD = rnorm(n_fish, mean = shift["BD"], sd = 0.5),
CW = rnorm(n_fish, mean = shift["CW"], sd = 0.2)
)
}))
head(morpho_data, 6)
#> Population TL HL BD CW
#> 1 River_A 22.31822 5.435375 5.605484 1.812157
#> 2 River_A 22.07691 5.041592 6.145151 1.972692
#> 3 River_A 20.94099 5.282860 5.595230 1.829734
#> 4 River_A 22.59340 5.254322 6.134524 2.257909
#> 5 River_A 23.23476 5.786759 6.319575 2.392531
#> 6 River_A 23.38073 4.793155 5.212262 1.967572The manova_analysis() function provides two
complementary visual perspectives: faceted univariate trait
distributions and multivariate canonical ordination.
Decomposes the multivariate space into separate panels for each continuous trait, showing median values, interquartile ranges, and individual observation distributions:
res_manova_box <- manova_analysis(
data = morpho_data,
response_vars = c("TL", "HL", "BD", "CW"),
factor_var = "Population",
plot_type = "boxplot",
color_palette = "Set2"
)
# Render faceted traits
res_manova_box$PlotProjects the multi-trait measurements into canonical discriminant
space (Linear Discriminant axes LD1 and LD2),
displaying multivariate centroid separation and 95% group confidence
ellipses:
res_manova_lda <- manova_analysis(
data = morpho_data,
response_vars = c("TL", "HL", "BD", "CW"),
factor_var = "Population",
plot_type = "lda",
color_palette = "Set2"
)
# Render canonical discriminant ordination
res_manova_lda$PlotThe returned list delivers both the global multivariate Pillai’s trace summary and the individual univariate ANOVA breakdowns:
# 1. Omnibus MANOVA Summary (Pillai's Trace, F-approximation, p-value)
res_manova_box$MANOVA_Summary
#> Df Pillai approx F num Df den Df Pr(>F)
#> Population 2 0.94978 15.826 8 140 < 2.2e-16 ***
#> Residuals 72
#> ---
#> Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
# 2. Univariate ANOVA Breakdowns for Each Morphometric Trait
res_manova_box$Univariate_ANOVAs
#> Response TL :
#> Df Sum Sq Mean Sq F value Pr(>F)
#> Population 2 755.7 377.85 162.71 < 2.2e-16 ***
#> Residuals 72 167.2 2.32
#> ---
#> Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
#>
#> Response HL :
#> Df Sum Sq Mean Sq F value Pr(>F)
#> Population 2 26.439 13.2195 93.691 < 2.2e-16 ***
#> Residuals 72 10.159 0.1411
#> ---
#> Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
#>
#> Response BD :
#> Df Sum Sq Mean Sq F value Pr(>F)
#> Population 2 47.020 23.5101 69.044 < 2.2e-16 ***
#> Residuals 72 24.517 0.3405
#> ---
#> Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
#>
#> Response CW :
#> Df Sum Sq Mean Sq F value Pr(>F)
#> Population 2 7.6740 3.837 78.335 < 2.2e-16 ***
#> Residuals 72 3.5267 0.049
#> ---
#> Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1The regression_analysis() function models functional
dependencies between continuous aquatic parameters—including
morphometric fish growth, allometric length-weight relationships (\(W = a \cdot L^b\)), nutrient saturation
kinetics, chemical calibration lines, and thermal performance
curves.
The function provides: 1. Dynamic Equation Generation: Automatically extracts model coefficients, evaluates statistical significance (\(R^2\) and \(p\)-value), and prints the parsed algebraic formula directly on the plot canvas. 2. Dual Uncertainty Bands: Toggles between Confidence Intervals (measuring precision of the expected mean response \(\mu_{y\mid x}\)) and Prediction Intervals (capturing individual biological variation of future single specimens). 3. Comprehensive Functional Forms: Fits linear (\(y = \beta_0 + \beta_1 x\)), allometric power (\(y = a \cdot x^b\)), exponential (\(y = a \cdot e^{kx}\)), logarithmic (\(y = \beta_0 + \beta_1 \ln(x)\)), quadratic polynomial (\(y = \beta_0 + \beta_1 x + \beta_2 x^2\)), or cubic polynomial (\(y = \beta_0 + \beta_1 x + \beta_2 x^2 + \beta_3 x^3\)) models across single-panel and multi-panel faceted designs.
For a linear or linearized model \(Y_i = \beta_0 + \beta_1 X_i + \varepsilon_i\) with residual variance \(s^2\):
Measures uncertainty in estimating the true conditional population mean \(\mathbb{E}(Y \mid x_0)\) at a specified predictor value \(x_0\):
\[\text{CI}_{95\%}(x_0) = \hat{y}_0 \pm t_{n-2, \, 0.975} \times s \sqrt{\frac{1}{n} + \frac{(x_0 - \bar{x})^2}{\sum_{i=1}^n (x_i - \bar{x})^2}}\]
As sample size increases (\(n \to \infty\)), the confidence band contracts toward zero width around the fitted trajectory.
Predicts the boundary within which a single newly sampled organism \(Y_{\text{new}}\) is expected to fall:
\[\text{PI}_{95\%}(x_0) = \hat{y}_0 \pm t_{n-2, \, 0.975} \times s \sqrt{1 + \frac{1}{n} + \frac{(x_0 - \bar{x})^2}{\sum_{i=1}^n (x_i - \bar{x})^2}}\]
The additional unit term (\(1\)) inside the radical accounts for irreducible biological variance (\(\sigma^2\)), making prediction bands substantially wider than confidence intervals.
We simulate a riverine fisheries biometric dataset tracking Total
Length (cm) and Body Weight (g) across 75 specimens collected from three
river basins (River A, River B,
River C):
set.seed(606)
n_fish <- 25
rivers <- c("River A", "River B", "River C")
fish_allometry <- do.call(rbind, lapply(rivers, function(rv) {
len <- runif(n_fish, min = 12, max = 38)
# Allometric power parameters: W = a * L^b
a_param <- switch(rv, "River A" = 0.012, "River B" = 0.009, "River C" = 0.015)
b_param <- switch(rv, "River A" = 3.05, "River B" = 3.18, "River C" = 2.92)
wt <- a_param * (len^b_param) * exp(rnorm(n_fish, mean = 0, sd = 0.12))
data.frame(
River = rv,
Length = len,
Weight = wt,
stringsAsFactors = FALSE
)
}))
head(fish_allometry, 6)
#> River Length Weight
#> 1 River A 26.15128 276.34037
#> 2 River A 33.53511 573.18190
#> 3 River A 16.57117 56.49807
#> 4 River A 32.41410 516.93445
#> 5 River A 14.74483 40.48794
#> 6 River A 37.76574 750.74098regression_analysis() handles linear, power-law,
exponential, logarithmic, and higher-order polynomial relationships.
Fits an allometric power curve, calculates wide 95% prediction intervals to accommodate individual specimen growth scatter, and plots the power formula on the canvas:
res_power_reg <- regression_analysis(
data = fish_allometry,
x_var = "Length",
y_var = "Weight",
fit_type = "power",
interval = "prediction",
show_equation = TRUE,
title = "Allometric Fish Length-Weight Relationship (95% PI)"
)
# Render allometric power plot
res_power_reg$PlotModels exponential trajectories (e.g., mortality catch curves, light attenuation) with confidence intervals around the expected conditional mean:
res_exp_reg <- regression_analysis(
data = fish_allometry,
x_var = "Length",
y_var = "Weight",
fit_type = "exponential",
interval = "confidence",
show_equation = TRUE,
title = "Exponential Length-Weight Curve (95% CI)"
)
# Render exponential curve
res_exp_reg$PlotFits 2nd-order polynomial curves across river systems with
facet = TRUE, placing panel-specific equations and \(R^2\) values in each sub-panel:
res_poly_reg <- regression_analysis(
data = fish_allometry,
x_var = "Length",
y_var = "Weight",
fit_type = "polynomial",
group_var = "River",
facet = TRUE,
interval = "confidence",
show_equation = TRUE,
color_palette = "Dark2",
title = "Faceted Quadratic Growth Trajectories by Basin"
)
# Render faceted quadratic curves
res_poly_reg$PlotAccommodates complex, multi-inflection trajectories (e.g., seasonal thermal tolerance limits or multi-stage cohort growth) using a 3rd-order polynomial:
res_cubic_reg <- regression_analysis(
data = fish_allometry,
x_var = "Length",
y_var = "Weight",
fit_type = "cubic",
group_var = "River",
facet = TRUE,
interval = "confidence",
show_equation = TRUE,
color_palette = "Set1",
title = "Faceted Cubic Growth Polynomials Across Basins"
)
# Render faceted cubic curves
res_cubic_reg$PlotThe returned list object provides access to algebraic formulas,
goodness-of-fit statistics (\(R^2\),
\(F\)-test \(p\)-values), fitted lm model
objects, and evaluation grids:
# 1. Tidy summary of model equations, R2 values, and p-values
res_poly_reg$Equation_Table
#> Group Fit_Type Equation R_Squared P_Value
#> 1 River A polynomial y == '117.91' - '19.05'*x + '0.948'*x^2 0.9766 0
#> 2 River B polynomial y == '163.88' - '24.71'*x + '1.134'*x^2 0.9846 0
#> 3 River C polynomial y == '66.86' - '11.83'*x + '0.671'*x^2 0.9795 0
#> Label
#> 1 atop(y == '117.91' - '19.05'*x + '0.948'*x^2, paste(R^2 == '0.98', ', ', p < 0.001))
#> 2 atop(y == '163.88' - '24.71'*x + '1.134'*x^2, paste(R^2 == '0.98', ', ', p < 0.001))
#> 3 atop(y == '66.86' - '11.83'*x + '0.671'*x^2, paste(R^2 == '0.98', ', ', p < 0.001))
# 2. Inspect underlying linear model coefficients for River A
summary(res_poly_reg$Models[["River A"]])
#>
#> Call:
#> stats::lm(formula = y ~ x + x2, data = df_fit)
#>
#> Residuals:
#> Min 1Q Median 3Q Max
#> -89.230 -13.681 1.514 8.028 60.074
#>
#> Coefficients:
#> Estimate Std. Error t value Pr(>|t|)
#> (Intercept) 117.9122 83.6118 1.410 0.1725
#> x -19.0516 7.3204 -2.603 0.0163 *
#> x2 0.9484 0.1476 6.424 1.83e-06 ***
#> ---
#> Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
#>
#> Residual standard error: 36.18 on 22 degrees of freedom
#> Multiple R-squared: 0.9766, Adjusted R-squared: 0.9745
#> F-statistic: 459 on 2 and 22 DF, p-value: < 2.2e-16
# 3. Preview calculated prediction grid with 95% confidence intervals
head(res_poly_reg$Predictions, 6)
#> x y_fit y_lwr y_upr Group
#> 1 12.70514 28.95552 -7.0132498 64.92429 River A
#> 2 12.91573 30.06073 -4.6373997 64.75886 River A
#> 3 13.12632 31.25007 -2.2206115 64.72076 River A
#> 4 13.33692 32.52354 0.2359541 64.81112 River A
#> 5 13.54751 33.88113 2.7310749 65.03118 River A
#> 6 13.75810 35.32284 5.2634742 65.38221 River AThe glm_analysis() function handles non-normal aquatic
survey data where standard ordinary least squares (OLS) assumptions are
violated. In fisheries surveys, biodiversity assessments, and habitat
modeling, empirical data frequently feature discrete integer counts with
positive skew (e.g., fish catch per unit effort, abundance of invasive
species) or binary presence/absence states (e.g., detection of
endangered fauna or disease occurrence).
The function fits models across Poisson, Quasipoisson (for overdispersed counts), or Binomial error distributions using link functions (\(g(\cdot)\)). It extracts Wald parameter estimates, derives multiplicative effect metrics—Incident Rate Ratios (IRR) for count families or Odds Ratios (OR) for binary families—computes 95% Wald confidence bounds, and generates a publication-ready forest plot.
A Generalized Linear Model consists of three components: 1. Random Component: An exponential dispersion family distribution for the response variable \(Y_i\). 2. Systematic Component: A linear predictor combination of \(p\) explanatory variables: \[\eta_i = \beta_0 + \sum_{j=1}^p \beta_j x_{ij}\] 3. Link Function: A monotonic function \(g(\cdot)\) mapping the expected mean \(\mu_i = \mathbb{E}(Y_i)\) to the linear predictor: \[\eta_i = g(\mu_i)\]
For count data (e.g., CPUE or species richness), the log link ensures strictly positive predicted yields:
\[\ln(\mu_i) = \beta_0 + \beta_1 x_{i1} + \dots + \beta_p x_{ip}\]
Exponentiating coefficients yields the Incident Rate Ratio (IRR):
\[\text{IRR}_j = \exp(\beta_j)\]
An \(\text{IRR} > 1\) denotes a
multiplicative increase in the expected count per unit increase in \(x_j\), while an \(\text{IRR} < 1\) represents a
proportional reduction. When the variance exceeds the mean (\(\text{Var}(Y) = \phi \mu\) with dispersion
parameter \(\phi > 1\)), the
quasipoisson family inflates parameter standard errors by
\(\sqrt{\phi}\) to account for
overdispersion without altering point estimates.
For binary occurrence states \(Y_i \in \{0, 1\}\), the logit link maps probability \(p_i = P(Y_i = 1)\) to continuous space:
\[\text{logit}(p_i) = \ln\left(\frac{p_i}{1 - p_i}\right) = \beta_0 + \beta_1 x_{i1} + \dots + \beta_p x_{ip}\]
Exponentiating estimates yields the Odds Ratio (OR):
\[\text{OR}_j = \exp(\beta_j)\]
The 95% Wald confidence intervals for effect sizes are calculated as:
\[\text{CI}_{95\%} = \exp(\hat{\beta}_j \pm 1.96 \times \text{SE}(\hat{\beta}_j))\]
If the confidence interval spans \(1.0\), the predictor does not exert a statistically detectable multiplicative effect at \(\alpha = 0.05\).
We simulate an estuarine fish survey monitoring the abundance (count
per haul) of a juvenile target species across 120 sampling trawls. The
response count is modeled as a function of Water Temperature (°C),
Salinity (PSU), Dissolved Oxygen (mg/L), and Substrate Type
(Mud vs. Sand):
set.seed(707)
n_trawls <- 120
temp_c <- runif(n_trawls, min = 18, max = 30)
sal_psu <- runif(n_trawls, min = 5, max = 32)
do_mgl <- runif(n_trawls, min = 3.5, max = 9.0)
substr <- sample(c("Mud", "Sand"), size = n_trawls, replace = TRUE)
# Multiplicative linear predictor
log_mu <- -1.8 +
0.08 * temp_c -
0.04 * sal_psu +
0.22 * do_mgl +
ifelse(substr == "Sand", 0.35, 0.0)
# Simulate Poisson counts
catch_counts <- rpois(n_trawls, lambda = exp(log_mu))
trawl_survey <- data.frame(
Abundance = catch_counts,
Temperature = temp_c,
Salinity = sal_psu,
DO = do_mgl,
Substrate = factor(substr, levels = c("Mud", "Sand"))
)
head(trawl_survey, 6)
#> Abundance Temperature Salinity DO Substrate
#> 1 1 19.19934 31.872330 4.420170 Mud
#> 2 3 23.25641 7.830847 5.040356 Sand
#> 3 1 21.22099 16.444491 5.220697 Sand
#> 4 2 24.38437 5.188938 4.339394 Sand
#> 5 4 27.09255 16.775980 5.855344 Mud
#> 6 0 26.38856 12.958779 5.937050 MudThe forest plot produced by glm_analysis() centers
predictor effects relative to the reference null line at \(\text{Effect Size} = 1.0\).
Evaluates the Incident Rate Ratio (IRR) across environmental drivers, with horizontal error bars showing 95% confidence intervals:
res_glm_count <- glm_analysis(
data = trawl_survey,
response_var = "Abundance",
predictor_vars = c("Temperature", "Salinity", "DO", "Substrate"),
family_type = "poisson",
color_palette = "Dark2"
)
# Render forest plot
res_glm_count$PlotWhen the response variable is converted into occurrence
(presence/absence), family_type = "binomial" generates Odds
Ratios (OR):
# Create binary presence/absence column
trawl_survey$Presence <- ifelse(trawl_survey$Abundance > 0, 1, 0)
res_glm_binary <- glm_analysis(
data = trawl_survey,
response_var = "Presence",
predictor_vars = c("Temperature", "Salinity", "DO", "Substrate"),
family_type = "binomial",
color_palette = "Set1"
)
res_glm_binary$PlotThe returned list provides the GLM model summary and a coefficient table with calculated effect sizes:
# 1. Global GLM Summary (Deviance, Dispersion, Estimates, z-values, p-values)
res_glm_count$Model_Summary
#>
#> Call:
#> stats::glm(formula = formula_str, family = fam_obj, data = clean_data)
#>
#> Coefficients:
#> Estimate Std. Error z value Pr(>|z|)
#> (Intercept) -1.896996 0.476145 -3.984 6.77e-05 ***
#> Temperature 0.073725 0.015557 4.739 2.15e-06 ***
#> Salinity -0.033417 0.007253 -4.607 4.08e-06 ***
#> DO 0.230767 0.035161 6.563 5.27e-11 ***
#> SubstrateSand 0.374581 0.112705 3.324 0.000889 ***
#> ---
#> Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
#>
#> (Dispersion parameter for poisson family taken to be 1)
#>
#> Null deviance: 225.54 on 119 degrees of freedom
#> Residual deviance: 116.91 on 115 degrees of freedom
#> AIC: 432.46
#>
#> Number of Fisher Scoring iterations: 5
# 2. Tidy Coefficient Table with Exponentiated Effect Sizes (IRR) and 95% CIs
res_glm_count$Coefficient_Table
#> Estimate Std_Error z_value p_value Term
#> Temperature 0.07372515 0.015556870 4.739074 2.146976e-06 Temperature
#> Salinity -0.03341746 0.007253455 -4.607110 4.083048e-06 Salinity
#> DO 0.23076685 0.035161128 6.563124 5.269193e-11 DO
#> SubstrateSand 0.37458124 0.112705395 3.323543 8.888167e-04 SubstrateSand
#> Effect_Size Lower_CI Upper_CI
#> Temperature 1.0765109 1.0441819 1.1098408
#> Salinity 0.9671347 0.9534825 0.9809825
#> DO 1.2595655 1.1756851 1.3494305
#> SubstrateSand 1.4543823 1.1661161 1.8139083The nmds_analysis() function performs unconstrained
multivariate ordination for community composition datasets (e.g.,
phytoplankton, zooplankton, macroinvertebrates, and fish assemblages).
Unlike eigenvalue-based ordination techniques (e.g., PCA), NMDS avoids
assuming linear relationships between species abundances and underlying
environmental gradients. Instead, it relies on rank-order
dissimilarities, making it resilient to zero-inflated ecological count
matrices.
The function supports input ingestion (data frames, matrices, lists),
implements standard ecological transformations (e.g., Hellinger,
Wisconsin double standardization), runs iterative ordination using
vegan::metaMDS, monitors convergence stress, overlays 95%
normal confidence ellipses, and projects significant environmental
vectors using vegan::envfit.
Let \(\mathbf{Y}_{n \times p}\) denote a community matrix of \(n\) sampling sites and \(p\) taxa. Pairwise ecological dissimilarity between site \(j\) and site \(k\) is calculated using an appropriate index (typically Bray-Curtis dissimilarity for abundance data):
\[d_{jk} = \frac{\sum_{i=1}^p |y_{ij} - y_{ik}|}{\sum_{i=1}^p (y_{ij} + y_{ik})}\]
NMDS seeks a low-dimensional configuration (\(k = 2\) or \(3\) dimensions) of site coordinates \(\mathbf{X}_{n \times k}\) such that the Euclidean distances between points in ordination space (\(\hat{d}_{jk} = \Vert{}\mathbf{x}_j - \mathbf{x}_k\Vert{}\)) preserve the monotonic rank order of the original dissimilarities (\(d_{jk}\)):
\[d_{jk} \approx f(\hat{d}_{jk})\]
where \(f\) is a monotonic step or regression function fitted using isotonic regression.
The goodness-of-fit of the configuration is measured by Kruskal’s Stress (\(S\)):
\[S = \sqrt{\frac{\sum_{j < k} (d_{jk} - \hat{d}_{jk})^2}{\sum_{j < k} d_{jk}^2}}\]
Stress measures the rank-order discrepancy between original ecological dissimilarities and low-dimensional mapped distances: - \(S < 0.05\): Excellent representation with minimal distortion. - \(0.05 \le S < 0.10\): Good ordination with reliable configuration. - \(0.10 \le S \le 0.20\): Usable ordination; 2D configuration provides an adequate representation, but higher-order details should be interpreted with care. - \(S > 0.20\): High stress; results should be interpreted cautiously, and increasing ordination dimensionality (\(k = 3\)) may be warranted.
To alleviate the “horseshoe” or “arch” effect common in raw Euclidean
distance models, transform = "hellinger" applies the
Hellinger metric:
\[y'_{ij} = \sqrt{\frac{y_{ij}}{\sum_{m=1}^p y_{mj}}}\]
This downweights dominant taxa and eliminates the zero-inflation paradox without dropping absent species.
We simulate a river basin macroinvertebrate community survey across
45 sampling stations distributed among three Habitat types
(Riffle, Run, Pool). Seven
diagnostic taxa counts are generated, along with three continuous water
quality parameters (Flow Velocity, Dissolved Oxygen, and Water
Temperature):
set.seed(808)
n_sites <- 15
habitats <- c("Riffle", "Run", "Pool")
# Macroinvertebrate community matrix (7 diagnostic taxa)
comm_list <- lapply(habitats, function(hab) {
# Baseline taxon abundance profiles
means <- switch(hab,
"Riffle" = c(Ephem = 28, Plecop = 22, Trichop = 18, Chirono = 4, Oligoch = 1, Odonat = 2, Gastrop = 1),
"Run" = c(Ephem = 12, Plecop = 8, Trichop = 14, Chirono = 16, Oligoch = 6, Odonat = 5, Gastrop = 3),
"Pool" = c(Ephem = 2, Plecop = 0, Trichop = 4, Chirono = 32, Oligoch = 24, Odonat = 12, Gastrop = 18)
)
# Simulate Poisson counts
mat <- do.call(cbind, lapply(means, function(m) rpois(n_sites, lambda = m)))
df <- as.data.frame(mat)
df$Habitat <- hab
df
})
macro_data <- do.call(rbind, comm_list)
# Place metadata as first column
macro_data <- macro_data[, c("Habitat", setdiff(names(macro_data), "Habitat"))]
# Corresponding environmental matrix
env_data <- data.frame(
Velocity_ms = c(rnorm(n_sites, 0.85, 0.1), rnorm(n_sites, 0.45, 0.08), rnorm(n_sites, 0.12, 0.04)),
DO_mgl = c(rnorm(n_sites, 9.2, 0.4), rnorm(n_sites, 7.8, 0.5), rnorm(n_sites, 5.4, 0.6)),
Temp_C = c(rnorm(n_sites, 19.5, 0.8), rnorm(n_sites, 22.0, 0.7), rnorm(n_sites, 25.8, 1.1))
)
head(macro_data[, 1:6], 6)
#> Habitat Ephem Plecop Trichop Chirono Oligoch
#> 1 Riffle 35 15 23 3 2
#> 2 Riffle 26 21 19 3 2
#> 3 Riffle 39 22 19 2 0
#> 4 Riffle 19 18 24 4 1
#> 5 Riffle 25 31 19 0 0
#> 6 Riffle 26 29 25 1 0nmds_analysis() displays sample site scores color-coded
by habitat with 95% confidence ellipses, and can overlay significant
environmental gradient vectors.
Computes Hellinger-transformed Bray-Curtis NMDS ordination and projects cluster confidence regions:
res_nmds <- nmds_analysis(
data = macro_data,
group_col = "Habitat",
transform = "hellinger",
dist_method = "bray",
palette = "Dark2"
)
# Render ordination plot
res_nmds$Plotenvfit)Supplying env_data automatically runs
vegan::envfit (999 permutations) and projects significant
(\(p \le 0.05\)) environmental
directional arrows into the ordination space:
res_nmds_env <- nmds_analysis(
data = macro_data,
group_col = "Habitat",
env_data = env_data,
transform = "hellinger",
dist_method = "bray",
palette = "Set2",
title = "Macroinvertebrate Community NMDS with Environmental Drivers"
)
res_nmds_env$PlotThe returned list provides the stress value, tidy site coordinate
scores, underlying metaMDS object, and permutation vector
fits:
# 1. Final Ordination Stress
res_nmds$Stress
#> [1] 0.03217201
# 2. Tidy NMDS Axis Coordinates
head(res_nmds$Plot_Data, 6)
#> NMDS1 NMDS2 Habitat
#> 1 -0.5252891 0.037631731 Riffle
#> 2 -0.4970030 -0.007769052 Riffle
#> 3 -0.5858803 -0.092789932 Riffle
#> 4 -0.4764525 -0.030577205 Riffle
#> 5 -0.6635751 -0.245218978 Riffle
#> 6 -0.7915610 -0.054026622 Riffle
# 3. Environmental Vector Fits (r2, p-values)
res_nmds_env$Envfit_Results
#>
#> ***VECTORS
#>
#> NMDS1 NMDS2 r2 Pr(>r)
#> Velocity_ms -0.92515 -0.37960 0.9032 0.001 ***
#> DO_mgl -0.99241 0.12297 0.9357 0.001 ***
#> Temp_C 0.97695 0.21348 0.8872 0.001 ***
#> ---
#> Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
#> Permutation: free
#> Number of permutations: 999The permanova_analysis() function performs
non-parametric multivariate hypothesis testing on ecological distance
matrices using vegan::adonis2. While ordination techniques
such as NMDS graphically portray patterns of community resemblance,
PERMANOVA tests whether spatial, seasonal, or experimental groups occupy
statistically distinct regions in multivariate ecological space.
Because community data are non-normal, zero-inflated, and contain complex species co-dependencies, standard parametric MANOVA assumptions (multivariate normality and homogeneous covariance matrices) are regularly violated. PERMANOVA resolves this by partitioning dissimilarity matrices and calculating significance via permutation testing.
Let \(\mathbf{D} = [d_{ij}]\) represent an \(N \times N\) symmetric ecological distance matrix (e.g., Bray-Curtis or Euclidean distances) among \(N\) samples across \(g\) distinct groups. The total sum of squared dissimilarities (\(\text{SS}_{\text{Total}}\)) is partitioned into within-group (\(\text{SS}_W\)) and between-group (\(\text{SS}_A\)) components:
\[\text{SS}_{\text{Total}} = \frac{1}{N} \sum_{i=1}^{N-1} \sum_{j=i+1}^N d_{ij}^2\]
\[\text{SS}_W = \sum_{k=1}^g \frac{1}{n_k} \sum_{i=1}^{n_k-1} \sum_{j=i+1}^{n_k} d_{ij(k)}^2\]
\[\text{SS}_A = \text{SS}_{\text{Total}} - \text{SS}_W\]
where \(n_k\) is the sample size in group \(k\).
The non-parametric pseudo-\(F\) statistic compares the between-group variance to within-group variance:
\[F = \frac{\text{SS}_A / (g - 1)}{\text{SS}_W / (N - g)}\]
Because the null distribution of pseudo-\(F\) does not follow an ordinary parametric \(F\)-distribution under arbitrary distance metrics, significance (\(p\)-value) is determined through Monte Carlo permutation of sample labels:
\[p = \frac{\sum_{m=1}^M \mathbb{I}(F^{\pi}_m \ge F_{\text{obs}}) + 1}{M + 1}\]
where \(M\) is the number of permutations (typically 999), \(F_{\text{obs}}\) is the pseudo-\(F\) calculated on the intact dataset, and \(F^{\pi}_m\) is the pseudo-\(F\) obtained from the \(m\)-th randomly permuted configuration.
We simulate a marine benthic community monitoring study assessing
macrobenthic invertebrate abundances across three seabed sediment
classes (Coarse Gravel, Fine Sand, and
Silty Mud):
set.seed(909)
n_reps <- 12
substrates <- c("Coarse Gravel", "Fine Sand", "Silty Mud")
benthic_survey <- do.call(rbind, lapply(substrates, function(sub) {
# Diagnostic species abundance baselines
means <- switch(sub,
"Coarse Gravel" = c(Polychaeta = 14, Bivalvia = 6, Amphipoda = 28, Decapoda = 12, Echinodermata = 16),
"Fine Sand" = c(Polychaeta = 22, Bivalvia = 18, Amphipoda = 15, Decapoda = 8, Echinodermata = 4),
"Silty Mud" = c(Polychaeta = 35, Bivalvia = 26, Amphipoda = 4, Decapoda = 2, Echinodermata = 1)
)
mat <- do.call(cbind, lapply(means, function(m) rpois(n_reps, lambda = m)))
df <- as.data.frame(mat)
df$Substrate <- sub
df
}))
# Organize Substrate as grouping metadata column
benthic_survey <- benthic_survey[, c("Substrate", setdiff(names(benthic_survey), "Substrate"))]
head(benthic_survey, 6)
#> Substrate Polychaeta Bivalvia Amphipoda Decapoda Echinodermata
#> 1 Coarse Gravel 17 10 23 11 15
#> 2 Coarse Gravel 14 15 33 9 21
#> 3 Coarse Gravel 14 12 40 15 23
#> 4 Coarse Gravel 11 6 31 13 14
#> 5 Coarse Gravel 19 5 26 13 15
#> 6 Coarse Gravel 9 7 23 10 14We test for differences in community structure across substrate types using the Bray-Curtis distance metric with Hellinger pre-transformation:
The returned list provides the ANOVA-style summary table detailing degrees of freedom, sum-of-squares, \(R^2\) (variation explained by the grouping factor), pseudo-\(F\) values, and permutation-based \(p\)-values:
# 1. PERMANOVA Table (adonis2 results)
res_permanova$PERMANOVA_Table
#> Permutation test for adonis under reduced model
#> Permutation: free
#> Number of permutations: 999
#>
#> vegan::adonis2(formula = formula_obj, data = design_df, permutations = permutations, method = dist_method)
#> Df SumOfSqs R2 F Pr(>F)
#> Model 2 0.86869 0.89866 146.32 0.001 ***
#> Residual 33 0.09796 0.10134
#> Total 35 0.96665 1.00000
#> ---
#> Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
# 2. Distance Metric Applied
res_permanova$Distance_Method
#> [1] "bray"
# 3. Cleaned and Transformed Data Preview
head(res_permanova$Cleaned_Data, 4)
#> Group Polychaeta Bivalvia Amphipoda Decapoda Echinodermata
#> 1 Coarse Gravel 0.4729527 0.3627381 0.5501196 0.3804430 0.4442617
#> 2 Coarse Gravel 0.3900947 0.4037864 0.5989121 0.3127716 0.4777665
#> 3 Coarse Gravel 0.3668997 0.3396831 0.6201737 0.3797773 0.4702700
#> 4 Coarse Gravel 0.3829708 0.2828427 0.6429101 0.4163332 0.4320494The simper_analysis() function identifies which
individual taxa or species drive community-level differences between
ecological groups. While NMDS visualizes spatial or experimental
separation and PERMANOVA evaluates whether those differences are
statistically significant, SIMPER (Similarity Percentages) decomposes
the overall Bray-Curtis dissimilarity into percentage contributions
attributable to each individual taxon.
In fisheries ecology, benthic surveys, and aquatic biomonitoring, SIMPER serves as an indicator species detection tool—revealing which particular taxa characterize undisturbed reference sites versus human-impacted habitats, or which species drive seasonal community turnover.
Consider two groups of samples, Group \(1\) and Group \(2\). The Bray-Curtis dissimilarity between sample \(j\) in Group \(1\) and sample \(k\) in Group \(2\) is given by:
\[d_{jk} = \frac{\sum_{i=1}^p |y_{ij} - y_{ik}|}{\sum_{i=1}^p (y_{ij} + y_{ik})}\]
SIMPER decomposes this total dissimilarity into the specific contribution of the \(i\)-th taxon:
\[\delta_{jk}^{(i)} = \frac{|y_{ij} - y_{ik}|}{\sum_{m=1}^p (y_{mj} + y_{mk})}\]
The average contribution of species \(i\) (\(\bar{\delta}_i\)) across all \(n_1 \times n_2\) pairwise comparisons between Group \(1\) and Group \(2\) is:
\[\bar{\delta}_i = \frac{1}{n_1 n_2} \sum_{j=1}^{n_1} \sum_{k=1}^{n_2} \delta_{jk}^{(i)}\]
Taxa are ranked in descending order of \(\bar{\delta}_i\). The cumulative percentage contribution of the top \(m\) species is:
\[\text{Cumulative Contribution (\%)} = \frac{\sum_{i=1}^m \bar{\delta}_i}{\sum_{i=1}^p \bar{\delta}_i} \times 100\]
A high average contribution combined with a consistent ratio of contribution to standard deviation indicates that a species is a reliable discriminator between the two environmental or biological groups.
We simulate a lake fish assemblage survey monitoring abundances
across three habitat zones (Littoral, Pelagic,
and Profundal). Six representative fish taxa are surveyed
across 36 sampling stations:
set.seed(1010)
n_stations <- 12
zones <- c("Littoral", "Pelagic", "Profundal")
lake_survey <- do.call(rbind, lapply(zones, function(zn) {
# Distinct abundance profiles across lake zones
means <- switch(zn,
"Littoral" = c(Perch = 32, Pike = 8, Roach = 45, Bream = 22, Bleak = 12, Zander = 2),
"Pelagic" = c(Perch = 10, Pike = 1, Roach = 18, Bream = 6, Bleak = 55, Zander = 14),
"Profundal" = c(Perch = 2, Pike = 0, Roach = 4, Bream = 1, Bleak = 2, Zander = 28)
)
mat <- do.call(cbind, lapply(means, function(m) rpois(n_stations, lambda = m)))
df <- as.data.frame(mat)
df$Zone <- zn
df
}))
# Organize Zone as grouping metadata column
lake_survey <- lake_survey[, c("Zone", setdiff(names(lake_survey), "Zone"))]
head(lake_survey, 6)
#> Zone Perch Pike Roach Bream Bleak Zander
#> 1 Littoral 32 10 45 34 8 0
#> 2 Littoral 26 8 49 17 12 3
#> 3 Littoral 37 6 38 25 11 3
#> 4 Littoral 43 7 47 24 15 0
#> 5 Littoral 36 6 30 20 17 1
#> 6 Littoral 37 11 42 21 11 1The simper_analysis() function extracts the top \(N\) taxa contributing to dissimilarity for
each pairwise comparison and displays them using faceted horizontal bar
charts.
Decomposes all pairwise group comparisons
(Littoral_Pelagic, Littoral_Profundal,
Pelagic_Profundal) and displays the top 5 discriminating
species per comparison:
res_simper <- simper_analysis(
data = lake_survey,
group_col = "Zone",
top_n = 5,
transform = "hellinger",
permutations = 999,
seed = 42
)
# Render faceted contribution plot
res_simper$PlotThe returned list contains the raw vegan::simper object,
along with a tidy data frame listing each species’ average contribution,
cumulative percentage, and permutation \(p\)-value:
# 1. Tidy summary table of top discriminating taxa
head(res_simper$Summary_Table, 10)
#> Comparison Species Contribution Cumulative p_value
#> 1 Littoral_Pelagic Bleak 0.08738423 0.2736940 0.001
#> 2 Littoral_Pelagic Zander 0.06289710 0.4706925 1.000
#> 3 Littoral_Pelagic Roach 0.04713252 0.6183151 0.010
#> 4 Littoral_Pelagic Pike 0.04134981 0.7478258 0.001
#> 5 Littoral_Pelagic Bream 0.04036191 0.8742423 0.976
#> 6 Littoral_Profundal Zander 0.19514771 0.3706790 0.001
#> 7 Littoral_Profundal Bream 0.08759999 0.5370734 0.001
#> 8 Littoral_Profundal Perch 0.08316557 0.6950446 0.001
#> 9 Littoral_Profundal Roach 0.07154487 0.8309426 0.001
#> 10 Littoral_Profundal Pike 0.06068598 0.9462144 0.001
# 2. Inspect raw vegan SIMPER summary for a specific comparison
summary(res_simper$SIMPER_Object)$Littoral_Pelagic[1:5, ]
#> average sd ratio ava avb cumsum p
#> Bleak 0.08738423 0.01342109 6.510964 0.3272274 0.70611825 0.2736940 0.001
#> Zander 0.06289710 0.01653698 3.803422 0.1018942 0.37505851 0.4706925 1.000
#> Roach 0.04713252 0.01538010 3.064513 0.6116993 0.40663320 0.6183151 0.010
#> Pike 0.04134981 0.01655756 2.497337 0.2377041 0.05898552 0.7478258 0.001
#> Bream 0.04036191 0.01918986 2.103294 0.4205971 0.24640228 0.8742423 0.976The pca_community_biplot() function performs
unconstrained linear ordination using Principal Component Analysis
(vegan::rda()) to project aquatic communities into a
reduced orthogonal subspace. While non-metric multidimensional scaling
(NMDS) is non-parametric and rank-based, PCA constructs synthetic axes
that maximize total explained variance.
A key advantage of PCA for ecological communities is the simultaneous display of both sampling sites (points) and taxa (loadings) as directional vectors—forming a classical biplot. In this representation, the cosine of the angle between two species vectors approximates their linear correlation, while the projection of site points onto a species arrow indicates the relative abundance of that taxon at those sampling stations.
Let \(\mathbf{Y}_{n \times p}\) denote an abundance or biomass matrix of \(n\) sampling sites and \(p\) aquatic species. PCA decomposes the centered (and optionally standardized) variance-covariance matrix \(\mathbf{S} = \frac{1}{n-1} \mathbf{Y}^T \mathbf{Y}\) via singular value decomposition (SVD) or spectral decomposition:
\[\mathbf{S} = \mathbf{V} \boldsymbol{\Lambda} \mathbf{V}^T\]
where: - \(\boldsymbol{\Lambda} = \text{diag}(\lambda_1, \lambda_2, \dots, \lambda_p)\) is the diagonal matrix of eigenvalues sorted in descending order (\(\lambda_1 \ge \lambda_2 \ge \dots \ge \lambda_p \ge 0\)), - \(\mathbf{V} = [\mathbf{v}_1, \mathbf{v}_2, \dots, \mathbf{v}_p]\) is the matrix of orthonormal eigenvectors containing the species loadings.
The percentage of total community variance explained by the \(k\)-th principal component (\(\text{PC}_k\)) is:
\[\text{Variance Explained (\%)} = \frac{\lambda_k}{\sum_{m=1}^p \lambda_m} \times 100\]
Site coordinates in the reduced 2D ordination space are computed as:
\[\mathbf{F} = \mathbf{Y} \mathbf{V}_2\]
where \(\mathbf{V}_2\) denotes the first two eigenvector columns corresponding to \(\lambda_1\) and \(\lambda_2\).
Raw Euclidean distance applied to unstandardized species counts can
suffer from the “horseshoe” or “arch” artifact because double zeros
(joint absences) are treated as evidence of ecological similarity. To
address this, transform = "hellinger" applies Legendre
& Gallagher’s Hellinger transformation prior to ordination:
\[y'_{ij} = \sqrt{\frac{y_{ij}}{\sum_{m=1}^p y_{mj}}}\]
This tb-PCA preserves Hellinger distances among samples, making PCA well suited for compositional abundance data.
We simulate an estuarine zooplankton community survey evaluating
counts of eight diagnostic taxa across three distinct salinity regimes
(Oligohaline, Mesohaline, and
Polyhaline):
set.seed(1111)
n_samples <- 14
salinity_regimes <- c("Oligohaline", "Mesohaline", "Polyhaline")
zooplankton_data <- do.call(rbind, lapply(salinity_regimes, function(reg) {
# Diagnostic zooplankton abundance profiles across salinity regimes
means <- switch(reg,
"Oligohaline" = c(Rotifer_A = 48, Rotifer_B = 35, Cladocera_A = 26, Cladocera_B = 18, Cyclopoid = 12, Calanoid = 4, Mysid = 1, Cirripede = 0),
"Mesohaline" = c(Rotifer_A = 12, Rotifer_B = 8, Cladocera_A = 14, Cladocera_B = 22, Cyclopoid = 32, Calanoid = 28, Mysid = 9, Cirripede = 5),
"Polyhaline" = c(Rotifer_A = 2, Rotifer_B = 0, Cladocera_A = 3, Cladocera_B = 4, Cyclopoid = 15, Calanoid = 42, Mysid = 24, Cirripede = 38)
)
mat <- do.call(cbind, lapply(means, function(m) rpois(n_samples, lambda = m)))
df <- as.data.frame(mat)
df$Salinity_Zone <- reg
df
}))
# Place metadata as first column
zooplankton_data <- zooplankton_data[, c("Salinity_Zone", setdiff(names(zooplankton_data), "Salinity_Zone"))]
head(zooplankton_data[, 1:6], 6)
#> Salinity_Zone Rotifer_A Rotifer_B Cladocera_A Cladocera_B Cyclopoid
#> 1 Oligohaline 47 34 26 15 9
#> 2 Oligohaline 57 29 28 12 12
#> 3 Oligohaline 52 48 19 15 16
#> 4 Oligohaline 56 30 26 15 10
#> 5 Oligohaline 48 42 29 16 13
#> 6 Oligohaline 27 36 27 23 14pca_community_biplot() plots site coordinates, adds 95%
normal confidence ellipses around environmental groups, and displays
directional species loading vectors.
Applies Hellinger transformation to avoid double-zero distortions while visualizing all species loading vectors alongside salinity cluster groupings:
res_pca_biplot <- pca_community_biplot(
data = zooplankton_data,
group_col = "Salinity_Zone",
transform = "hellinger",
scale = FALSE,
color_palette = "Dark2",
arrow_multiplier = 0.8
)
# Render community biplot
res_pca_biplot$PlotIn species-rich datasets, plotting all vectors can lead to visual
clutter. Setting top_n_species = 5 selectively displays
only the taxa with the longest loadings in the ordination plane:
res_pca_top5 <- pca_community_biplot(
data = zooplankton_data,
group_col = "Salinity_Zone",
transform = "hellinger",
top_n_species = 5,
color_palette = "Set1",
arrow_multiplier = 0.85,
title = "Zooplankton PCA Biplot (Top 5 Indicator Species)"
)
res_pca_top5$PlotThe returned list provides the underlying vegan::rda
model, coordinates of site scores with group labels, and species loading
vector magnitudes:
# 1. Inspect species vector coordinates and loading lengths
head(res_pca_biplot$Species_Scores, 6)
#> PC1 PC2 Species Vector_Length
#> Rotifer_A -0.55197481 0.078411347 Rotifer_A 0.5575164
#> Rotifer_B -0.59577347 0.007136423 Rotifer_B 0.5958162
#> Cladocera_A -0.34527594 -0.064900831 Cladocera_A 0.3513226
#> Cladocera_B -0.21734139 -0.190612221 Cladocera_B 0.2890853
#> Cyclopoid 0.05250865 -0.259792134 Cyclopoid 0.2650455
#> Calanoid 0.52674486 -0.185815423 Calanoid 0.5585584
# 2. Site ordination coordinates (PC1 and PC2 scores)
head(res_pca_biplot$Plot_Data, 6)
#> PC1 PC2 Salinity_Zone
#> sit1 -0.3314896 0.22274565 Oligohaline
#> sit2 -0.3336086 0.25922694 Oligohaline
#> sit3 -0.3051546 0.17602933 Oligohaline
#> sit4 -0.3545453 0.24684867 Oligohaline
#> sit5 -0.3411016 0.24157699 Oligohaline
#> sit6 -0.3295956 0.05113759 Oligohaline
# 3. Eigenvalues and total variance summary
summary(res_pca_biplot$PCA_Object)$cont$importance[, 1:2]
#> PC1 PC2
#> Eigenvalue 0.2023484 0.01946336
#> Proportion Explained 0.8698830 0.08367175
#> Cumulative Proportion 0.8698830 0.95355472The pca_correlation_circle() function visualizes the
relationship among multidimensional environmental, limnological, and
hydrochemical variables using a PCA variables factor map (correlation
circle). When analyzing water quality matrices (e.g., pH, dissolved
oxygen, nutrients, heavy metal concentrations, conductivity), variables
are measured on different physical scales and often exhibit
collinearities.
The function standardizes environmental variables to unit variance (\(\mu = 0, \sigma = 1\)), extracts variable correlations with the leading principal components, evaluates the quality of representation using the squared cosine (\(\cos^2\)) metric, and projects the variables within a unit correlation circle.
Let \(\mathbf{X}_{n \times p}\) denote an environmental dataset containing \(n\) water sampling stations and \(p\) physico-chemical parameters. Standardizing columns to unit variance yields:
\[z_{ij} = \frac{x_{ij} - \bar{x}_j}{s_j}\]
PCA on the standardized matrix decomposes the Pearson correlation matrix \(\mathbf{R} = \frac{1}{n-1} \mathbf{Z}^T \mathbf{Z}\):
\[\mathbf{R} = \mathbf{V} \boldsymbol{\Lambda} \mathbf{V}^T\]
The coordinate of variable \(j\) along principal component \(k\) represents the Pearson correlation coefficient between that environmental variable and component axis \(k\):
\[r(x_j, \text{PC}_k) = v_{jk} \sqrt{\lambda_k}\]
where \(v_{jk}\) is the eigenvector loading and \(\lambda_k\) is the eigenvalue of the \(k\)-th principal component.
Because the variables are normalized, the maximum theoretical length of any vector in the full \(p\)-dimensional space is \(1.0\). When projected onto the 2D plane defined by \(\text{PC}_1\) and \(\text{PC}_2\), the squared distance from the origin represents the squared cosine (\(\cos^2\)):
\[\cos^2_j = r(x_j, \text{PC}_1)^2 + r(x_j, \text{PC}_2)^2\]
Interpretation criteria: - \(\cos^2 \to 1.0\) (Vector touches the unit circle): The variable is well represented in the selected 2D ordination plane; its position and angles accurately reflect relationships with other variables. - \(\cos^2 \to 0\) (Vector remains near the center): The variable is poorly represented in this 2D projection; inferences about its dynamics should be confirmed using higher-order principal components. - Angle Between Vectors: - \(\theta \approx 0^\circ\) (\(\cos \theta \to +1\)): Strong positive correlation. - \(\theta \approx 90^\circ\) (\(\cos \theta \to 0\)): Orthogonal (uncorrelated) variables. - \(\theta \approx 180^\circ\) (\(\cos \theta \to -1\)): Strong inverse correlation.
We simulate a limnological survey measuring eight physico-chemical water quality parameters across 40 sampling stations along a river-to-reservoir transition:
set.seed(1212)
n_stations <- 40
# Simulate correlated water quality gradients
temperature <- rnorm(n_stations, mean = 24.5, sd = 3.0)
do_mgl <- 14.5 - (0.35 * temperature) + rnorm(n_stations, 0, 0.5)
ph_val <- 7.2 + (0.04 * temperature) + rnorm(n_stations, 0, 0.3)
turbidity <- runif(n_stations, 5, 45)
secchi_cm <- 120 - (2.1 * turbidity) + rnorm(n_stations, 0, 4)
bod_mgl <- 1.2 + (0.08 * turbidity) + rnorm(n_stations, 0, 0.4)
nitrate_mgl <- rnorm(n_stations, mean = 2.4, sd = 0.6)
phosphate <- 0.15 + (0.05 * nitrate_mgl) + rnorm(n_stations, 0, 0.03)
water_quality <- data.frame(
StationID = paste0("ST_", sprintf("%02d", 1:n_stations)),
Temperature = temperature,
DO = do_mgl,
pH = ph_val,
Turbidity = turbidity,
Secchi = secchi_cm,
BOD = bod_mgl,
Nitrate = nitrate_mgl,
Phosphate = phosphate
)
head(water_quality, 6)
#> StationID Temperature DO pH Turbidity Secchi BOD Nitrate
#> 1 ST_01 22.61274 6.874055 7.905515 10.44149 104.99044 2.405120 2.033309
#> 2 ST_02 30.01227 3.943311 7.917997 16.77372 88.49811 2.137321 2.090744
#> 3 ST_03 25.53104 5.307665 8.238213 11.67667 97.99624 2.446044 2.602819
#> 4 ST_04 20.41971 6.597412 8.412772 18.24393 85.96159 2.811601 2.239365
#> 5 ST_05 26.10744 5.302458 7.879704 32.13295 45.15147 3.525980 2.781280
#> 6 ST_06 24.61160 5.835160 8.248662 19.75954 74.96844 3.245458 1.809519
#> Phosphate
#> 1 0.2599896
#> 2 0.2041083
#> 3 0.2793542
#> 4 0.2462373
#> 5 0.2738273
#> 6 0.2417423The pca_correlation_circle() function generates a unit
circle with variables colored according to their representation quality
(\(\cos^2\)).
Excludes the identifier column
(exclude_cols = "StationID"), projects variables onto \(\text{PC}_1\) and \(\text{PC}_2\), and applies a color scale
based on representation quality:
res_pca_circle <- pca_correlation_circle(
data = water_quality,
exclude_cols = "StationID",
dim1 = 1,
dim2 = 2,
palette = c("#2A9D8F", "#E9C46A", "#F4A261", "#E76F51"),
title = "Water Quality Parameters Correlation Circle"
)
# Render correlation circle
res_pca_circle$PlotThe returned list provides the variable coordinate correlations, \(\cos^2\) representation values, and the variance breakdown for each principal component:
# 1. Variable coordinates along PC1 and PC2 with cos2 representation quality
res_pca_circle$Variable_Stats
#> x y Variable cos2
#> Temperature 0.6248845 0.6942111 Temperature 0.87240966
#> DO -0.5404295 -0.7708735 DO 0.88631008
#> pH 0.2395007 0.5537403 pH 0.36398892
#> Turbidity 0.9181992 -0.3522868 Turbidity 0.96719586
#> Secchi -0.9160075 0.3283661 Secchi 0.94689408
#> BOD 0.8790032 -0.3569626 BOD 0.90006888
#> Nitrate -0.1358684 0.2706143 Nitrate 0.09169232
#> Phosphate -0.0388324 0.2119688 Phosphate 0.04643871
# 2. Percentage and cumulative variance explained per principal component
res_pca_circle$Variance_Summary
#> Dimension Eigenvalue Variance_Percent Cumulative_Variance
#> 1 PC1 3.21467972 40.1834965 40.18350
#> 2 PC2 1.86031879 23.2539849 63.43748
#> 3 PC3 1.61039716 20.1299644 83.56745
#> 4 PC4 0.79743347 9.9679184 93.53536
#> 5 PC5 0.35996347 4.4995434 98.03491
#> 6 PC6 0.10354818 1.2943522 99.32926
#> 7 PC7 0.04227383 0.5284228 99.85768
#> 8 PC8 0.01138539 0.1423173 100.00000
# 3. Individual station ordination scores (first 6 samples)
head(res_pca_circle$Sample_Scores[, 1:4], 6)
#> PC1 PC2 PC3 PC4
#> 1 -1.7931263 -0.34086385 0.70640904 0.5390157
#> 2 0.1295898 1.55184446 2.16313645 1.4574737
#> 3 -0.8845816 1.32301245 -0.06146701 0.2188447
#> 4 -0.8460891 -0.38276489 0.51581956 -1.1101072
#> 5 1.5072193 -0.22394477 -0.70224218 0.9365320
#> 6 0.2438725 -0.05787865 1.22990777 -0.1559714The rda_analysis() function performs direct gradient
analysis (constrained linear ordination) to model the variation in
aquatic community composition directly explained by a set of continuous
environmental variables. While unconstrained methods (PCA, NMDS)
ordinate community matrices independently of external drivers,
Redundancy Analysis combines multivariate multiple linear regression
with principal component analysis.
The resulting ordination axes represent linear combinations of explanatory environmental predictors, ordered by the amount of community variance they explain. The function coordinates data cleaning across both matrices, evaluates global and marginal variable significance through non-parametric Monte Carlo permutations, and renders a publication-ready ordination Triplot (simultaneously presenting sampling sites, species loadings, and environmental vectors).
Let \(\mathbf{Y}_{n \times p}\) denote the community abundance matrix (Hellinger-transformed) across \(n\) sampling stations and \(p\) aquatic species, and let \(\mathbf{X}_{n \times m}\) denote the standardized matrix of \(m\) environmental predictors.
RDA executes in two mathematical phases:
Each species column \(\mathbf{y}_j\) is regressed onto the complete environmental matrix \(\mathbf{X}\):
\[\hat{\mathbf{Y}} = \mathbf{X} (\mathbf{X}^T \mathbf{X})^{-1} \mathbf{X}^T \mathbf{Y} = \mathbf{H} \mathbf{Y}\]
where \(\hat{\mathbf{Y}}\) contains the fitted values explained by the environmental variables, and \(\mathbf{H}\) represents the projection “hat” matrix. The residual matrix \(\mathbf{Y}_{\text{res}} = \mathbf{Y} - \hat{\mathbf{Y}}\) captures unconstrained community variation.
PCA is conducted on the matrix of fitted values \(\hat{\mathbf{Y}}\) via singular value decomposition:
\[\frac{1}{n-1} \hat{\mathbf{Y}}^T \hat{\mathbf{Y}} = \mathbf{V} \boldsymbol{\Lambda} \mathbf{V}^T\]
The canonical eigenvalues \(\lambda_k \in \boldsymbol{\Lambda}\) measure the variance explained by each constrained canonical axis (\(\text{RDA}_k\)). The constrained proportion of total community inertia is given by:
\[R^2_{\text{biplot}} = \frac{\sum_{k=1}^m \lambda_k}{\text{Total Community Inertia}}\]
Significance is determined via permutation testing
(permutations = 999) using pseudo-\(F\) statistics:
\[F = \frac{\text{SS}_{\text{constrained}} / m}{\text{SS}_{\text{residual}} / (n - m - 1)}\]
Marginal permutation testing (by = "margin") tests the
unique partial contribution of each environmental variable while
controlling for all other predictors in the model.
We simulate a riverine fish community monitoring study assessing the
abundances of seven freshwater fish species across 36 sampling stations
spanning three distinct River Zones (Headwater,
Mid-River, Lowland Floodplain). The community
matrix is matched with four continuous water quality parameters (Water
Temperature, Dissolved Oxygen, Flow Velocity, and Conductivity):
set.seed(1313)
n_sites <- 12
zones <- c("Headwater", "Mid-River", "Lowland")
# Simulated environmental matrix
env_list <- lapply(zones, function(zn) {
switch(zn,
"Headwater" = data.frame(
Temp_C = rnorm(n_sites, 14.5, 0.8),
DO_mgL = rnorm(n_sites, 10.2, 0.4),
Flow_ms = rnorm(n_sites, 0.95, 0.1),
Cond_uS = rnorm(n_sites, 120, 15)
),
"Mid-River" = data.frame(
Temp_C = rnorm(n_sites, 19.0, 0.7),
DO_mgL = rnorm(n_sites, 8.1, 0.5),
Flow_ms = rnorm(n_sites, 0.55, 0.08),
Cond_uS = rnorm(n_sites, 280, 25)
),
"Lowland" = data.frame(
Temp_C = rnorm(n_sites, 24.5, 1.0),
DO_mgL = rnorm(n_sites, 5.8, 0.6),
Flow_ms = rnorm(n_sites, 0.18, 0.05),
Cond_uS = rnorm(n_sites, 490, 35)
)
)
})
env_survey <- do.call(rbind, env_list)
# Matched community counts (7 fish species)
comm_list <- lapply(zones, function(zn) {
means <- switch(zn,
"Headwater" = c(Trout = 30, Sculpin = 22, Dace = 12, Minnow = 4, Barb = 1, Catfish = 0, Carp = 0),
"Mid-River" = c(Trout = 6, Sculpin = 8, Dace = 24, Minnow = 28, Barb = 14, Catfish = 4, Carp = 2),
"Lowland" = c(Trout = 0, Sculpin = 1, Dace = 5, Minnow = 12, Barb = 32, Catfish = 26, Carp = 38)
)
mat <- do.call(cbind, lapply(means, function(m) rpois(n_sites, lambda = m)))
df <- as.data.frame(mat)
df$Zone <- zn
df
})
comm_survey <- do.call(rbind, comm_list)
# Metadata as first column
comm_survey <- comm_survey[, c("Zone", setdiff(names(comm_survey), "Zone"))]
head(comm_survey[, 1:6], 4)
#> Zone Trout Sculpin Dace Minnow Barb
#> 1 Headwater 25 22 11 7 0
#> 2 Headwater 26 23 19 6 0
#> 3 Headwater 32 15 12 3 2
#> 4 Headwater 29 17 7 6 1
head(env_survey, 4)
#> Temp_C DO_mgL Flow_ms Cond_uS
#> 1 15.45148 10.011911 0.9047721 125.05351
#> 2 14.94111 9.966365 0.9534446 127.99818
#> 3 14.78291 10.219464 0.9291478 135.87260
#> 4 13.93682 10.100078 1.1271965 95.68784The rda_analysis() function creates a triplot
integrating site distributions, taxon vectors, and environmental
directional arrows.
Applies Hellinger pre-transformation to the community matrix, standardizes environmental variables, projects 95% group confidence ellipses, and displays repelled labels for species and environmental drivers:
res_rda <- rda_analysis(
comm_data = comm_survey,
env_data = env_survey,
group_col = "Zone",
transform = "hellinger",
color_palette = "Dark2",
species_arrow_mult = 1.0,
env_arrow_mult = 1.2
)
# Render publication triplot
res_rda$PlotThe returned list provides the underlying vegan::rda
object, global model permutation significance, and marginal variable
significance:
# 1. Global RDA Model Permutation Test (adonis/anova)
res_rda$Model_Significance
#> Permutation test for rda under reduced model
#> Permutation: free
#> Number of permutations: 999
#>
#> Model: rda(formula = comm_mat ~ Temp_C + DO_mgL + Flow_ms + Cond_uS, data = env_mat_scaled)
#> Df Variance F Pr(>F)
#> Model 4 0.268865 40.447 0.001 ***
#> Residual 31 0.051517
#> ---
#> Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
# 2. Marginal Environmental Term Significance (unique partial effects)
res_rda$Variable_Significance
#> Permutation test for rda under reduced model
#> Marginal effects of terms
#> Permutation: free
#> Number of permutations: 999
#>
#> Model: rda(formula = comm_mat ~ Temp_C + DO_mgL + Flow_ms + Cond_uS, data = env_mat_scaled)
#> Df Variance F Pr(>F)
#> Temp_C 1 0.010027 6.0334 0.014 *
#> DO_mgL 1 0.005364 3.2277 0.066 .
#> Flow_ms 1 0.002476 1.4900 0.232
#> Cond_uS 1 0.002079 1.2511 0.271
#> Residual 31 0.051517
#> ---
#> Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
# 3. Constrained vs Unconstrained Inertia Breakdown
summary(res_rda$RDA_Object)$tot.chi
#> [1] 0.3203818
summary(res_rda$RDA_Object)$concont$importance[, 1:2]
#> RDA1 RDA2
#> Eigenvalue 0.2582838 0.009324137
#> Proportion Explained 0.9606456 0.034679644
#> Cumulative Proportion 0.9606456 0.995325281The dbrda_analysis() function performs constrained
ordination using non-Euclidean ecological distance metrics via
Distance-based Redundancy Analysis (vegan::dbrda).
Classical Redundancy Analysis (RDA) relies on Euclidean distances, which
can lead to distortion when analyzing zero-skewed ecological abundance
matrices.
Distance-based RDA resolves this limitation by conducting Principal Coordinate Analysis (PCoA) on an arbitrary dissimilarity matrix (e.g., Bray-Curtis, Jaccard, or Kulczynski) and using the resulting metric coordinate axes as the response matrix in a standard RDA. This enables direct multivariate regression of semi-metric ecological dissimilarities against continuous environmental predictors while avoiding negative eigenvalue artifacts.
Let \(\mathbf{D} = [d_{ij}]_{n \times n}\) denote an ecological distance matrix among \(n\) sampling sites. The matrix \(\mathbf{D}\) is centered via Gower’s centering transformation:
\[\mathbf{G} = -\frac{1}{2} \left( \mathbf{I} - \frac{1}{n} \mathbf{11}^T \right) (\mathbf{D}^{(2)}) \left( \mathbf{I} - \frac{1}{n} \mathbf{11}^T \right)\]
where \(\mathbf{D}^{(2)} = [d_{ij}^2]\) is the matrix of squared dissimilarities.
Spectral decomposition of \(\mathbf{G}\) produces eigenvalues \(\boldsymbol{\Lambda}\) and eigenvectors \(\mathbf{V}\):
\[\mathbf{G} = \mathbf{V} \boldsymbol{\Lambda} \mathbf{V}^T\]
Principal coordinate vectors with positive eigenvalues \(\mathbf{Z} = \mathbf{V}_+ \boldsymbol{\Lambda}_+^{1/2}\) represent the Euclidean embedding of the original dissimilarity matrix.
The coordinate matrix \(\mathbf{Z}\) is regressed onto the standardized environmental predictor matrix \(\mathbf{X}_{n \times m}\):
\[\hat{\mathbf{Z}} = \mathbf{X} (\mathbf{X}^T \mathbf{X})^{-1} \mathbf{X}^T \mathbf{Z}\]
PCA is subsequently applied to \(\hat{\mathbf{Z}}\) to construct the canonical axes (\(\text{dbRDA}_1, \text{dbRDA}_2\)). Overall model significance and the marginal contributions of individual environmental variables are evaluated through permutation testing (pseudo-\(F\) tests with 999 Monte Carlo permutations):
\[F = \frac{\text{tr}(\hat{\mathbf{Z}}^T \hat{\mathbf{Z}}) / m}{\text{tr}(\mathbf{E}^T \mathbf{E}) / (n - m - 1)}\]
where \(\mathbf{E} = \mathbf{Z} - \hat{\mathbf{Z}}\) represents the residual matrix of unconstrained variation.
We simulate an estuarine benthic macroinvertebrate community across
36 stations classified into three Hydrological Zones
(Upper Estuary, Middle Basin,
Outer Marine). The community dataset is paired with four
continuous sediment and water column metrics (Salinity, Total Organic
Carbon, Silt Fraction, and Water Depth):
set.seed(1414)
n_stations <- 12
zones <- c("Upper Estuary", "Middle Basin", "Outer Marine")
# Simulated environmental predictors
env_list <- lapply(zones, function(zn) {
switch(zn,
"Upper Estuary" = data.frame(
Salinity_PSU = rnorm(n_stations, 8.5, 1.2),
TOC_percent = rnorm(n_stations, 4.2, 0.5),
Silt_percent = rnorm(n_stations, 68, 6),
Depth_m = rnorm(n_stations, 3.5, 0.4)
),
"Middle Basin" = data.frame(
Salinity_PSU = rnorm(n_stations, 18.0, 1.5),
TOC_percent = rnorm(n_stations, 2.6, 0.4),
Silt_percent = rnorm(n_stations, 42, 5),
Depth_m = rnorm(n_stations, 8.2, 0.8)
),
"Outer Marine" = data.frame(
Salinity_PSU = rnorm(n_stations, 32.5, 0.9),
TOC_percent = rnorm(n_stations, 1.1, 0.2),
Silt_percent = rnorm(n_stations, 15, 3),
Depth_m = rnorm(n_stations, 18.5, 1.5)
)
)
})
env_estuary <- do.call(rbind, env_list)
# Simulated benthic community matrix (6 diagnostic taxa)
comm_list <- lapply(zones, function(zn) {
means <- switch(zn,
"Upper Estuary" = c(Oligochaeta = 42, Chironomidae = 35, Polychaeta_A = 6, Bivalvia_A = 2, Amphipoda_A = 4, Decapoda_A = 0),
"Middle Basin" = c(Oligochaeta = 14, Chironomidae = 18, Polychaeta_A = 28, Bivalvia_A = 22, Amphipoda_A = 16, Decapoda_A = 5),
"Outer Marine" = c(Oligochaeta = 1, Chironomidae = 0, Polychaeta_A = 48, Bivalvia_A = 34, Amphipoda_A = 30, Decapoda_A = 18)
)
mat <- do.call(cbind, lapply(means, function(m) rpois(n_stations, lambda = m)))
df <- as.data.frame(mat)
df$Estuary_Zone <- zn
df
})
comm_estuary <- do.call(rbind, comm_list)
# Set grouping metadata as first column
comm_estuary <- comm_estuary[, c("Estuary_Zone", setdiff(names(comm_estuary), "Estuary_Zone"))]
head(comm_estuary[, 1:6], 4)
#> Estuary_Zone Oligochaeta Chironomidae Polychaeta_A Bivalvia_A Amphipoda_A
#> 1 Upper Estuary 45 32 11 0 3
#> 2 Upper Estuary 35 26 6 3 11
#> 3 Upper Estuary 38 29 4 1 1
#> 4 Upper Estuary 47 37 3 2 3
head(env_estuary, 4)
#> Salinity_PSU TOC_percent Silt_percent Depth_m
#> 1 7.599730 3.919065 56.09989 3.218138
#> 2 8.395406 3.957087 79.05169 3.487593
#> 3 9.941565 3.902267 64.37852 3.872967
#> 4 8.293515 4.436544 50.99208 3.262603The dbrda_analysis() function displays the sample
coordinates, 95% group confidence ellipses, species vectors, and
environmental directional arrows on a single coordinate grid.
Computes Bray-Curtis dissimilarity, applies environmental scaling, and produces the publication triplot:
res_dbrda <- dbrda_analysis(
comm_data = comm_estuary,
env_data = env_estuary,
group_col = "Estuary_Zone",
dist_method = "bray",
transform = "none",
color_palette = "Dark2",
species_arrow_mult = 1.0,
env_arrow_mult = 1.1
)
# Render publication triplot
res_dbrda$PlotThe returned list provides the underlying vegan::dbrda
model object, the global permutation significance test, and marginal
term significance:
# 1. Global db-RDA Permutation Test (adonis/anova)
res_dbrda$Model_Significance
#> Permutation test for dbrda under reduced model
#> Permutation: free
#> Number of permutations: 999
#>
#> Model: vegan::dbrda(formula = comm_mat ~ Salinity_PSU + TOC_percent + Silt_percent + Depth_m, data = env_mat_scaled, distance = dist_method)
#> Df SumOfSqs F Pr(>F)
#> Model 4 4.2675 60.5 0.001 ***
#> Residual 31 0.5467
#> ---
#> Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
# 2. Marginal Environmental Predictor Significance (unique partial variance)
res_dbrda$Variable_Significance
#> Permutation test for dbrda under reduced model
#> Marginal effects of terms
#> Permutation: free
#> Number of permutations: 999
#>
#> Model: vegan::dbrda(formula = comm_mat ~ Salinity_PSU + TOC_percent + Silt_percent + Depth_m, data = env_mat_scaled, distance = dist_method)
#> Df SumOfSqs F Pr(>F)
#> Salinity_PSU 1 0.04813 2.7295 0.108
#> TOC_percent 1 0.12682 7.1919 0.012 *
#> Silt_percent 1 -0.00053 -0.0303 0.998
#> Depth_m 1 0.02884 1.6355 0.225
#> Residual 31 0.54666
#> ---
#> Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
# 3. Constrained vs Unconstrained Variance Breakdown
summary(res_dbrda$dbRDA_Object)$tot.chi
#> [1] 4.814109
summary(res_dbrda$dbRDA_Object)$concont$importance[, 1:2]
#> dbRDA1 dbRDA2
#> Eigenvalue 4.2176612 0.05124736
#> Proportion Explained 0.9883327 0.01200889
#> Cumulative Proportion 0.9883327 1.00034161The cluster_dendrogram() function performs agglomerative
hierarchical cluster analysis (HCA) on ecological community abundance
matrices or multi-parameter environmental datasets. While ordination
approaches (e.g., NMDS, PCA, RDA) display continuous gradients across
reduced orthogonal dimensions, hierarchical clustering groups sampling
units into discrete nested clusters based on pairwise resemblance.
The function computes dissimilarity matrices using standard
ecological distance metrics (e.g., Bray-Curtis, Jaccard, Euclidean),
applies optional community transformations, conducts agglomerative
clustering across user-selected linkage criteria (UPGMA, Ward’s minimum
variance, Complete), and generates a native ggplot2 tree
diagram complete with color-coded group leaves, an optional cut-off
threshold line, and sample cluster assignments.
Let \(\mathbf{Y}_{n \times p}\) denote a community matrix of \(n\) sampling stations and \(p\) aquatic taxa. Pairwise ecological dissimilarity \(d_{ij}\) is computed using the Bray-Curtis metric:
\[d_{ij} = \frac{\sum_{k=1}^p |y_{ik} - y_{jk}|}{\sum_{k=1}^p (y_{ik} + y_{jk})}\]
Agglomerative hierarchical clustering begins with each sampling unit treated as a single-element cluster (\(C_i = \{i\}\)). At each sequential step, the two clusters with the minimum inter-cluster distance are merged until all samples are unified into a single root node.
The definition of distance between two composite clusters \(A\) and \(B\) depends on the chosen linkage method:
Unweighted Pair-Group Method with Arithmetic Mean (UPGMA /
cluster_method = "average"): Calculates the mean pairwise
dissimilarity between all members of cluster \(A\) and cluster \(B\): \[D(A, B) =
\frac{1}{|A| \cdot |B|} \sum_{i \in A} \sum_{j \in B} d_{ij}\]
Preserves cophenetic correlation with minimal spatial
distortion.
Ward’s Minimum Variance
(cluster_method = "ward.D2"): Merges the cluster pair that
minimizes the increase in total within-cluster error sum of squares
(ESS): \[\Delta \text{ESS}_{AB} = \frac{|A|
\cdot |B|}{|A| + |B|} \|\bar{\mathbf{y}}_A -
\bar{\mathbf{y}}_B\|^2\] Tends to produce compact, spherical, and
balanced cluster topologies.
Complete Linkage (cluster_method = "complete"):
Defines distance as the maximum pairwise dissimilarity between clusters:
\[D(A, B) = \max \{d_{ij} : i \in A, \, j \in
B\}\]
We simulate a limnological biomonitoring survey assessing
phytoplankton community counts across 24 sampling stations situated in
three lake zones (Littoral, Pelagic,
Hypolimnetic Inlet):
set.seed(1515)
n_per_zone <- 8
lake_zones <- c("Littoral", "Pelagic", "Hypolimnetic")
phyto_list <- lapply(lake_zones, function(zn) {
means <- switch(zn,
"Littoral" = c(Diatom_A = 35, Diatom_B = 24, Cyanob_A = 4, Cyanob_B = 1, Green_A = 18, Green_B = 12),
"Pelagic" = c(Diatom_A = 12, Diatom_B = 8, Cyanob_A = 42, Cyanob_B = 30, Green_A = 25, Green_B = 18),
"Hypolimnetic" = c(Diatom_A = 4, Diatom_B = 2, Cyanob_A = 2, Cyanob_B = 0, Green_A = 6, Green_B = 4)
)
mat <- do.call(cbind, lapply(means, function(m) rpois(n_per_zone, lambda = m)))
df <- as.data.frame(mat)
df$Zone <- zn
df
})
phyto_survey <- do.call(rbind, phyto_list)
rownames(phyto_survey) <- paste0("ST_", sprintf("%02d", seq_len(nrow(phyto_survey))))
# Organize grouping metadata column
phyto_survey <- phyto_survey[, c("Zone", setdiff(names(phyto_survey), "Zone"))]
head(phyto_survey[, 1:5], 5)
#> Zone Diatom_A Diatom_B Cyanob_A Cyanob_B
#> ST_01 Littoral 29 24 7 3
#> ST_02 Littoral 33 16 3 0
#> ST_03 Littoral 40 14 1 1
#> ST_04 Littoral 40 30 1 3
#> ST_05 Littoral 34 29 4 5The cluster_dendrogram() function generates vertical and
horizontal dendrogram views with group-coded leaf tips.
Applies Hellinger pre-transformation with Bray-Curtis dissimilarity
and UPGMA (average) clustering, displaying a threshold line
at \(k = 3\):
res_dendro <- cluster_dendrogram(
data = phyto_survey,
group_col = "Zone",
dist_method = "bray",
transform = "hellinger",
cluster_method = "average",
k = 3,
palette = "Dark2",
orientation = "vertical"
)
# Render publication tree
res_dendro$PlotUsing orientation = "horizontal" flips the coordinate
axes, making station labels easier to read when sample sizes are
larger:
res_dendro_horiz <- cluster_dendrogram(
data = phyto_survey,
group_col = "Zone",
dist_method = "bray",
transform = "hellinger",
cluster_method = "ward.D2",
k = 3,
palette = "Dark2",
orientation = "horizontal",
title = "Phytoplankton Assemblage Clusters (Ward.D2 Linkage)"
)
res_dendro_horiz$PlotThe returned list provides the sample-to-cluster assignment table,
dissimilarity matrix, and underlying hclust object:
# 1. Sample Cluster Membership Assignments
head(res_dendro$Cluster_Assignments, 10)
#> Sample Zone Cluster
#> 1 ST_01 Littoral Cluster_1
#> 2 ST_02 Littoral Cluster_1
#> 3 ST_03 Littoral Cluster_1
#> 4 ST_04 Littoral Cluster_1
#> 5 ST_05 Littoral Cluster_1
#> 6 ST_06 Littoral Cluster_1
#> 7 ST_07 Littoral Cluster_1
#> 8 ST_08 Littoral Cluster_1
#> 9 ST_09 Pelagic Cluster_2
#> 10 ST_10 Pelagic Cluster_2
# 2. Inspect Agglomerative Cluster Merges
res_dendro$HClust_Object$merge[1:6, ]
#> [,1] [,2]
#> [1,] -13 -16
#> [2,] -14 1
#> [3,] -4 -7
#> [4,] -5 -6
#> [5,] -15 2
#> [6,] -9 5
# 3. Distance Matrix Preview (First 4 Samples)
as.matrix(res_dendro$Distance_Matrix)[1:4, 1:4]
#> ST_01 ST_02 ST_03 ST_04
#> ST_01 0.00000000 0.1173526 0.11920702 0.06805391
#> ST_02 0.11735263 0.0000000 0.10490610 0.10833905
#> ST_03 0.11920702 0.1049061 0.00000000 0.08964695
#> ST_04 0.06805391 0.1083391 0.08964695 0.00000000The correlation_heatmap() function generates
publication-ready correlation heatmaps for multidimensional
environmental, limnological, and fisheries datasets. In aquatic
ecosystem monitoring, evaluating pairwise associations among water
quality parameters (e.g., Temperature, Dissolved Oxygen, Nutrients,
Turbidity) is an essential exploratory step before conducting
constrained ordinations or regression modeling.
The function computes correlation coefficients across Pearson,
Spearman, or Kendall formulations, extracts pairwise significance \(p\)-values, supports directional matrix
views (lower, upper, or full),
provides alternative tile geometries (square or
circle), and dynamically adjusts annotation font contrast
across diverging color palettes.
For two continuous variables \(X\) and \(Y\) observed across \(n\) sampling stations:
Measures linear dependency between normally distributed continuous variables:
\[r_{xy} = \frac{\sum_{i=1}^n (x_i - \bar{x})(y_i - \bar{y})}{\sqrt{\sum_{i=1}^n (x_i - \bar{x})^2 \sum_{i=1}^n (y_i - \bar{y})^2}}\]
The statistical significance of \(r_{xy}\) is evaluated using a two-tailed Student’s \(t\)-distribution with \(\nu = n - 2\) degrees of freedom:
\[t = r_{xy} \sqrt{\frac{n - 2}{1 - r_{xy}^2}}\]
Measures monotonic relationships between non-normally distributed or ordinal parameters by computing Pearson’s coefficient on ranked values:
\[\rho = 1 - \frac{6 \sum_{i=1}^n d_i^2}{n(n^2 - 1)}\]
where \(d_i = \text{rank}(x_i) - \text{rank}(y_i)\).
Pairwise significance thresholds are mapped to standard compact asterisks: - \(\text{ns}: p \ge 0.05\) - \(*: p < 0.05\) - \(**: p < 0.01\) - \(***: p < 0.001\)
We simulate a multi-parameter aquatic environmental survey evaluating seven correlated physico-chemical variables recorded across 50 limnological stations:
set.seed(1616)
n_stations <- 50
temp_c <- rnorm(n_stations, mean = 23.5, sd = 2.8)
do_mgl <- 13.8 - (0.32 * temp_c) + rnorm(n_stations, 0, 0.4)
ph_val <- 7.1 + (0.05 * temp_c) + rnorm(n_stations, 0, 0.25)
turb_ntu <- runif(n_stations, 4, 38)
secchi_m <- 2.8 - (0.055 * turb_ntu) + rnorm(n_stations, 0, 0.2)
tp_mgl <- 0.04 + (0.006 * turb_ntu) + rnorm(n_stations, 0, 0.02)
tn_mgl <- 0.85 + (12.5 * tp_mgl) + rnorm(n_stations, 0, 0.15)
limno_env <- data.frame(
Temperature = temp_c,
DO = do_mgl,
pH = ph_val,
Turbidity = turb_ntu,
Secchi = secchi_m,
Total_P = tp_mgl,
Total_N = tn_mgl
)
head(limno_env, 6)
#> Temperature DO pH Turbidity Secchi Total_P Total_N
#> 1 22.19114 5.982808 8.407656 8.327564 2.3928749 0.09957881 1.912191
#> 2 26.59665 6.100091 8.240560 24.557620 1.2303458 0.15707745 2.792743
#> 3 25.11247 6.161079 8.381893 34.516946 0.6536182 0.24425880 3.752906
#> 4 22.64647 6.449037 8.274918 9.993436 2.0100879 0.11489375 2.498569
#> 5 23.06031 7.278349 8.215123 30.431676 1.0822492 0.19719694 3.357934
#> 6 30.20779 2.871502 8.630534 9.455666 2.2882730 0.09409752 2.190112The correlation_heatmap() function provides tile-based
and bubble-based visual styles.
Displays the lower triangle with correlation values and significance stars, automatically adjusting text color for high-contrast visibility:
res_cor_square <- correlation_heatmap(
data = limno_env,
method = "pearson",
shape = "square",
view = "lower",
diag = FALSE,
label_type = "both",
color_palette = "RdBu"
)
# Render lower-triangle heatmap
res_cor_square$PlotUses circle markers where bubble diameter corresponds to the absolute correlation magnitude (\(\vert{}r\vert{}\)) and color corresponds to directional correlation sign:
res_cor_circle <- correlation_heatmap(
data = limno_env,
method = "spearman",
shape = "circle",
view = "lower",
diag = FALSE,
label_type = "stars",
color_palette = "RdYlBu",
title = "Environmental Matrix Spearman Correlations"
)
res_cor_circle$PlotThe returned list provides the symmetric correlation coefficient matrix, exact \(p\)-values, and a tidy summary data frame:
# 1. Pearson Correlation Matrix Preview
round(res_cor_square$Correlation_Matrix[1:4, 1:4], 3)
#> Temperature DO pH Turbidity
#> Temperature 1.000 -0.915 0.652 0.218
#> DO -0.915 1.000 -0.618 -0.110
#> pH 0.652 -0.618 1.000 0.036
#> Turbidity 0.218 -0.110 0.036 1.000
# 2. Exact P-Value Matrix Preview
signif(res_cor_square$P_Value_Matrix[1:4, 1:4], 3)
#> Temperature DO pH Turbidity
#> Temperature 0.00e+00 1.67e-20 2.99e-07 0.129
#> DO 1.67e-20 0.00e+00 1.71e-06 0.446
#> pH 2.99e-07 1.71e-06 0.00e+00 0.802
#> Turbidity 1.29e-01 4.46e-01 8.02e-01 0.000
# 3. Tidy Pairwise Summary Table
head(res_cor_square$Summary_Table, 8)
#> Var1 Var2 Correlation P_Value Significance
#> 3 DO Temperature -0.91457645 1.674026e-20 ***
#> 8 Secchi DO 0.16852240 2.420479e-01 ns
#> 10 Secchi Temperature -0.27450426 5.370824e-02 ns
#> 13 Secchi Turbidity -0.93266093 6.826376e-23 ***
#> 14 Secchi pH -0.05415836 7.087424e-01 ns
#> 22 Total_N DO -0.08583431 5.533923e-01 ns
#> 23 Total_N Secchi -0.86826669 3.197710e-16 ***
#> 24 Total_N Temperature 0.13998938 3.322344e-01 nsThe mantel_heatmap_analysis() function models and
visualizes the coupling between abiotic environmental gradients and
biological community structures. In aquatic ecology, researchers often
analyze pairwise physico-chemical parameters alongside multivariate
dissimilarity matrices across multiple trophic levels (e.g.,
Phytoplankton, Zooplankton, Benthos, Fish).
This function integrates these two workflows into a single display: 1. A pairwise environmental correlation matrix (Pearson or Spearman) rendered as a lower-triangular heatmap. 2. Community-wide Mantel tests connecting biological community nodes to corresponding environmental parameters using curved or straight linkage vectors.
Linkage attributes convey statistical properties: line thickness indicates the magnitude of the Mantel correlation coefficient (\(r\)), while color and line style denote permutation-derived significance thresholds (\(p < 0.01\), \(p < 0.05\), \(p \ge 0.05\)).
The analytical framework combines pairwise variable correlation with matrix permutation testing.
Let \(\mathbf{D}_M\) represent the ecological dissimilarity matrix (e.g., Bray-Curtis) among \(n\) sampling stations for a biological community:
\[d_{ij}^{(M)} = \frac{\sum_{k=1}^p |y_{ik} - y_{jk}|}{\sum_{k=1}^p (y_{ik} + y_{jk})}\]
Let \(\mathbf{D}_E\) denote the standardized Euclidean distance matrix computed across one or more environmental variables between the same sampling stations:
\[d_{ij}^{(E)} = \sqrt{\sum_{m=1}^q (z_{im} - z_{jm})^2}\]
The Mantel test computes the Pearson correlation coefficient between the lower triangular elements (\(j < i\)) of the two unfolded distance matrices:
\[r_M = \frac{\sum_{i > j} (d_{ij}^{(M)} - \bar{d}_M)(d_{ij}^{(E)} - \bar{d}_E)}{\sqrt{\sum_{i > j} (d_{ij}^{(M)} - \bar{d}_M)^2 \sum_{i > j} (d_{ij}^{(E)} - \bar{d}_E)^2}}\]
where \(r_M \in [-1, 1]\).
Because elements within a distance matrix are not mutually independent, standard parametric tests for correlation significance are invalid. Instead, significance is assessed through Monte Carlo permutation of the rows and columns of one distance matrix (e.g., \(\mathbf{D}_M\)) while holding the second (\(\mathbf{D}_E\)) fixed:
\[p = \frac{\sum_{\pi=1}^B \mathbb{I}(r_M^{(\pi)} \ge r_M^{\text{obs}}) + 1}{B + 1}\]
where \(B\) is the number of random permutations (default: 999).
We simulate a limnological survey across 35 sampling stations containing six continuous environmental variables (Water Temperature, Dissolved Oxygen, pH, Turbidity, Total Phosphorus, Total Nitrogen) paired with two biological trophic communities: Phytoplankton (6 taxa) and Zooplankton (5 taxa):
set.seed(1717)
n_stations <- 35
# 1. Environmental Gradients
temp_c <- rnorm(n_stations, mean = 24.0, sd = 2.5)
do_mgl <- 13.5 - (0.30 * temp_c) + rnorm(n_stations, 0, 0.4)
ph_val <- 7.2 + (0.04 * temp_c) + rnorm(n_stations, 0, 0.2)
turb_ntu <- runif(n_stations, 5, 40)
tp_mgl <- 0.03 + (0.007 * turb_ntu) + rnorm(n_stations, 0, 0.015)
tn_mgl <- 0.70 + (14.0 * tp_mgl) + rnorm(n_stations, 0, 0.12)
env_matrix <- data.frame(
Temp = temp_c,
DO = do_mgl,
pH = ph_val,
Turb = turb_ntu,
Total_P = tp_mgl,
Total_N = tn_mgl
)
# 2. Phytoplankton Community Counts (sensitive to nutrients and turbidity)
phyto_mat <- data.frame(
Cyanob_1 = rpois(n_stations, lambda = pmax(1, round(5 + 80 * tp_mgl))),
Cyanob_2 = rpois(n_stations, lambda = pmax(1, round(3 + 60 * tp_mgl))),
Diatom_1 = rpois(n_stations, lambda = pmax(1, round(28 - 0.4 * turb_ntu))),
Diatom_2 = rpois(n_stations, lambda = pmax(1, round(20 - 0.3 * turb_ntu))),
Chloroph_1 = rpois(n_stations, lambda = pmax(1, round(12 + 2.5 * ph_val))),
Chloroph_2 = rpois(n_stations, lambda = pmax(1, round(10 + 1.8 * ph_val)))
)
# 3. Zooplankton Community Counts (sensitive to temperature and dissolved oxygen)
zoo_mat <- data.frame(
Cladocera_1 = rpois(n_stations, lambda = pmax(1, round(2 + 2.2 * do_mgl))),
Cladocera_2 = rpois(n_stations, lambda = pmax(1, round(4 + 1.8 * do_mgl))),
Copepod_1 = rpois(n_stations, lambda = pmax(1, round(8 + 0.8 * temp_c))),
Copepod_2 = rpois(n_stations, lambda = pmax(1, round(6 + 0.6 * temp_c))),
Rotifer_1 = rpois(n_stations, lambda = pmax(1, round(15 + 0.3 * turb_ntu)))
)
# Bundle biological communities into a named list
aquatic_trophic_comms <- list(
"Phytoplankton" = phyto_mat,
"Zooplankton" = zoo_mat
)
head(env_matrix, 4)
#> Temp DO pH Turb Total_P Total_N
#> 1 20.36277 7.331757 7.902365 7.094070 0.07979408 2.036248
#> 2 19.72894 7.526729 8.056922 27.895527 0.20880550 3.590681
#> 3 29.25114 3.755476 8.352574 10.632814 0.09551539 1.887045
#> 4 21.42924 7.085186 8.155138 6.198184 0.08728681 2.002546The mantel_heatmap_analysis() function provides both an
integrated network linkage diagram and a side-by-side barplot
comparison.
Projects environmental correlations as lower-triangular tiles while connecting community nodes to environmental anchors with curved vectors:
res_mantel_net <- mantel_heatmap_analysis(
comm_data = aquatic_trophic_comms,
env_data = env_matrix,
plot_type = "network",
line_style = "curve",
method = "pearson",
spec_dist = "bray",
transform = "hellinger",
only_significant = FALSE,
line_color_by = "significance",
permutations = 999,
seed = 42,
show_cor_text = TRUE,
color_palette = "RdBu"
)
# Render publication network plot
res_mantel_net$PlotDisplays the full environmental correlation matrix alongside horizontal bars showing Mantel \(r\) effect sizes faceted across trophic communities:
res_mantel_bars <- mantel_heatmap_analysis(
comm_data = aquatic_trophic_comms,
env_data = env_matrix,
plot_type = "bars",
method = "pearson",
spec_dist = "bray",
transform = "hellinger",
permutations = 999,
seed = 42,
color_palette = "RdBu"
)
res_mantel_bars$PlotThe returned list provides the pairwise environmental correlation matrix and a tidy data frame of community-specific Mantel statistics:
# 1. Environmental Correlation Matrix (First 4 Variables)
round(res_mantel_net$Correlation_Matrix[1:4, 1:4], 3)
#> Temp DO pH Turb
#> Temp 1.000 -0.926 0.623 0.151
#> DO -0.926 1.000 -0.593 -0.145
#> pH 0.623 -0.593 1.000 0.004
#> Turb 0.151 -0.145 0.004 1.000
# 2. Tidy Mantel Test Statistics and Permutation P-Values
head(res_mantel_net$Mantel_Results, 8)
#> Community Variable Mantel_r p_value Significance Mantel_R_Tier Line_Type
#> 1 Phytoplankton Temp 0.032 0.303 p ≥ 0.05 < 0.20 dashed
#> 2 Phytoplankton DO 0.074 0.177 p ≥ 0.05 < 0.20 dashed
#> 3 Phytoplankton pH 0.043 0.282 p ≥ 0.05 < 0.20 dashed
#> 4 Phytoplankton Turb 0.718 0.001 p < 0.01 ≥ 0.40 solid
#> 5 Phytoplankton Total_P 0.709 0.001 p < 0.01 ≥ 0.40 solid
#> 6 Phytoplankton Total_N 0.686 0.001 p < 0.01 ≥ 0.40 solid
#> 7 Zooplankton Temp 0.148 0.045 p < 0.05 < 0.20 solid
#> 8 Zooplankton DO 0.120 0.120 p ≥ 0.05 < 0.20 dashedThe calc_diversity() and plot_diversity()
functions provide an integrated quantitative suite for estimating,
testing, and visualizing community alpha diversity patterns in fisheries
and aquatic ecology. Understanding local species richness, compositional
dominance, and structural evenness is foundational for assessing habitat
degradation, monitoring marine protected areas (MPAs), and comparing
stocking strategies across culture environments.
The package calculates seven primary ecological metrics simultaneously: 1. Total Abundance (\(N\)): Overall individual organism counts. 2. Species Richness (\(S\)): Total count of unique taxa observed. 3. Shannon-Wiener Index (\(H'\)): Information-theoretic entropy capturing both richness and proportional abundance. 4. Gini-Simpson Index (\(1 - D\)): The probability that two randomly sampled individuals belong to different species. 5. Margalef’s Richness (\(d\)): Species richness normalized relative to the natural logarithm of sample size. 6. Menhinick’s Index (\(D_{mn}\)): Species richness adjusted relative to the square root of total abundance. 7. Pielou’s Evenness (\(J'\)): The ratio of observed Shannon diversity relative to maximum possible diversity.
The visualization function plot_diversity() pairs these
indices with parametric (One-Way / Two-Way Factorial ANOVA) or
non-parametric (Kruskal-Wallis) omnibus tests, calculates
1,000-iteration bootstrap confidence intervals, and formats
publication-ready boxplots with raw observation overlays.
Let a community sample contain \(S\) observed species and total organism abundance \(N = \sum_{i=1}^S n_i\), where \(n_i\) represents the count of the \(i\)-th taxon. The relative abundance (proportion) of species \(i\) is \(p_i = n_i / N\).
Derived from communication theory, \(H'\) measures the uncertainty in predicting the species identity of a randomly sampled individual:
\[H' = -\sum_{i=1}^S p_i \ln(p_i)\]
Values typically range between \(1.5\) and \(3.5\) in natural aquatic systems, with higher values reflecting richer and more equitable communities.
Simpson’s index of dominance measures the probability that two randomly selected individuals belong to the same species: \(D = \sum_{i=1}^S p_i^2\). The complementary Gini-Simpson diversity metric is:
\[1 - D = 1 - \sum_{i=1}^S p_i^2\]
This metric ranges from \(0\) (monospecific culture) to \(1.0\) (infinite community of equally abundant species).
Because raw observed richness (\(S\)) increases monotonically with sampling effort (\(N\)), Margalef (\(d\)) and Menhinick (\(D_{mn}\)) normalize richness across sample size discrepancies:
\[d = \frac{S - 1}{\ln(N)} \quad (N > 1)\]
\[D_{mn} = \frac{S}{\sqrt{N}} \quad (N > 0)\]
Measures how close the observed distribution of abundances is to maximum theoretical evenness (\(H'_{\max} = \ln(S)\)):
\[J' = \frac{H'}{\ln(S)} \quad (S > 1)\]
where \(J' \in [0, 1]\). Values approaching \(1.0\) indicate equal abundances across taxa, whereas lower values denote heavy dominance by one or two species.
We simulate a riverine fish biomonitoring survey assessing the
abundances of 10 freshwater species across 36 sampling reaches
categorized by River Zone (Headwater,
Mid-River, Lowland) and Sampling Season
(Dry Season, Wet Season):
set.seed(1818)
n_reps <- 6
zones <- c("Headwater", "Mid-River", "Lowland")
seasons <- c("Dry Season", "Wet Season")
design <- expand.grid(Zone = zones, Season = seasons)
comm_runs <- lapply(seq_len(nrow(design)), function(idx) {
zn <- as.character(design$Zone[idx])
sn <- as.character(design$Season[idx])
# Base abundance profiles
means <- switch(zn,
"Headwater" = c(Sp1 = 45, Sp2 = 38, Sp3 = 12, Sp4 = 2, Sp5 = 0, Sp6 = 0, Sp7 = 0, Sp8 = 0, Sp9 = 0, Sp10 = 0),
"Mid-River" = c(Sp1 = 18, Sp2 = 22, Sp3 = 25, Sp4 = 16, Sp5 = 14, Sp6 = 8, Sp7 = 4, Sp8 = 1, Sp9 = 0, Sp10 = 0),
"Lowland" = c(Sp1 = 6, Sp2 = 8, Sp3 = 12, Sp4 = 24, Sp5 = 28, Sp6 = 32, Sp7 = 18, Sp8 = 15, Sp9 = 10, Sp10 = 6)
)
if (sn == "Wet Season") {
means <- round(means * 1.35 + 2) # Seasonal flood pulse enhancement
}
mat <- do.call(cbind, lapply(means, function(m) rpois(n_reps, lambda = m)))
df <- as.data.frame(mat)
df$Zone <- zn
df$Season <- sn
df
})
fish_comm_data <- do.call(rbind, comm_runs)
# Organize grouping metadata columns at the front
fish_comm_data <- fish_comm_data[, c("Zone", "Season", paste0("Sp", 1:10))]
head(fish_comm_data[, 1:7], 6)
#> Zone Season Sp1 Sp2 Sp3 Sp4 Sp5
#> 1 Headwater Dry Season 38 39 16 0 0
#> 2 Headwater Dry Season 49 38 7 3 0
#> 3 Headwater Dry Season 49 47 14 2 0
#> 4 Headwater Dry Season 46 41 11 1 0
#> 5 Headwater Dry Season 56 47 17 4 0
#> 6 Headwater Dry Season 33 23 9 3 0First, calculate the complete alpha diversity table across all
sampling stations using calc_diversity():
alpha_table <- calc_diversity(
data = fish_comm_data,
group_col = c("Zone", "Season")
)
# Display sample output with computed diversity indices
head(alpha_table, 6)
#> Sample Zone Season Abundance Richness Shannon Simpson Margalef
#> 1 1 Headwater Dry Season 93 3 1.033 0.628 0.441
#> 2 2 Headwater Dry Season 97 4 1.009 0.585 0.656
#> 3 3 Headwater Dry Season 112 4 1.058 0.617 0.636
#> 4 4 Headwater Dry Season 99 4 1.012 0.600 0.653
#> 5 5 Headwater Dry Season 124 4 1.110 0.633 0.622
#> 6 6 Headwater Dry Season 68 4 1.123 0.631 0.711
#> Menhinick Pielou
#> 1 0.311 0.940
#> 2 0.406 0.728
#> 3 0.378 0.763
#> 4 0.402 0.730
#> 5 0.359 0.801
#> 6 0.485 0.810plot_diversity() generates single-factor and two-factor
(factorial) comparative displays.
Compares Shannon diversity across river zones, displaying raw sample jitter overlays and non-parametric test statistics:
res_div_single <- plot_diversity(
data = fish_comm_data,
group_col = "Zone",
index = "shannon",
test = "kruskal",
color_palette = "Dark2",
permutations = 1000
)
# Render single-factor diversity plot
res_div_single$PlotEvaluates how Pielou’s Evenness (\(J'\)) responds across the interaction of Zone and Season using dodged boxplots and parametric Two-Way ANOVA:
res_div_factorial <- plot_diversity(
data = fish_comm_data,
group_col = c("Zone", "Season"),
index = "pielou",
test = "anova",
color_palette = "Set2",
permutations = 1000
)
# Render factorial diversity plot
res_div_factorial$PlotThe returned list provides the underlying tidy diversity dataframe, bootstrap 95% confidence intervals, and hypothesis test output:
# 1. Omnibus Hypothesis Test Result (Kruskal-Wallis / ANOVA)
res_div_single$Test_Result
#>
#> Kruskal-Wallis rank sum test
#>
#> data: Metric_Value by Zone
#> Kruskal-Wallis chi-squared = 30.298, df = 2, p-value = 2.635e-07
# 2. Bootstrap Summary Statistics (Means, 95% Lower and Upper Confidence Limits)
res_div_single$Group_Summary
#> Group Index N Mean LCI UCI
#> 1 Headwater Shannon 12 1.242 1.139 1.348
#> 2 Lowland Shannon 12 2.138 2.108 2.160
#> 3 Mid-River Shannon 12 1.911 1.862 1.955
# 3. Two-Way ANOVA Factorial Summary Table
res_div_factorial$Test_Result
#> Df Sum Sq Mean Sq F value Pr(>F)
#> Zone 2 0.29240 0.14620 97.19 7.80e-14 ***
#> Season 1 0.02987 0.02987 19.86 0.000108 ***
#> Zone:Season 2 0.04179 0.02090 13.89 5.37e-05 ***
#> Residuals 30 0.04513 0.00150
#> ---
#> Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1The specaccum_plot() function constructs and visualizes
species accumulation curves (collector’s curves) to evaluate inventory
completeness, sampling sufficiency, and species richness scaling in
aquatic biomonitoring and fisheries assessments. When conducting
community surveys (e.g., river electrofishing, benthic grab surveys,
plankton trawls), raw observed species counts are constrained by
sampling intensity.
Species accumulation curves model the expected rate at which new species are recorded as additional sampling units or hauls are aggregated. Asymptotic leveling of the curve signifies sampling saturation—demonstrating that the sampling protocol has captured the majority of common and rare taxa within the target habitat.
The function computes curves using vegan::specaccum,
accommodates multiple analytical accumulation methods (e.g.,
exact analytical solution, coleman
rarefaction, or random permutations), calculates 95% confidence
intervals, and generates publication-grade visualizations for single
pooled inventories or comparative multi-habitat groupings
(by_group = TRUE).
Let a community dataset comprise \(N\) total sampling units and \(S_{\text{obs}}\) total observed species across the pooled inventory.
Rather than relying solely on empirical randomizations,
method = "exact" calculates the expected species richness
\(\bar{S}_n\) for any sub-sample of
\(n\) sampling units (\(1 \le n \le N\)) using combinatorial
probability:
\[\bar{S}_n = S_{\text{obs}} - \sum_{i=1}^{S_{\text{obs}}} \frac{\binom{N - f_i}{n}}{\binom{N}{n}}\]
where \(f_i\) denotes the incidence frequency (number of sampling units containing species \(i\)). The combinatorial term represents the hyper-geometric probability that species \(i\) is absent from a random selection of \(n\) samples.
Under method = "coleman", the non-interactive
expectation models individuals randomly distributed across samples
independently of spatial aggregation:
\[S_n = S_{\text{obs}} - \sum_{i=1}^{S_{\text{obs}}} \left(1 - \frac{n}{N}\right)^{n_i}\]
where \(n_i\) represents the total organism abundance of the \(i\)-th species.
For methods with analytical variance derivations, the 95% confidence boundaries around expected richness \(\bar{S}_n\) are computed as:
\[\text{CI}_{95\%} = \bar{S}_n \pm 1.96 \times \sigma(\bar{S}_n)\]
where \(\sigma(\bar{S}_n)\) represents the standard deviation of species richness at sample size \(n\).
We simulate a riverine fish biodiversity survey comprising 45 electrofishing reaches divided across three distinct aquatic habitat zones: Highland Headwater (15 reaches), Midland Reach (15 reaches), and Lowland Floodplain (15 reaches). A total of 14 fish species with varying rarity and spatial distribution are surveyed:
set.seed(1919)
n_samples_per_hab <- 15
habitats <- c("Headwater", "Midland", "Lowland")
sim_list <- lapply(habitats, function(hab) {
# Diagnostic baseline probabilities of encounter
lambdas <- switch(hab,
"Headwater" = c(Sp01 = 25, Sp02 = 18, Sp03 = 8, Sp04 = 2, Sp05 = 0, Sp06 = 0, Sp07 = 0, Sp08 = 0, Sp09 = 0, Sp10 = 0, Sp11 = 0, Sp12 = 0, Sp13 = 0, Sp14 = 0),
"Midland" = c(Sp01 = 8, Sp02 = 12, Sp03 = 20, Sp04 = 15, Sp05 = 10, Sp06 = 6, Sp07 = 3, Sp08 = 1, Sp09 = 0, Sp10 = 0, Sp11 = 0, Sp12 = 0, Sp13 = 0, Sp14 = 0),
"Lowland" = c(Sp01 = 1, Sp02 = 2, Sp03 = 6, Sp04 = 14, Sp05 = 22, Sp06 = 28, Sp07 = 18, Sp08 = 12, Sp09 = 8, Sp10 = 6, Sp11 = 4, Sp12 = 3, Sp13 = 2, Sp14 = 1)
)
mat <- do.call(cbind, lapply(lambdas, function(l) rpois(n_samples_per_hab, lambda = l)))
df <- as.data.frame(mat)
df$Habitat <- hab
df
})
fish_inventory <- do.call(rbind, sim_list)
# Place metadata at the front
fish_inventory <- fish_inventory[, c("Habitat", setdiff(names(fish_inventory), "Habitat"))]
head(fish_inventory[, 1:7], 6)
#> Habitat Sp01 Sp02 Sp03 Sp04 Sp05 Sp06
#> 1 Headwater 24 16 11 5 0 0
#> 2 Headwater 23 12 8 1 0 0
#> 3 Headwater 22 10 12 2 0 0
#> 4 Headwater 29 11 8 1 0 0
#> 5 Headwater 31 21 6 1 0 0
#> 6 Headwater 25 14 5 2 0 0The specaccum_plot() function supports both
whole-ecosystem pooled curves and multi-habitat comparative curves.
Evaluates overall survey completeness across all pooled sampling
units using the analytical exact method:
res_acc_pooled <- specaccum_plot(
data = fish_inventory,
group_col = "Habitat",
by_group = FALSE,
method = "exact",
line_color = "#2A9D8F",
ci = 1.96,
show_ci = TRUE,
title = "River Basin Fish Species Accumulation Curve"
)
# Render pooled curve
res_acc_pooled$PlotBy specifying by_group = TRUE, the function estimates
accumulation curves separately for each habitat level, allowing direct
visual comparison of species richness saturation rates across
environmental strata:
res_acc_grouped <- specaccum_plot(
data = fish_inventory,
group_col = "Habitat",
by_group = TRUE,
method = "exact",
palette = "Dark2",
show_ci = TRUE,
title = "Comparative Species Accumulation Across River Zones"
)
res_acc_grouped$PlotThe returned list provides the underlying
vegan::specaccum object and the tidy data frame containing
exact site counts, expected richness, standard deviations, and
confidence intervals:
# 1. Inspect accumulation curve trajectory table (First 8 sample intervals)
head(res_acc_pooled$Curve_Data, 8)
#> Sites Richness SD Lower Upper
#> 1 1 8.088889 3.656484 0.9221799 15.25560
#> 2 2 10.459596 3.597171 3.4091399 17.51005
#> 3 3 11.723679 3.137314 5.5745433 17.87281
#> 4 4 12.487298 2.638540 7.3157596 17.65884
#> 5 5 12.978136 2.187067 8.6914857 17.26479
#> 6 6 13.304413 1.798105 9.7801273 16.82870
#> 7 7 13.525083 1.469828 10.6442192 16.40595
#> 8 8 13.675544 1.195670 11.3320315 16.01906
# 2. Inspect grouped curve summary table preview
head(res_acc_grouped$Curve_Data, 6)
#> Group Sites Richness SD Lower Upper
#> 1 Headwater 1 4 0 4 4
#> 2 Headwater 2 4 0 4 4
#> 3 Headwater 3 4 0 4 4
#> 4 Headwater 4 4 0 4 4
#> 5 Headwater 5 4 0 4 4
#> 6 Headwater 6 4 0 4 4The calc_wqi() and plot_wqi() functions
calculate and visualize the Weighted Arithmetic Water Quality Index
(WAWQI) across spatial monitoring stations and temporal sampling
campaigns. In fisheries management and aquatic environmental impact
assessments, raw multi-parameter water chemistry matrices (e.g., pH,
Dissolved Oxygen, Biological Oxygen Demand, Total Dissolved Solids,
Turbidity) can be difficult to interpret individually.
The WAWQI methodology aggregates multiple physico-chemical metrics into a single score that quantifies overall ecosystem water quality. The functions compute parameter-specific unit weights, derive quality ratings, handle station replicates with Student’s \(t\)-distribution 95% confidence intervals, assign ecological suitability tiers, and generate publication-ready bar and lollipop plots overlaid with regulatory benchmark bands.
Let \(n\) denote the number of evaluated water quality parameters. Each parameter is evaluated against recommended regulatory thresholds (e.g., WHO, BIS, or national environmental standards).
The unit weight \(W_i\) for the \(i\)-th parameter is inversely proportional to its standard permissible limit (\(S_i\)), ensuring that toxic or sensitive parameters (such as low dissolved oxygen thresholds or high pesticide levels) carry greater weight in the final index:
\[W_i = \frac{K}{S_i} = \frac{1}{S_i \sum_{j=1}^n \frac{1}{S_j}}\]
where \(K = \frac{1}{\sum_{j=1}^n (1 / S_j)}\) is the proportionality normalization constant, ensuring that \(\sum_{i=1}^n W_i = 1.0\).
The sub-index rating \(Q_i\) for parameter \(i\) reflects the observed concentration (\(V_i\)) relative to its ideal standard value (\(V_0\)) and standard limit (\(S_i\)):
\[Q_i = \frac{V_i - V_0}{S_i - V_0} \times 100\]
For pollutants and nutrient parameters (e.g., BOD, TDS, Turbidity), the ideal zero-effect concentration is \(V_0 = 0\text{ mg/L}\). For physiological parameters with specific natural equilibria: - pH: \(V_0 = 7.0\) (neutral baseline) - Dissolved Oxygen (DO): \(V_0 = 14.6\text{ mg/L}\) (theoretical saturation baseline at standard temperature and pressure), formulated as: \[Q_{\text{DO}} = \frac{14.6 - V_{\text{DO}}}{14.6 - S_{\text{DO}}} \times 100\] Lower dissolved oxygen concentrations yield higher \(Q_i\) penalty scores.
The aggregate WAWQI is computed as the weighted arithmetic mean of all sub-indices:
\[\text{WAWQI} = \frac{\sum_{i=1}^n Q_i W_i}{\sum_{i=1}^n W_i} = \sum_{i=1}^n Q_i W_i\]
Ecological water quality ratings are classified according to established benchmark boundaries: - \(0 \le \text{WAWQI} \le 25\): Excellent Water Quality (Pristine aquatic habitat) - \(26 \le \text{WAWQI} \le 50\): Good Water Quality (Suitable for aquaculture and fisheries) - \(51 \le \text{WAWQI} \le 75\): Poor Water Quality (Threatened aquatic biodiversity) - \(76 \le \text{WAWQI} \le 100\): Very Poor Water Quality (Severe eutrophication and habitat degradation) - \(\text{WAWQI} > 100\): Unsuitable for Aquatic Life (Acute toxicity / critical hypoxia)
We simulate a seasonal river biomonitoring dataset evaluating five
standard water quality parameters (pH, DO, BOD, TDS, and Turbidity)
across four monitoring stations (Station_1 to
Station_4), with 6 replicate water samples collected at
each station:
set.seed(2026)
n_reps <- 6
stations <- c("Station_1 (Upstream)", "Station_2 (Agricultural)", "Station_3 (Urban Discharge)", "Station_4 (Estuarine Mouth)")
wqi_survey <- do.call(rbind, lapply(stations, function(stn) {
# Parameter baseline values reflecting an anthropogenic pollution gradient
param_means <- switch(stn,
"Station_1 (Upstream)" = c(pH = 7.4, DO = 8.8, BOD = 1.8, TDS = 140, Turbidity = 2.5),
"Station_2 (Agricultural)" = c(pH = 7.8, DO = 6.8, BOD = 3.6, TDS = 320, Turbidity = 4.8),
"Station_3 (Urban Discharge)" = c(pH = 8.3, DO = 4.1, BOD = 8.5, TDS = 680, Turbidity = 9.4),
"Station_4 (Estuarine Mouth)" = c(pH = 7.9, DO = 5.4, BOD = 5.2, TDS = 510, Turbidity = 6.2)
)
data.frame(
Station = stn,
pH = rnorm(n_reps, mean = param_means["pH"], sd = 0.2),
DO = rnorm(n_reps, mean = param_means["DO"], sd = 0.35),
BOD = rnorm(n_reps, mean = param_means["BOD"], sd = 0.4),
TDS = rnorm(n_reps, mean = param_means["TDS"], sd = 25),
Turbidity = rnorm(n_reps, mean = param_means["Turbidity"], sd = 0.5)
)
}))
head(wqi_survey, 6)
#> Station pH DO BOD TDS Turbidity
#> 1 Station_1 (Upstream) 7.504118 8.542699 1.7114254 119.8820 2.524103
#> 2 Station_1 (Upstream) 7.184062 8.442957 1.7096734 157.2437 3.454042
#> 3 Station_1 (Upstream) 7.427848 8.839744 0.7812474 131.7832 3.365472
#> 4 Station_1 (Upstream) 7.383050 8.634173 2.3388006 135.8830 2.529073
#> 5 Station_1 (Upstream) 7.266672 8.657125 2.0465633 105.1993 2.822664
#> 6 Station_1 (Upstream) 6.896782 8.544348 1.8870257 176.6456 3.362814Run calc_wqi() to derive individual replicate
sub-indices and station-level summary statistics:
res_wqi <- calc_wqi(
data = wqi_survey,
id_col = "Station",
standards = c(
pH = 8.5,
DO = 5.0,
BOD = 5.0,
TDS = 500,
Turbidity = 5.0
),
ci = 0.95
)
# Station-level summary table with 95% Confidence Intervals
res_wqi$Summary_Data
#> Station N WQI_Mean WQI_SD WQI_SE WQI_LCI WQI_UCI
#> Mean Station_1 (Upstream) 6 47.06 1.53 0.63 45.45 48.67
#> Mean1 Station_2 (Agricultural) 6 76.88 4.29 1.75 72.37 81.39
#> Mean2 Station_3 (Urban Discharge) 6 146.40 4.36 1.78 141.83 150.98
#> Mean3 Station_4 (Estuarine Mouth) 6 100.18 4.02 1.64 95.96 104.39
#> Status
#> Mean Good
#> Mean1 Very Poor
#> Mean2 Unsuitable
#> Mean3 Unsuitableplot_wqi() provides both columnar bar charts and
lollipop geometries with optional background benchmark classification
shading.
Displays station scores against regulatory classification tiers, attaching 95% confidence intervals to convey measurement variability:
p_wqi_bar <- plot_wqi(
data = res_wqi,
id_col = "Station",
type = "bar",
show_ci = TRUE,
show_bands = TRUE,
title = "River Basin Water Quality Index (WAWQI) with 95% CI"
)
# Render publication plot
p_wqi_barProvides an alternative lollipop format without background shading, focusing on discrete station comparisons:
p_wqi_lollipop <- plot_wqi(
data = res_wqi,
id_col = "Station",
type = "lollipop",
show_ci = TRUE,
show_bands = FALSE,
title = "Station-Level Water Quality Ratings"
)
p_wqi_lollipopInspect individual replicate sub-indices (\(Q_i\)) and corresponding final sample scores:
# 1. Preview individual replicate evaluations (first 6 samples)
head(res_wqi$Replicate_Data, 6)
#> Station pH DO BOD TDS Turbidity Q_pH
#> 1 Station_1 (Upstream) 7.504118 8.542699 1.7114254 119.8820 2.524103 33.61
#> 2 Station_1 (Upstream) 7.184062 8.442957 1.7096734 157.2437 3.454042 12.27
#> 3 Station_1 (Upstream) 7.427848 8.839744 0.7812474 131.7832 3.365472 28.52
#> 4 Station_1 (Upstream) 7.383050 8.634173 2.3388006 135.8830 2.529073 25.54
#> 5 Station_1 (Upstream) 7.266672 8.657125 2.0465633 105.1993 2.822664 17.78
#> 6 Station_1 (Upstream) 6.896782 8.544348 1.8870257 176.6456 3.362814 0.00
#> Q_DO Q_BOD Q_TDS Q_Turbidity WQI
#> 1 63.10 34.23 23.98 50.48 46.64
#> 2 64.14 34.19 31.45 69.08 48.62
#> 3 60.00 15.62 26.36 67.31 44.46
#> 4 62.14 46.78 27.18 50.58 48.58
#> 5 61.90 40.93 21.04 56.45 47.23
#> 6 63.08 37.74 35.33 67.26 46.81
# 2. Inspect station-level summary statistics
res_wqi$Summary_Data[, c("Station", "N", "WQI_Mean", "WQI_SE", "Status")]
#> Station N WQI_Mean WQI_SE Status
#> Mean Station_1 (Upstream) 6 47.06 0.63 Good
#> Mean1 Station_2 (Agricultural) 6 76.88 1.75 Very Poor
#> Mean2 Station_3 (Urban Discharge) 6 146.40 1.78 Unsuitable
#> Mean3 Station_4 (Estuarine Mouth) 6 100.18 1.64 UnsuitableThe calc_metal_indices() and
plot_metal_indices() functions evaluate heavy metal
pollution in aquatic ecosystems (water columns, riverbed sediments, and
fish tissue matrices). Aquatic toxicology and environmental impact
assessments require multi-element risk indices to convert raw trace
element concentrations into standardized toxicological risk scores.
The package computes five core international heavy metal contamination indices simultaneously: 1. Contamination Factor (\(CF_i\)): Ratio of observed metal concentration to its regulatory permissible limit. 2. Heavy Metal Pollution Index (HPI): Weighted arithmetic rating model balancing individual toxicities and guideline limits. 3. Heavy Metal Evaluation Index (HEI): Additive sum of contamination factors evaluating collective toxicity. 4. Degree of Contamination (\(C_d\)): Cumulative contamination degradation reflecting net excessive pollution above unity. 5. Pollution Load Index (PLI): Geometric mean of contamination factors quantifying multi-metal bioaccumulation. 6. Comprehensive Nemerow Pollution Index (\(P_N\)): Weighted combination highlighting peak extreme pollutant concentrations.
The visualization function plot_metal_indices() renders
column and lollipop graphics with 95% confidence intervals and dashed
benchmark thresholds.
Let \(C_i\) denote the observed concentration of the \(i\)-th heavy metal, and let \(S_i\) denote its corresponding national or international drinking/aquatic life permissible standard limit.
The base metric for single-metal contamination is the ratio:
\[CF_i = \frac{C_i}{S_i}\]
HEI expresses the cumulative additive load across all \(n\) analyzed trace metals:
\[\text{HEI} = \sum_{i=1}^n CF_i = \sum_{i=1}^n \frac{C_i}{S_i}\]
Benchmark classification: \(\text{HEI} < 10\) (Low); \(10 \le \text{HEI} \le 20\) (Medium); \(\text{HEI} > 20\) (High pollution).
The Degree of Contamination (\(C_d\)) computes the net excess above the permissible threshold (\(CF_i = 1\)):
\[C_d = \sum_{i=1}^n (CF_i - 1)\]
Benchmark classification: \(C_d < 6\) (Low); \(6 \le C_d < 12\) (Moderate); \(C_d \ge 12\) (High contamination).
Originally formulated by Tomlinson et al., PLI calculates the geometric mean of contamination factors:
\[\text{PLI} = \left( \prod_{i=1}^n CF_i \right)^{\frac{1}{n}} = \sqrt[n]{CF_1 \times CF_2 \times \dots \times CF_n}\]
Interpretation threshold: \(\text{PLI} \le 1.0\) indicates baseline unpolluted conditions, whereas \(\text{PLI} > 1.0\) indicates progressive heavy metal deterioration.
Evaluates total quality while giving special weight to the worst single pollutant:
\[P_N = \sqrt{\frac{\overline{CF}^2 + CF_{\max}^2}{2}}\]
where \(\overline{CF} = \frac{1}{n} \sum CF_i\) is the average contamination factor and \(CF_{\max}\) is the maximum observed single contamination factor.
Computes a weighted arithmetic aggregation:
\[\text{HPI} = \frac{\sum_{i=1}^n W_i Q_i}{\sum_{i=1}^n W_i}\]
where unit weights are inversely proportional to guidelines (\(W_i = 1 / S_i\)), and sub-index ratings are \(Q_i = (C_i / S_i) \times 100\). The critical threshold is \(\text{HPI} = 100\); values exceeding \(100\) indicate critical metal pollution unsuitable for aquatic habitats.
We simulate a river basin heavy metal survey measuring six trace
elements (\(\mu\text{g/L}\)): Arsenic
(As), Cadmium (Cd), Chromium
(Cr), Copper (Cu), Lead (Pb), and
Zinc (Zn) across five monitoring reaches with 5 sample
replicates per reach:
set.seed(2121)
n_reps <- 5
sites <- c("Site_1 (Forest)", "Site_2 (Mining)", "Site_3 (Smelting)", "Site_4 (Urban)", "Site_5 (Estuary)")
metal_survey <- do.call(rbind, lapply(sites, function(stn) {
# Baseline metal concentrations (ug/L)
means <- switch(stn,
"Site_1 (Forest)" = c(As = 1.2, Cd = 0.2, Cr = 4.5, Cu = 45, Pb = 1.1, Zn = 120),
"Site_2 (Mining)" = c(As = 18.5, Cd = 5.8, Cr = 68.0, Cu = 1850, Pb = 24.5, Zn = 4200),
"Site_3 (Smelting)" = c(As = 26.0, Cd = 8.2, Cr = 95.0, Cu = 2600, Pb = 38.0, Zn = 5800),
"Site_4 (Urban)" = c(As = 6.5, Cd = 1.4, Cr = 28.0, Cu = 420, Pb = 8.5, Zn = 1450),
"Site_5 (Estuary)" = c(As = 4.2, Cd = 0.9, Cr = 16.5, Cu = 210, Pb = 4.2, Zn = 850)
)
data.frame(
Station = stn,
As = pmax(0.1, rnorm(n_reps, mean = means["As"], sd = means["As"] * 0.12)),
Cd = pmax(0.05, rnorm(n_reps, mean = means["Cd"], sd = means["Cd"] * 0.15)),
Cr = pmax(0.5, rnorm(n_reps, mean = means["Cr"], sd = means["Cr"] * 0.10)),
Cu = pmax(2.0, rnorm(n_reps, mean = means["Cu"], sd = means["Cu"] * 0.12)),
Pb = pmax(0.1, rnorm(n_reps, mean = means["Pb"], sd = means["Pb"] * 0.14)),
Zn = pmax(5.0, rnorm(n_reps, mean = means["Zn"], sd = means["Zn"] * 0.10))
)
}))
head(metal_survey, 6)
#> Station As Cd Cr Cu Pb Zn
#> 1 Site_1 (Forest) 1.085621 0.1844308 4.358769 45.40370 1.167778 132.3617
#> 2 Site_1 (Forest) 1.081720 0.1949841 4.678270 49.65574 1.101673 134.3707
#> 3 Site_1 (Forest) 1.209242 0.1466376 3.586198 46.37561 1.233846 124.5558
#> 4 Site_1 (Forest) 1.160399 0.1911586 4.680002 48.77459 1.350155 120.0001
#> 5 Site_1 (Forest) 1.220738 0.1577921 4.466422 41.96124 1.161292 118.0802
#> 6 Site_2 (Mining) 18.100377 4.5200358 71.436150 1746.83159 20.417828 4330.6734Run calc_metal_indices() using standard international
drinking and aquatic life guideline limits:
res_metals <- calc_metal_indices(
data = metal_survey,
id_col = "Station",
standards = c(
As = 10,
Cd = 3,
Cr = 50,
Cu = 2000,
Pb = 10,
Zn = 3000
),
ci = 0.95
)
# Station summary statistics table (Means, 95% CIs, and assigned status)
head(res_metals$Summary_Data[, c("Station", "HPI_Mean", "HPI_Status", "PLI_Mean", "PLI_Status")], 5)
#> Station HPI_Mean HPI_Status PLI_Mean PLI_Status
#> Mean Site_1 (Forest) 8.08 Suitable 0.06 Unpolluted
#> Mean1 Site_2 (Mining) 205.36 Polluted/Critical 1.62 Polluted
#> Mean2 Site_3 (Smelting) 295.52 Polluted/Critical 2.30 Polluted
#> Mean3 Site_4 (Urban) 56.92 Suitable 0.49 Unpolluted
#> Mean4 Site_5 (Estuary) 34.26 Suitable 0.29 Unpollutedplot_metal_indices() creates status-filled charts with
95% confidence intervals and dashed benchmark thresholds.
Displays mean HPI values with 95% confidence intervals against the critical threshold line (\(\text{HPI} = 100\)):
p_hpi <- plot_metal_indices(
data = res_metals,
metric = "HPI",
id_col = "Station",
type = "bar",
show_ci = TRUE,
show_threshold = TRUE,
palette = "auto",
title = "Heavy Metal Pollution Index (HPI) Across River Stations"
)
# Render HPI plot
p_hpiVisualizes the multi-metal Pollution Load Index (PLI) relative to the baseline threshold (\(\text{PLI} = 1.0\)):
p_pli <- plot_metal_indices(
data = res_metals,
metric = "PLI",
id_col = "Station",
type = "lollipop",
show_ci = TRUE,
show_threshold = TRUE,
palette = "auto",
title = "Pollution Load Index (PLI) Across Sampling Reaches"
)
p_pliEvaluates cumulative excess contamination (\(C_d\)) with contamination limits (\(C_d = 6, 12\)):
p_cd <- plot_metal_indices(
data = res_metals,
metric = "Cd",
id_col = "Station",
type = "bar",
show_ci = TRUE,
show_threshold = TRUE,
palette = "auto"
)
p_cdInspect sample-level contamination factors (\(CF_i\)) and overall indices:
# 1. Preview replicate contamination factors and computed indices (first 4 samples)
head(res_metals$Replicate_Data[, c("Station", "CF_As", "CF_Cd", "CF_Pb", "HPI", "PLI")], 4)
#> Station CF_As CF_Cd CF_Pb HPI PLI
#> 1 Site_1 (Forest) 0.11 0.06 0.12 8.08 0.06
#> 2 Site_1 (Forest) 0.11 0.06 0.11 8.19 0.07
#> 3 Site_1 (Forest) 0.12 0.05 0.12 7.61 0.06
#> 4 Site_1 (Forest) 0.12 0.06 0.14 8.71 0.07
# 2. Comprehensive Station Summary
res_metals$Summary_Data[, c("Station", "HEI_Mean", "HEI_Status", "Nemerow_PN_Mean", "Nemerow_Status")]
#> Station HEI_Mean HEI_Status Nemerow_PN_Mean Nemerow_Status
#> Mean Site_1 (Forest) 0.45 Low 0.10 Clean
#> Mean1 Site_2 (Mining) 10.14 Medium 2.14 Moderately Polluted
#> Mean2 Site_3 (Smelting) 14.49 Medium 3.11 Heavily Polluted
#> Mean3 Site_4 (Urban) 3.20 Low 0.73 Warning Limit
#> Mean4 Site_5 (Estuary) 1.85 Low 0.37 CleanThe summarize_obs() function generates comprehensive
descriptive summaries for multi-parameter limnological, water quality,
and fisheries biometric matrices. Before applying formal parametric or
multivariate models, empirical researchers require rigorous tabular
summaries detailing central tendency, variance, standard errors, and
sample dispersion across spatial or experimental factor levels.
The function computes both parametric metrics (Sample Size \(N\), Arithmetic Mean \(\bar{x}\), Standard Deviation \(s\), Standard Error of the Mean \(\text{SE}\)) and non-parametric metrics
(Median \(\tilde{x}\), Minimum,
Maximum), constructs ready-to-publish composite notation (\(\bar{x} \pm s\)), accommodates multi-factor
grouping strata, and supports direct export to formatted Excel workbooks
(.xlsx) or comma-separated tables (.csv).
Let \(\{x_1, x_2, \dots, x_n\}\) denote a set of \(n\) complete observations recorded for a continuous aquatic variable \(X\) within a given sampling stratum.
Arithmetic Mean (\(\bar{x}\)): \[\bar{x} = \frac{1}{n} \sum_{i=1}^n x_i\]
Sample Standard Deviation (\(s\)): Measures sample dispersion around the mean using Bessel’s correction for unbiased estimation: \[s = \sqrt{\frac{1}{n - 1} \sum_{i=1}^n (x_i - \bar{x})^2} \quad (n > 1)\]
Standard Error of the Mean (\(\text{SE}\)): Quantifies the theoretical precision of the sample mean as an estimator of the true population parameter \(\mu\): \[\text{SE} = \frac{s}{\sqrt{n}}\]
For non-normal, skewed, or zero-inflated aquatic measurements (e.g., bacterial coliform densities, chemical trace contaminants):
Sample Median (\(\tilde{x}\)): The 50th percentile order statistic, robust against extreme outlier observations: \[\tilde{x} = \begin{cases} x_{\left(\frac{n+1}{2}\right)} & \text{if } n \text{ is odd} \\ \frac{1}{2}\left(x_{\left(\frac{n}{2}\right)} + x_{\left(\frac{n}{2} + 1\right)}\right) & \text{if } n \text{ is even} \end{cases}\]
Sample Extrema: \[\text{Min} = x_{(1)}, \quad \text{Max} = x_{(n)}\]
We simulate a river basin fisheries survey evaluating fish Total
Length (cm), Total Weight (g), and Condition Factor (\(K\)) across 60 specimens sampled across
three distinct River Systems (Karnafuli,
Halda, Sangu) during two hydrologic periods
(Dry, Wet):
set.seed(2222)
n_per_combo <- 10
systems <- c("Karnafuli", "Halda", "Sangu")
seasons <- c("Dry", "Wet")
survey_grid <- expand.grid(River = systems, Season = seasons)
survey_rows <- lapply(seq_len(nrow(survey_grid)), function(i) {
rv <- as.character(survey_grid$River[i])
sn <- as.character(survey_grid$Season[i])
mean_len <- switch(rv, "Karnafuli" = 28.5, "Halda" = 34.0, "Sangu" = 22.0)
if (sn == "Wet") mean_len <- mean_len * 1.15
len <- rnorm(n_per_combo, mean = mean_len, sd = 3.2)
# Allometric length-weight relationship: W = 0.012 * L^3.05
wt <- 0.012 * (len^3.05) + rnorm(n_per_combo, mean = 0, sd = 15)
k <- (wt / (len^3)) * 100
data.frame(
River = rv,
Season = sn,
Total_Length_cm = pmax(10, len),
Body_Weight_g = pmax(20, wt),
Condition_Factor = k,
stringsAsFactors = FALSE
)
})
fish_morph_survey <- do.call(rbind, survey_rows)
head(fish_morph_survey, 6)
#> River Season Total_Length_cm Body_Weight_g Condition_Factor
#> 1 Karnafuli Dry 27.41820 324.7617 1.575607
#> 2 Karnafuli Dry 31.50533 432.1249 1.381839
#> 3 Karnafuli Dry 34.06070 552.2470 1.397567
#> 4 Karnafuli Dry 30.72824 369.5970 1.273841
#> 5 Karnafuli Dry 29.97935 401.8828 1.491533
#> 6 Karnafuli Dry 27.49172 304.6220 1.466073summarize_obs() supports global overall summaries as
well as single-factor and multi-factor stratified analyses.
Summarizes length, weight, and condition factor across river systems:
res_summary_river <- summarize_obs(
data = fish_morph_survey,
vars = c("Total_Length_cm", "Body_Weight_g", "Condition_Factor"),
group = "River",
digits = 2
)
# Display tabular summary
res_summary_river[, c("River", "Variable", "N", "Mean_SD", "SE", "Median", "Min", "Max")]
#> River Variable N Mean_SD SE Median Min Max
#> 1 Halda Total_Length_cm 20 35.47 ± 3.74 0.84 34.54 30.10 43.62
#> 2 Karnafuli Total_Length_cm 20 31.87 ± 3.09 0.69 32.24 25.00 37.11
#> 3 Sangu Total_Length_cm 20 23.56 ± 4.16 0.93 23.28 17.32 31.51
#> 4 Halda Body_Weight_g 20 663.07 ± 227.50 50.87 600.84 396.03 1211.61
#> 5 Karnafuli Body_Weight_g 20 474.78 ± 129.99 29.07 480.26 228.20 730.16
#> 6 Sangu Body_Weight_g 20 195.32 ± 112.50 25.16 177.42 59.98 452.93
#> 7 Halda Condition_Factor 20 1.44 ± 0.04 0.01 1.44 1.34 1.49
#> 8 Karnafuli Condition_Factor 20 1.43 ± 0.06 0.01 1.44 1.27 1.58
#> 9 Sangu Condition_Factor 20 1.34 ± 0.13 0.03 1.38 0.99 1.50Evaluates biometric traits across both River System and Season simultaneously:
res_summary_factorial <- summarize_obs(
data = fish_morph_survey,
vars = c("Total_Length_cm", "Body_Weight_g"),
group = c("River", "Season"),
digits = 2
)
head(res_summary_factorial[, c("River", "Season", "Variable", "Mean_SD", "SE", "Median")], 6)
#> River Season Variable Mean_SD SE Median
#> 1 Halda Dry Total_Length_cm 32.87 ± 1.58 0.50 33.18
#> 2 Karnafuli Dry Total_Length_cm 30.96 ± 2.52 0.80 31.12
#> 3 Sangu Dry Total_Length_cm 20.95 ± 2.50 0.79 20.82
#> 4 Halda Wet Total_Length_cm 38.07 ± 3.47 1.10 37.24
#> 5 Karnafuli Wet Total_Length_cm 32.78 ± 3.45 1.09 32.82
#> 6 Sangu Wet Total_Length_cm 26.17 ± 3.91 1.24 26.17The returned data frame provides publication-ready string representations alongside raw numeric columns suitable for downstream reporting or plotting:
# 1. Structure of the exported summary table
str(res_summary_river)
#> 'data.frame': 9 obs. of 10 variables:
#> $ River : chr "Halda" "Karnafuli" "Sangu" "Halda" ...
#> $ Variable: chr "Total_Length_cm" "Total_Length_cm" "Total_Length_cm" "Body_Weight_g" ...
#> $ N : int 20 20 20 20 20 20 20 20 20
#> $ Mean : num 35.5 31.9 23.6 663.1 474.8 ...
#> $ SD : num 3.74 3.09 4.16 227.5 129.99 ...
#> $ SE : num 0.84 0.69 0.93 50.87 29.07 ...
#> $ Median : num 34.5 32.2 23.3 600.8 480.3 ...
#> $ Min : num 30.1 25 17.3 396 228.2 ...
#> $ Max : num 43.6 37.1 31.5 1211.6 730.2 ...
#> $ Mean_SD : chr "35.47 ± 3.74" "31.87 ± 3.09" "23.56 ± 4.16" "663.07 ± 227.50" ...
# 2. Inspect Mean +/- SD column for direct manuscript inclusion
res_summary_river[, c("River", "Variable", "Mean_SD")]
#> River Variable Mean_SD
#> 1 Halda Total_Length_cm 35.47 ± 3.74
#> 2 Karnafuli Total_Length_cm 31.87 ± 3.09
#> 3 Sangu Total_Length_cm 23.56 ± 4.16
#> 4 Halda Body_Weight_g 663.07 ± 227.50
#> 5 Karnafuli Body_Weight_g 474.78 ± 129.99
#> 6 Sangu Body_Weight_g 195.32 ± 112.50
#> 7 Halda Condition_Factor 1.44 ± 0.04
#> 8 Karnafuli Condition_Factor 1.43 ± 0.06
#> 9 Sangu Condition_Factor 1.34 ± 0.13The plot_histogram() function generates
publication-grade frequency distributions, empirical kernel density
estimates (KDE), and theoretical normal curves for fisheries
morphometrics and environmental water quality metrics. Inspecting
distributional skewness, modality (e.g., polymodal fish cohorts), and
kurtosis is a prerequisite for evaluating parametric model assumptions
(ANOVA, linear regression) and detecting recruitment pulses in
length-frequency data.
The function supports automatic dynamic scaling between raw counts and probability density space, overlays theoretical Gaussian profiles with empirical kernel fits, displays parametric mean (solid red line) and non-parametric median (dashed blue line) references, and supports multi-panel faceting across environmental strata.
Let \(\{x_1, x_2, \dots, x_n\}\) denote \(n\) independent observations of a continuous fisheries variable \(X\) (e.g., fork length or body weight).
To estimate the probability density function \(f(x)\) continuously without discretization artifacts, kernel smoothing is defined as:
\[\hat{f}_h(x) = \frac{1}{n h} \sum_{i=1}^n K\left(\frac{x - x_i}{h}\right)\]
where \(K(\cdot)\) is a standard Gaussian kernel function \(K(u) = \frac{1}{\sqrt{2\pi}} e^{-\frac{1}{2}u^2}\) and \(h > 0\) is the bandwidth smoothing parameter.
When add_normal = TRUE is enabled, the function projects
the parameterized normal distribution curve evaluated at sample
estimates \((\bar{x}, s)\):
\[f(x; \bar{x}, s) = \frac{1}{s \sqrt{2\pi}} \exp\left( -\frac{(x - \bar{x})^2}{2 s^2} \right)\]
Discrepancies between the empirical kernel curve (red) and the theoretical Gaussian curve (green dashed) highlight skewness and kurtosis deviations from normality.
The function evaluates asymmetry using the divergence between: - Sample Mean: \(\bar{x} = \frac{1}{n} \sum_{i=1}^n x_i\) (solid red line) - Sample Median: \(\tilde{x} = x_{(0.5)}\) (dashed blue line)
In right-skewed data (common in juvenile fish counts or pollutant spikes), \(\bar{x} > \tilde{x}\), whereas left-skewed distributions exhibit \(\bar{x} < \tilde{x}\).
We simulate a length-frequency fisheries survey measuring Total
Length (cm) across 250 specimens of an estuarine fish population sampled
across three estuarine habitats (Mangrove Creek,
Mudflat, Open Channel):
set.seed(2323)
n_fish <- 250
habitats <- sample(c("Mangrove Creek", "Mudflat", "Open Channel"), size = n_fish, replace = TRUE, prob = c(0.4, 0.35, 0.25))
# Simulate bimodal cohort growth
lengths <- vapply(habitats, function(hab) {
cohort <- rbinom(1, size = 1, prob = 0.65) # Age-0 vs Age-1 cohort
if (hab == "Mangrove Creek") {
if (cohort == 1) rnorm(1, 14.5, 1.8) else rnorm(1, 22.0, 2.1)
} else if (hab == "Mudflat") {
if (cohort == 1) rnorm(1, 18.2, 2.0) else rnorm(1, 26.5, 2.4)
} else {
if (cohort == 1) rnorm(1, 22.8, 2.2) else rnorm(1, 31.0, 2.8)
}
}, numeric(1))
estuary_fish <- data.frame(
Habitat = factor(habitats),
Total_Length = pmax(8.0, lengths)
)
head(estuary_fish, 6)
#> Habitat Total_Length
#> 1 Open Channel 29.48856
#> 2 Open Channel 20.06740
#> 3 Mangrove Creek 14.41802
#> 4 Mangrove Creek 26.79226
#> 5 Mudflat 19.81244
#> 6 Open Channel 27.19829plot_histogram() provides single-distribution
diagnostics with distribution curves and comparative multi-panel faceted
views.
Overlays the empirical kernel density curve, theoretical Gaussian curve, and mean/median reference lines:
p_hist_single <- plot_histogram(
data = estuary_fish,
x = "Total_Length",
bins = 25,
add_density = TRUE,
add_normal = TRUE,
show_stats = TRUE,
xlab = "Total Length (cm)",
ylab = "Density",
title = "Fish Length-Frequency Distribution Diagnostics"
)
# Render distribution plot
p_hist_singleUsing facet = TRUE separates the length-frequency
distribution across habitats into separate panels:
p_hist_faceted <- plot_histogram(
data = estuary_fish,
x = "Total_Length",
group = "Habitat",
facet = TRUE,
bins = 20,
add_density = TRUE,
palette = "Dark2",
xlab = "Total Length (cm)",
title = "Comparative Length Distributions Across Habitats"
)
p_hist_facetedExamine the divergence between mean and median to evaluate skewness:
# 1. Compare central tendency benchmarks
mean_len <- mean(estuary_fish$Total_Length)
median_len <- median(estuary_fish$Total_Length)
sd_len <- sd(estuary_fish$Total_Length)
data.frame(
Metric = c("Mean (cm)", "Median (cm)", "Std. Deviation (cm)", "Skewness Offset"),
Value = round(c(mean_len, median_len, sd_len, mean_len - median_len), 3)
)
#> Metric Value
#> 1 Mean (cm) 21.033
#> 2 Median (cm) 20.625
#> 3 Std. Deviation (cm) 5.279
#> 4 Skewness Offset 0.408The plot_bar() and plot_lollipop()
functions render standardized bar and lollipop charts for categorical
inventories, survey frequency counts, and metric aggregations across
fisheries and aquatic ecology studies. Whether reporting catch
frequencies across fishing gear types, species occurrence counts across
sampling zones, or aggregated landings per river basin, these functions
provide direct comparative visualization.
The functions provide three operating modes: 1. Frequency
Tabulation (y = NULL): Computes categorical
occurrence counts automatically. 2. Metric Summary
Aggregation (stat = "mean" or "sum"):
Aggregates continuous numeric responses across single or multi-factor
grouping levels. 3. Identity Plotting
(stat = "identity"): Plots pre-computed values
directly.
In addition to standard bars (type = "bar"), lollipop
geometry (type = "lollipop" or via
plot_lollipop()) replaces solid bars with a slender stem
and circular head, reducing ink-to-data ratio while enhancing clarity
when comparing numerous categories. Both modes support side-by-side
grouped arrangements (position = "dodge"), cumulative
stacking (position = "stack"), 100% proportional fills
(position = "fill"), automated numeric value labelling,
coordinate flipping, and multi-column batch plotting.
Let a survey dataset contain observations indexed by \(i = 1, \dots, n\) across categorical predictor \(X\) with \(K\) unique levels.
When no response metric \(Y\) is supplied, the plotted height for factor level \(k\) is the sample frequency:
\[f_k = \sum_{i=1}^n \mathbb{I}(x_i = k)\]
When numeric variable \(Y\) is
specified: - Mean Aggregation (stat = "mean"): \[\bar{y}_k = \frac{1}{n_k} \sum_{i: x_i = k}
y_i\] - Sum Aggregation (stat = "sum"): \[Y_k = \sum_{i: x_i = k} y_i\]
position = "fill")When examining proportional composition across a secondary grouping factor \(G\) with \(M\) categories:
\[p_{km} = \frac{\sum_{i: x_i = k, g_i = m} y_i}{\sum_{m'=1}^M \sum_{i: x_i = k, g_i = m'} y_i}\]
The height of each stack normalizes to \(1.0\) (or 100%), allowing comparison of relative composition independently of differences in total sample size.
We simulate a marine small-scale fisheries monitoring survey
evaluating fish landings (kg per trip) and catch values across 90
fishing trips distributed among three Gear Types (Gillnet,
Longline, Trap) across three Coastal Sectors
(North Sector, Central Sector,
South Sector):
set.seed(2424)
n_trips <- 90
gears <- sample(c("Gillnet", "Longline", "Trap"), size = n_trips, replace = TRUE, prob = c(0.45, 0.35, 0.20))
sectors <- sample(c("North Sector", "Central Sector", "South Sector"), size = n_trips, replace = TRUE)
landings_kg <- vapply(seq_len(n_trips), function(i) {
g <- gears[i]
base <- switch(g, "Gillnet" = 120, "Longline" = 210, "Trap" = 65)
round(pmax(15, rnorm(1, mean = base, sd = base * 0.22)), 1)
}, numeric(1))
fisheries_catch_survey <- data.frame(
Gear_Type = factor(gears),
Sector = factor(sectors),
Landings_kg = landings_kg
)
head(fisheries_catch_survey, 6)
#> Gear_Type Sector Landings_kg
#> 1 Longline Central Sector 227.8
#> 2 Longline South Sector 225.8
#> 3 Gillnet North Sector 146.2
#> 4 Longline Central Sector 178.5
#> 5 Longline Central Sector 244.7
#> 6 Trap North Sector 75.6Calculates mean landings per trip across Gear Types grouped by Coastal Sector, placing numeric value labels above each bar:
p_bar_dodge <- plot_bar(
data = fisheries_catch_survey,
x = "Gear_Type",
y = "Landings_kg",
group = "Sector",
stat = "mean",
position = "dodge",
show_labels = TRUE,
palette = "Dark2",
title_prefix = "Small-Scale Fisheries"
)
p_bar_dodgeposition = "fill")Visualizes the relative proportion of fishing effort (trip count) per Gear Type across coastal sectors:
p_bar_fill <- plot_bar(
data = fisheries_catch_survey,
x = "Sector",
group = "Gear_Type",
position = "fill",
palette = "Set2",
ylab = "Proportion of Fishing Trips",
title_prefix = "Gear Composition"
)
p_bar_fillWhen category labels are long, horizontal = TRUE flips
coordinate axes to preserve legibility:
p_bar_horiz <- plot_bar(
data = fisheries_catch_survey,
x = "Gear_Type",
y = "Landings_kg",
stat = "sum",
horizontal = TRUE,
show_labels = TRUE,
palette = "Blues",
ylab = "Total Catch Landings (kg)"
)
p_bar_horizLollipop plots provide high contrast for ranked or discrete totals while eliminating visual clutter:
p_lollipop <- plot_lollipop(
data = fisheries_catch_survey,
x = "Gear_Type",
y = "Landings_kg",
stat = "sum",
point_size = 4.0,
line_size = 1.0,
show_labels = TRUE,
palette = "Set1",
ylab = "Total Catch Landings (kg)",
title_prefix = "Lollipop Summary"
)
p_lollipopCompares mean landings across gears dodged by coastal sector using lollipop geometry:
p_lollipop_grouped <- plot_bar(
data = fisheries_catch_survey,
x = "Gear_Type",
y = "Landings_kg",
group = "Sector",
type = "lollipop",
stat = "mean",
point_size = 3.8,
line_size = 0.9,
palette = "Dark2",
title_prefix = "Dodged Lollipop Comparison"
)
p_lollipop_grouped# Summary table of Mean Landings per Gear and Sector
aggregate(Landings_kg ~ Gear_Type + Sector, data = fisheries_catch_survey, FUN = function(x) round(mean(x), 2))
#> Gear_Type Sector Landings_kg
#> 1 Gillnet Central Sector 126.12
#> 2 Longline Central Sector 227.89
#> 3 Trap Central Sector 70.24
#> 4 Gillnet North Sector 130.36
#> 5 Longline North Sector 262.84
#> 6 Trap North Sector 62.66
#> 7 Gillnet South Sector 104.42
#> 8 Longline South Sector 205.10
#> 9 Trap South Sector 62.73In fisheries management, aquaculture surveys, and aquatic biomonitoring, reporting categorical proportions is standard practice. Whether portraying the percentage contribution of distinct species to total commercial landings, evaluating multi-tiered habitat classifications, or quantifying fisher community perceptions regarding marine protected areas (MPAs), standard plots often underperform in clarity or visual engagement.
The aquats package provides four specialized
compositional and perception graphics: 1. plot_pie3d():
Native isometric 3D pie charts with perspective compression, extruded
depth rims, and shadow rendering. 2. plot_circular_bar():
Radial bar charts with concentric reference gridlines and dynamic label
angles for many-category comparisons. 3. plot_donut():
Single-, double-, or triple-tiered nested sunburst/donut charts
representing hierarchical ecological structures. 4.
plot_likert(): Diverging and 100% stacked horizontal bar
charts designed for socio-economic survey evaluation.
plot_pie3d)The plot_pie3d() function renders an isometric 3D pie
chart directly in ggplot2 by calculating geometric polygon
coordinates for both the top circular face (compressed by perspective
factor \(\lambda \in [0.2, 0.8]\)) and
the extruded bottom rim:
\[x(\theta) = r \cos(\theta), \quad y(\theta) = r \sin(\theta) \times \lambda\]
The front rim is extruded over the interval \(\theta \in [\pi, 2\pi]\) (where \(y \le 0\)) with vertical depth \(\delta\):
\[y_{\text{rim\_bottom}}(\theta) = y(\theta) - \delta\]
We simulate commercial fisheries catch composition data across five key target species:
catch_composition <- data.frame(
Species = c("Hilsa Shad", "Rohu Carp", "Catla", "Prawn", "Catfish"),
Landings_MT = c(420, 210, 150, 95, 45)
)
p_pie <- plot_pie3d(
data = catch_composition,
x = "Species",
y = "Landings_MT",
stat = "identity",
tilt = 0.52,
depth = 0.22,
show_labels = TRUE,
min_percent = 5,
palette = "Set2",
title = "Annual Landings Composition by Target Taxa (MT)"
)
# Render 3D pie chart
p_pieplot_circular_bar)When a categorical variable contains eight or more levels (e.g.,
river stations, fishing gears, sub-districts), standard vertical bar
charts require horizontal label rotation that impairs readability.
plot_circular_bar() maps values along a polar coordinate
axis (coord_polar), reserving a central void fraction
(\(\rho\)) and rendering dashed
concentric reference rings with scale labels:
\[\theta_i = \frac{2\pi (i - 0.5)}{K}, \quad r_i = y_i\]
We simulate gear-specific catch per unit effort (CPUE in kg/haul) across 10 artisanal gear types:
gear_cpue <- data.frame(
Gear = c("Current Net", "Set Bag Net", "Longline", "Cast Net", "Gillnet",
"Push Net", "Drag Net", "Stake Net", "Traps", "Hook & Line"),
CPUE = c(42.5, 38.2, 29.0, 14.8, 24.5, 11.2, 18.6, 21.4, 8.5, 16.0)
)
p_circular <- plot_circular_bar(
data = gear_cpue,
x = "Gear",
y = "CPUE",
stat = "identity",
inner_radius = 0.35,
show_grid = TRUE,
palette = "Spectral",
title = "Artisanal Gear Catch per Unit Effort (kg/haul)"
)
# Render circular bar plot
p_circularplot_donut)The plot_donut() function displays hierarchical
groupings (e.g., Habitat Zone \(\to\)
Feeding Guild \(\to\) Target Species)
across up to three concentric rings. Each layer aligns angular
boundaries (\(\theta_{\min},
\theta_{\max}\)) with parent categories while varying radial
boundaries (\(r_{\text{in}},
r_{\text{out}}\)):
\[\theta_{\text{start}}^{(k)} = \sum_{j=1}^{k-1} p_j \times 2\pi, \quad \theta_{\text{end}}^{(k)} = \sum_{j=1}^k p_j \times 2\pi\]
We simulate an estuarine survey categorizing fish biomass across Zone (Inner vs Outer Estuary) and Trophic Guild (Carnivore, Herbivore, Planktivore):
hierarchical_survey <- data.frame(
Zone = c("Inner", "Inner", "Inner", "Outer", "Outer", "Outer"),
Guild = c("Carnivore", "Herbivore", "Planktivore", "Carnivore", "Herbivore", "Planktivore"),
Biomass = c(120, 85, 145, 210, 60, 95)
)
p_donut <- plot_donut(
data = hierarchical_survey,
levels = c("Zone", "Guild"),
value = "Biomass",
show_labels = TRUE,
min_percent = 5,
palette = "Dark2",
title = "Hierarchical Estuarine Biomass (Zone > Trophic Guild)"
)
# Render nested donut chart
p_donutplot_likert)Socio-economic evaluations in fisheries (e.g., fisher compliance,
willingness to support seasonal bans, perception of catch declines) are
recorded on ordinal Likert scales. plot_likert() aligns
responses using either a diverging layout (centered on the neutral
mid-point) or a 100% stacked layout.
For an odd number of categories \(K\) with mid-point \(M = \lceil K/2 \rceil\), the diverging coordinate offset is:
\[x_{\text{start}} = -\left( \sum_{k=1}^{M-1} p_k + \frac{1}{2} p_M \right)\]
This splits the neutral category symmetrically across the vertical \(x = 0\) axis while anchoring negative responses (Disagree) to the left and positive responses (Agree) to the right.
We simulate a community perception survey evaluating fisher responses across four socio-economic items:
set.seed(2525)
n_fishers <- 100
likert_levels <- c("Strongly Disagree", "Disagree", "Neutral", "Agree", "Strongly Agree")
survey_perceptions <- data.frame(
Sanctuary_Support = sample(likert_levels, n_fishers, replace = TRUE, prob = c(0.10, 0.15, 0.10, 0.35, 0.30)),
Seasonal_Ban_Compliance = sample(likert_levels, n_fishers, replace = TRUE, prob = c(0.08, 0.12, 0.15, 0.40, 0.25)),
Catch_Decline_Observed = sample(likert_levels, n_fishers, replace = TRUE, prob = c(0.05, 0.08, 0.12, 0.35, 0.40)),
Subsidy_Sufficiency = sample(likert_levels, n_fishers, replace = TRUE, prob = c(0.35, 0.30, 0.15, 0.12, 0.08))
)
p_likert_diverging <- plot_likert(
data = survey_perceptions,
items = c("Sanctuary_Support", "Seasonal_Ban_Compliance", "Catch_Decline_Observed", "Subsidy_Sufficiency"),
levels = likert_levels,
type = "diverging",
palette = "RdYlBu",
show_labels = TRUE,
clean_labels = TRUE,
title = "Fisher Community Conservation Perception Survey (Diverging %)"
)
# Render diverging Likert plot
p_likert_divergingVisualizing interconnected pathways—such as energy transfer across trophic webs, ontogenetic dietary shifts across life stages, contaminant routing through environmental compartments, or fish migration across river catchments—requires representing both categorical transitions and volumetric flow magnitudes.
Alluvial and Sankey diagrams represent discrete categories as
vertically stacked blocks (strata) distributed across
consecutive parallel stages (axes), interconnected by
curvilinear ribbons (alluvia) whose heights are
proportional to flow frequency, biomass, or energy flux. The
plot_sankey() function provides a publication-ready
interface built on top of ggalluvial and
ggplot2, offering automated frequency aggregation, weighted
flows, missing-case filtering, and adaptive color palette
interpolation.
The diagram decomposes relational data across \(k\) ordered categorical axes: 1.
Strata (Nodes): Represent discrete factor levels \(A_{i, j}\) at stage \(i\). 2. Alluvia (Flow
Ribbons): Trace the path taken by observations across all
stages \(i \in \{1, \ldots, k\}\). 3.
Weighting Metrics: * Unweighted Frequency
(\(N\)): When
value = NULL, ribbon thickness represents sample counts:
\[W = \sum 1\] * Weighted Flux
(\(V\)): When
value is assigned (e.g., ingested biomass \(\text{g}\), energy flow \(\text{t}\cdot\text{km}^{-2}\cdot\text{year}^{-1}\),
or contaminant concentration), ribbon thickness scales to cumulative
quantity: \[W = \sum V\]
We simulate an aquatic dietary study evaluating stomach content analysis across life stages, predatory feeding guilds, and preferred prey taxa:
set.seed(42)
diet_survey <- data.frame(
Life_Stage = factor(
sample(c("Juvenile", "Sub-Adult", "Adult"), 200, replace = TRUE, prob = c(0.40, 0.35, 0.25)),
levels = c("Juvenile", "Sub-Adult", "Adult")
),
Feeding_Guild = factor(
sample(c("Planktivore", "Benthivore", "Piscivore"), 200, replace = TRUE, prob = c(0.45, 0.35, 0.20)),
levels = c("Planktivore", "Benthivore", "Piscivore")
),
Prey_Taxa = factor(
sample(c("Cladocera", "Chironomidae", "Small Teleost", "Detritus"), 200, replace = TRUE),
levels = c("Cladocera", "Chironomidae", "Small Teleost", "Detritus")
),
Biomass_g = round(runif(200, min = 0.5, max = 18.5), 2)
)
head(diet_survey)
#> Life_Stage Feeding_Guild Prey_Taxa Biomass_g
#> 1 Adult Piscivore Detritus 9.47
#> 2 Adult Benthivore Detritus 5.59
#> 3 Juvenile Piscivore Detritus 14.48
#> 4 Adult Planktivore Cladocera 5.97
#> 5 Sub-Adult Planktivore Detritus 9.78
#> 6 Sub-Adult Planktivore Chironomidae 9.10Flow ribbons are scaled to total prey biomass (\(\text{g}\)) and colored according to the
primary origin axis (Life_Stage):
p_weighted <- plot_sankey(
data = diet_survey,
axes = c("Life_Stage", "Feeding_Guild", "Prey_Taxa"),
value = "Biomass_g",
fill_axis = "Life_Stage",
alpha = 0.70,
width = 0.28,
palette = "Dark2",
title = "Style A: Biomass-Weighted Dietary Flux Across Life History Stages"
)
print(p_weighted)When individual sample records or capture occurrences are tracked
without biomass weighting (value = NULL), ribbons reflect
observation frequencies and are colored by the intermediate axis
(Feeding_Guild):
p_counts <- plot_sankey(
data = diet_survey,
axes = c("Life_Stage", "Feeding_Guild", "Prey_Taxa"),
value = NULL, # Automatically computes record counts
fill_axis = "Feeding_Guild", # Colors ribbons by guild category
alpha = 0.60,
palette = "Set2",
title = "Style B: Specimen Frequency Pathways Across Trophic Strata"
)
print(p_counts)Sankey diagrams can also model spatial routing or migration between two distinct environmental zones:
# Simulate upstream-to-downstream fish passage counts
set.seed(101)
passage_data <- data.frame(
Release_Reach = sample(c("Headwater", "Mainstem Reservoir", "Tributary"), 150, replace = TRUE),
Recapture_Zone = sample(c("Estuary Lower", "Brackish Bay", "Nearshore Marine"), 150, replace = TRUE)
)
p_passage <- plot_sankey(
data = passage_data,
axes = c("Release_Reach", "Recapture_Zone"),
fill_axis = 1,
stratum_fill = "grey96",
stratum_color = "black",
label_size = 4.0,
palette = "Set1",
title = "Style C: Two-Tier Longitudinal Catchment Migration"
)
print(p_passage)Because plot_sankey() returns a native
ggplot2 object, it integrates with standard
ggplot2 layers and themes:
# Add custom axis adjustments and styling layers
p_custom <- p_weighted +
ggplot2::theme(
legend.position = "bottom",
plot.title = ggplot2::element_text(color = "navy", face = "bold", size = 14),
axis.title.y = ggplot2::element_text(size = 11, face = "bold")
) +
ggplot2::guides(fill = ggplot2::guide_legend(nrow = 1))
print(p_custom)To export the diagram at publication resolution:
ggplot2::ggsave(
filename = "trophic_sankey_diagram.png",
plot = p_custom,
width = 9,
height = 5.5,
dpi = 300
)The plot_timeseries() function provides
publication-grade line tracking and longitudinal time-series
visualization for limnological water chemistry, estuarine hydrology, and
fisheries stock surveys. In long-term aquatic studies, monitoring
parameters across temporal gradients (e.g., daily tidal cycles, monthly
flood pulses, annual recruitment oscillations) requires clear
visualization of trends while accounting for within-season sampling
error.
The function handles: 1. Diverse Time-Step Ingestion: Parses date
formats (Date, POSIXct), continuous numeric
steps (e.g., Year: 2018–2026), or ordered calendar factors
("Jan", "Feb", "Mar" or seasonal
pulses: "Pre-Monsoon", "Monsoon",
"Post-Monsoon"). 2. Multi-Factor Hierarchical Partitioning:
- Single-Factor: Unstratified ecosystem temporal tracking. - Two-Factor:
Multi-station or inter-species trajectories differentiated by color
(color_var). - Three-Factor (Factorial Interaction):
High-dimensional tracking combining color, line styles
(linetype_var), and multi-panel faceting
(facet_var). 3. Automated Replicate Aggregation:
Automatically averages replicate samples per time-step and constructs
shaded uncertainty bands: 95% Confidence Intervals ("ci"),
Standard Error ("se"), or Standard Deviation
("sd").
Let \(y_{t, i}\) denote the \(i\)-th sample replicate (\(i = 1, \dots, n_t\)) observed at temporal interval \(t\).
\[\bar{y}_t = \frac{1}{n_t} \sum_{i=1}^{n_t} y_{t, i}\]
Quantifies the precision of the estimated mean trajectory across repeated sampling visits: \[\text{SE}_t = \frac{s_t}{\sqrt{n_t}}, \quad \text{Ribbon}_{\text{SE}} = \bar{y}_t \pm \text{SE}_t\]
Constructs the confidence boundary containing the true population parameter \(\mu_t\) at level \(\alpha = 0.05\): \[\text{CI}_{95\%, \, t} = \bar{y}_t \pm t_{n_t - 1, \, 0.975} \times \frac{s_t}{\sqrt{n_t}}\]
We simulate a 12-month estuarine monitoring survey tracking Dissolved
Oxygen (mg/L) across three River Stations (Upper River,
Middle Basin, Estuary Mouth) at two distinct
Water Depths (Surface vs. Bottom), collecting
4 replicate water column grabs per visit:
set.seed(2929)
months_seq <- c("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec")
stations <- c("Upper River", "Middle Basin", "Estuary Mouth")
depths <- c("Surface", "Bottom")
n_reps <- 4
ts_grid <- expand.grid(Month = months_seq, Station = stations, Depth = depths)
ts_rows <- lapply(seq_len(nrow(ts_grid)), function(i) {
m <- as.character(ts_grid$Month[i])
st <- as.character(ts_grid$Station[i])
dp <- as.character(ts_grid$Depth[i])
# Base seasonal oxygen curve (higher in winter, lower during summer stratification)
m_idx <- match(m, months_seq)
base_do <- 7.5 + 2.2 * cos((m_idx - 1) * (2 * pi / 12))
# Spatial and depth adjustments
st_offset <- switch(st, "Upper River" = 0.8, "Middle Basin" = 0.0, "Estuary Mouth" = -0.6)
dp_offset <- if (dp == "Bottom") -1.4 else 0.0
sim_do <- pmax(2.0, rnorm(n_reps, mean = base_do + st_offset + dp_offset, sd = 0.35))
data.frame(
Month = factor(m, levels = months_seq),
Station = st,
Depth = dp,
DO_mgL = round(sim_do, 2),
stringsAsFactors = FALSE
)
})
aquatic_timeseries <- do.call(rbind, ts_rows)
head(aquatic_timeseries, 8)
#> Month Station Depth DO_mgL
#> 1 Jan Upper River Surface 10.63
#> 2 Jan Upper River Surface 10.49
#> 3 Jan Upper River Surface 10.59
#> 4 Jan Upper River Surface 10.39
#> 5 Feb Upper River Surface 9.88
#> 6 Feb Upper River Surface 9.65
#> 7 Feb Upper River Surface 9.64
#> 8 Feb Upper River Surface 10.59plot_timeseries() provides single-series trajectories,
multi-line comparisons, and full three-factor factorial faceted
layouts.
Tracks overall monthly mean dissolved oxygen pooled across the watershed with 95% Confidence Interval whiskers:
res_ts_single <- plot_timeseries(
data = aquatic_timeseries,
time_var = "Month",
y_var = "DO_mgL",
ribbon = "ci",
point_size = 2.8,
line_width = 1.1,
ylab = "Dissolved Oxygen (mg/L)",
title = "Seasonal Dissolved Oxygen Dynamics (Mean \u00b1 95% CI)"
)
# Render single-series plot
res_ts_single$PlotCompares monthly trajectories across three river monitoring stations using color-coded lines and Standard Error (\(\pm 1\text{ SE}\)) ribbons:
res_ts_stations <- plot_timeseries(
data = aquatic_timeseries,
time_var = "Month",
y_var = "DO_mgL",
color_var = "Station",
ribbon = "se",
color_palette = "Dark2",
point_size = 2.4,
line_width = 1.0,
ylab = "Dissolved Oxygen (mg/L)",
title = "Comparative Station Dissolved Oxygen Trends"
)
res_ts_stations$PlotDemonstrates a high-dimensional layout tracking Month \(\times\) Station \(\times\) Depth: Stations are color-coded,
Water Depths are mapped to distinct line types (Surface
solid vs. Bottom dashed), and stations are faceted into
individual panels:
res_ts_factorial <- plot_timeseries(
data = aquatic_timeseries,
time_var = "Month",
y_var = "DO_mgL",
color_var = "Depth",
linetype_var = "Depth",
facet_var = "Station",
ribbon = "ci",
color_palette = "Set1",
point_size = 2.2,
ylab = "Dissolved Oxygen (mg/L)",
title = "Spatial and Bathymetric Dissolved Oxygen Profiles Across Seasons"
)
res_ts_factorial$PlotThe returned list contains the aggregated statistical table, detailing replicate counts (\(N\)), Means, Standard Deviations, Standard Errors, and Confidence Limits for each discrete temporal coordinate:
# 1. Preview aggregated summary statistics table (First 6 records)
head(res_ts_factorial$Summary_Data, 6)
#> Month Depth Depth.1 Station N Mean SD SE LCI
#> 1 Jan Bottom Bottom Estuary Mouth 4 7.7525 0.1992277 0.09961384 7.435484
#> 2 Feb Bottom Bottom Estuary Mouth 4 7.6200 0.2141650 0.10708252 7.279216
#> 3 Mar Bottom Bottom Estuary Mouth 4 6.4825 0.3854327 0.19271633 5.869191
#> 4 Apr Bottom Bottom Estuary Mouth 4 5.4100 0.6069596 0.30347982 4.444192
#> 5 May Bottom Bottom Estuary Mouth 4 4.3450 0.2272297 0.11361485 3.983427
#> 6 Jun Bottom Bottom Estuary Mouth 4 3.6150 0.3055596 0.15277980 3.128786
#> UCI
#> 1 8.069516
#> 2 7.960784
#> 3 7.095809
#> 4 6.375808
#> 5 4.706573
#> 6 4.101214The aquats package bridges rigorous statistical
modeling, ecological ordination, and publication-ready visualization
into a unified, reproducible framework tailored for observe data in
experimental studies, aquatic ecology, and fisheries science. By
integrating standard hypothesis tests, generalized linear models,
multi-metric water quality indices, multivariate community dynamics, and
longitudinal time-series tracking, aquats streamlines
complex analytical workflows from raw survey matrices to finalized
decision metrics. Users are empowered to conduct transparent, robust
evaluations that support evidence-based management and conservation of
aquatic ecosystems.
I would like to express my heartfelt gratitude and sincere respect to my supervisor, Dr. Mohammed Shahidul Alam, Associate Professor, Department of Fisheries, University of Chittagong, for his invaluable guidance, continuous encouragement, and scholarly support throughout the conceptualization and development of this package.
I also extend my sincere appreciation to the open-source R development community, the members of the aquatic ecology research, and all peer reviewers whose constructive feedback and testing helped refine the package’s numerical routines and visualization standards.
Anderson, M. J. (2001). A new method for non-parametric multivariate analysis of variance. Austral Ecology, 26(1), 32–46. https://doi.org/10.1111/j.1442-9993.2001.01070.pp.x
Brown, R. M., McClelland, N. I., Deininger, R. A., & Tozer, R. G. (1970). A water quality index—do we dare? Water & Sewage Works, 117(10), 339–343.
Clarke, K. R. (1993). Non-parametric multivariate analyses of changes in community structure. Australian Journal of Ecology, 18(1), 117–143. https://doi.org/10.1111/j.1442-9993.1993.tb00438.x
Coleman, B. D., Mares, M. A., Willig, M. R., & Hsieh, Y.-H. (1982). Randomness, area, and species richness. Ecology, 63(4), 1121–1133. https://doi.org/10.2307/1937249
Gotelli, N. J., & Colwell, R. K. (2001). Quantifying biodiversity: procedures and pitfalls in the measurement and comparison of species-richness. Ecology Letters, 4(4), 379–391. https://doi.org/10.1046/j.1461-0248.2001.00230.x
Hakanson, L. (1980). An ecological risk index for aquatic pollution control: A sedimentological approach. Water Research, 14(8), 975–1001. https://doi.org/10.1016/0043-1354(80)90143-8
Kruskal, J. B. (1964). Multidimensional scaling by optimizing goodness of fit to a nonmetric hypothesis. Psychometrika, 29(1), 1–27. https://doi.org/10.1007/BF02289565
Legendre, P., & Anderson, M. J. (1999). Distance-based redundancy analysis: Testing multispecies responses in multifactorial ecological experiments. Ecological Monographs, 69(1), 1–24. https://doi.org/10.1890/0012-9615(1999)069[0001:DBRATM]2.0.CO;2
Legendre, P., & Gallagher, E. D. (2001). Ecologically meaningful transformations for ordination of species data. Oecologia, 129(2), 271–280. https://doi.org/10.1007/s004420100716
Legendre, P., & Legendre, L. (2012). Numerical Ecology (3rd English ed.). Elsevier Science. ISBN: 978-0-444-53868-0.
Mantel, N. (1967). The detection of disease clustering and a generalized regression approach. Cancer Research, 27(2), 209–220.
Margalef, R. (1958). Information theory in ecology. General Systems, 3, 36–71.
McCullagh, P., & Nelder, J. A. (1989). Generalized Linear Models (2nd ed.). Chapman & Hall/CRC. ISBN: 978-0-412-31760-6.
Oksanen, J., Simpson, G. L., Blanchet, F. G., Kindt, R., Legendre, P., Minchin, P. R., O’Hara, R. B., et al. (2022). vegan: Community Ecology Package. R package version 2.6-4. https://CRAN.R-project.org/package=vegan
Pielou, E. C. (1966). The measurement of diversity in different types of biological collections. Journal of Theoretical Biology, 13, 131–144. https://doi.org/10.1016/0022-5193(66)90013-0
Pillai, K. C. S. (1955). Some new test criteria in multivariate analysis. The Annals of Mathematical Statistics, 26(1), 117–121. https://doi.org/10.1214/aoms/1177728607
Shannon, C. E. (1948). A mathematical theory of communication. Bell System Technical Journal, 27(3), 379–423. https://doi.org/10.1002/j.1538-7305.1948.tb01338.x
Simpson, E. H. (1949). Measurement of diversity. Nature, 163(4148), 688. https://doi.org/10.1038/163688a0
Tomlinson, D. L., Wilson, J. G., Harris, C. R., & Jeffrey, D. W. (1980). Problems in the assessment of heavy-metal levels in estuaries and the formation of a pollution index. Helgoländer Meeresuntersuchungen, 33(1), 566–575. https://doi.org/10.1007/BF02414780
Ward, J. H. (1963). Hierarchical grouping to optimize an objective function. Journal of the American Statistical Association, 58(301), 236–244. https://doi.org/10.1080/01621459.1963.10500845
Wickham, H. (2016). ggplot2: Elegant Graphics for Data Analysis. Springer-Verlag New York. https://doi.org/10.1007/978-3-319-24277-4
If you encounter unexpected errors, package bugs, or would like to request new ecological functions or visualization styles, please submit an issue report on GitHub or contact the author directly.
To cite the aquats package in scholarly articles,
reports, or research presentations, please use:
Ali, A., & Alam, M. S. (2026). aquats: Robust Ecological and Fisheries Data Analysis and Visualization (v1.2.1). Zenodo. https://doi.org/10.5281/zenodo.22885025
You can also generate the BibTeX entry directly in R:
@Manual{,
title = {aquats: Robust Ecological and Fisheries Data Analysis and Visualization},
author = {Ataher Ali and Mohammed Shahidul Alam},
year = {2026},
note = {R package version 1.2.1},
doi = {10.5281/zenodo.22885025},
url = {https://github.com/Ataher76/aquats}
}#> R version 4.5.1 (2025-06-13 ucrt)
#> Platform: x86_64-w64-mingw32/x64
#> Running under: Windows 11 x64 (build 26300)
#>
#> Matrix products: default
#> LAPACK version 3.12.1
#>
#> locale:
#> [1] LC_COLLATE=C
#> [2] LC_CTYPE=English_United States.utf8
#> [3] LC_MONETARY=English_United States.utf8
#> [4] LC_NUMERIC=C
#> [5] LC_TIME=English_United States.utf8
#>
#> time zone: Asia/Dhaka
#> tzcode source: internal
#>
#> attached base packages:
#> [1] stats graphics grDevices utils datasets methods base
#>
#> other attached packages:
#> [1] aquats_1.2.2 multcomp_1.4-32 TH.data_1.1-3 MASS_7.3-65
#> [5] survival_3.8-3 mvtnorm_1.3-3 emmeans_2.0.4 dplyr_1.2.1
#> [9] ggplot2_4.0.1
#>
#> loaded via a namespace (and not attached):
#> [1] tidyselect_1.2.1 farver_2.1.2 S7_0.2.0
#> [4] fastmap_1.2.0 digest_0.6.37 rpart_4.1.24
#> [7] estimability_2.0.0 lifecycle_1.0.5 cluster_2.1.8.1
#> [10] multcompView_0.1-10 ggalluvial_0.12.6 magrittr_2.0.3
#> [13] compiler_4.5.1 rlang_1.3.0 Hmisc_5.2-3
#> [16] sass_0.4.10 tools_4.5.1 utf8_1.2.6
#> [19] yaml_2.3.10 data.table_1.17.4 knitr_1.50
#> [22] ggsignif_0.6.4 labeling_0.4.3 htmlwidgets_1.6.4
#> [25] RColorBrewer_1.1-3 abind_1.4-8 withr_3.0.3
#> [28] foreign_0.8-90 purrr_1.0.4 nnet_7.3-20
#> [31] grid_4.5.1 ggpubr_1.0.0 xtable_1.8-4
#> [34] colorspace_2.1-3 scales_1.4.0 dichromat_2.0-0.1
#> [37] cli_3.6.5 rmarkdown_2.29 vegan_2.7-1
#> [40] generics_0.1.4 rstudioapi_0.17.1 cachem_1.1.0
#> [43] stringr_1.6.0 splines_4.5.1 parallel_4.5.1
#> [46] base64enc_0.1-3 vctrs_0.7.3 Matrix_1.7-3
#> [49] sandwich_3.1-3 jsonlite_2.0.0 carData_3.0-6
#> [52] car_3.1-5 patchwork_1.3.0 rstatix_1.1.0
#> [55] ggrepel_0.9.6 Formula_1.2-6 htmlTable_2.4.3
#> [58] tidyr_1.3.2 jquerylib_0.1.4 glue_1.8.0
#> [61] codetools_0.2-20 stringi_1.8.7 gtable_0.3.6
#> [64] tibble_3.3.0 pillar_1.11.1 htmltools_0.5.8.1
#> [67] R6_2.6.1 evaluate_1.0.5 lattice_0.22-7
#> [70] backports_1.5.0 broom_1.0.13 bslib_0.9.0
#> [73] Rcpp_1.1.1-1.1 coda_0.19-4.1 gridExtra_2.3.1
#> [76] nlme_3.1-168 permute_0.9-10 checkmate_2.3.3
#> [79] mgcv_1.9-3 xfun_0.52 zoo_1.8-14
#> [82] pkgconfig_2.0.3