Skip to contents

add_model() defines one statistical design shared by every compatible differential-abundance step. Its targets and tax_level arguments are the single source of truth for the analysis configuration; other fixed terms provide covariate adjustment. Method-specific thresholds, normalization and rarefaction settings remain active. Preprocessing can run without a model, but differential-abundance execution requires add_model() before prep(). Steps can be configured before or after adding the model. Inspect the current specification with get_model(rec).

Small example data

These simulated counts illustrate model configuration, not biological findings, statistical performance or power. There are 12 subjects, each measured at three visits. The cross-sectional example uses only their baseline samples. Messages and warnings remain visible; this small example does not establish estimation quality or validate a method’s assumptions.

metadata <- expand.grid(
  visit = 0:2, subject_index = 1:6,
  condition = c("control", "treated"),
  KEEP.OUT.ATTRS = FALSE, stringsAsFactors = FALSE
)
metadata$subject <- paste(metadata$condition, metadata$subject_index, sep = "_")
metadata$batch <- factor(ifelse(metadata$subject_index %% 2 == 0, "A", "B"))
metadata$age <- rep(c(25, 42, 31, 56, 38, 49), each = 3, times = 2)
rownames(metadata) <- paste(metadata$subject, metadata$visit, sep = "_")
means <- outer(seq(40, 150, length.out = 12), rep(1, nrow(metadata)))
means[1, ] <- means[1, ] * ifelse(metadata$condition == "treated", 2, 1)
means[2, ] <- means[2, ] * exp(
  0.3 * metadata$visit * (metadata$condition == "treated")
)
counts <- matrix(
  rnbinom(length(means), mu = as.vector(means), size = 20),
  nrow = nrow(means),
  dimnames = list(paste0("taxon_", seq_len(nrow(means))), rownames(metadata))
)
taxonomy <- cbind(
  Genus = paste0("Genus_", seq_len(nrow(counts))),
  Species = paste0("Species_", seq_len(nrow(counts)))
)
rownames(taxonomy) <- rownames(counts)
longitudinal_phy <- phyloseq::phyloseq(
  phyloseq::otu_table(counts, taxa_are_rows = TRUE),
  phyloseq::tax_table(taxonomy),
  phyloseq::sample_data(metadata)
)
phy <- phyloseq::prune_samples(metadata$visit == 0, longitudinal_phy)

Cross-sectional adjustment

rec <- recipe(phy) |>
  add_model(
    ~ condition + batch + age,
    targets = "condition",
    tax_level = "Species",
    reference = c(condition = "control"),
    na_action = "error"
  ) |>
  step_deseq()

get_model(rec)
#> $formula
#> ~condition + batch + age
#> 
#> $targets
#> [1] "condition"
#> 
#> $tax_level
#> [1] "Species"
#> 
#> $time
#> NULL
#> 
#> $reference
#> condition 
#> "control" 
#> 
#> $contrasts
#> [1] "simple"
#> 
#> $na_action
#> [1] "error"
#> 
#> $max_time_levels
#> [1] 12
cross_sectional_fit <- prep(rec, parallel = FALSE)

The reference is the denominator, so a positive effect for condition[treated-control] means higher abundance in treated. With na_action = "drop", prep() removes one common complete-case cohort before any engine runs and records the sample identifiers in the execution manifest. Use na_action = "error" when missing model variables should stop the analysis instead of changing the cohort.

Longitudinal adjustment

Numeric time values are converted internally to a factor whose levels are sorted by numeric value. The original sample metadata are not modified.

longitudinal <- recipe(longitudinal_phy) |>
  add_model(
    ~ condition * visit + batch + (1 | subject),
    targets = "condition",
    tax_level = "Species",
    time = "visit",
    reference = c(condition = "control", visit = "0"),
    contrasts = "all",
    na_action = "drop"
  ) |>
  step_linda()

fit <- prep(longitudinal, parallel = FALSE)
tidy_results(fit) |>
  dplyr::distinct(contrast_id, comparison, contrast_type)
