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.0.0 · DOI 10.5281/zenodo.23081712

Computational Provenance & Reproducibility Record

RAISINS · Pooled Split Plot Module

This Computational Provenance Record documents the statistical computing environment, software dependencies, computational provenance, and bibliographic references associated with the RAISINS Pooled Split Plot 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
Module Version 2.0.0
DOI 10.5281/zenodo.23081712
Document Type Computational workflow
Statistical Engine R
R Version 4.5.2
Design One main-plot factor (A) and one sub-plot factor (B); RCBD blocks nested within Location; pooled over Locations
Error strata Two — Pooled Error (a) = L:Block:A, Pooled Error (b) = Residuals
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(), aov(), bartlett.test(), pf(), residuals(), df.residual(), prcomp(), shapiro.test(), ks.test()
car 3.1-3 CRAN Anova()
agricolae 1.3-7 CRAN LSD.test(), HSD.test(); duncan.test() (as the basis of the module’s duncan_test())
gtools 3.9.5 CRAN mixedsort(), mixedorder()
dplyr 1.1.4 CRAN group_by(), summarise()
moments 0.14.1 CRAN skewness(), kurtosis()
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()
phia 0.3-2 CRAN interactionMeans()
GGally 2.4.0 CRAN ggpairs()
ggdist 3.3.3 CRAN stat_halfeye()

3 Statistical Function Registry

Analytical Role Primary Function(s)
Pooled split-plot model fit stats::lm(Y ~ L + L:Block + A + B + A:B + L:A + L:B + L:A:B + L:Block:A)
Sums of squares car::Anova(type = "II")
Pooled Error (a) — main-plot stratum row 9 of the Anova() table, L:Block:A
Pooled Error (b) — sub-plot stratum row 10 of the Anova() table, Residuals
F-test p-values against the assigned stratum stats::pf()
Homogeneity of error variances over Locations (pooling test) stats::bartlett.test() on the per-Location aov() residuals, labelled by Location
Aitken transformation for heterogeneous errors per-Location division by sqrt(MSE), from sum(resid(m)^2) / df.residual(m)
Post-hoc mean comparison & letter grouping agricolae::LSD.test(), agricolae::HSD.test(), duncan_test() (module copy of agricolae::duncan.test())
DMRT tabulated & critical range values duncan_test()$duncan (columns 1 and 2)
Letter alignment $groups[rownames(<means table>), ]
Natural ordering of treatment labels gtools::mixedsort(), gtools::mixedorder()
Summary statistics by Main plot, Sub plot and Location dplyr::group_by(), dplyr::summarise(), moments::skewness(), moments::kurtosis()
Principal component analysis on Location × Main × Sub 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))
Interaction plot means phia::interactionMeans()
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() +
car::Anova())
Model and sums of squares A single fit, lm(Y ~ L + L:Block + A + B + A:B + L:A + L:B + L:A:B + L:Block:A), with sums of squares from car::Anova(type = "II"). Block enters only through L:Block, so blocks are nested within Location. The ANOVA is run on the Aitken-transformed values for any character that failed Bartlett's test
Pooled Error (a) — main-plot stratum Row 9, the L:Block:A term, giving df = Locations × (Blocks − 1) × (Main-plot levels − 1). Tests Main plot (A), Location × Block and Location × Main plot
Pooled Error (b) — sub-plot stratum Row 10, the Residuals, giving df = Locations × Main-plot levels × (Blocks − 1) × (Sub-plot levels − 1). Tests the Location main effect, Sub plot (B), Main × Sub, Location × Sub, Location × Main × Sub, and Pooled Error (a) itself (F = MSEa / MSEb)
Where Location is tested Note the asymmetry. The Location main effect is tested against Pooled Error (b), while Location × Main plot and Location × Block are tested against Pooled Error (a). The Location post-hoc comparison likewise uses Error (b). This is how the source is written, and it must be reproduced to get the same p-values
Reported sources 10 rows, shown in the Individual ANOVA tab in this order: Location, Main plot, Location × Main plot, Location × Block, Pooled Error (a), Sub plot, Location × Sub plot, Main plot × Sub plot, Location × Main plot × Sub plot, Pooled Error (b)
Homogeneity
& Pooling

