Introduction

fastrda is a high-performance implementation of Redundancy Analysis (RDA) written in C++ with Armadillo and OpenMP. It is designed for large-scale ecological, genomic, and other multivariate datasets where computational efficiency is critical.

Key Features:

  • High-performance C++ implementation: Uses Armadillo for linear algebra and OpenMP for parallel processing.
  • Partial RDA: Condition out covariates using efficient QR decomposition.
  • Permutation Tests: Parallel permutation tests via anova_fastrda() and the S3 anova() method, supporting both overall and axis-wise tests.
  • Comprehensive S3 Interface: Seamless integration with R standards (print(), summary(), predict(), scores(), plot(), and biplot()).
  • Flexible Workspace Modes: Choose between "minimal", "compact", "full", or "none" to balance memory and functionality.
  • Multiple Scaling Options: Four scaling types (0–3) for site and species scores.
  • Biplot Visualization: Plots via biplotrda() or S3 generics plot() / biplot().
  • Memory Efficient: Uses scope-based RAII and avoids unnecessary copies.
  • Thread Control: Set OpenMP threads directly from R.

Installation

# Install from CRAN (when available)
install.packages("fastrda")

# Or install development version from GitHub
# devtools::install_github("zcebeci/fastrda")

System Requirements:

  • OpenMP is required for parallel processing. On Linux/macOS this is usually pre-installed. On Windows, ensure you have RTools with OpenMP support.
  • BLAS library (e.g., OpenBLAS, MKL) is recommended for optimal performance.

Quick Start

To demonstrate fastrda, we will use the classic mite (oribatid mite) ecological dataset from the vegan package.

Load the package:

library(fastrda)

Prepare the data:

# Load real-world ecological data: Mite dataset
if (!requireNamespace("vegan", quietly = TRUE)) {
  stop("The 'vegan' package is required to run this example.")
}
data(mite, package = "vegan")
data(mite.env, package = "vegan")

# Apply Hellinger standardization to species abundance data
Y <- vegan::decostand(mite, "hellinger")

# Select environmental variables (e.g., Substrate Density and Water Content)
# The intercept is removed because RDA expects only explanatory variables.
X <- model.matrix(~ SubsDens + WatrCont, mite.env)[, -1]

Basic Redundancy Analysis

Fit a standard RDA model using fastrda:

fit <- fastrda(
  genotype = Y,
  environment = X,
  axes = 2,
  scaling = 2,
  keep_workspace = "minimal",
  threads = 1,  # Increase to use multiple CPU cores
  verbose = TRUE
)

Inspect the results using S3 methods:

# Inspect the components of model fit
names(fit)
#>  [1] "raw_site_scores"       "site_scores"           "species_scores"       
#>  [4] "scaling_constants"     "loadings"              "coefficients"         
#>  [7] "lc_coefficients_raw"   "eigenvalues"           "prop_total"           
#> [10] "prop_constrained"      "total_inertia"         "conditioned_inertia"  
#> [13] "constrained_inertia"   "unconstrained_inertia" "R2"                   
#> [16] "adj_R2"                "rank"                  "n_axes"               
#> [19] "n_samples"             "df_model"              "pseudo_F"             
#> [22] "df_resid"              "df_cond"               "X_rcond"              
#> [25] "X_rank_deficient"      "Z_rcond"               "Z_rank_deficient"     
#> [28] "fitted"                "scaling"               "X_center"             
#> [31] "X_scale"               "Y_center"              "Y_scale"              
#> [34] "Z_center"              "Z_scale"               "center"               
#> [37] "scale_x"               "scale_y"               "workspace"            
#> [40] "call"                  "env_names"             "species_names"        
#> [43] "site_names"

# Print concise model output
print(fit)
#> 
#> Call:
#> fastrda(genotype = Y, environment = X, axes = 2, scaling = 2, 
#>     keep_workspace = "minimal", threads = 1, verbose = TRUE)
#> 
#>               Inertia Proportion Rank Df
#> Total          35.000     1.0000   NA 69
#> Constrained     8.477     0.2422    2  2
#> Unconstrained  26.523     0.7578   NA 67
#> 
#> Eigenvalues for constrained axes:
#>        [,1]
#> [1,] 7.3817
#> [2,] 1.0953
#> attr(,"names")
#> [1] "RDA1" "RDA2"
#> 
#> Pseudo-F     : 10.7070
#> Adjusted R2  : 0.2196