#> # A tibble: 12 × 3
#>    contrast_id                           comparison                contrast_type
#>    <chr>                                 <chr>                     <chr>        
#>  1 condition[treated-control]@visit[0]   condition[treated-contro… condition_wi…
#>  2 condition[treated-control]@visit[1]   condition[treated-contro… condition_wi…
#>  3 condition[treated-control]@visit[2]   condition[treated-contro… condition_wi…
#>  4 visit[1-0]@condition[control]         visit[1-0]@condition[con… time_within_…
#>  5 visit[2-0]@condition[control]         visit[2-0]@condition[con… time_within_…
#>  6 visit[2-1]@condition[control]         visit[2-1]@condition[con… time_within_…
#>  7 visit[1-0]@condition[treated]         visit[1-0]@condition[tre… time_within_…
#>  8 visit[2-0]@condition[treated]         visit[2-0]@condition[tre… time_within_…
#>  9 visit[2-1]@condition[treated]         visit[2-1]@condition[tre… time_within_…
#> 10 condition[treated-control]:visit[1-0] condition[treated-contro… difference_i…
#> 11 condition[treated-control]:visit[2-0] condition[treated-contro… difference_i…
#> 12 condition[treated-control]:visit[2-1] condition[treated-contro… difference_i…

For condition * visit, the simple plan contains condition comparisons within every visit and all pairwise visit comparisons within every condition. Later time values are numerators. contrasts = "all" additionally requests difference-in-differences. More than 12 time points requires an explicit larger max_time_levels, preventing accidental contrast explosions.

(1 | subject) specifies a subject-level random intercept for repeated measurements. Declaring time does not itself add a random effect or establish that observations are independent. Include the subject structure required by the study design and choose a compatible method.

Engine capabilities

Engine Fixed adjustment Time interaction Random effects
DESeq2 yes yes no
ALDEx2 yes yes no
ALDEx3 yes yes yes (exact lme4)
ANCOM-BC2 yes yes yes
corncob yes yes no
LinDA yes yes yes
MaAsLin3 yes yes yes
radEmu yes yes no; explicit GEE clustering
Wilcoxon no no no
LEfSe no no no

Fixed adjustment includes an additive time term, even when that column is declared with time. Wilcoxon and LEfSe support one categorical target without adjustment; they cannot execute ~ condition + time with targets = "condition".

Incompatible steps are warned about when the chain is built and rechecked after preprocessing. prep() records them rather than treating them as failed fits. If no method is compatible, preparation stops. Real engine failures and missing planned contrasts remain errors.

Use contrast_id, comparison and contrast_type to identify the hypothesis represented by each result. See Getting started with dar for extracting results, inspecting execution records and plotting selected contrasts.

Interpret returned results

tidy_results() returns one row per returned taxon, contrast and configured step, not a complete grid of all possible tests. method identifies the engine and step_id identifies its configuration. Custom IDs must not collide with internal columns such as effect, taxa_id or sum; choose distinct IDs such as linda_sensitivity.

The numerator and denominator define each contrast’s orientation. Positive effects point toward the numerator on the reported metric, but the metrics are not numerically interchangeable:

Engine Effect reported by the centralized API
DESeq2 Log2 fold change, with the configured shrinkage.
ALDEx2 Contrast-oriented GLM coefficient.
ALDEx3 Log2 abundance coefficient conditional on the scale model.
ANCOM-BC2 Bias-corrected log fold change.
corncob Logit mean-abundance coefficient.
LinDA Bias-corrected log2 fold change.
MaAsLin3 Coefficient on the configured transformed abundance scale.
radEmu Centred natural-log fold difference across analysis taxa.
Wilcoxon Difference in medians on the configured abundance scale.
LEfSe Direction-oriented LDA score.

Inspect effect_metric and the configured engine options before interpreting an estimate. significant preserves the originating method’s decision, including any additional criteria such as ANCOM-BC2’s pseudocount-sensitivity filter. Missing statistics remain missing: a nonsignificant result does not prove that every estimate was available or that there is no effect. Absent rows are not automatically nonsignificant results. Agreement plots do not establish calibrated false-discovery rates.

