High-throughput transcriptomic datasets frequently contain unwanted
technical variation arising from batch effects. Numerous batch
correction methods exist, but selecting an appropriate method is
challenging because correction can remove biological signal or distort
sample relationships. BatchVaria provides a framework for quantifying
variance components, tracking variance changes after correction, and
evaluating correction performance using multiple metrics, enabling
evidence-based selection of batch correction strategies.
While
sva,
limma and
BatchQC
provide batch correction and diagnostic visualisations, BatchVaria
focuses specifically on quantitative evaluation of correction
performance across multiple assays and correction strategies within a
unified framework. BatchVaria unifies variance decomposition, tracking,
and comparison into a single coherent workflow, built on SummarizedExperiment
and using variancePartition
as one of its two variance engines.
If you use BatchVaria in published work, please cite Moir N, Sherwood
K, Simpson TI (2026), BatchVaria: a variance-aware framework for
evaluating batch correction in high-dimensional omics data,
bioRxiv, doi:10.64898/2026.05.07.721996.
citation("BatchVaria") prints the same entry.
Install the release version with BiocManager:
if (!requireNamespace("BiocManager", quietly = TRUE)) {
install.packages("BiocManager")
}
BiocManager::install("BatchVaria")For the development version:
The development source is at BatchVaria.
We begin with a synthetic dataset with partially confounded batch and biology. This creates a situation where effective batch correction can be challenging. Within this dataset, most control samples sit in one batch and most treated samples in the other, so effects are not cleanly separable.
## exampleBatchVaria() draws random data; seed for a reproducible vignette
set.seed(1)
bv <- exampleBatchVaria(confounding = 0.6)
class(bv)## [1] "SummarizedExperiment"
## attr(,"package")
## [1] "SummarizedExperiment"
## group
## batch Ctrl Trt
## B1 8 2
## B2 2 8
BatchVaria leverages SummarizedExperiment
for storage. Expression matrices are assays, sample annotation is
colData(), and everything the package records goes in
metadata(). See ?"BatchVaria-requirements" for
further detail.
Correction methods are registered rather than hard-coded, so the set available is extensible in the same way the variance engines are:
## [1] "combat" "limma"
This vignette uses combat throughout, so that the
comparison below isolates the effect of the design rather than
differences between methods.
ComBat is applied twice. The first call corrects for batch alone. The
second additionally names group as a variable to preserve,
so the batch effect is estimated adjusted for the named preservation
variable.
bv <- runCorrection(
bv,
method = "combat", batch = "batch",
newAssayName = "naive"
)
bv <- runCorrection(
bv,
method = "combat", batch = "batch", preserve = "group",
newAssayName = "protected"
)
assayNames(bv)## [1] "raw" "raw_center" "raw_scale" "raw_noise" "naive"
## [6] "protected"
raw_noise is a deliberately distorted assay supplied
with the example data, kept here as a reference point for what a
genuinely destructive transformation looks like. raw_center
and raw_scale are no-op controls: centring and scaling
change no variance fraction, so a diagnostic that reports a change on
them is reporting an artefact of the diagnostic rather than of the
data.
We explicitly quantify the contribution of known covariates.
assaysUsed <- c("raw", "naive", "protected", "raw_noise")
bv <- profileVariance(
bv,
formula = ~ batch + group,
assays = assaysUsed,
methods = c("anova", "variancePartition")
)## Warning: The shared variance component is negative (-0.035), meaning the model
## terms explain more jointly than separately. This indicates a suppression effect
## in the design
This step fits the specified model to each assay and decomposes
variance into batch (unwanted), group (biological) and residual
(unexplained). Two engines are run: anova, which fits
linear models per feature and partitions variance with Type II sums of
squares, and variancePartition, which fits mixed models.
Every result is stored in an internal variance ledger keyed on assay,
method, formula and term.
Because the ledger now holds two decompositions per assay, summaries must say which one they mean. Asking for a table without choosing returns an error rather than a silent mixture, since the engines’ terms are not interchangeable:
## Error in `.resolveMethod()`:
## ! The variance ledger holds results for 2 methods (anova, variancePartition). Their terms are not comparable, so specify method = to choose one
provenance() reports what has been done, reading both
ledgers:
## BatchVaria provenance
## ---------------------
##
## Dimensions:
## features: 800
## samples : 20
##
## Assays (6):
## [1] "raw" "raw_center" "raw_scale" "raw_noise" "naive"
## [6] "protected"
##
## Correction history:
## step method input output batch preserve no_op timestamp
## 1 combat raw naive batch - FALSE 2026-10-14 06:33:17.634363
## 2 combat raw protected batch group FALSE 2026-10-14 06:33:17.66258
##
## Variance profiling history:
## step assay formula methods timestamp
## 1 raw ~batch + group anova 2026-10-14 06:33:17.69581
## 2 raw ~batch + group variancePartition 2026-10-14 06:33:19.10812
## 3 naive ~batch + group anova 2026-10-14 06:33:19.115876
## 4 naive ~batch + group variancePartition 2026-10-14 06:33:20.167725
## 5 protected ~batch + group anova 2026-10-14 06:33:20.175017
## 6 protected ~batch + group variancePartition 2026-10-14 06:33:21.332679
## 7 raw_noise ~batch + group anova 2026-10-14 06:33:21.341396
## 8 raw_noise ~batch + group variancePartition 2026-10-14 06:33:22.416018
Two things there are worth pointing at. The correction table’s
preserve column is what distinguishes the two ComBat runs -
without it the rows are identical but for the assay names, and the
comparison this vignette is built on would not be reconstructable from
the object. The no_op column flags a correction that
returned an assay indistinguishable from its input; a method can succeed
and change nothing, and recording it on the object means the condition
outlives the console session that produced it.
It prints that report and returns the same content as a data.frame, so the history can be filtered or joined rather than only read:
## method output preserve no_op
## 1 combat naive - FALSE
## 2 combat protected group FALSE
A core output of BatchVaria is a pair of summary tables.
This table shows the percentage of variance explained by each component for each assay.
## # A tibble: 4 × 5
## component raw naive protected raw_noise
## <chr> <dbl> <dbl> <dbl> <dbl>
## 1 batch 11.0 5.6 1.07 7.86
## 2 group 11.3 15.2 13.7 7.89
## 3 shared 11.4 -3.5 6.96 4.94
## 4 residual 66.3 82.7 78.3 79.3
This table shows how each correction changed variance relative to the raw data. Every entry is a change in a fraction, which constrains what it can be read to mean; the next section sets out what that constraint is.
## # A tibble: 4 × 4
## component naive protected raw_noise
## <chr> <chr> <chr> <chr>
## 1 batch -5.41 -9.94 -3.15
## 2 group +3.92 +2.41 -3.4
## 3 shared -14.91 -4.45 -6.48
## 4 residual +16.4 +11.98 +13.02
Every number above is a fraction. Within an assay, the fractions sum to one. This has a consequence that must be accounted for before any of these tables can be interpreted: removing variance associated with one term mechanically inflates the fraction attributed to every remaining term, even when the absolute variance of those terms has not increased at all.
assayVariance() supplies the missing denominator.
## assay total_variance n_features n_samples
## 1 raw 1281.1431 800 20
## 2 naive 828.6556 800 20
## 3 protected 927.4081 800 20
## 4 raw_noise 3077.1481 800 20
Multiplying a fraction by its assay’s total variance converts it to an absolute quantity, which is directly comparable across assays.
vr <- varianceResults(bv, method = "anova")
absVar <- merge(
vr[, c("assay", "term", "variance_fraction")],
av[, c("assay", "total_variance")],
by = "assay"
)
absVar$absolute <- round(absVar$variance_fraction * absVar$total_variance, 1)
## as.data.frame(), because pivot_wider() returns a tibble and tibble
## prints three significant figure.
as.data.frame(tidyr::pivot_wider(
absVar[, c("assay", "term", "absolute")],
names_from = "term",
values_from = "absolute"
))## assay batch group shared residual
## 1 naive 46.4 126.0 -29.0 685.3
## 2 protected 9.9 127.0 64.6 725.9
## 3 raw 141.0 144.6 146.2 849.4
## 4 raw_noise 241.9 242.7 151.9 2440.6
Read down the group column: 144.6 in raw,
126.0 after the naive correction and 127.0 after the protected one. In
absolute terms group variance barely moves. Its fraction
meanwhile climbs from 11.3 to 15.2 per cent, because the denominator
fell from 1281 to 829. The fraction rose because something else was
removed, not because biological signal grew.
BatchVaria harmonises independent variance-profiling engines onto a
common schema: the same columns, and the same names for the two terms
that mean the same thing in any engine - residual and
shared. Both runs above reach their decompositions by
different routes: anova fits a linear model per feature and
partitions with Type II sums of squares, while
variancePartition fits mixed models via
lme4.
cmp <- varianceResults(bv, assayName = "raw")[
, c("method", "term", "variance_fraction")
]
as.data.frame(
tidyr::pivot_wider(
cmp,
names_from = "method",
values_from = "variance_fraction"
)
)## term anova variancePartition
## 1 batch 0.1100799 0.1698180
## 2 group 0.1128310 0.1751827
## 3 shared 0.1141206 NA
## 4 residual 0.6629684 0.6549993
Here they disagree, and the disagreement is the point. On an orthogonal design they do not:
orth <- profileVariance(
exampleBatchVaria(confounding = 0),
~ batch + group,
assays = "raw",
methods = c("anova", "variancePartition")
)
## Printed at full precision deliberately: at three significant figures
## both columns read 0.163 and the agreement below cannot be checked.
as.data.frame(
tidyr::pivot_wider(
varianceResults(orth, assayName = "raw")[
, c("method", "term", "variance_fraction")
],
names_from = "method",
values_from = "variance_fraction"
)
)## term anova variancePartition
## 1 batch 0.1695154 0.1695154
## 2 group 0.1598141 0.1598141
## 3 shared 0.0000000 NA
## 4 residual 0.6706704 0.6706704
Every term the two engines share agrees to floating-point precision.
shared is exactly zero, because an orthogonal design leaves
nothing unattributable, and variancePartition reports
NA for it because a mixed model has no such parameter to
report.
The divergence above is therefore a property of the design, not of
either implementation. The two engines estimate different quantities:
variancePartition reports how much variance is
associated with a term, anova with Type II sums of
squares reports how much is uniquely attributable to it. When a
design is orthogonal these coincide, which is why the two tables differ
as they do. When batch and group are partially aliased they cannot
coincide, because roughly 11 per cent of the variance here is consistent
with either term and identifiable to neither.
anova reports that share separately, as
shared. A per-gene mixed model has no corresponding
parameter - and could not, since the effects are not separately
identifiable in this design - so the ambiguous variance is reflected in
both terms, each rising from about 11 per cent to about 17. Neither
number is wrong for the quantity it estimates; they simply stop being
interchangeable once the design cannot separate the effects.
The gap between them is therefore a measurement of the design rather than a disagreement between tools: it says how much of the variance the experiment permits you to attribute at all. Running both, and seeing them coincide on a clean design and separate on a compromised one, is more informative than trusting either alone.
The naive correction looks like a success. Read the fraction table on its own: batch variance halves, from 11.0 per cent to 5.6, and group variance rises, from 11.3 per cent to 15.2. Unwanted variation down, biological signal up. By the standard before-and-after comparison - decompose variance, correct, decompose again - this is a correction working exactly as intended.
It is not. Three things the fraction table cannot show say otherwise.
Total variance fell by 35 per cent, from 1281 to 829. The group fraction rose because the denominator collapsed, not because biology was preserved. In absolute terms group variance fell from 145 to 126. This is the compositional artefact: within an assay the fractions must sum to one, so removing variance from any term mechanically inflates the share of every other. A rising fraction is not evidence of preserved signal, and reading it as such is the easiest way to certify a bad correction as a good one.
The batch effect was not removed. 5.6 per cent remains, half of what was there.
shared went negative, from +11.4 per
cent to -3.5. The correction did not merely fail to resolve the
confounding; it distorted the design’s attributability, leaving the
model terms explaining more jointly than they do separately.
Protecting the covariate addresses the second of these. Telling
ComBat that group matters lets it estimate the batch effect
adjusted for biology, and batch variance drops to 1.1 per cent -
essentially removed - while total variance falls less far, to 927. It
does not rescue biological signal on this data: absolute group variance
is 126 for the naive correction and 127 for the protected one. The case
for protecting the covariate here is that the correction works, not that
the biology survives better.
That balance is specific to this level of confounding. This vignette
uses a single value, confounding = 0.6; the trade-off moves
with it, because as batch and group approach aliasing, protecting the
covariate increasingly means protecting the batch effect alongside it.
Raising or lowering the parameter will change which correction looks
better on absolute group variance, so treat that comparison as a
property of the design rather than a general ranking of the two
calls.
The sign of shared is the more dependable signal.
Wherever there is any confounding to resolve it is consistently negative
after naive correction and positive after protected correction, so a
correction that drives it below zero has distorted the design’s
attributability regardless of what the other fractions report. Being a
sign rather than a magnitude, that reading needs no threshold and no
reference distribution - but note it is demonstrated here on simulated
data with one correction method and one design shape.
For contrast, raw_noise shows what a destructive
transformation looks like under the same instruments. Its batch and
group fractions both fall, which reads as damage. But its total variance
more than doubles, to 3077, so in absolute terms every
component grew: nothing was removed, the batch effect was diluted.
basisRetention() is unambiguous about the difference.
## assay reference retention off_basis n_features
## 1 raw raw 1.0000000 0.000000000 800
## 2 naive raw 0.9891832 0.010816832 800
## 3 protected raw 0.9951292 0.004870824 800
## 4 raw_noise raw 0.3697882 0.630211773 800
It retains 0.37 of the original variance structure against 0.99 for both ComBat runs, because added noise occupies directions the raw data did not. Note what this measure does and does not separate: it distinguishes a correction from a distortion, not a good correction from a bad one. Both ComBat runs stay on the original axes because ComBat adjusts each feature’s location and scale within a batch, which rescales the existing structure rather than introducing new directions.
The general rule: a variance fraction is a statement about composition, not about quantity. Use fractions to compare terms within an assay, and totals or absolute variances to compare the same term across assays.
This is also the answer to what BatchVaria offers over calling the underlying tools twice. Decomposing variance before and after correction is exactly what certified the naive correction as successful. Recording each decomposition alongside the total variance it is relative to, the share the design cannot attribute, and the structure the correction preserved is what catches it.
evaluateCorrections() runs the diagnostics above in one
call and returns them in a form the plotting layer reads directly.
ev <- evaluateCorrections(
bv,
baseline = "raw",
assays = assaysUsed,
diagnostics = "variance"
)
plotVarianceDelta(ev)One group of bars per model term, one bar per assay, and height is
the signed change in that term’s fraction relative to raw -
as a fraction here, where the delta table above reported the same
quantities as percentages. Terms are drawn in alphabetical order:
batch, group, residual,
shared.
Both corrections cut batch, the protected run roughly
twice as hard, and both raise residual. The bar that
separates them is shared. Both push it down, and by
different amounts - 0.149 for the naive run against 0.044 for the
protected one - but the amount is not the point. What distinguishes them
is where they land: the naive run takes shared from +0.114
to -0.035, while the protected run takes it from +0.114
to +0.070. As above, a negative shared
means the terms explain more jointly than they do separately, so it is
the side of zero that carries the finding, not the size of the drop.
raw_noise cuts batch too, by dilution rather
than correction, which is why no single bar here is sufficient on its
own.
The argument above is made with tables, deliberately: every claim in it is a number that can be checked. The plotting layer draws those same quantities, and is collected here rather than interleaved so that the two are not confused. Each plot below answers one question.
evaluateCorrections() computes all three diagnostic
families in one call and returns them in the form the plotting functions
read:
ev <- evaluateCorrections(
bv,
baseline = "raw",
assays = assaysUsed,
varianceMethod = "anova"
)
names(ev)## [1] "variance_delta" "pca_comparison" "correlation_change"
plotVarianceComposition() draws each assay’s
decomposition as a composition: one stacked bar per assay, summing to
100 per cent. It answers “what is this assay made of”, and needs no
baseline - an assay profiled on its own can be drawn.
## Warning: Negative variance percentages for term(s) shared. The stacked bars
## extend below zero and above 100 per cent; read the values from varianceTable()
## rather than the chart
Read it against the totals from assayVariance() above,
not on its own: the bars are shares, so raw_noise shows a
smaller batch share than raw while its
absolute batch variance has risen, from 141 to 242. The share
fell only because the total more than doubled underneath it.
plotVarianceRadar() shows the same deltas as the summary
figure on polar axes, one spoke per term. It is the quicker read when
several corrections are being compared at once and the question is the
shape of the change rather than its size - two corrections that
move the same terms in the same proportions trace the same outline.
Every assay is projected onto a single basis fitted on the reference
assay, rather than each being fitted separately. This is what makes the
panels comparable: a per-assay fit gives PC1 a different
meaning in each, so samples appear to move when only the axes have.
The coordinates are available directly, not only through a plot:
## assay sample reference PC1 PC2
## 1 raw S1 raw -15.28668 0.6697611
## 2 raw S2 raw -16.21937 -1.0138277
## 3 raw S3 raw -16.29337 2.1242652
## 4 raw S4 raw -15.99028 -0.0769103
## 5 raw S5 raw -15.06171 -0.9763562
## 6 raw S6 raw -16.15882 -1.0309512
plotBasisProjection() draws exactly what that returns,
one panel per assay:
for (p in plotBasisProjection(
bv,
assays = c("raw", "naive", "protected"),
reference = "raw"
)) {
print(p)
}The naive panel repays a careful look, because it does
not show what a reader expects of a corrected assay: the batches still
appear separated. They have in fact been brought together - the distance
between batch means along PC1 falls from 1.73 to 0.54
standard deviations. What remains on the panel is group,
whose separation rises over the same step, from 1.75 to 1.81.
At confounding = 0.6 the batch and group labels agree for
sixteen of the twenty samples, so a panel coloured by batch draws
surviving group structure in batch colours, and the four samples whose
labels disagree are the only ones that give it away.
Colouring by the other variable shows what is actually there:
This is a limit of the panel rather than of the correction, and it is
not peculiar to this function: wherever batch and biology are
confounded, a projection coloured by batch cannot distinguish a batch
effect that survived from biological signal that was preserved. Here it
ranks the two corrections backwards: set the two corrected panels side
by side and protected looks more batch-separated
than naive - 1.31 standard deviations against 0.54 - while
carrying a fifth of the batch variance, 1.1 per cent against 5.6. The
variance table settles what the panel cannot.
There is a second thing these panels cannot show.
raw_noise, drawn on this basis, is nearly indistinguishable
from raw: the noise it added lies almost entirely
off the reference basis, so it never enters these coordinates,
despite the assay carrying more than twice the variance. Projecting onto
a basis shows what happened within the retained structure and is silent
about structure that left it, which is why basisRetention()
is reported alongside it - scoring raw_noise at 0.37
against 0.99 for both ComBat runs.
plotPCATrajectory() makes the same comparison as a
single panel, drawing an arrow from each sample’s position before
correction to its position after. Use it when the question is which
samples moved rather than whether the groups separate.
It inherits the caveat above: an arrow shows that a sample moved, not which of the two confounded variables moved it.
A correction can leave every variance fraction looking reasonable and still rearrange which samples resemble which. Two diagnostics check that directly.
plotSampleDistance() draws the sample-sample distance
matrix, so blocks of mutually similar samples are visible as blocks:
plotCorrelationChange() reduces that to one number per
assay: the mean absolute change in sample-sample correlation relative to
the baseline. Large values mean the correction rewrote the relationships
between samples, which is a different failure from removing too much
variance and is not visible in any of the variance tables.
Note which assay scores best here: raw_noise, at 0.100
against 0.187 for the naive correction. The assay this vignette has used
throughout as its example of a destructive transformation is the one
that disturbs sample relationships least. That is not a defect in the
diagnostic, it is what the diagnostic measures - adding independent
noise to every feature shrinks all sample correlations towards zero
fairly evenly, which is a small change in the correlation
structure, whereas shifting feature means per batch rearranges it more.
The two diagnostics agree that the protected run is gentler than the
naive one; where they part company is raw_noise, which this
measure ranks first and basisRetention() ranks last. That
single disagreement is the clearest illustration in this vignette of why
no one diagnostic settles the question.
plotBatchEntropy() measures local batch mixing: for each
sample it looks at its k nearest neighbours and computes
the entropy of their batch labels. High entropy means a sample’s
neighbourhood is batch-mixed; low entropy means it still sits among its
own batch. This is the neighbourhood-level question the global variance
fractions cannot answer.
It draws one assay at a time, so the comparison is made by drawing two:
plotBatchEntropy(bv, assayName = "raw", batchVar = "batch", k = 10)
plotBatchEntropy(bv, assayName = "naive", batchVar = "batch", k = 10)The distribution moves right after correction, which is the batch effect leaving the local structure.
Under partial confounding this should be read with care, and this
design shows why. With two batches the entropy is bounded above by
log(2), about 0.693; after the naive correction the mean
neighbourhood entropy is 0.65, close to that ceiling. But batch and
group overlap here, so neighbourhoods that are perfectly batch-mixed are
neighbourhoods in which the biological groups have been mixed too.
Entropy approaching its maximum is evidence of overcorrection in this
design rather than of a job well done - which is the same verdict the
shared term reached, arrived at independently.
Batch correction is frequently applied as a routine preprocessing step, yet it is often performed without systematic or quantitative validation. This can lead to suboptimal outcomes, where overcorrection removes meaningful biological signal or undercorrection leaves residual technical artefacts that confound downstream analyses. BatchVaria addresses this gap by providing a transparent and reproducible framework for evaluating correction strategies. By enabling direct comparison of variance components across multiple corrected assays, the package supports quantitative validation of correction outcomes and facilitates more robust, interpretable analytical pipelines.
The typical BatchVaria workflow is:
BatchVaria provides a coherent and extensible framework for evaluating batch correction in high-dimensional expression data. By combining variance decomposition, structured tracking of results across multiple assays, and intuitive summary outputs, the package enables users to quantitatively assess how different correction strategies impact both unwanted technical variation and biological signal. The integration of variance composition tables, relative change metrics, and visual summaries supports transparent comparison of correction methods within a single workflow. Together, these features facilitate reproducible and interpretable evaluation of batch correction, addressing a key gap in many transcriptomic analysis pipelines.
## R version 4.6.1 (2026-06-24)
## Platform: x86_64-pc-linux-gnu
## Running under: Ubuntu 26.04.1 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.32.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: Etc/UTC
## tzcode source: system (glibc)
##
## attached base packages:
## [1] stats4 stats graphics grDevices utils datasets methods
## [8] base
##
## other attached packages:
## [1] BatchVaria_0.99.2 SummarizedExperiment_1.43.0
## [3] Biobase_2.73.2 GenomicRanges_1.65.4
## [5] Seqinfo_1.3.2 IRanges_2.47.5
## [7] S4Vectors_0.51.10 BiocGenerics_0.59.12
## [9] generics_0.1.4 MatrixGenerics_1.25.0
## [11] matrixStats_1.5.0 BiocStyle_2.41.0
##
## loaded via a namespace (and not attached):
## [1] Rdpack_2.6.6 DBI_1.3.0 bitops_1.1-0
## [4] rlang_1.3.0 magrittr_2.0.5 otel_0.2.0
## [7] compiler_4.6.1 RSQLite_3.53.3 mgcv_1.9-4
## [10] reshape2_1.4.5 png_0.1-9 vctrs_0.7.3
## [13] stringr_1.6.0 sva_3.61.0 pkgconfig_2.0.3
## [16] crayon_1.5.3 fastmap_1.2.0 backports_1.5.1
## [19] XVector_0.53.0 labeling_0.4.3 utf8_1.2.6
## [22] caTools_1.18.4 rmarkdown_2.32 nloptr_2.2.1
## [25] purrr_1.2.2 bit_4.6.0 xfun_0.61
## [28] cachem_1.1.0 jsonlite_2.0.0 EnvStats_3.1.0
## [31] blob_1.3.0 remaCor_0.0.20 DelayedArray_0.39.8
## [34] BiocParallel_1.47.0 broom_1.0.13 parallel_4.6.1
## [37] R6_2.6.1 stringi_1.8.9 bslib_0.12.0
## [40] RColorBrewer_1.1-3 limma_3.99.2 genefilter_1.95.0
## [43] boot_1.3-32 numDeriv_2016.8-1.1 jquerylib_0.1.4
## [46] Rcpp_1.1.2 iterators_1.0.14 knitr_1.52
## [49] FNN_1.1.4.1 BiocBaseUtils_1.15.1 Matrix_1.7-6
## [52] splines_4.6.1 tidyselect_1.2.1 abind_1.4-8
## [55] yaml_2.3.12 gplots_3.3.0 codetools_0.2-20
## [58] plyr_1.8.9 lmerTest_3.2-1 lattice_0.23-1
## [61] tibble_3.3.1 withr_3.0.3 KEGGREST_1.53.6
## [64] S7_0.2.2 evaluate_1.0.5 survival_3.8-12
## [67] Biostrings_2.81.9 pillar_1.11.1 BiocManager_1.30.27
## [70] KernSmooth_2.23-27 reformulas_0.4.4 ggplot2_4.0.3
## [73] scales_1.4.0 aod_1.3.3 minqa_1.2.8
## [76] gtools_3.9.5 xtable_1.8-8 RhpcBLASctl_0.23-42
## [79] glue_1.8.1 maketools_1.3.2 tools_4.6.1
## [82] fANCOVA_0.6-1 variancePartition_1.43.1 sys_3.4.3
## [85] lme4_2.0-6 annotate_1.91.0 locfit_1.5-9.12
## [88] mvtnorm_1.4-2 buildtools_1.0.0 XML_3.99-0.25
## [91] grid_4.6.1 tidyr_1.3.2 rbibutils_2.4.1
## [94] AnnotationDbi_1.75.2 edgeR_4.99.7 nlme_3.1-171
## [97] cli_3.6.6 viridisLite_0.4.3 S4Arrays_1.13.2
## [100] dplyr_1.2.1 corpcor_1.6.10 gtable_0.3.6
## [103] sass_0.4.10 digest_0.6.39 pbkrtest_0.5.5
## [106] SparseArray_1.13.4 farver_2.1.2 memoise_2.0.1
## [109] htmltools_0.5.9 lifecycle_1.0.5 httr_1.4.9
## [112] statmod_1.5.2 bit64_4.8.6 MASS_7.3-66