# Summary of ordination model
summary(fit)
#> 
#> Call:
#> fastrda(genotype = Y, environment = X, axes = 2, scaling = 2, 
#>     keep_workspace = "minimal", threads = 1, verbose = TRUE)
#> 
#>               Inertia Proportion Rank Df
#> Total          35.000     1.0000   NA 69
#> Constrained     8.477     0.2422    2  2
#> Unconstrained  26.523     0.7578   NA 67
#> 
#> Eigenvalues for constrained axes:
#>        [,1]
#> [1,] 7.3817
#> [2,] 1.0953
#> attr(,"names")
#> [1] "RDA1" "RDA2"
#> 
#> Pseudo-F     : 10.7070
#> Adjusted R2  : 0.2196

# Eigenvalues
fit$eigenvalues
#>          [,1]
#> [1,] 7.381739
#> [2,] 1.095284
#> attr(,"names")
#> [1] "RDA1" "RDA2"

# R-squared values
fit$R2
#> [1] 0.2422007
fit$adj_R2
#> [1] 0.2195798

# Pre-computed site scores (samples)
head(fit$site_scores)
#>        RDA1        RDA2
#> 1 0.3758482  0.06010833
#> 2 0.4178976 -1.06204778
#> 3 0.4903470 -0.41542980
#> 4 0.6376417 -0.54602140
#> 5 0.7254019  1.22438141
#> 6 1.2759345 -1.10787286

# Pre-computed species scores (response variables)
head(fit$species_scores)
#>               RDA1         RDA2
#> Brachy   0.1950925  0.191384927
#> PHTH     0.7445179  0.008595452
#> HPAV    -0.1803905  0.307530506
#> RARD     0.6724093  0.092191148
#> SSTR     0.4388376 -0.369165090
#> Protopl  0.4654368  0.000886920

# Canonical loadings (correlations)
head(fit$loadings)
#>                RDA1          RDA2
#> Brachy   0.06059908  0.1543295346
#> PHTH     0.23126000  0.0069312260
#> HPAV    -0.05603237  0.2479873453
#> RARD     0.20886184  0.0743413663
#> SSTR     0.13631046 -0.2976884206
#> Protopl  0.14457262  0.0007151972

Workspace Modes

fastrda offers four workspace modes to balance memory usage and functionality:

Mode Stores Recommended use
"minimal" Q and QtY Permutation testing (recommended default)
"compact" QtY and R Prediction for new data
"full" X, Q, QtY, Y_res, R Prediction and permutation testing
"none" Nothing Lowest memory usage (no permutation testing)
# Minimal workspace (default) - supports permutation tests
fit_min <- fastrda(Y, X, keep_workspace = "minimal", verbose = FALSE)

# Compact workspace - supports prediction with newdata
fit_compact <- fastrda(Y, X, keep_workspace = "compact", verbose = FALSE)

# Full workspace - supports everything
fit_full <- fastrda(Y, X, keep_workspace = "full", verbose = FALSE)

# No workspace - fastest, no permutation tests
fit_none <- fastrda(Y, X, keep_workspace = "none", verbose = FALSE)

Partial RDA (Conditioning on Covariates)

Often you want to remove the effect of known covariates (e.g., spatial structure, microhabitat type like Shrub) before testing environmental variables.

# Define conditioning matrix Z (e.g., Shrub presence/absence or type)
Z <- model.matrix(~ Shrub, mite.env)[, -1]

# Partial RDA: Y ~ SubsDens + WatrCont | Shrub
fit_partial <- fastrda(
  genotype = Y,
  environment = X,
  covariates = Z,
  axes = 2,
  scaling = 2,
  keep_workspace = "minimal",
  threads = 1, 
  verbose = FALSE
)

print(fit_partial)
#> 
#> Call:
#> fastrda(genotype = Y, environment = X, covariates = Z, axes = 2, 
#>     scaling = 2, keep_workspace = "minimal", threads = 1, verbose = FALSE)
#> 
#>               Inertia Proportion Rank Df
#> Total         35.0000     1.0000   NA 69
#> Conditioned    5.5976     0.1599    2  2
#> Constrained    4.4394     0.1268    2  2
#> Unconstrained 24.9629     0.7132   NA 65
#> 
#> Eigenvalues for constrained axes:
#>        [,1]
#> [1,] 3.3923
#> [2,] 1.0472
#> attr(,"names")
#> [1] "RDA1" "RDA2"
#> 
#> Pseudo-F     : 5.7798
#> Adjusted R2  : 0.1080

The conditioned inertia represents the variation explained exclusively by the conditioning variables before fitting the constrained model.


Permutation Tests

Assess the statistical significance of the constrained model using anova_fastrda() or the S3 generic anova().

Overall Model Test