For centralized ALDEx2 analyses, effect is the contrast-oriented coefficient from the same GLM as the adjusted P value, averaged over Monte Carlo instances and labeled aldex_glm_coefficient. With default settings it is on the base-2 CLR response scale, not an absolute-abundance fold change or ALDEx2’s standardized two-group effect. Advanced native settings can change its scale.

Advanced native-engine arguments

All DA steps accept engine_args, a named list grouped by native-call stage. This keeps advanced settings attached to the configured step and avoids an ambiguous .... For example:

advanced <- recipe(phy) |>
  add_model(
    ~ condition + batch + age, targets = "condition", tax_level = "Species",
    reference = c(condition = "control")
  ) |>
  step_deseq(
    engine_args = list(
      fit = list(minReplicatesForReplace = Inf),
      shrink = list(lfcThreshold = 1)
    )
  ) |>
  step_ancom(
    engine_args = list(
      fit = list(iter_control = list(tol = 1e-3, max_iter = 50, verbose = FALSE))
    )
  )
#> Registered S3 method overwritten by 'httr':
#>   method         from  
#>   print.response rmutil
advanced
#> ── DAR Recipe ──────────────────────────────────────────────────────────────────
#> Inputs:
#> 
#>      ℹ phyloseq object with 12 taxa and 12 samples 
#>      ℹ variable of interes condition (class: character, levels: control, treated) 
#>      ℹ taxonomic level Species 
#> 
#> Statistical model:
#> 
#>      ℹ ~condition + batch + age 
#> 
#> Preprocessing steps:
#> 
#> 
#> DA steps:
#> 
#>      ◉ step_deseq() id = deseq__Burek 
#>      ◉ step_ancom() id = ancom__Marillenknödel

Nested native control lists replace the corresponding native argument; they are not merged with its defaults. For ANCOM-BC2, supply the complete iter_control list, including verbose, rather than only the values changed.

Available stages are size_factors, fit, results, and shrink for DESeq2; clr and fit for ALDEx2; fit for ALDEx3, ANCOM-BC2, corncob, LinDA and radEmu; fit and contrast for MaAsLin3; test for Wilcoxon; and transform and fit for LEfSe. Data, formulas, contrasts, output paths and arguments already exposed by each dar wrapper are protected. A configured stage that is not used by the selected execution route is reported as an error. Engine-specific validation and errors are left to the engine. dar checks its own configuration, design compatibility and the identities used to align data and results. ALDEx2 effects come directly from the GLM fit.

See Saving and sharing analysis recipes for exporting model settings and native options without executing an analysis.

ALDEx2 and ALDEx3

Use step_aldex2() for ALDEx2 and step_aldex3() for ALDEx3.

Supply sequence counts for ALDEx3 inference. Fixed models use native lm; mixed models use exact lme4, with one native fit per planned contrast. The approximate blmm engine is not enabled. Expect substantial cost for many taxa, Monte Carlo draws and longitudinal contrasts; nsample = 2000 follows the native default.

aldex3_rec <- recipe(longitudinal_phy) |>
  add_model(
    ~ condition * visit + (1 | subject), targets = "condition",
    tax_level = "Species", time = "visit", contrasts = "all"
  ) |>
  step_aldex3(scale_model = "clr", scale_args = list(gamma = 0.5))
aldex3_rec
#> ── DAR Recipe ──────────────────────────────────────────────────────────────────
#> Inputs:
#> 
#>      ℹ phyloseq object with 12 taxa and 36 samples 
#>      ℹ variable of interes condition (class: character, levels: control, treated) 
#>      ℹ taxonomic level Species 
#> 
#> Statistical model:
#> 
#>      ℹ ~condition * visit + (1 | subject) 
#> 
#> Preprocessing steps:
#> 
#> 
#> DA steps:
#> 
#>      ◉ step_aldex3() id = aldex3__Tortita_negra