(bartlett.test())
Test performed Run per character. Within each Location, aov(Y ~ Block + FactorA + Block:FactorA + FactorB + FactorA:FactorB) is fitted. Its residuals from every Location are joined into one vector, labelled by Location, and passed to bartlett.test(residuals, group_labels). A single test is performed. Because Block:FactorA absorbs the whole-plot error, the residuals being compared are the sub-plot errors
Minimum data requirement A Location contributes only if it has ≥ 2 levels each of Block, FactorA and FactorB. Fewer than 2 valid Locations ⇒ the chi-square and p-value are reported as NA and no transformation is applied
Trigger for Aitken transformation Hardcoded at p ≤ 0.05, independent of the user's significance level α
Aitken transformation applied Within each Location, every observation of the affected character is divided by sqrt(MSE) for that Location, where MSE = sum(resid(m)^2) / df.residual(m) from the model above. The per-Location MSE values are printed beneath the Bartlett table. One consequence is that Pooled Error (b) of a transformed character is close to 1
User
Transformation

(optional)
Default Off. Enabled by the transformation checkbox; variables are then assigned to the log, square-root or arcsine pickers
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), where n is the number of rows. Refused entirely if any value falls outside [0, 1]
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 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 Supplied explicitly per effect: DFerror / MSerror of Pooled Error (a) for Main plot and Location × Main plot; Pooled Error (b) for Location, Sub plot, Main × Sub, Location × Sub and Location × Main × Sub
DMRT implementation duncan_test(), a copy of agricolae::duncan.test() shipped in duncan_test.R. The only change is in how the studentized range is found: when qtukey() returns NaN (many treatments), the value is solved with uniroot() instead, and ranks that still cannot be found are dropped. Where qtukey() succeeds the output is identical to duncan.test()
Critical difference reported One value per effect from the statistics component: column 6 for LSD, column 5 for Tukey's HSD. For DMRT, the critical ranges from $duncan are reported in separate tables
CD suppression & unrounded CD CD is shown only when the effect's F-test p ≤ the selected α; otherwise -. The letters are decided at full precision, and the unrounded CD is kept separately (rcbd_CD*_exact) for the reference note beside the rounded value
Letter order The $groups letters are re-indexed by the row names of the matching means table ([rownames(rcbd_result_mean*), ]), which is ordered by gtools::mixedsort(). Letters therefore follow the means table row for row
Dispersion
Statistics
SE(m), SE(d) sqrt(MSerror / r) and sqrt(2 * MSerror / r), where MSerror is statistics[1, 1] of the corresponding agricolae call and r is the replication of its first mean
CV (%) statistics[1, 4] of the corresponding agricolae call
Summary
Stats tab
Per group For each character, grouped separately by Main plot, Sub plot and Location, on the uploaded (untransformed) values: Mean, SD, SE = SD / √n, Min, Max, CV = SD / Mean × 100, moments::skewness(), moments::kurtosis() (not excess kurtosis)
Precision &
Presentation
Decimal places numericInput("rcbd_digit", value = 2, min = 1, max = 4) ⇒ 2 decimal places
Table font selected = "cambria"; affects table rendering only, not computation
Principal
Component
Analysis

(prcomp())
Input matrix Character means per Location × Main × Sub combination, row-ordered by gtools::mixedorder(), computed on demand from the uploaded values (after any user transformation, but not Aitken-scaled). Requires at least two response variables (req(ncol(rcbd_df) > 9) on data carrying 8 structural columns)
Scaling prcomp(center = TRUE, scale = TRUE) ⇒ correlation-matrix PCA via SVD, so no trait dominates PC1 merely through its unit of measurement
Sign convention Both $x and $rotation are multiplied by −1 after fitting. This is a presentation choice (PC signs are arbitrary), but it must be reproduced to match the app's loadings and index scores
Index scores Index 1 = PC1 scores; Index 2 = PC2 scores, each reported raw and rescaled to [0, 1] with scales::rescale(). The index plots mark a combination as Selected against a cutoff, default 0.75 (choices 0.50, 0.75, 0.80, 0.90, 0.95)
Plots &
Normality
Interaction plots Means from phia::interactionMeans() on lm(response ~ FactorA + FactorB + Location + FactorA:FactorB + FactorA:Location + FactorB:Location + FactorA:FactorB:Location)
Normality (Q-Q panel) Residuals of lm(y ~ L + L:Block + A + L:A + L:Block:A + B + L:B + L:Block:B + A:B + L:A:B); note this model also carries L:Block:B, unlike the ANOVA model. Optional annotations: Shapiro-Wilk (3 ≤ n ≤ 5000), Anderson-Darling (nortest::ad.test(), n ≥ 7), Kolmogorov-Smirnov against a fitted normal (n ≥ 2); each needs non-zero variance

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. Keeping two of its seven concentrations gives a perfectly balanced 2 × 3 × 2 × 2 = 24-observation layout with the structure the module expects. Every chunk has been executed against the locked package versions, and the numbers in the comments are the actual outputs.

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. Keeping two
# concentrations gives a perfectly balanced pooled split plot (1,1):
#   Location = Type       (2: Quebec, Mississippi)
#   Block    = replicate  (3, nested within Location)
#   FactorA  = Treatment  (2: nonchilled, chilled)   - main plot
#   FactorB  = conc       (2: 175, 675 mL/L)         - sub plot
d <- subset(CO2, conc %in% c(175, 675))
d <- data.frame(
  Location   = factor(d$Type, labels = c("Quebec", "Mississippi")),
  FactorA    = factor(d$Treatment, labels = c("A_nonchilled", "A_chilled")),
  FactorB    = factor(ifelse(d$conc == 175, "B_175", "B_675")),
  Block      = factor(substr(as.character(d$Plant), 3, 3)),
  Uptake     = d$uptake,
  Efficiency = d$uptake / d$conc * 100          # second trait, needed for PCA
)
nrow(d)                                                         # 24
all(table(d$Location, d$Block, d$FactorA, d$FactorB) == 1)      # TRUE

