BatchVaria 0.99.2
library(BatchVaria)
library(SummarizedExperiment)
library(BiocStyle)
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:
BiocManager::install(version = "devel")
BiocManager::install("BatchVaria")
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"
table(
batch = bv$batch,
group = bv$group
)
## 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:
availableCorrectionMethods()
## [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:
varianceTable(bv, assays = assaysUsed)
## 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:
p <- provenance(bv)
## 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-13 16:26:15.980271
## 2 combat raw protected batch group FALSE 2026-10-13 16:26:16.060592
##
## Variance profiling history:
## step assay formula methods timestamp
## 1 raw ~batch + group anova 2026-10-13 16:26:16.133166
## 2 raw ~batch + group variancePartition 2026-10-13 16:26:20.021419
## 3 naive ~batch + group anova 2026-10-13 16:26:20.038455
## 4 naive ~batch + group variancePartition 2026-10-13 16:26:23.032021
## 5 protected ~batch + group anova 2026-10-13 16:26:23.049358
## 6 protected ~batch + group variancePartition 2026-10-13 16:26:26.120103
## 7 raw_noise ~batch + group anova 2026-10-13 16:26:26.138851
## 8 raw_noise ~batch + group variancePartition 2026-10-13 16:26:29.179335
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:
p$corrections[, c("method", "output", "preserve", "no_op")]
## 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.
vt <- varianceTable(bv, assays = assaysUsed, method = "anova")
This table shows the percentage of variance explained by each component for each assay.
vt$percent
## # 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.
varianceTable(
bv,
assays = assaysUsed,
method = "anova",
formatDelta = TRUE
)$delta
## # 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.
av <- assayVariance(bv, assays = assaysUsed)
av
## 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.
basisRetention(bv, assays = assaysUsed)
## assay reference retention off_basis n_features
## 1 raw raw 1.0000000 0.00000000 800
## 2 naive raw 0.9891828 0.01081725 800
## 3 protected raw 0.9951290 0.00487105 800
## 4 raw_noise raw 0.3699325 0.63006753 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.
plotVarianceComposition(bv, assays = assaysUsed, method = "anova")
## 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.
plotVarianceRadar(bv, assays = assaysUsed, method = "anova")
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:
coords <- basisProjection(
bv,
assays = c("raw", "naive"),
reference = "raw"
)
head(coords)
## 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:
plotBasisProjection(
bv,
assays = "naive",
reference = "raw",
colourBy = "group"
)[[1]]
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.
plotPCATrajectory(bv, assayBefore = "raw", assayAfter = "naive")
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:
plotSampleDistance(bv, assayName = "raw")
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.
plotCorrelationChange(ev)
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.
sessionInfo()
## R version 4.6.1 (2026-06-24)
## Platform: x86_64-pc-linux-gnu
## Running under: Ubuntu 24.04.5 LTS
##
## Matrix products: default
## BLAS: /home/biocbuild/bbs-3.24-bioc/R/lib/libRblas.so
## LAPACK: /usr/lib/x86_64-linux-gnu/lapack/liblapack.so.3.12.0 LAPACK version 3.12.0
##
## locale:
## [1] LC_CTYPE=en_US.UTF-8 LC_NUMERIC=C
## [3] LC_TIME=en_GB LC_COLLATE=C
## [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: America/New_York
## 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] RColorBrewer_1.1-3 jsonlite_2.0.0 magrittr_2.0.5
## [4] magick_2.9.1 farver_2.1.2 nloptr_2.2.1
## [7] rmarkdown_2.32 vctrs_0.7.3 memoise_2.0.1
## [10] minqa_1.2.8 tinytex_0.61 htmltools_0.5.9
## [13] S4Arrays_1.13.2 BiocBaseUtils_1.15.1 broom_1.0.13
## [16] SparseArray_1.13.4 variancePartition_1.43.1 sass_0.4.10
## [19] KernSmooth_2.23-27 bslib_0.12.0 pbkrtest_0.5.5
## [22] plyr_1.8.9 cachem_1.1.0 lifecycle_1.0.5
## [25] iterators_1.0.14 pkgconfig_2.0.3 Matrix_1.7-6
## [28] R6_2.6.1 fastmap_1.2.0 rbibutils_2.4.1
## [31] digest_0.6.39 numDeriv_2016.8-1.1 AnnotationDbi_1.75.2
## [34] RSQLite_3.53.3 labeling_0.4.3 httr_1.4.9
## [37] abind_1.4-8 mgcv_1.9-4 compiler_4.6.1
## [40] bit64_4.8.6 aod_1.3.3 withr_3.0.3
## [43] S7_0.2.2 backports_1.5.1 BiocParallel_1.47.0
## [46] DBI_1.3.0 gplots_3.3.0 MASS_7.3-66
## [49] DelayedArray_0.39.8 corpcor_1.6.10 gtools_3.9.5
## [52] caTools_1.18.4 tools_4.6.1 otel_0.2.0
## [55] remaCor_0.0.20 glue_1.8.1 nlme_3.1-171
## [58] grid_4.6.1 reshape2_1.4.5 sva_3.61.0
## [61] gtable_0.3.6 tidyr_1.3.2 utf8_1.2.6
## [64] XVector_0.53.0 pillar_1.11.1 stringr_1.6.0
## [67] limma_3.99.2 genefilter_1.95.0 splines_4.6.1
## [70] dplyr_1.2.1 lattice_0.23-1 survival_3.8-12
## [73] FNN_1.1.4.1 bit_4.6.0 annotate_1.91.0
## [76] tidyselect_1.2.1 locfit_1.5-9.12 Biostrings_2.81.9
## [79] knitr_1.52 reformulas_0.4.4 bookdown_0.48
## [82] edgeR_4.99.7 RhpcBLASctl_0.23-42 xfun_0.61
## [85] statmod_1.5.2 stringi_1.8.9 yaml_2.3.12
## [88] boot_1.3-32 evaluate_1.0.5 codetools_0.2-20
## [91] tibble_3.3.1 BiocManager_1.30.27 cli_3.6.6
## [94] xtable_1.8-8 Rdpack_2.6.6 jquerylib_0.1.4
## [97] dichromat_2.0-1 Rcpp_1.1.2 EnvStats_3.1.0
## [100] png_0.1-9 XML_3.99-0.25 parallel_4.6.1
## [103] ggplot2_4.0.3 blob_1.3.0 bitops_1.1-0
## [106] lme4_2.0-6 viridisLite_0.4.3 mvtnorm_1.4-2
## [109] lmerTest_3.2-1 scales_1.4.0 purrr_1.2.2
## [112] crayon_1.5.3 fANCOVA_0.6-1 rlang_1.3.0
## [115] KEGGREST_1.53.6