This is a scale assumption, not a measurement of microbial load. clr and tss accept a non-negative log2 coefficient SD gamma; zero fixes their scale assumption. sample accepts external log2 means and SDs named by sample ID (s.mu, s.sd); coefficient accepts means and SDs named by the columns of the resolved fixed design (c.mu, c.sd). Only independent SDs are supported, not covariance matrices or arbitrary functions. Complete-case selection subsets external information by sample ID. The scale model always uses the original resolved design, so contrast releveling does not change its prior. Shared preprocessing defines the analyzed composition: excluded features are not silently restored as an “other” feature. The default uncertainty is a modeling choice, not a data-derived quantity; coefficient coding and numeric covariate units affect these scale priors.

Effects retain their scale-model-dependent interpretation and are labeled aldex3_log2_abundance_coefficient. Native P values and adjusted P values include directional Monte Carlo averaging, with adjustment across taxa per coefficient, not across all planned contrasts. Missing estimates stay missing. get_execution() records the ALDEx3 version, seed, Monte Carlo settings, scale specification, native engine and elapsed time.

radEmu: fixed models and clustered inference

step_rademu() reports native robust score tests by default. Use test = "wald" explicitly for native Wald tests. Effects are natural-log fold differences relative to radEmu’s pseudo-Huber centre across analysis taxa, not unqualified absolute changes. Each contrast is reparameterized and fitted with its native centring constraint; a nonlinear centre is not replaced by a linear combination of previously centred coefficients. dar adjusts the selected native P values across returned taxa within each contrast, using BH by default. This correction is not joint across planned contrasts. Native missing values and zero-comparison exclusions stay missing.

For dependent observations, name a metadata column in cluster. This uses radEmu’s GEE-based covariance, not a random-intercept model. A formula with random effects is incompatible even if cluster is supplied; dar does not silently translate (1 | subject) into another dependence model.

rademu_rec <- recipe(longitudinal_phy) |>
  add_model(
    ~ condition * visit, targets = "condition", time = "visit",
    tax_level = "Species", contrasts = "all"
  ) |>
  step_rademu(cluster = "subject")
rademu_rec
#> ── DAR Recipe ──────────────────────────────────────────────────────────────────
#> Inputs:
#> 
#>      ℹ phyloseq object with 12 taxa and 36 samples 
#>      ℹ variable of interes condition (class: character, levels: control, treated) 
#>      ℹ taxonomic level Species 
#> 
#> Statistical model:
#> 
#>      ℹ ~condition * visit 
#> 
#> Preprocessing steps:
#> 
#> 
#> DA steps:
#> 
#>      ◉ step_rademu() id = rademu__Sfogliatelle

Cluster labels follow the common model cohort. Missing cluster IDs are errors, not grounds for a private sample exclusion. Native options can be passed in engine_args$fit, for example tolerance, maxit or remove_zero_comparison_pvals; data, tested coordinates and test selection are managed by dar. Native numerical validation stays in radEmu. get_execution() records the effective package version, test, centre, requested and effective cluster labels, seed and native convergence diagnostics. Score testing may be expensive because each taxon requires a restricted fit.