5.2 Homogeneity of Error Variances and the Aitken Transformation

chars <- c("Uptake", "Efficiency")
d_tr  <- d                                       # Aitken-transformed copy

for (ch in chars) {
  # One aov per Location. Block:FactorA is the whole-plot error, so the
  # residuals are the sub-plot error of that Location.
  fits <- lapply(split(d, d$Location), function(sub) {
    sub <- droplevels(sub)
    aov(as.formula(paste(ch, "~ Block + FactorA + Block:FactorA + FactorB + FactorA:FactorB")),
        data = sub)
  })
  mse <- sapply(fits, function(m) sum(resid(m)^2) / df.residual(m))

  # Residual-based Bartlett test: all residuals in one vector, labelled by Location
  res  <- lapply(fits, residuals)
  test <- bartlett.test(unlist(res), rep(names(fits), sapply(res, length)))
  print(c(chisq = unname(test$statistic), p = test$p.value, mse))

  # Aitken fires on a HARDCODED 0.05, not the user's alpha
  if (test$p.value <= 0.05) {
    for (loc in names(mse)) {
      rows <- d$Location == loc
      d_tr[rows, ch] <- d[rows, ch] / sqrt(mse[loc])   # divide by sqrt(MSE)
    }
  }
}
#> Uptake:     chisq = 0.56  p = 0.4538  MSE Quebec = 6.97, Mississippi = 4.38
#> Efficiency: chisq = 0.83  p = 0.3616  MSE Quebec = 1.43, Mississippi = 0.81
#> -> both homogeneous, no Aitken transformation

5.3 Pooled ANOVA and F-tests Against the Correct Stratum

library(car)
L <- d_tr$Location; A <- d_tr$FactorA; B <- d_tr$FactorB; Block <- d_tr$Block
y <- d_tr$Uptake

fit <- lm(y ~ L + L:Block + A + B + A:B + L:A + L:B + L:A:B + L:Block:A)
at  <- as.data.frame(car::Anova(fit, type = "II"))
rownames(at)
#> "L" "A" "B" "L:Block" "A:B" "L:A" "L:B" "L:A:B" "L:Block:A" "Residuals"
MS  <- at[, "Sum Sq"] / at[, "Df"]

MS_Ea <- MS[9];  df_Ea <- at[9, "Df"]            # L:Block:A  -> Pooled Error (a)
MS_Eb <- MS[10]; df_Eb <- at[10, "Df"]           # Residuals  -> Pooled Error (b)
c(df_Ea = df_Ea, MS_Ea = MS_Ea, df_Eb = df_Eb, MS_Eb = MS_Eb)
#> df_Ea = 4  MS_Ea = 14.78   df_Eb = 8  MS_Eb = 5.67
# matching 2*(3-1)*(2-1) = 4 and 2*2*(3-1)*(2-1) = 8

# Each row is tested against the stratum the module assigns to it
Fp <- function(k, MS_err, df_err) {
  Fv <- MS[k] / MS_err
  c(F = Fv, p = pf(Fv, at[k, "Df"], df_err, lower.tail = FALSE))
}
Fp(2, MS_Ea, df_Ea)      # Main plot A      -> Error (a)   F = 19.33   p = 0.0117
Fp(1, MS_Eb, df_Eb)      # Location main    -> Error (b)   F = 161.29  p < 0.0001
Fp(3, MS_Eb, df_Eb)      # Sub plot B       -> Error (b)   F = 98.82   p < 0.0001
Fp(7, MS_Eb, df_Eb)      # Location x Sub   -> Error (b)   F = 8.00    p = 0.0222
Fp(9, MS_Eb, df_Eb)      # Error (a) itself -> Error (b)   F = 2.60    p = 0.1161

