A cohort assembled over a decade is usually assembled over two array generations as well: earlier visits on HumanMethylation450, later ones on MethylationEPIC. Array type is then not a nuisance variable that happens to be present, it is a variable that moves with calendar time and therefore with follow-up, treatment duration and often with the exposure itself. Regressing out an array indicator does not repair that, because the indicator and the contrast of interest are partly the same column.
crossarrayEWAS takes the other route: the probe-level
effect is identified within subject. Anything constant
within a subject’s visit series is absorbed by the transformation rather
than modelled: subject identity, and in a cohort where each subject sits
on one array generation, array type and chip as well. What is left is
estimated once, grouped by observed co-methylation, and turned into
regions and long-range blocks by two estimators that place boundaries
rather than threshold a smooth curve.
The design this was written for is a longitudinal cohort, sampled repeatedly within subject over a recruitment window long enough that the array generation changed mid-study: earlier subjects assayed on 450K, later subjects on EPIC, and no subject spanning the two. The validation cohort is GSE237561, a clozapine treatment series of 126 arrays from 38 subjects, 92 on 450K and 34 on EPIC. What makes that split non-exchangeable with exposure is that one variable decides both: subjects recruited later sit on EPIC and also have longer treatment duration, so an array indicator and the contrast of interest are partly the same column. In GSE237561 neither a subject nor a chip spans the two generations, so cohort is nested inside array generation rather than crossed with it, and a batch correction taken on cohort is not identifiable there.
ewas_regions() and ewas_blocks() take a
SummarizedExperiment — including a
GenomicRatioSet from minfi,
which extends one — and return a GRanges. Each runs the
analysis the pipeline’s corresponding stage runs: assay and design
extraction, within-subject probe effects, co-methylation clustering,
smoothness chosen by held-out subjects, region or block calling, and,
when asked for, a permutation null and subject-level selection
stability. Everything else exported here is a building block one of them
assembles, and the later sections take those apart.
No study data ships with the package, so this vignette simulates a cohort with the structure the estimators are designed for: subjects followed over an unbalanced number of visits, a continuous exposure that varies within subject, two array generations nested in subject, probes spaced in co-methylated clusters, and two planted signals: a short region and a long low-amplitude block.
set.seed(20240607)
n_sub <- 24L
visits <- sample(2:4, n_sub, replace = TRUE)
subject <- rep(sprintf("S%02d", seq_len(n_sub)), visits)
n <- length(subject)
# array generation is a property of the subject, not of the visit: this is the
# confounding the within-subject transform absorbs
array_type <- rep(ifelse(seq_len(n_sub) <= 14L, "450K", "EPIC"), visits)
# exposure varies between visits of the same subject, which is what makes the
# longitudinal contrast identified at all
exposure <- round(runif(n, 0, 9), 3)
covars <- cbind(cell_fraction = rnorm(n, 0.20, 0.05),
smoking_score = rnorm(n, -1.0, 0.40))
c(subjects = n_sub, samples = n, on_450K = sum(array_type == "450K"))
#> subjects samples on_450K
#> 24 66 38Probe positions are drawn as tight clusters separated by wide gaps,
which is what comethylation_clusters() looks for and
roughly what a methylation array gives you.
n_clust <- 640L
sizes <- sample(2:5, n_clust, replace = TRUE)
pos <- integer(0)
p <- 1000000L
for (k in seq_len(n_clust)) {
within <- c(0L, cumsum(sample(80:600, sizes[k] - 1L, replace = TRUE)))
pos <- c(pos, p + within)
p <- p + max(within) + sample(3000:20000, 1L)
}
n_probes <- length(pos)
chrom <- rep("chr21", n_probes)
cluster_of <- rep(seq_len(n_clust), sizes) # the truth, for comparison below
c(probes = n_probes, planted_clusters = n_clust)
#> probes planted_clusters
#> 2225 640M-values are assembled from the terms the model assumes: a probe baseline, a subject intercept, an array offset, a per-cluster latent component that makes neighbouring probes co-methylated, independent noise, and the exposure effect.
sub_idx <- match(subject, unique(subject))
baseline <- rnorm(n_probes, 0, 1.5)
subject_eff <- matrix(rnorm(n_probes * n_sub, 0, 0.30), n_probes, n_sub)
array_off <- rnorm(n_probes, 0, 0.40)
latent <- matrix(rnorm(n_clust * n, 0, 0.25), n_clust, n)[cluster_of, ]
# planted signal: one short region, one long shallow block
beta_true <- numeric(n_probes)
region_probes <- which(cluster_of %in% 300:301)
block_probes <- which(cluster_of %in% 420:470)
beta_true[region_probes] <- 0.30
beta_true[block_probes] <- 0.06
M <- baseline +
subject_eff[, sub_idx] +
outer(array_off, as.numeric(array_type == "EPIC")) +
latent +
matrix(rnorm(n_probes * n, 0, 0.20), n_probes, n) +
outer(beta_true, exposure - mean(exposure))
rownames(M) <- sprintf("cg%07d", seq_len(n_probes))
colnames(M) <- sprintf("%s_v%d", subject,
unlist(lapply(visits, seq_len), use.names = FALSE))
dim(M)
#> [1] 2225 66Assembled into a SummarizedExperiment, this is the shape
the package works on: an M-value assay, probe coordinates in
rowRanges(), and the design in colData(). A
GenomicRatioSet carries the same three things, which is why
one can be passed straight in.
se <- SummarizedExperiment::SummarizedExperiment(
assays = list(M = M),
rowRanges = GenomicRanges::GRanges(chrom, IRanges::IRanges(pos, width = 2L)),
colData = data.frame(exposure = exposure, subject = subject, covars,
array_type = array_type))
se
#> class: RangedSummarizedExperiment
#> dim: 2225 66
#> metadata(0):
#> assays(1): M
#> rownames(2225): cg0000001 cg0000002 ... cg0002224 cg0002225
#> rowData names(0):
#> colnames(66): S01_v1 S01_v2 ... S24_v2 S24_v3
#> colData names(5): exposure subject cell_fraction smoking_score
#> array_typeregions <- ewas_regions(se, exposure = "exposure", subject = "subject",
covars = c("cell_fraction", "smoking_score"),
lambda = 0.5, min_probes = 3L, min_effect = 0.05,
n_perm = 20L, n_boot = 8L, perm_iter = 400L)
regions[order(-abs(regions$z))][seq_len(min(5L, length(regions)))]
#> GRanges object with 5 ranges and 11 metadata columns:
#> seqnames ranges strand | n_probes effect_M se
#> <Rle> <IRanges> <Rle> | <integer> <numeric> <numeric>
#> 303 chr21 4727790-4729167 * | 4 0.3029088 0.00987657
#> 304 chr21 4735476-4736527 * | 3 0.3035786 0.01138946
#> 456 chr21 6486497-6488011 * | 5 0.0958516 0.00881359
#> 443 chr21 6315208-6315667 * | 3 0.1027870 0.01138555
#> 433 chr21 6200642-6201611 * | 4 0.0838046 0.00989106
#> z p_pointwise theta probe_start probe_end p_fwer
#> <numeric> <numeric> <numeric> <integer> <integer> <numeric>
#> 303 30.66944 1.45539e-206 0.3029088 1042 1045 0.0476190
#> 304 26.65435 1.59346e-156 0.3035786 1046 1048 0.0476190
#> 456 10.87544 1.50927e-27 0.0958516 1575 1579 0.0952381
#> 443 9.02785 1.75082e-19 0.1027870 1535 1537 0.1428571
#> 433 8.47276 2.39646e-17 0.0838046 1506 1509 0.1428571
#> p_fwer_mcse stability
#> <numeric> <numeric>
#> 303 0.0476190 1
#> 304 0.0476190 1
#> 456 0.0656383 1
#> 443 0.0782461 1
#> 433 0.0782461 1
#> -------
#> seqinfo: 1 sequence from an unspecified genome; no seqlengthsThe ranges come back in genomic order with the per-region statistics
as metadata columns, and the run’s own summary in
metadata(): the observed maximum |z| the null is compared
against, the penalty used, the number of co-methylation clusters, and
the degrees of freedom the within-subject transform left.
md <- S4Vectors::metadata(regions)
c(max_abs_z = round(md$max_abs_z, 2), lambda = md$lambda,
clusters = md$n_clusters, df = md$fit$df)
#> max_abs_z lambda clusters df
#> 30.67 0.50 642.00 39.00The planted region is recovered and the rest of the genome is not:
planted <- range(pos[region_probes])
hit <- GenomicRanges::start(regions) <= planted[2] &
planted[1] <= GenomicRanges::end(regions)
c(regions = length(regions), overlapping_the_planted_region = sum(hit))
#> regions overlapping_the_planted_region
#> 26 2Three arguments carry most of the scientific content of a run.
min_effect is on the M-value scale and is a choice about
what counts as an effect, not a tuning knob. In the clozapine analysis
it was 0.05 M-value units per 100 days of treatment: a segment has to
move at least that much to be called, whatever its z. At mid-range
methylation that is about 0.9 percentage points of beta per 100 days, so
roughly 3 points over a year of treatment and 6 over two, falling to
about 2 and 4 points at beta 0.2 or 0.8. The floor is set there because
a slope below it accumulates to less than the change a single array can
resolve over a realistic follow-up window, so a segment that clears it
is reportable on the beta scale as well as on the z scale.
lambda is fixed above to keep the vignette quick. Left
at its default it is a vector, and the penalty is then chosen by
cross-validation over folds of whole subjects, with the
score table returned in metadata()$lambda_cv. Held-out
subjects rather than held-out samples are required: a subject’s visits
are the unit the within-subject effect is estimated from, so splitting
them across train and test would leak the quantity being validated.
var_method chooses how the per-probe residual variance
is moderated before the z statistics are formed. The default
"limma" calls limma::squeezeVar(), the
reference implementation of that model. "mom" is the
method-of-moments fit ported from the Python twin of this core, and it
exists so the two implementations can be compared arithmetically on
identical input. The stage drivers default to "limma" and
the published analysis was run with it, so report with the default and
name "mom" in the methods if a reported result uses it.
Blocks are a coarser statement: co-methylated probes are collapsed to one profile per cluster, effects are refitted at cluster level, and a three-state hidden semi-Markov model runs along the chromosome with transition probabilities that decay with genomic distance.
blocks <- ewas_blocks(se, exposure = "exposure", subject = "subject",
covars = c("cell_fraction", "smoking_score"),
max_gap = 1500L, rho_min = 0.20,
min_fit_clusters = 100L)
blocks
#> GRanges object with 1 range and 3 metadata columns:
#> seqnames ranges strand | n_clusters direction posterior
#> <Rle> <IRanges> <Rle> | <integer> <character> <numeric>
#> [1] chr21 6078741-6696728 * | 51 hyper 0.997761
#> -------
#> seqinfo: 1 sequence from an unspecified genome; no seqlengthsThe fitted states, the collapsed clusters and their posteriors are in
metadata(). hsmm$neutral is the column of the
posterior holding the state pinned at mu = 0, and it is
not always the middle one: if every cluster effect
falls on one side of zero that column sorts to an end and only one
direction can be called, which hsmm$one_sided reports.
hs <- S4Vectors::metadata(blocks)$hsmm
data.frame(state = hs$states, mu = round(hs$mu, 4),
sigma = round(hs$sigma, 4), pi = round(hs$pi, 3))
#> state mu sigma pi
#> 1 hypo -0.0052 0.0069 0.000
#> 2 neutral 0.0000 0.0001 0.914
#> 3 hyper 0.0681 0.0466 0.086
clusters <- S4Vectors::metadata(blocks)$clusters
length(clusters)
#> [1] 641planted_block <- range(pos[block_probes])
cmid <- (GenomicRanges::start(clusters) + GenomicRanges::end(clusters)) / 2
plot(cmid / 1e6, clusters$effect_M, pch = 16, cex = 0.5, col = "grey40",
xlab = "position (Mb)", ylab = "effect (M-value per exposure unit)")
rect(planted_block[1] / 1e6, par("usr")[3], planted_block[2] / 1e6,
par("usr")[4], col = "#1b9e7733", border = NA)
abline(h = 0, col = "grey70")
if (length(blocks))
segments(GenomicRanges::start(blocks) / 1e6, 0,
GenomicRanges::end(blocks) / 1e6, 0, col = "#d95f02", lwd = 4)Cluster-level effects along the chromosome. Shaded span: the planted block. Bars: the blocks called.
A region and a block are not two attempts at the same quantity. Regions are called on every harmonised probe at the resolution of a few probes, median 84 bp over 3 probes in the published run, and locate a focal effect. Blocks are called on open-sea probes only, at cluster resolution, and in the same run returned 38 blocks of median width 134 kb spanning a median of 9 clusters, against 207 regions. A block is therefore evidence of a shallow shift across a domain that is largely unmeasured between clusters, and a region is evidence of a local one; the two can coexist, and neither subsumes the other.
When they disagree, they are reported as what they are: two independent lines of evidence, each on its own scale. A block with no region inside it is not demoted, and a region inside a neutral block is not discounted; the region table and the block table are read side by side rather than one filtering the other.
ewas_regions() needs one object spanning both array
generations. Building it is minfi’s
job, and it is three calls: combine the two RGChannelSets
on the probes they share, convert to ratios, and map to the genome. The
pipeline’s stage 01 does exactly this, with quality control and probe
filtering around it, and writes the matrix read_f64() reads
back.
library(minfi)
rg450 <- read.metharray.exp(targets = targets_450k)
rgepic <- read.metharray.exp(targets = targets_epic)
# combineArrays() keeps the probes shared by both designs and sets the
# resulting object's array to the older of the two
rg <- combineArrays(rg450, rgepic, outType = "IlluminaHumanMethylation450k")
grset <- mapToGenome(ratioConvert(preprocessQuantile(rg)))
SummarizedExperiment::colData(grset)$subject <- targets$Subject_ID
SummarizedExperiment::colData(grset)$exposure <- targets$days_on_drug / 100
regions <- ewas_regions(grset, "exposure", "subject")Two things to check before the fit, because neither is detectable
afterwards. The exposure has to vary between visits of the same subject,
or there is nothing within-subject to estimate and
ewas_regions() stops. And a covariate that is constant
within subject — sex, genotype, array type in a cohort where no subject
spans both generations — is absorbed by the transform and cannot be
passed as a covariate; this is the point of the design rather than a
limitation of it.
A 2,000-probe subset of GSE237561 ships with the package as a
GenomicRatioSet, with the script that built it in
inst/scripts/. It is the clozapine treatment series the
method was developed on: 126 arrays from 38 subjects, 92 on 450K and 34
on EPIC, with the exposure being days on treatment and no subject
spanning the two array generations.
path <- system.file("extdata", "GSE237561_chr21_subset.rds",
package = "crossarrayEWAS")
grset <- readRDS(path)
grset
#> Loading required namespace: minfi
#> Setting options('download.file.method.GEOquery'='auto')
#> Setting options('GEOquery.inmemory.gpl'=FALSE)
#> class: GenomicRatioSet
#> dim: 2000 126
#> metadata(0):
#> assays(1): Beta
#> rownames(2000): cg17035109 cg12459059 ... cg14296488 cg15214567
#> rowData names(0):
#> colnames(126): 3998567005_R01C01 3998567005_R02C01 ...
#> 204026590037_R02C01 204026590037_R03C01
#> colData names(4): subject visit exposure array_type
#> Annotation
#> array: IlluminaHumanMethylation450k
#> annotation: ilmn12.hg19
#> Preprocessing
#> Method: GEO GSE237561 normalised beta matrix
#> minfi version: NA
#> Manifest version: NA
table(SummarizedExperiment::colData(grset)$array_type)
#>
#> 450K EPIC
#> 92 34real <- ewas_regions(grset, exposure = "exposure", subject = "subject",
lambda = 0.5, min_probes = 3L, min_effect = 0.05)
#> assay "Beta" holds beta values; converting to M-values with log2(beta / (1 - beta))
length(real)
#> [1] 1
if (length(real))
real[order(-abs(real$z))][seq_len(min(3L, length(real)))]
#> GRanges object with 1 range and 8 metadata columns:
#> seqnames ranges strand | n_probes effect_M se
#> <Rle> <IRanges> <Rle> | <integer> <numeric> <numeric>
#> 1398 chr21 42218932-42219139 * | 3 -0.0510731 0.0211092
#> z p_pointwise theta probe_start probe_end
#> <numeric> <numeric> <numeric> <integer> <integer>
#> 1398 -2.41947 0.0155432 -0.00633606 1493 1495
#> -------
#> seqinfo: 1 sequence from an unspecified genome; no seqlengthsThe subset is a thousandth of the harmonised panel and is here so the interface can be exercised on real data, not to reproduce the published result: regions called on 2,000 probes of one chromosome are not the regions called on 422,898 probes genome-wide, and the permutation null is not run here because its cost is what the pipeline exists for.
The two functions above are assembled from exported pieces, and an analysis that needs to depart from them can use those directly. The clustering step is representative: probes are grouped by their residual co-methylation, after subject means and the design have been projected out.
design <- stats::model.matrix(
~ subject + exposure + cell_fraction + smoking_score,
data = as.data.frame(SummarizedExperiment::colData(se)))
resid <- t(stats::lm.fit(design, t(M))$residuals)
cl <- comethylation_clusters(chrom, pos, resid, max_gap = 1000L,
rho_min = 0.30)
c(clusters = max(cl) + 1L, clustered = sum(cl >= 0), singletons = sum(cl < 0))
#> clusters clustered singletons
#> 642 2220 5Cluster labels are 0-based and -1 marks a probe that
joined nothing, the convention the Python implementation of the same
estimator uses.
fit <- fit_within_se(se, "exposure", "subject",
covars = c("cell_fraction", "smoking_score"))
w <- 1 / fit$se^2
lam <- distance_penalty(pos, lam0 = 0.5, decay_bp = 1000, w = w)
theta <- tv_denoise(fit$beta, w, lam, n_iter = 3000L)
obs <- call_regions(chrom, pos, fit$beta, fit$se, theta, min_probes = 3L,
min_effect = 0.05, cluster = ifelse(cl >= 0, cl, -seq_along(cl)))
identical(nrow(obs$table), length(regions))
#> [1] TRUEas_granges() converts either caller’s table, keeping
max_abs_z in metadata();
sort_probes() and probe_coords() are the
object accessors the high-level functions use, and are exported because
an analysis that calls fit_within() directly needs the same
ordering guarantee.
Both null constructions resample at the subject level, so a subject’s
visit series is never split across a fold or a permutation.
within_subject_permutation() permutes exposure
inside each subject, which leaves the between-subject
distribution intact and destroys only the within-subject contrast, which
is the thing being tested.
perm <- within_subject_permutation(exposure, subject)
identical(sort(exposure[subject == "S01"]), sort(perm[subject == "S01"]))
#> [1] TRUE
fit_perm <- fit_within(M, perm, subject, covars)
c(observed = round(max(abs(fit$z)), 2),
permuted = round(max(abs(fit_perm$z)), 2))
#> observed permuted
#> 15.91 3.23n_perm runs that null inside
ewas_regions(), and p_fwer is the fraction of
null replicates whose maximum |z| reaches a region’s own, so it is a
family-wise statement across the genome rather than a pointwise one.
p_fwer_mcse is the Monte Carlo standard error of that
fraction, reported because the decision at 0.05 is a step function of
the null tail: a p-value whose error straddles the threshold is evidence
about n_perm, not about the region.
perm_scheme = "naive" shuffles across subjects instead, the
scheme bumphunter
uses, and exists to quantify its mis-calibration in a longitudinal
design rather than to be used.
GenomicRanges::mcols(regions)[order(-abs(regions$z)),
c("z", "p_fwer", "p_fwer_mcse")][1:3, ]
#> DataFrame with 3 rows and 3 columns
#> z p_fwer p_fwer_mcse
#> <numeric> <numeric> <numeric>
#> 303 30.6694 0.0476190 0.0476190
#> 304 26.6544 0.0476190 0.0476190
#> 456 10.8754 0.0952381 0.0656383
S4Vectors::metadata(regions)$perm_scheme
#> [1] "within"n_boot adds selection stability: regions are re-called
on repeated half-samples of subjects, and stability is the
fraction of subsamples in which an overlapping region was called again.
Boundaries move between subsamples, so overlap rather than matching
endpoints is the criterion. stability_selection() is
exported for analyses that want the same resampling around a different
estimator; the callback is handed subject identifiers, not column
indices, and returns the keys it re-discovered.
GenomicRanges::mcols(regions)[order(-regions$stability, -abs(regions$z)),
c("n_probes", "z", "stability")][1:3, ]
#> DataFrame with 3 rows and 3 columns
#> n_probes z stability
#> <integer> <numeric> <numeric>
#> 303 4 30.6694 1
#> 304 3 26.6544 1
#> 456 5 10.8754 1The published run used 200 permutations and 50 half-samples; the small numbers here keep the vignette quick. Selection frequency is carried beside z rather than used as a filter, so a region that survives the permutation null but is unstable across subsamples stays visible and is read as exactly that. The threshold reported on is 0.6, conventional in the stability-selection literature: regions selected in at least 60% of subsamples are described as stable, and the column annotates the table rather than removing rows from it.
These functions are the numerical core of a Nextflow pipeline whose earlier stages harmonise 450K and EPIC intensities, and of a Galaxy tool suite that wraps the same stage drivers. The pipeline is kkamieniecka/ewas-crossarray-harmonise, which generates this package from its numerical core, and the wrappers are published as a Tool Shed-installable suite at bioinfbrad/ewas-crossarray-harmonise; both extend the EWASGalaxy tool suite (Murat et al., bioRxiv 2019, doi:10.1101/553784).
sessionInfo()
#> 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] stats graphics grDevices utils datasets methods base
#>
#> other attached packages:
#> [1] crossarrayEWAS_0.99.1 BiocStyle_2.41.0
#>
#> loaded via a namespace (and not attached):
#> [1] beanplot_1.3.1 DBI_1.3.0
#> [3] bitops_1.1-0 magrittr_2.0.5
#> [5] rlang_1.3.0 scrime_1.3.7
#> [7] otel_0.2.0 matrixStats_1.5.0
#> [9] compiler_4.6.1 RSQLite_3.53.3
#> [11] GenomicFeatures_1.65.0 DelayedMatrixStats_1.35.0
#> [13] png_0.1-9 vctrs_0.7.3
#> [15] quadprog_1.5-8 minfi_1.59.1
#> [17] pkgconfig_2.0.3 crayon_1.5.3
#> [19] fastmap_1.2.0 XVector_0.53.0
#> [21] Rsamtools_2.29.0 rmarkdown_2.32
#> [23] tzdb_0.5.0 preprocessCore_1.75.1
#> [25] purrr_1.2.2 bit_4.6.0
#> [27] xfun_0.61 cachem_1.1.0
#> [29] cigarillo_1.3.1 jsonlite_2.0.0
#> [31] blob_1.3.0 rhdf5filters_1.25.4
#> [33] DelayedArray_0.39.8 reshape_0.8.10
#> [35] Rhdf5lib_2.1.0 BiocParallel_1.47.0
#> [37] parallel_4.6.1 R6_2.6.1
#> [39] bslib_0.12.0 RColorBrewer_1.1-3
#> [41] limma_3.99.2 rtracklayer_1.73.0
#> [43] genefilter_1.95.0 GenomicRanges_1.65.4
#> [45] jquerylib_0.1.4 Rcpp_1.1.2
#> [47] Seqinfo_1.3.2 SummarizedExperiment_1.43.0
#> [49] iterators_1.0.14 knitr_1.52
#> [51] readr_2.2.0 IRanges_2.47.5
#> [53] BiocBaseUtils_1.15.1 tidyselect_1.2.1
#> [55] rentrez_1.2.4 illuminaio_0.55.0
#> [57] Matrix_1.7-6 splines_4.6.1
#> [59] abind_1.4-8 yaml_2.3.12
#> [61] siggenes_1.87.0 codetools_0.2-20
#> [63] curl_8.0.0 doRNG_1.8.6.3
#> [65] tibble_3.3.1 lattice_0.23-1
#> [67] plyr_1.8.9 Biobase_2.73.2
#> [69] KEGGREST_1.53.6 askpass_1.2.1
#> [71] evaluate_1.0.5 survival_3.8-12
#> [73] xml2_1.6.0 mclust_6.1.3
#> [75] Biostrings_2.81.9 pillar_1.11.1
#> [77] BiocManager_1.30.27 MatrixGenerics_1.25.0
#> [79] rngtools_1.5.2 foreach_1.5.2
#> [81] stats4_4.6.1 generics_0.1.4
#> [83] RCurl_1.98-1.20 hms_1.1.4
#> [85] S4Vectors_0.51.10 sparseMatrixStats_1.25.0
#> [87] bumphunter_1.55.1 xtable_1.8-8
#> [89] glue_1.8.1 maketools_1.3.2
#> [91] tools_4.6.1 BiocIO_1.23.3
#> [93] data.table_1.18.6.1 sys_3.4.3
#> [95] GenomicAlignments_1.49.2 annotate_1.91.0
#> [97] locfit_1.5-9.12 GEOquery_2.81.28
#> [99] buildtools_1.0.0 XML_3.99-0.25
#> [101] rhdf5_2.57.20 grid_4.6.1
#> [103] tidyr_1.3.2 AnnotationDbi_1.75.2
#> [105] base64_2.0.2 nlme_3.1-171
#> [107] nor1mix_1.3-3 HDF5Array_1.41.3
#> [109] restfulr_0.0.17 cli_3.6.6
#> [111] S4Arrays_1.13.2 dplyr_1.2.1
#> [113] sass_0.4.10 digest_0.6.39
#> [115] BiocGenerics_0.59.12 SparseArray_1.13.4
#> [117] rjson_0.2.23 memoise_2.0.1
#> [119] htmltools_0.5.9 multtest_2.69.0
#> [121] lifecycle_1.0.5 h5mread_1.5.3
#> [123] httr_1.4.9 statmod_1.5.2
#> [125] openssl_2.4.2 bit64_4.8.6
#> [127] MASS_7.3-66