Computational Provenance & Reproducibility Record

RAISINS - R and AI Solutions for INferential Statistics · Online Statistical Analysis Platform for Agricultural Research

Computational Provenance & Reproducibility Record Pooled Split Plot (2,1) · 2.0.0 · DOI 10.5281/zenodo.23081898

Computational Provenance & Reproducibility Record

RAISINS · Pooled Split Plot (2,1) Module

This Computational Provenance Record documents the statistical computing environment, software dependencies, computational provenance, and bibliographic references associated with the RAISINS Pooled Split Plot (2,1) module. It is intended to support computational reproducibility and software transparency. Detailed statistical methodology, mathematical derivations, and user guidance are provided separately in the official module documentation.

Any issues or updates required please comment here

Go to the App from here

1 Module Metadata

Parameter Specification
Module Pooled Split Plot (2,1)
Module Version 2.0.0
DOI 10.5281/zenodo.23081898
Document Type Computational workflow
Statistical Engine R
R Version 4.5.2
Design Two main-plot factors (A, B) in factorial arrangement; one sub-plot factor (C); RCBD blocks nested within Location; pooled over Locations
Error strata Two - Error(a) = L:Block:A, Error(b) = L:Block:AB
Reproducibility Execution Environment: Posit Connect · GCR · renv-locked

2 Statistical Dependency Manifest

Package Version Repository Core Statistical Functions
stats 4.5.2 Base R lm(), anova(), aggregate(), bartlett.test(), pf(), prcomp(), cor(), shapiro.test(), ks.test(), df.residual(), resid()
agricolae 1.3-7 CRAN LSD.test(), HSD.test(), duncan.test()
gtools 3.9.5 CRAN mixedsort(), mixedorder()
factoextra 1.0.7 CRAN get_eigenvalue(), fviz_eig(), fviz_pca_biplot()
scales 1.4.0 CRAN rescale()
nortest 1.0-4 CRAN ad.test()
PerformanceAnalytics 2.0.8 CRAN chart.Correlation()
GGally 2.4.0 CRAN ggpairs()
ggdist 3.3.3 CRAN stat_halfeye(), stat_slab()

3 Statistical Function Registry

Analytical Role Primary Function(s)
Sequential sums of squares for the pooled split-plot model stats::lm(), stats::anova()
Error(a) extraction (main-plot stratum) stats::anova(lm(Y ~ L + Block + L:Block + A + B + A:B + L:A + L:B + L:A:B + L:Block:A)) - the L:Block:A row
Error(b) extraction (sub-plot stratum) stats::anova(lm(Y ~ Block + L + L:Block + AB + L:AB + L:Block:AB + C + AB:C + L:AB:C)) - the Residuals row
Main-plot partitioning (A, B, A×B) stats::anova(lm(Y ~ A + B + A:B))
Sub-plot & higher-order partitioning (C, A×C, B×C, A×B×C, L×C, L×A×C, L×B×C, L×A×B×C) stats::anova(lm(Y ~ A * B * C * L))
F-test p-values against the correct error stratum stats::pf()
Post-hoc mean comparison & letter grouping agricolae::LSD.test(), agricolae::HSD.test(), agricolae::duncan.test()
DMRT tabulated & critical range values agricolae::duncan.test()$duncan (columns 1 and 2)
Homogeneity of error variances over Locations (pooling test) stats::bartlett.test() applied to a list of per-Location lm models - separately for Error(a) and Error(b)
Aitken transformation for heterogeneous errors per-Location division by sqrt(MSE_b), from sum(resid(m)^2) / df.residual(m)
Effect size (Cohen’s f) sqrt(eta_sq / (1 - eta_sq)) where eta_sq = SS_effect / (SS_effect + SS_error)
Natural ordering of treatment labels gtools::mixedsort(), gtools::mixedorder()
Principal component analysis on treatment-combination means stats::prcomp(center = TRUE, scale = TRUE)
PCA eigenvalues, scree plot, biplot factoextra::get_eigenvalue(), factoextra::fviz_eig(), factoextra::fviz_pca_biplot()
PCA index score scaling scales::rescale(to = c(0, 1))
Correlation matrix & correlation chart stats::cor(use = "pairwise.complete.obs"), PerformanceAnalytics::chart.Correlation()
Pairwise scatter matrix GGally::ggpairs()
Normality assessment (Q-Q panel) stats::shapiro.test(), nortest::ad.test(), stats::ks.test()