5.4 Post-hoc Comparison, CD and Letter Grouping

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

# Means table first, in natural (mixedsort) order - the letters follow it
meanL <- aggregate(y ~ L, FUN = mean); rownames(meanL) <- meanL$L
meanL <- meanL[mixedsort(rownames(meanL)), , drop = FALSE]

# Location -> Error (b)
outL <- LSD.test(y, L, DFerror = df_Eb, MSerror = MS_Eb, alpha = alpha)
outL$groups[rownames(meanL), ]                   # Mississippi 20.94 b, Quebec 33.29 a
outL$statistics[1, 6]                            # CD = 2.2424  (column 6 for LSD)

# Main plot -> Error (a)
outA <- LSD.test(y, A, DFerror = df_Ea, MSerror = MS_Ea, alpha = alpha)
outA$statistics[1, 6]                            # CD = 4.3571
r <- outA$means[1, 3]                            # replications per mean
sqrt(outA$statistics[1, 1] / r)                  # SE(m) = 1.1097
sqrt(2 * outA$statistics[1, 1] / r)              # SE(d) = 1.5693
outA$statistics[1, 4]                            # CV(%) = 14.1757

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

# DMRT: the module's duncan_test() (duncan_test.R) - same output as
# agricolae::duncan.test() whenever qtukey() is finite
dun <- duncan.test(y, B, DFerror = df_Eb, MSerror = MS_Eb, alpha = alpha)
dun$duncan[, 1]                                  # tabulated value  = 3.2612
dun$duncan[, 2]                                  # critical range   = 2.2424

5.5 Summary Statistics

library(dplyr)
# Computed on the uploaded (untransformed) values, grouped by one factor at a time
d %>% group_by(FactorA) %>%
  summarise(Mean = mean(Uptake), SD = sd(Uptake), SE = SD / sqrt(n()),
            Min = min(Uptake), Max = max(Uptake), CV = SD / Mean * 100,
            Skewness = moments::skewness(Uptake), Kurtosis = moments::kurtosis(Uptake))
#> A_nonchilled  Mean 30.6  SD 8.09  SE 2.34  Min 19.2  Max 43.9  CV 26.5  Skew 0.136  Kurt 2.05
#> A_chilled     Mean 23.7  SD 9.47  SE 2.73  Min 11.4  Max 39.6  CV 40.0  Skew 0.506  Kurt 1.97

5.6 Principal Component Analysis and Index Scores

library(factoextra); library(scales)

# Means per Location x Main x Sub combination, row-ordered by mixedorder().
# PCA uses the uploaded (user-transformed) values, not the Aitken-scaled copy.
d$AxBxL <- interaction(d$Location, d$FactorA, d$FactorB, sep = "x")
means <- aggregate(cbind(Uptake, Efficiency) ~ AxBxL, data = d, FUN = mean)
rownames(means) <- means$AxBxL
means <- means[mixedorder(rownames(means)), -1]           # 8 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.114 (55.68%), PC2 = 0.886 (44.32%)
fviz_eig(res.pca, addlabels = TRUE)
fviz_pca_biplot(res.pca, axes = c(1, 2), repel = TRUE)

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

5.7 Normality of Residuals

# The Q-Q panel uses this model, which adds L:Block:B to the ANOVA model
m <- lm(y ~ L + L:Block + A + L:A + L:Block:A + B + L:B + L:Block:B + A:B + L:A:B)
z <- residuals(m); 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 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/

Fox, J., & Weisberg, S. (2024). car: Companion to Applied Regression (R package version 3.1-3). https://doi.org/10.32614/CRAN.package.car

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

Wickham, H., François, R., Henry, L., Müller, K., & Vaughan, D. (2023). dplyr: A Grammar of Data Manipulation (R package version 1.1.4). https://doi.org/10.32614/CRAN.package.dplyr

Komsta, L., & Novomestky, F. (2022). moments: Moments, Cumulants, Skewness, Kurtosis and Related Tests (R package version 0.14.1). https://doi.org/10.32614/CRAN.package.moments

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

De Rosario-Martinez, H. (2015). phia: Post-Hoc Interaction Analysis (R package version 0.3-2). https://doi.org/10.32614/CRAN.package.phia

Schloerke, B., Cook, D., Larmarange, J., Briatte, F., Marbach, M., Thoen, E., Elberg, A., & Crowley, J. (2024). 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

Feedback & Discussion