# 999 permutations correspond to the minimum attainable p-value of 0.001.
# Larger numbers of permutations provide finer p-value resolution at the expense of longer computation time.

res_overall <- anova(fit, permutations = 999, threads = 1)
print(res_overall)
#> 
#> Permutation Test for Redundancy Analysis (fastrda v1.4.4)
#> Model: Constrained Inertia = 8.4770 (R2 = 0.2422)
#> Test Statistic (Pseudo-F): 10.7070
#> Degrees of Freedom: Model = 2, Residuals = 67
#> Permutations: 999
#> Pr(>F) / p-value: 0.0010 ***

Axis-wise Tests

# Test each constrained axis individually
res_axis <- anova_fastrda(
  fit,
  by = "axis",
  permutations = 999,
  threads = 1
)
print(res_axis)
#> 
#> Marginal Permutation Test for Redundancy Analysis Axes (fastrda v1.4.4)
#> Permutations: 999
#> 
#>  Axis Eigenvalue Variance (%) Marginal p Sig
#>  RDA1     7.3817       87.08%     0.0010 ***
#>  RDA2     1.0953       12.92%     0.0010 ***
#> ---
#> Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
#> 
#> Disclaimer:
#>  Axis-wise permutation p-values quantify the extremeness of each constrained eigenvalue relative to its permutation distribution.

Axis-wise tests are marginal, not sequential. They should be interpreted descriptively rather than as formal significance tests for axis inclusion. Each canonical axis is evaluated against its own permutation distribution.


Scaling Options

The scaling parameter controls how site and species scores are scaled:

Scaling Site Scores Species Scores Description
0 Unscaled Unscaled Raw scores
1 s * const / const Sites scaled by sqrt(eigenvalue)
2 * const s / const Species scaled by sqrt(eigenvalue) (default)
3 sqrt(s) * const sqrt(s) / const Symmetric scaling

Where s = sqrt(eigenvalue) and const = ((n - 1) * total_inertia)^(0.25).

# Compare different scaling options
# Scaling = 2 is the default because it emphasizes species relationships and matches the most 
# commonly used scaling in ecological RDA applications.

fit0 <- fastrda(Y, X, scaling = 0, axes = 2, keep_workspace = "none", verbose = FALSE)
fit2 <- fastrda(Y, X, scaling = 2, axes = 2, keep_workspace = "none", verbose = FALSE)

head(fit2$site_scores)
#>        RDA1        RDA2
#> 1 0.3758482  0.06010833
#> 2 0.4178976 -1.06204778
#> 3 0.4903470 -0.41542980
#> 4 0.6376417 -0.54602140
#> 5 0.7254019  1.22438141
#> 6 1.2759345 -1.10787286
head(fit2$species_scores)
#>               RDA1         RDA2
#> Brachy   0.1950925  0.191384927
#> PHTH     0.7445179  0.008595452
#> HPAV    -0.1803905  0.307530506
#> RARD     0.6724093  0.092191148
#> SSTR     0.4388376 -0.369165090
#> Protopl  0.4654368  0.000886920

Score Extraction

Extract site, species, or environmental scores using the standard S3 scores() method, get_site_scores(), or biplot_scores():

# Extract both site and species scores via S3 generic scores()
sc_both <- scores(fit, display = "both", choices = 1:2)
head(sc_both$sites)
#>        RDA1        RDA2
#> 1 0.3758482  0.06010833
#> 2 0.4178976 -1.06204778
#> 3 0.4903470 -0.41542980
#> 4 0.6376417 -0.54602140
#> 5 0.7254019  1.22438141
#> 6 1.2759345 -1.10787286
head(sc_both$species)
#>               RDA1         RDA2
#> Brachy   0.1950925  0.191384927
#> PHTH     0.7445179  0.008595452
#> HPAV    -0.1803905  0.307530506
#> RARD     0.6724093  0.092191148
#> SSTR     0.4388376 -0.369165090
#> Protopl  0.4654368  0.000886920

# Get site scores with dynamic re-scaling (e.g., scaling = 1)
site_sc1 <- get_site_scores(fit, scaling = 1)
head(site_sc1)
#>       RDA1        RDA2
#> 1 1.021155  0.06290687
#> 2 1.135401 -1.11149491
#> 3 1.332241 -0.43477150
#> 4 1.732431 -0.57144322
#> 5 1.970870  1.28138651
#> 6 3.466632 -1.15945353