4 Default Methods & Parameters

Analysis Step / Parameter Default Method / Value
ANOVA
Structure

(lm() +
anova())
Sums of squares Sequential (Type I). Four separate lm() fits are used and the required rows are read off each: a main-plot fit for A, B and A×B; a whole-plot fit carrying L, L×A, L×B, L×A×B and Error(a); a fully crossed Y ~ A * B * C * L fit for C and every effect involving C; and a block-first fit whose Residuals row supplies Error(b)
Error(a) - main-plot stratum The L:Block:A term, fitted last in Y ~ L + Block + L:Block + A + B + A:B + L:A + L:B + L:A:B + L:Block:A. Because Block:A is absent from the model, this term absorbs both Block:A and L:Block:A, giving df = Locations × (Blocks − 1) × (A levels − 1). Tests Block, A, B, A×B, L×A, L×B and L×A×B
Error(b) - sub-plot stratum The Residuals row of Y ~ Block + L + L:Block + AB + L:AB + L:Block:AB + C + AB:C + L:AB:C, i.e. the L:Block:AB pooling, giving df = Locations × A × B × (Blocks − 1) × (C levels − 1). Tests the Location main effect, C, and every effect involving C
Where Location is tested Note the asymmetry. The Location main effect is tested against Error(b), while Location's interactions with the main-plot factors (L×A, L×B, L×A×B) are tested against Error(a). The post-hoc comparison for Location is likewise called with DFerror = gl.b, MSerror = Eb, and its Cohen's f uses the Error(b) sum of squares. This is a deliberate choice in the source, matching the pooled split-plot (1,1) module, and must be reproduced to obtain the same p-values
Reported sources 18 rows in fixed order: Block, L, A, B, A×B, L×A, L×B, L×A×B, Error(a), C, A×C, B×C, A×B×C, L×C, L×A×C, L×B×C, L×A×B×C, Error(b)
Homogeneity
& Pooling

(bartlett.test())
Test performed Run per character, on a list of per-Location lm models. Two tests are performed: one on the main-plot models (Y ~ Block + FactorA*FactorB, fitted to cell means averaged over FactorC) giving chisq_Ea / pval_Ea, and one on the sub-plot models (Y ~ Block + FactorA*FactorB*FactorC) giving chisq_Eb / pval_Eb
Minimum data requirement A Location contributes only if it has ≥ 2 levels each of Block, FactorA, FactorB and FactorC and positive residual df in both models. Fewer than 2 valid Locations ⇒ Bartlett's test is reported as NA and no Aitken transformation is applied
Trigger for Aitken transformation Hardcoded at p ≤ 0.05 - independent of the user's selected significance level α. The transformation fires if either pval_Ea or pval_Eb is significant
Aitken transformation applied Within each Location, every observation of the affected character is divided by sqrt(MSE_b) for that Location, where MSE_b = sum(resid(m_b)^2) / df.residual(m_b). Note the sub-plot MSE is used as the divisor even when only Error(a) was heterogeneous
User
Transformation

(optional)
Default Off. Enabled by the "Click for Transformation" toggle; each response variable can then be assigned to at most one of log / square-root / arcsine (the three pickers are mutually exclusive)
Log log10(x) when all values > 0; otherwise the column is shifted first: log10(x - min(x) + 1)
Square root sqrt(x) when all values > 0; sqrt(x + 0.5) when any value equals 0; refused with a warning if any value is negative
Arcsine asin(sqrt(x)), requiring proportions in [0, 1]; exact 0 and 1 are first replaced by 1/(4n) and 1 - 1/(4n) respectively, where n is the number of rows. Refused entirely if any value falls outside [0, 1]
Reporting of
transformed data
Table presentation ANOVA, CD/HSD and letter grouping are computed on the transformed scale. Result tables show the untransformed mean ± SD first, with the transformed mean in parentheses beneath it
Multiple
Comparison