sessionInfo()
#> R version 4.6.1 (2026-06-24)
#> Platform: x86_64-pc-linux-gnu
#> Running under: Ubuntu 24.04.4 LTS
#> 
#> Matrix products: default
#> BLAS:   /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3 
#> LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.26.so;  LAPACK version 3.12.0
#> 
#> locale:
#>  [1] LC_CTYPE=en_US.UTF-8       LC_NUMERIC=C              
#>  [3] LC_TIME=en_US.UTF-8        LC_COLLATE=en_US.UTF-8    
#>  [5] LC_MONETARY=en_US.UTF-8    LC_MESSAGES=en_US.UTF-8   
#>  [7] LC_PAPER=en_US.UTF-8       LC_NAME=C                 
#>  [9] LC_ADDRESS=C               LC_TELEPHONE=C            
#> [11] LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C       
#> 
#> time zone: UTC
#> tzcode source: system (glibc)
#> 
#> attached base packages:
#> [1] stats     graphics  grDevices utils     datasets  methods   base     
#> 
#> other attached packages:
#> [1] dar_1.9.12
#> 
#> loaded via a namespace (and not attached):
#>   [1] fs_2.1.0                        matrixStats_1.5.0              
#>   [3] DirichletMultinomial_1.55.1     doParallel_1.0.17              
#>   [5] httr_1.4.9                      RColorBrewer_1.1-3             
#>   [7] numDeriv_2016.8-1.1             doRNG_1.8.6.3                  
#>   [9] tools_4.6.1                     backports_1.5.1                
#>  [11] utf8_1.2.6                      R6_2.6.1                       
#>  [13] vegan_2.7-6                     lazyeval_0.2.3                 
#>  [15] mgcv_1.9-4                      apeglm_1.35.0                  
#>  [17] permute_0.9-10                  withr_3.0.3                    
#>  [19] gridExtra_2.3.1                 cli_3.6.6                      
#>  [21] Biobase_2.73.2                  textshaping_1.0.5              
#>  [23] sandwich_3.1-3                  sass_0.4.10                    
#>  [25] SQUAREM_2026.1                  mvtnorm_1.4-2                  
#>  [27] S7_0.2.2                        readr_2.2.0                    
#>  [29] proxy_0.4-29                    pkgdown_2.2.1                  
#>  [31] mixsqp_0.3-54                   mlr3tuning_1.7.0               
#>  [33] systemfonts_1.3.2               yulab.utils_0.2.5              
#>  [35] foreign_0.8-91                  paradox_1.0.1                  
#>  [37] scater_1.41.2                   decontam_1.33.0                
#>  [39] parallelly_1.48.0               invgamma_1.2                   
#>  [41] bbmle_1.0.26                    readxl_1.5.0.1                 
#>  [43] rstudioapi_0.19.0               generics_0.1.4                 
#>  [45] gtools_3.9.5                    dplyr_1.2.1                    
#>  [47] Matrix_1.7-6                    biomformat_1.41.0              
#>  [49] ggbeeswarm_0.7.3                DescTools_0.99.60              
#>  [51] S4Vectors_0.51.10               DECIPHER_3.9.3                 
#>  [53] abind_1.4-8                     lifecycle_1.0.5                
#>  [55] multcomp_1.4-32                 yaml_2.3.12                    
#>  [57] SummarizedExperiment_1.43.0     SparseArray_1.13.4             
#>  [59] grid_4.6.1                      crayon_1.5.3                   
#>  [61] bdsmatrix_1.3-7                 lattice_0.23-1                 
#>  [63] haven_2.5.5                     beachmat_2.29.3                
#>  [65] pillar_1.11.1                   knitr_1.52                     
#>  [67] GenomicRanges_1.65.4            statip_0.2.3                   
#>  [69] boot_1.3-32                     gld_2.6.8                      
#>  [71] codetools_0.2-20                glue_1.8.1                     
#>  [73] spacefillr_0.4.0                data.table_1.18.6.1            
#>  [75] MultiAssayExperiment_1.39.1     vctrs_0.7.3                    
#>  [77] treeio_1.37.1                   Rdpack_2.6.6                   
#>  [79] cellranger_1.1.0                gtable_0.3.6                   
#>  [81] emdbook_1.3.14                  cachem_1.1.0                   
#>  [83] xfun_0.61                       rbibutils_2.4.1                
#>  [85] S4Arrays_1.13.2                 Seqinfo_1.3.2                  
#>  [87] coda_0.19-4.1                   reformulas_0.4.4               
#>  [89] modeest_2.5.0                   survival_3.8-12                
#>  [91] timeDate_4052.112               SingleCellExperiment_1.35.2    
#>  [93] iterators_1.0.14                statmod_1.5.2                  
#>  [95] bluster_1.23.1                  TH.data_1.1-5                  
#>  [97] nlme_3.1-171                    ANCOMBC_2.15.1                 
#>  [99] radEmu_2.3.2.0                  phyloseq_1.57.0                
#> [101] bbotk_1.13.1                    fBasics_4052.98                
#> [103] TMB_1.9.25                      bslib_0.12.0                   
#> [105] MicrobiomeStat_1.4              mlr3_1.8.0                     
#> [107] irlba_2.4.1                     vipor_0.4.7                    
#> [109] otel_0.2.0                      rpart_4.1.27                   
#> [111] colorspace_2.1-4                Hmisc_5.3-0                    
#> [113] mlr3mbo_1.2.1                   moocore_0.3.2                  
#> [115] BiocGenerics_0.59.12            DBI_1.3.0                      
#> [117] nnet_7.3-21                     ade4_1.7-24                    
#> [119] mlr3misc_0.23.0                 Exact_3.3                      
#> [121] DESeq2_1.53.6                   tidyselect_1.2.1               
#> [123] timeSeries_4052.112             compiler_4.6.1                 
#> [125] htmlTable_2.5.0                 BiocNeighbors_2.7.3            
#> [127] lgr_0.5.2                       expm_1.0-1                     
#> [129] desc_1.4.3                      DelayedArray_0.39.8            
#> [131] checkmate_2.3.4                 scales_1.4.0                   
#> [133] quadprog_1.5-8                  spatial_7.3-19                 
#> [135] rappdirs_0.3.4                  palmerpenguins_0.1.1           
#> [137] stringr_1.6.0                   digest_0.6.39                  
#> [139] minqa_1.2.8                     rmarkdown_2.32                 
#> [141] XVector_0.53.0                  base64enc_0.1-6                
#> [143] htmltools_0.5.9                 pkgconfig_2.0.3                
#> [145] lme4_2.0-6                      sparseMatrixStats_1.25.0       
#> [147] MatrixGenerics_1.25.0           stabledist_0.7-2               
#> [149] fastmap_1.2.0                   rlang_1.3.0                    
#> [151] htmlwidgets_1.6.4               DelayedMatrixStats_1.35.0      
#> [153] farver_2.1.2                    jquerylib_0.1.4                
#> [155] zoo_1.9-1                       energy_1.7-12                  
#> [157] jsonlite_2.0.0                  BiocParallel_1.47.0            
#> [159] BiocSingular_1.29.2             magrittr_2.0.5                 
#> [161] Formula_1.2-6                   scuttle_1.23.2                 
#> [163] Rcpp_1.1.2                      ape_5.8-1                      
#> [165] viridis_0.6.5                   stringi_1.8.9                  
#> [167] rootSolve_1.8.2.4               stable_1.1.7                   
#> [169] MASS_7.3-66                     plyr_1.8.9                     
#> [171] parallel_4.6.1                  listenv_1.1.0                  
#> [173] ggrepel_0.9.8                   forcats_1.0.1                  
#> [175] lmom_3.3                        Biostrings_2.81.9              
#> [177] splines_4.6.1                   multtest_2.69.0                
#> [179] hms_1.1.4                       locfit_1.5-9.12                
#> [181] ALDEx3_1.3.1                    igraph_2.3.4                   
#> [183] uuid_1.2-2                      rngtools_1.5.2                 
#> [185] reshape2_1.4.5                  stats4_4.6.1                   
#> [187] ScaledMatrix_1.21.0             rmutil_1.1.10                  
#> [189] evaluate_1.0.5                  nloptr_2.2.1                   
#> [191] tzdb_0.5.0                      foreach_1.5.2                  
#> [193] tidyr_1.3.2                     purrr_1.2.2                    
#> [195] future_1.80.0                   clue_0.3-68                    
#> [197] ggplot2_4.0.3                   ashr_2.2-63                    
#> [199] rsvd_1.0.5                      e1071_1.7-17                   
#> [201] tidytree_0.4.8                  viridisLite_0.4.3              
#> [203] class_7.3-24                    ragg_1.5.2                     
#> [205] gsl_2.1-9                       truncnorm_1.0-9                
#> [207] tibble_3.3.1                    lmerTest_3.2-1                 
#> [209] beeswarm_0.4.0                  IRanges_2.47.5                 
#> [211] cluster_2.1.8.3                 TreeSummarizedExperiment_2.21.0
#> [213] globals_0.20.0                  mia_1.21.11