# Get vegan-compatible species and environmental biplot scores
bp_scores <- biplot_scores(fit, type = "both", scaling = 2)
head(bp_scores$species)
#>               RDA1         RDA2
#> Brachy   0.1950925  0.191384927
#> PHTH     0.7445179  0.008595452
#> HPAV    -0.1803905  0.307530506
#> RARD     0.6724093  0.092191148
#> SSTR     0.4388376 -0.369165090
#> Protopl  0.4654368  0.000886920
head(bp_scores$environment)
#>                RDA1        RDA2
#> SubsDens  0.1989174 -0.14000060
#> WatrCont -0.4103032 -0.02218149

Prediction

Use predict.fastrda() to get linear combination (LC) scores or fitted response values. The columns of newdata must have the same variables, order, and encoding as the environmental matrix used to fit the model.

# In-sample LC scores
lc_scores <- predict(fit, type = "lc")
head(lc_scores)
#>        RDA1        RDA2
#> 1 0.3758482  0.06010833
#> 2 0.4178976 -1.06204778
#> 3 0.4903470 -0.41542980
#> 4 0.6376417 -0.54602140
#> 5 0.7254019  1.22438141
#> 6 1.2759345 -1.10787286

# Predict for new environmental data
new_X <- head(X, 10)
new_lc <- predict(fit, newdata = new_X, type = "lc")
head(new_lc)
#>        RDA1       RDA2
#> 1  8.482354  0.5225437
#> 2  9.431348 -9.2327702
#> 3 11.066428 -3.6114834
#> 4 14.390656 -4.7467639
#> 5 16.371279 10.6439958
#> 6 28.796007 -9.6311444

# Reconstruct response matrix (type = "response")
pred_resp <- predict(fit, type = "response", rank = 2)
head(pred_resp[, 1:5])
#>      Brachy       PHTH      HPAV       RARD        SSTR
#> 1 0.2333160 0.06887523 0.2321238 0.06351154 0.019324967
#> 2 0.2048422 0.07041546 0.1887400 0.05790424 0.035560321
#> 3 0.2238497 0.07464611 0.2115936 0.06598406 0.027796892
#> 4 0.2243656 0.08236401 0.2033849 0.07247331 0.032030942
#> 5 0.2734324 0.08809205 0.2684112 0.08904840 0.008954043
#> 6 0.2267085 0.11581156 0.1679666 0.10062199 0.050322724

Biplot Visualization

fastrda provides publication-ready ggplot2-based biplots through direct calls to biplotrda() or S3 methods plot() and biplot(). Environmental arrows indicate the direction of increasing values for each explanatory variable, whereas site and species positions summarize their relationships in canonical space.

Basic Biplot

# Create biplot with all components using S3 plot generic
plot(fit, axes = 1:2, scaling = 2, title = "Mite RDA Biplot (S3 method)")

Direct Function Call with biplotrda()

# Create biplot directly
biplotrda(fit, axes = 1:2, scaling = 2, title = "Mite RDA Biplot (Direct Call)")

Customizing Biplots

# Sites and environment only
biplotrda(fit,
          type = "sites_environment",
          site_col = "darkblue",
          env_col = "darkred",
          title = "Mite RDA - Sites and Environment")

# Biplot with labels enabled
biplotrda(fit,
          show_ids = TRUE,
          max_labels = 20,
          title = "Labeled Mite RDA Biplot")


Performance Tuning

OpenMP Threads

Control the number of parallel threads used by C++ routines in fastrda:

# Use all cores (default)
fit <- fastrda(Y, X, threads = parallel::detectCores())

# Use 4 threads
fit <- fastrda(Y, X, threads = 4)

Internal benchmarks on synthetic datasets containing up to 10,000 response variables demonstrated median speedups of approximately 100×.


Help and package manual

For detailed documentation, argument descriptions, and additional examples for specific functions, you can access the built-in help pages directly in R:

help(package = "fastrda")

?fastrda
?anova.fastrda
?predict.fastrda
?scores.fastrda
?biplotrda

Summary

fastrda provides a fast, memory-efficient implementation of redundancy analysis for large ecological, genomic, and other multivariate datasets. The package supports partial RDA, permutation testing, prediction, multiple scaling options, and flexible visualization through a consistent S3 interface.

References

  • 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.
  • Oksanen, J., et al. (2022). vegan: Community ecology package (R package version 2.6-4) [Computer software]. https://CRAN.R-project.org/package=vegan
  • R Core Team. (2026). R: A language and environment for statistical computing. R Foundation for Statistical Computing.
  • ter Braak, C. J. F. (1994). Canonical community ordination. Part I: Basic theory and linear methods. Ecoscience, 1(2), 127–140. https://doi.org/10.1080/11956860.1994.11682237

```