(agricolae)
Procedure selectInput(choices = c("LSD","TUKEY","DMRT"), selected = "LSD") ⇒ LSD is the shipped default
Significance level α Choices are 0.05 and 0.01. The selected = argument is set to a value not present in the choice list, so the control falls back to its first entry - the effective default is 0.05
Error term passed to each test Explicitly supplied per effect, never inferred: DFerror = gl.a, MSerror = Ea for A, B, A×B, L×A, L×B and L×A×B; DFerror = gl.b, MSerror = Eb for Location, C, and every effect involving C
Critical difference reported One CD value per effect, taken from the statistics component of the corresponding agricolae call (column 6 for LSD/DMRT, column 5 for Tukey's HSD). The in-app information modals describe a second, Satterthwaite-approximated CD for mixed-stratum comparisons such as CD[C(A)]; that second value is not computed in this version - the vectors reserved for it are never populated
CD suppression CD is displayed only when the corresponding ANOVA F-test is significant; otherwise a - is shown
DMRT
Tables
Availability Produced only when the multiple comparison procedure is DMRT. Under LSD or Tukey the tables are not rendered at all
Content Two tables per effect, taken from duncan.test()$duncan: column 1 gives the tabulated (studentized range) values and column 2 the critical range values. Row names indicate rank separation between means
Effect Size
(Cohen's f)
Formula eta_sq = SS_effect / (SS_effect + SS_error), then Cohen's f = sqrt(eta_sq / (1 - eta_sq)) - a partial η² using only the effect's own error stratum in the denominator
Error stratum used SS of Error(a) (row 9 of the ANOVA table) for the main-plot effects; SS of Error(b) (row 18) for the Location main effect and for every effect involving C - the same split used for the F-tests
Precision &
Presentation
Decimal places numericInput("split_digit", value = 2, min = 1, max = 4) ⇒ 2 decimal places; applied to every rounded quantity via split_roundvalue
Table font selected = "cambria" from a 9-font list; affects kableExtra rendering only, not computation
Principal
Component
Analysis

(prcomp())
Input matrix Response-variable means per L×A×B×C treatment combination, row-ordered by gtools::mixedorder(). PCA is computed on demand - it is not part of the main analysis run
Scaling prcomp(center = TRUE, scale = TRUE) ⇒ correlation-matrix PCA via SVD. Scaling is required because response variables are measured on different units and scales; without it a large-variance trait would dominate PC1 purely through its unit of measurement
Sign convention Both $x and $rotation are multiplied by −1 after fitting. This is a presentation choice only - PC signs are arbitrary in PCA - but it must be reproduced to match the app's loadings and index scores exactly
Index scores Index 1 = PC1 scores; Index 2 = PC2 scores. Each is reported both raw and rescaled to [0, 1] with scales::rescale(). The index plots classify a treatment combination as Selected above (or below) a user cutoff, default 0.75
Correlation &
Normality
Correlation stats::cor(use = "pairwise.complete.obs"), Pearson, rounded to the selected decimal places. The correlation chart uses PerformanceAnalytics::chart.Correlation(histogram = TRUE, pch = 19)
Normality (Q-Q panel) Optional annotations: Shapiro-Wilk (3 ≤ n ≤ 5000), Anderson-Darling (nortest::ad.test(), n ≥ 7), Kolmogorov-Smirnov against a fitted normal (n ≥ 2). All require non-zero variance; each is skipped with an explanatory label otherwise

5 R Code for Key Analytical Steps

The chunks below reproduce the module’s computations on a real, inspectable dataset: datasets::CO2, a chilled-plant CO2 uptake trial. Restricting it to four concentration levels yields a perfectly balanced 2 x 3 x 2 x 2 x 2 = 48-observation layout with the same structure the module expects. Every chunk has been executed against the locked package versions.

5.1 Model Dataset

data(CO2, package = "datasets")

# CO2 is a real split-plot-style trial: 12 plants (2 Types x 2 Treatments x 3
# replicates), each measured at 7 CO2 concentrations. Restricting to four
# concentrations and reading them as a 2 x 2 factorial gives a perfectly
# balanced pooled split-plot (2,1):
#   Location = Type       (2: Quebec, Mississippi)
#   Block    = replicate  (3, nested within Location)
#   FactorA  = Treatment  (2: nonchilled, chilled)   - main plot 1
#   FactorB  = conc range (2: low {95,175}, high {350,675}) - main plot 2
#   FactorC  = conc within range (2: {95,350}, {175,675})   - sub plot
d <- subset(CO2, conc %in% c(95, 175, 350, 675))
d <- transform(
  d,
  Location = factor(Type, labels = c("Quebec", "Mississippi")),
  Block    = factor(substr(as.character(Plant), 3, 3)),
  FactorA  = factor(Treatment, labels = c("A_nonchilled", "A_chilled")),
  FactorB  = factor(ifelse(conc <= 175, "B_low", "B_high"),
                    levels = c("B_low", "B_high")),
  FactorC  = factor(ifelse(conc %in% c(95, 350), "C1", "C2")),
  Y        = uptake
)
d <- d[, c("Location", "Block", "FactorA", "FactorB", "FactorC", "Y")]

nrow(d)                                                    # 48
all(table(d$Location, d$Block, d$FactorA, d$FactorB, d$FactorC) == 1)  # TRUE

# The app renames the user's columns to exactly these five structural names,
# then builds the interaction factors it needs:
L  <- d$Location; Block <- d$Block
A  <- d$FactorA;  B <- d$FactorB; C <- d$FactorC
AB <- interaction(A, B, sep = "x", lex.order = TRUE)

5.2 Error(a) and Error(b)

data_temp <- data.frame(Y = d$Y, L = L, Block = Block, A = A, B = B, C = C, AB = AB)

# Whole-plot fit: the LAST term, L:Block:A, is Error(a).
# Block:A is deliberately absent from the model, so this term absorbs both
# Block:A and L:Block:A  ->  df_Ea = nLoc * (nBlock - 1) * (nA - 1)
model_wp <- lm(Y ~ L + Block + L:Block + A + B + A:B + L:A + L:B + L:A:B + L:Block:A,
               data = data_temp)
aov_wp   <- anova(model_wp)
rownames(aov_wp)[10]                       # "L:Block:A"
df_Ea <- aov_wp[10, "Df"];  MS_Ea <- aov_wp[10, "Mean Sq"]

# Block-first fit: its Residuals row (the L:Block:AB pooling) is Error(b).
#   df_Eb = nLoc * nA * nB * (nBlock - 1) * (nC - 1)
model_block <- lm(Y ~ Block + L + L:Block + AB + L:AB + L:Block:AB + C + AB:C + L:AB:C,
                  data = data_temp)
aov_block <- anova(model_block)
df_Eb <- aov_block["Residuals", "Df"];  MS_Eb <- aov_block["Residuals", "Mean Sq"]

c(df_Ea = df_Ea, MS_Ea = MS_Ea, df_Eb = df_Eb, MS_Eb = MS_Eb)
#> df_Ea = 4  MS_Ea = 19.368   df_Eb = 16  MS_Eb = 3.959
# matching 2*(3-1)*(2-1) = 4 and 2*2*2*(3-1)*(2-1) = 16

5.3 Sums of Squares and F-tests Against the Correct Stratum

# Main-plot partition: A, B, A:B
aov_main <- anova(lm(Y ~ A + B + A:B, data = data_temp))

# Sub-plot / higher-order partition: C, A:C, B:C, A:B:C, C:L, A:C:L, B:C:L, A:B:C:L
aov_sub  <- anova(lm(Y ~ A * B * C * L, data = data_temp))

# Each effect is tested against ITS OWN stratum - never a pooled residual.
f_and_p <- function(SS, df_eff, MS_error, df_error) {
  MS <- SS / df_eff
  Fv <- MS / MS_error
  c(MS = MS, F = Fv, p = pf(Fv, df_eff, df_error, lower.tail = FALSE))
}

# Main plot A -> Error(a)
f_and_p(aov_main["A", "Sum Sq"], aov_main["A", "Df"], MS_Ea, df_Ea)
#> MS = 460.660  F = 23.785  p = 0.0082

# Sub plot C -> Error(b)
f_and_p(aov_sub["C", "Sum Sq"], aov_sub["C", "Df"], MS_Eb, df_Eb)
#> MS = 383.635  F = 96.893  p < 0.0001

# Location MAIN effect -> Error(b), NOT Error(a). Location's INTERACTIONS with
# the main-plot factors (L:A, L:B, L:A:B) are tested against Error(a) instead.
f_and_p(aov_wp[1, "Sum Sq"], aov_wp[1, "Df"], MS_Eb, df_Eb)
#> MS = 1396.442  F = 352.693  p < 0.0001

5.4 Homogeneity of Error Variances and the Aitken Transformation

# One lm per Location, in BOTH strata. The main-plot model is fitted to cell
# means averaged over FactorC; the sub-plot model uses the raw observations.
per_loc <- lapply(split(d, d$Location), function(sub) {
  sub <- droplevels(sub)
  m_b <- lm(Y ~ Block + FactorA * FactorB * FactorC, data = sub)
  cm  <- aggregate(Y ~ Block + FactorA + FactorB, data = sub, FUN = mean)
  m_a <- lm(Y ~ Block + FactorA * FactorB, data = cm)
  if (df.residual(m_a) <= 0 || df.residual(m_b) <= 0) return(NULL)
  list(m_a = m_a, m_b = m_b)
})
valid <- Filter(Negate(is.null), per_loc)          # needs >= 2 Locations

test_a <- bartlett.test(lapply(valid, `[[`, "m_a"))  # chisq_Ea / pval_Ea
test_b <- bartlett.test(lapply(valid, `[[`, "m_b"))  # chisq_Eb / pval_Eb
c(pval_Ea = test_a$p.value, pval_Eb = test_b$p.value)
#> pval_Ea = 0.4148   pval_Eb = 0.9446   -> homogeneous, no transformation

# Aitken fires on a HARDCODED 0.05, not the user's alpha, and on EITHER stratum.
if (test_a$p.value <= 0.05 || test_b$p.value <= 0.05) {
  mse_b <- sapply(valid, function(v) sum(resid(v$m_b)^2) / df.residual(v$m_b))
  for (loc in names(mse_b)) {
    rows <- d$Location == loc
    d$Y[rows] <- d$Y[rows] / sqrt(mse_b[loc])      # divide by sqrt(MSE_b)
  }
}

5.5 Post-hoc Comparison, CD and Letter Grouping

library(agricolae); library(gtools)
alpha <- 0.05                                   # effective shipped default

# Main-plot factor A -> Error(a)
outA <- LSD.test(data_temp$Y, A, DFerror = df_Ea, MSerror = MS_Ea, alpha = alpha)
outA$groups[mixedsort(rownames(outA$groups)), ]
outA$statistics[1, 6]                           # CD = 3.5273  (column 6 for LSD)

# Sub-plot factor C -> Error(b)
outC <- LSD.test(data_temp$Y, C, DFerror = df_Eb, MSerror = MS_Eb, alpha = alpha)
outC$statistics[1, 6]                           # CD = 1.2177

# Tukey: the CD equivalent is in column 5, not 6
HSD.test(data_temp$Y, A, DFerror = df_Ea, MSerror = MS_Ea, alpha = alpha)$statistics[1, 5]

# DMRT: tabulated values and critical range values (only produced under DMRT)
dun <- duncan.test(data_temp$Y, A, DFerror = df_Ea, MSerror = MS_Ea, alpha = alpha)
dun$duncan[, 1]                                 # tabulated (studentized range) = 3.9265
dun$duncan[, 2]                                 # critical range               = 3.5273

# Reported dispersion statistics, all from the same agricolae object
r <- outA$means[1, 3]                           # replications per mean
sqrt(outA$statistics[1, 1] / r)                 # SE(m)  = 0.8983
sqrt(2 * outA$statistics[1, 1] / r)             # SE(d)  = 1.2704
outA$statistics[1, 4]                           # CV(%)  = 18.1184

5.6 Effect Size (Cohen’s f)

# Partial eta-squared uses ONLY the effect's own error stratum in the denominator.
SS_A  <- aov_main["A", "Sum Sq"]
SS_Ea <- aov_wp[10, "Sum Sq"]

eta_sq_A <- SS_A / (SS_A + SS_Ea)
cohens_f <- sqrt(eta_sq_A / (1 - eta_sq_A))
round(cohens_f, 2)                              # 2.44, rounded to split_digit (default 2)

5.7 Principal Component Analysis and Index Scores

library(factoextra); library(scales)

# PCA needs at least two response variables. Uptake and a derived efficiency
# measure (uptake per unit concentration) are used here.
p <- subset(CO2, conc %in% c(95, 175, 350, 675))
resp <- data.frame(
  LxAxBxC    = interaction(d$Location, d$FactorA, d$FactorB, d$FactorC,
                           sep = "x", lex.order = TRUE),
  Uptake     = p$uptake,
  Efficiency = p$uptake / p$conc * 100
)

# Means per L x A x B x C treatment combination, row-ordered by mixedorder().
means <- aggregate(cbind(Uptake, Efficiency) ~ LxAxBxC, data = resp, FUN = mean)
rownames(means) <- means$LxAxBxC
means <- means[mixedorder(rownames(means)), -1, drop = FALSE]   # 16 combinations

res.pca <- prcomp(means, center = TRUE, scale = TRUE)   # correlation PCA via SVD

# The app reverses both sign conventions - reproduce this to match its output.
res.pca$x        <- -res.pca$x
res.pca$rotation <- -res.pca$rotation

get_eigenvalue(res.pca)          # PC1 = 1.185 (59.23%), PC2 = 0.815 (40.77%)
res.pca$rotation                 # loadings
fviz_eig(res.pca, addlabels = TRUE)
fviz_pca_biplot(res.pca, axes = c(1, 2), repel = TRUE)

index1 <- res.pca$x[, 1]; index2 <- res.pca$x[, 2]
cbind(`Index Score` = index1, `Scaled Index` = rescale(index1, to = c(0, 1)))
cbind(`Index Score` = index2, `Scaled Index` = rescale(index2, to = c(0, 1)))

5.8 Correlation and Normality

round(cor(means, use = "pairwise.complete.obs"), 2)
PerformanceAnalytics::chart.Correlation(means, histogram = TRUE, pch = 19)

z <- na.omit(means$Uptake); n <- length(z)
if (n >= 3 && n <= 5000 && var(z) > 0) shapiro.test(z)
if (n >= 7 && var(z) > 0)              nortest::ad.test(z)
if (n >= 2 && var(z) > 0)              ks.test(z, "pnorm", mean(z), sd(z))

Explore the entire Pooled Split Plot (2,1) module in preview mode using our demo datasets. To submit suggestions or report a workflow issue, please use the discussion section below, or visit the official RAISINS website.

6 RAISINS Native Statistical Framework

RAISINS uses R for all its statistical computations. Every package used to generate major results is listed and demonstrated with examples, so results can be reproduced independently. These results are then organized and formatted on the RAISINS website along with visualisation to make them easier to use and interpret. RAISINS also has its own custom-built statistical tools for managing workflows, validating results, and generating reports. Details of these are not fully covered here, they’re shared with outside researchers only on request, and are subject to licensing terms.

7 Package References

R Core Team. (2025). R: A language and environment for statistical computing. R Foundation for Statistical Computing, Vienna, Austria. https://www.R-project.org/

de Mendiburu, F. (2023). agricolae: Statistical Procedures for Agricultural Research (R package version 1.3-7). https://doi.org/10.32614/CRAN.package.agricolae

Warnes, G. R., Bolker, B., & Lumley, T. (2023). gtools: Various R Programming Tools (R package version 3.9.5). https://doi.org/10.32614/CRAN.package.gtools

Kassambara, A., & Mundt, F. (2020). factoextra: Extract and Visualize the Results of Multivariate Data Analyses (R package version 1.0.7). https://doi.org/10.32614/CRAN.package.factoextra

Wickham, H., Pedersen, T. L., & Seidel, D. (2025). scales: Scale Functions for Visualization (R package version 1.4.0). https://doi.org/10.32614/CRAN.package.scales

Gross, J., & Ligges, U. (2015). nortest: Tests for Normality (R package version 1.0-4). https://doi.org/10.32614/CRAN.package.nortest

Peterson, B. G., & Carl, P. (2025). PerformanceAnalytics: Econometric Tools for Performance and Risk Analysis (R package version 2.0.8). https://doi.org/10.32614/CRAN.package.PerformanceAnalytics

Schloerke, B., Cook, D., Larmarange, J., Briatte, F., Marbach, M., Thoen, E., Elberg, A., & Crowley, J. (2025). GGally: Extension to ‘ggplot2’ (R package version 2.4.0). https://doi.org/10.32614/CRAN.package.GGally

Kay, M. (2025). ggdist: Visualizations of Distributions and Uncertainty (R package version 3.3.3). https://doi.org/10.32614/CRAN.package.ggdist

Allaire, J. J., Xie, Y., Dervieux, C., McPherson, J., Luraschi, J., Ushey, K., Atkins, A., Wickham, H., Cheng, J., Chang, W., & Iannone, R. (2026). rmarkdown: Dynamic Documents for R (R package version 2.31). https://github.com/rstudio/rmarkdown

Feedback & Discussion