This tutorial shows the available tools in COTAN to help with genes’ clustering
Define a directory where the data and the output will be stored
dataDir <- file.path(tempdir(), "COTAN_vignette_data")
dir.create(dataDir, recursive = TRUE, showWarnings = FALSE)
print(dataDir)
#> [1] "/tmp/RtmpdrRmYh/COTAN_vignette_data"
outDir <- dataDir
# Log-level 2 was chosen to showcase better how the package works
# In normal usage a level of 0 or 1 is more appropriate
setLoggingLevel(2L)
#> Setting new log level to 2
# This file will contain all the logs produced by the package
# as if at the highest logging level
setLoggingFile(file.path(outDir, "vignette_DEA.log"))
#> Setting log file to be: /tmp/RtmpdrRmYh/COTAN_vignette_data/vignette_DEA.logDownload the data-set for mouse cortex E17.5 from Yuzwa et al. (2017)
GEO <- "GSM2861514"
dir.create(file.path(dataDir, GEO), showWarnings = FALSE)
datasetFileName <-
file.path(dataDir, GEO, "GSM2861514_E175_All_Cells_DGE.txt.gz")
# retries up to 5 times to get the dataset
attempts <- 0L
maxAttempts <- 5L
ok <- FALSE
while (attempts < maxAttempts && !ok) {
attempts <- attempts + 1L
if (!file.exists(datasetFileName)) {
res <- try(
GEOquery::getGEOSuppFiles(
GEO = GEO,
makeDirectory = TRUE,
baseDir = dataDir,
filter_regex = base::basename(datasetFileName),
fetch_files = TRUE
),
silent = TRUE
)
}
ok <- file.exists(datasetFileName)
if (!ok && attempts < maxAttempts) {
Sys.sleep(1)
}
}
assertthat::assert_that(
ok,
msg = paste0(
"Failed to retrieve file '", datasetFileName,
"' after ", maxAttempts, " attempts."
)
)
#> [1] TRUE
rawDataset <- read.csv(datasetFileName, sep = "\t", row.names = 1L)
print(dim(rawDataset))
#> [1] 17085 2000Initialize the COTAN object with the row count table and
the metadata from the experiment.
cond <- "mouse_cortex_E17.5"
obj <- COTAN(raw = rawDataset)
obj <-
initializeMetaDataset(
obj,
GEO = GEO,
sequencingMethod = "Drop_seq",
sampleCondition = cond
)
#> Initializing `COTAN` meta-data
logThis(paste0("Condition ", getMetadataElement(obj, datasetTags()[["cond"]])),
logLevel = 1L)
#> Condition mouse_cortex_E17.5Assign to each cell its origin
# mark cells origin
data(vignette.cells.origin)
head(vignette.cells.origin, 18)
#> GCGAATTGTGAA ACCGATAACTGA CCCCGGGTGCGA CTGTAGATGTTA TCATCGAAGCGC TTCTACCGAGTC
#> Other Other Other Other Other Other
#> CTGTTCCCGGCG GCGTGTTAGTTC CTCGCGCGTTTA GATGTATAACTT GCGCTATGATTT CGTTTAGTTTAC
#> Other Other Other Other Other Cortical
#> GTGGAGGCCCAT TGTCACTACATC TCTAGAACAACG ACCTTTGTTCGT TTGTCTTCTTCG TAAAATATCGCC
#> Other Other Cortical Cortical Cortical Cortical
#> Levels: Cortical Other
obj <-
addCondition(obj, condName = "origin", conditions = vignette.cells.origin)
rm(vignette.cells.origin)# use previously established results to determine
# which cells were dropped in the cleaning stage
data(vignette.split.clusters)
cellsToDrop <-
getCells(obj)[!(getCells(obj) %in% names(vignette.split.clusters))]
obj <- dropGenesCells(obj, cells = cellsToDrop)
# Log the remaining number of cells
logThis(paste("n cells", getNumCells(obj)), logLevel = 1L)
#> n cells 1783
table(getCondition(obj, condName = "origin"))
#>
#> Cortical Other
#> 844 939
rm(vignette.split.clusters)In this part, the COTAN model is calibrated, but the
COEX matrix is not evaluated as it is not strictly
necessary
obj <-
proceedToCoex(
obj,
calcCoex = FALSE,
optimizeForSpeed = TRUE,
cores = 3L,
deviceStr = "cuda"
)
#> COTAN dataset analysis: START
#> Genes/cells selection done: dropped [4330] genes and [0] cells
#> Working on [12755] genes and [1783] cells
#> Estimate `dispersion`: START
#> Total calculations elapsed time: 8.90903949737549
#> Estimate `dispersion`: DONE
#> Estimate `dispersion`: DONE
#> `dispersion` | min: -0.0349260648055935 | max: 760.574661197634 | % negative: 12.5362602900823
#> COTAN genes' COEX estimation not requested
#> COTAN dataset analysis: DONETODO:
Loading pre-calculated clusterizations from data and call
the DEAOnClusters(). This method estimates assess the
differential expression of genes when partitioning cells into two
subsets: the cluster and all other cells in the dataset. For each
cluster COTAN calculates a correlation coefficient
based on contingency tables that counts whether each gene occurs or not
in cells inside or outside of the cluster.
These numbers are organized as one column for each cluster with a row for each gene and are a measure of the enrichment or depletion of the genes inside the clusters.
data("vignette.split.clusters", package = "COTAN")
data("vignette.merge.clusters", package = "COTAN")
vignette.split.clusters <-
asClusterization(
vignette.split.clusters, # a named vector/factor/data.frame
allCells = getCells(obj) # used only to check names are coherent
)
vignette.merge.clusters <-
asClusterization(
vignette.merge.clusters, # a named vector/factor/data.frame
allCells = getCells(obj) # used only to check names are coherent
)
# explicitly calculate the DEA of the clusterization to store it
vignette.split.coexDF <-
DEAOnClusters(obj, clusters = vignette.split.clusters)
#> Differential Expression Analysis - START
#> ****************
#> Total calculations elapsed time: 0.781831502914429
#> Differential Expression Analysis - DONE
vignette.merge.coexDF <-
DEAOnClusters(obj, clusters = vignette.merge.clusters)
#> Differential Expression Analysis - START
#> *********
#> Total calculations elapsed time: 0.701536655426025
#> Differential Expression Analysis - DONE
obj <-
addClusterization(
obj,
clName = "split",
clusters = vignette.split.clusters,
coexDF = vignette.split.coexDF,
override = FALSE
)
obj <-
addClusterization(
obj,
clName = "merge",
clusters = vignette.merge.clusters,
coexDF = vignette.merge.coexDF,
override = FALSE
)
# these will be recovered from the COTAN obj as needed
rm(vignette.split.clusters, vignette.split.coexDF)
rm(vignette.merge.clusters, vignette.merge.coexDF)It is possible to improve on the labels in a clusterization, so that they look nicer in plots and lists, for example to ensure all labels have the same length.
Another improvement is to reorder the labels of the clusterization so that near clusters have near labels
# COTAn always stores clusterizations as factors.
splitClusters <- getClusters(obj, clName = "split")
# Use the utility factorToVector() to properly decay any named factor
# to a named char array
# splitClusters <- factorToVector(splitClusters)
# In this case the following calls are no-op since the clusterization
# were created by COTAN and so they already made nice and reordered
splitClusters <- niceFactorLevels(splitClusters)
c(splitClusters, splitCoexDF, perm) %<-%
reorderClusterization(
obj,
clName = "split",
reverse = FALSE,
coexDF = NULL,
useDEA = TRUE, # T: Cosine dist. on DEA, F: Eucl. dist. on avg. zero/one
distance = NULL,
hclustMethod = "ward.D2"
)
#> Applied reordering to clusterization is:
#> 01 -> 01, 02 -> 02, 03 -> 03, 04 -> 04, 05 -> 05, 06 -> 06, 07 -> 07, 08 -> 08, 09 -> 09, 10 -> 10, 11 -> 11, 12 -> 12, 13 -> 13, 14 -> 14, 15 -> 15, -1 -> -1
mergeClusters <- getClusters(obj, clName = "merge")
table(splitClusters, mergeClusters)
#> mergeClusters
#> splitClusters -1 2 3 4 5 6 7 8 9
#> -1 76 0 0 0 0 0 0 0 0
#> 01 0 140 0 0 0 0 0 0 0
#> 02 0 86 0 0 0 0 0 0 0
#> 03 0 0 57 0 0 0 0 0 0
#> 04 0 0 0 126 0 0 0 0 0
#> 05 0 0 159 0 0 0 0 0 0
#> 06 0 152 0 0 0 0 0 0 0
#> 07 0 0 0 0 64 0 0 0 0
#> 08 0 0 0 0 0 75 0 0 0
#> 09 0 0 0 0 0 61 0 0 0
#> 10 0 0 0 0 0 0 197 0 0
#> 11 0 0 50 0 0 0 0 0 0
#> 12 0 0 0 0 0 0 137 0 0
#> 13 0 0 0 0 0 0 134 0 0
#> 14 0 0 0 0 0 0 0 145 0
#> 15 0 0 0 0 0 0 0 0 124COTAN provides also easy facility to convert a
clusterization from a factor to a list of
array of cells and vice-versa. This can come useful if one
wants to iterate separately on the cells of each cluster.
# this has an inverse `fromClustersList()`
splitClustersAsList <- toClustersList(splitClusters)
assertthat::assert_that(length(splitClustersAsList) == nlevels(splitClusters))
#> [1] TRUE
splitClustersOrigin <-
rlang::set_names(
rlang::rep_along(x = NA_character_, along = splitClustersAsList),
names(splitClustersAsList)
)
origin <- getCondition(obj, "origin")
for (clName in names(splitClustersAsList)) {
cluster <- splitClustersAsList[[clName]]
# assign most common origin to the cluster
splitClustersOrigin[[clName]] <- names(which.max(table(origin[cluster])))
# print the average non-zero expression in the cluster
clRawData <- getRawData(obj)[, cluster, drop = FALSE]
clRawData <- clRawData[clRawData > 0.0]
cat(
paste("Cluster", clName, "of", splitClustersOrigin[[clName]],
"\torigin - average non-zero expression:", mean(clRawData)), "\n")
rm(clRawData)
}
#> Cluster -1 of Cortical origin - average non-zero expression: 1.71180287120671
#> Cluster 01 of Cortical origin - average non-zero expression: 1.52191105207488
#> Cluster 02 of Cortical origin - average non-zero expression: 1.48066495798062
#> Cluster 03 of Cortical origin - average non-zero expression: 1.66657934898977
#> Cluster 04 of Cortical origin - average non-zero expression: 1.82214332239237
#> Cluster 05 of Cortical origin - average non-zero expression: 1.67546880177161
#> Cluster 06 of Cortical origin - average non-zero expression: 1.48983230228427
#> Cluster 07 of Cortical origin - average non-zero expression: 1.61160678767489
#> Cluster 08 of Other origin - average non-zero expression: 1.70266450993636
#> Cluster 09 of Other origin - average non-zero expression: 1.56338877443345
#> Cluster 10 of Other origin - average non-zero expression: 1.45037851037851
#> Cluster 11 of Other origin - average non-zero expression: 1.59020765860164
#> Cluster 12 of Other origin - average non-zero expression: 1.4464105396024
#> Cluster 13 of Other origin - average non-zero expression: 1.50418066318555
#> Cluster 14 of Other origin - average non-zero expression: 1.46783633861169
#> Cluster 15 of Other origin - average non-zero expression: 1.52325240621915
# If one needs to reorder the cells by cluster,
# labels are ordered as in the clusterization
orderCellsByMergeCluster <- groupByClusters(mergeClusters)
plot(getNumExpressedGenes(obj)[orderCellsByMergeCluster],
ylab = "Num expressed genes", xlab = NA_character_)
mergeClustersAsList <- toClustersList(mergeClusters)
mergeClustersOrigin <-
vapply(
mergeClustersAsList,
\(cluster, origin) {
names(which.max(table(origin[cluster])))
},
FUN.VALUE = character(1L),
origin
)
names(mergeClustersOrigin) <- names(mergeClustersAsList)
# It is also possible to reorder a subset of cells using
orderCellsBySomeMergeClusters <-
groupByClustersList(
getCells(obj),
mergeClustersAsList[c(2, 4, 6)]
)
plot(getNumExpressedGenes(obj)[orderCellsBySomeMergeClusters],
ylab = "Library Size", xlab = NA_character_)It is possible to create a dendogram of the
clusterization that uses a cluster distance based on the
clusters’ COEX.
treePlot <-
clustersTreePlot(
obj,
kCuts = 2L,
clName = "split",
useDEA = TRUE # T: Cosine dist. on DEA, F: Eucl. dist. on avg. zero/one
)[["dend"]]
plot(treePlot)
# use origin to mark the clusters
dendextend::labels(treePlot) <- splitClustersOrigin[base::labels(treePlot)]
plot(treePlot)It is easy to see that the dendogram splits according to origin
except for cluster 07 that is deemed more similar to the
some of the non cortical cells.
It is possible to visualize how relevant are some marker genes for the clusters comprising a given clusterization
# these are some genes associated to each cortical layer
layersGenes <- list(
"L1" = c("Reln", "Lhx5"),
"L2/3" = c("Cux1", "Satb2"),
"L4" = c("Rorb", "Sox5"),
"L5/6" = c("Bcl11b", "Fezf2"),
"Prog" = c("Hes1", "Vim")
)
neuralTypeGenes <- list(
# Neural Progenitor Genes
"NPGs" = c("Nes", "Vim", "Sox2", "Sox1", "Notch1", "Hes1", "Hes5", "Pax6"),
# Pan Neural Genes
"PNGs" = c("Map2", "Tubb3", "Neurod1", "Nefm", "Nefl", "Dcx", "Tbr1"),
# House Keeping
"hk" = c("Calm1", "Cox6b1", "Ppia", "Rpl18", "Cox7c", "Erh", "H3f3a",
"Taf1", "Taf2", "Gapdh", "Actb", "Golph3", "Zfr", "Sub1",
"Tars", "Amacr")
)The following is one of the most comprehensive plot available in
COTAN to visually summarize whether any of the given genes
is strongly over/under expressed in the clusters. The heat-map shows the
COEX score and the characters signal the corresponding
adjusted p-value: *** for p < 0.001,
** for p < 0.01, * for p
< 0.05, . for p < 0.1.
On the left it also allows to see how much each cluster is characterized by the passed conditions.
c(splitHeatmap, splitScoreDF, splitPValueDF) %<-%
clustersMarkersHeatmapPlot(
obj,
groupMarkers = layersGenes,
clName = "split",
kCuts = 2L,
adjustmentMethod = "bonferroni",
condNameList = list("origin")
)
ComplexHeatmap::draw(splitHeatmap)
c(mergeHeatmap, mergeScoreDF, mergePValueDF) %<-%
clustersMarkersHeatmapPlot(
obj,
groupMarkers = neuralTypeGenes,
clName = "merge",
kCuts = 3L,
adjustmentMethod = "bonferroni",
condNameList = list("origin")
)
ComplexHeatmap::draw(mergeHeatmap)In the above graph, it is possible to see that the found clusters
align well to the expression of the layers’ genes. Again it is possible
to see that cluster 07 of the "split"
clusterization (05 of the "merge") is
likely composed by a progenitor cells instead of
mature layers cells, like it happens for cluster
08 and 09 (06 of the
"merge") even if they have supposedly different
origin.
The following function uses the calculated clusters’
COEX to select, for each cluster, the n genes that
are over-expressed and the n genes that are under-expressed
with respect to a neutral model assumption.
For each gene found, the function returns the COEX
value, the adjusted p-value and the log-fold-change. It also
flags whether the found gene is one of the markers provided by the
user.
mergeClusterMarkers <-
findClustersMarkers(
obj,
clName = "merge",
n = 5L,
markers = unlist(layersGenes),
adjustmentMethod = "bonferroni"
)
#> findClustersMarkers - START
#> Log Fold Change Analysis - START
#> *********
#> Total calculations elapsed time: 6.23998236656189
#> Log Fold Change Analysis - DONE
#> Total calculations elapsed time: 6.29602766036987
#> findClustersMarkers - DONE
foundMarkers <- list()
# All relevant genes with strong `p-values`
geneIsEnriched <- mergeClusterMarkers[, "adjPVal"] < 1e-10
for (clName in levels(mergeClusters)) {
geneIsInCluster <- mergeClusterMarkers[, "CL"] == clName
foundMarkers[[clName]] <-
mergeClusterMarkers[geneIsEnriched & geneIsInCluster, "Gene", drop = TRUE]
}
# number of genes per cluster
lengths(foundMarkers)
#> -1 2 3 4 5 6 7 8 9
#> 8 10 8 5 10 10 10 5 5merge clustersAgain, it is possible to visualize how relevant are the marker genes from above:
c(mergeHeatmap, ..) %<-%
clustersMarkersHeatmapPlot(
obj,
groupMarkers = foundMarkers,
clName = "merge",
condNameList = list("origin")
)
ComplexHeatmap::draw(mergeHeatmap)Sometimes it is useful to compare the genes’ expression of a pair of clusters.
In such cases the simplest thing to do is to drop all other
cells so to obtain a leaner COTAN object with just the 2
clusters and then use the procedures shown above to analyze the
resulting object.
# We will focus on the clusters `03` (likely part of layers 5/6) and
# `05` (likely part of layers 2/3) of the `split` clusterization
cellsToDrop <- getCells(obj)[!(splitClusters %in% c("03", "05"))]
obj2 <- dropGenesCells(obj, cells = cellsToDrop)
obj2 <- proceedToCoex(obj2, calcCoex = FALSE)
#> COTAN dataset analysis: START
#> Genes/cells selection done: dropped [1847] genes and [0] cells
#> Working on [10908] genes and [216] cells
#> Estimate `dispersion`: START
#> Total calculations elapsed time: 8.17634725570679
#> Estimate `dispersion`: DONE
#> Estimate `dispersion`: DONE
#> `dispersion` | min: -0.255047679698161 | max: 329.177575870151 | % negative: 39.035570223689
#> COTAN genes' COEX estimation not requested
#> COTAN dataset analysis: DONE
table(getClusters(obj2, clName = "split"))
#>
#> 03 05
#> 57 159# this does not give more information than the same full-plot above
c(splitHeatmap2, ., .) %<-%
clustersMarkersHeatmapPlot(
obj2,
groupMarkers = layersGenes,
clName = "split",
kCuts = 2L,
adjustmentMethod = "bonferroni"
)
ComplexHeatmap::draw(splitHeatmap2)In the case of only two clusters, over-expressed genes in a cluster correspond exactly to under-expressed genes in the other, so one can just look at the former
deaMarkers <- findClustersMarkers(
obj2,
n = 10L,
clName = "split",
adjustmentMethod = "bonferroni",
markers = layersGenes[c("L2/3", "L5/6")]
)
#> findClustersMarkers - START
#> Differential Expression Analysis - START
#> **
#> Total calculations elapsed time: 0.0966966152191162
#> Differential Expression Analysis - DONE
#> Log Fold Change Analysis - START
#> **
#> Total calculations elapsed time: 1.32348227500916
#> Log Fold Change Analysis - DONE
#> Total calculations elapsed time: 1.43708848953247
#> findClustersMarkers - DONE
# over-expressed genes follow the under-expressed ones
deaMarkers[11:20, ]
#> CL Gene DEA adjPVal IsMarker logFoldCh
#> 11 03 Tle4 0.6217894 6.913771e-16 0 1.8051700
#> 12 03 Sox5 0.5951961 2.378094e-14 0 1.0266744
#> 13 03 Fezf2 0.5728092 4.158114e-13 1 1.1712330
#> 14 03 Islr2 0.5653865 1.048763e-12 0 0.9064809
#> 15 03 Igfbp3 0.5410088 2.015290e-11 0 1.8890878
#> 16 03 Meg3 0.4998109 2.232866e-09 0 0.8873548
#> 17 03 Rprm 0.4844676 1.175645e-08 0 0.8973052
#> 18 03 Lmo3 0.4827412 1.412830e-08 0 0.9724055
#> 19 03 Xpr1 0.4753437 3.083012e-08 0 0.8104060
#> 20 03 Bcl11b 0.4735215 3.729749e-08 1 0.9109299
deaMarkers[31:40, ]
#> CL Gene DEA adjPVal IsMarker logFoldCh
#> 31 05 Ptn 0.6066787 5.258093e-15 0 0.8646113
#> 32 05 2610017I09Rik 0.5719150 4.651280e-13 0 0.8172259
#> 33 05 9130024F11Rik 0.5436780 1.467117e-11 0 0.7805391
#> 34 05 Satb2 0.4949984 3.779829e-09 1 0.8713031
#> 35 05 Eif1b 0.4836402 1.283989e-08 0 0.9989785
#> 36 05 Abracl 0.4442443 7.220736e-07 0 0.8424504
#> 37 05 Cux1 0.4055112 2.755236e-05 1 1.4075842
#> 38 05 Ttc28 0.3995315 4.699765e-05 0 0.7459037
#> 39 05 Hmgn1 0.3332606 1.056564e-02 0 0.5423027
#> 40 05 Macrod2 0.3172522 3.405310e-02 0 0.8404755We can see that in this case we recover the layers genes.
The next few lines are just to clean.
if (file.exists(file.path(outDir, paste0(cond, ".cotan.RDS")))) {
# delete file if it exists
file.remove(file.path(outDir, paste0(cond, ".cotan.RDS")))
}
if (file.exists(file.path(outDir, paste0(cond, "_times.csv")))) {
# delete file if it exists
file.remove(file.path(outDir, paste0(cond, "_times.csv")))
}
if (dir.exists(file.path(outDir, cond))) {
unlink(file.path(outDir, cond), recursive = TRUE)
}
# if (dir.exists(file.path(outDir, GEO))) {
# unlink(file.path(outDir, GEO), recursive = TRUE)
# }
# stop logging to file
setLoggingFile("")
#> Closing previous log file - Setting log file to be:
file.remove(file.path(outDir, "vignette_uniform_clustering.log"))
#> Warning in file.remove(file.path(outDir, "vignette_uniform_clustering.log")):
#> cannot remove file
#> '/tmp/RtmpdrRmYh/COTAN_vignette_data/vignette_uniform_clustering.log', reason
#> 'No such file or directory'
#> [1] FALSE
options(prevOptState)Sys.time()
#> [1] "2026-04-29 20:48:31 UTC"
sessionInfo()
#> R version 4.6.0 (2026-04-24)
#> Platform: x86_64-pc-linux-gnu
#> Running under: Ubuntu 24.04.4 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.26.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] zeallot_0.2.0 COTAN_2.12.0 BiocStyle_2.40.0
#>
#> loaded via a namespace (and not attached):
#> [1] RcppAnnoy_0.0.23 splines_4.6.0
#> [3] later_1.4.8 tibble_3.3.1
#> [5] polyclip_1.10-7 XML_3.99-0.23
#> [7] fastDummies_1.7.6 httr2_1.2.2
#> [9] lifecycle_1.0.5 doParallel_1.0.17
#> [11] globals_0.19.1 lattice_0.22-9
#> [13] MASS_7.3-65 ggdist_3.3.3
#> [15] dendextend_1.19.1 magrittr_2.0.5
#> [17] limma_3.68.0 plotly_4.12.0
#> [19] sass_0.4.10 rmarkdown_2.31
#> [21] jquerylib_0.1.4 yaml_2.3.12
#> [23] httpuv_1.6.17 otel_0.2.0
#> [25] Seurat_5.5.0 sctransform_0.4.3
#> [27] spam_2.11-3 sp_2.2-1
#> [29] spatstat.sparse_3.1-0 reticulate_1.46.0
#> [31] cowplot_1.2.0 pbapply_1.7-4
#> [33] buildtools_1.0.0 RColorBrewer_1.1-3
#> [35] abind_1.4-8 Rtsne_0.17
#> [37] GenomicRanges_1.64.0 purrr_1.2.2
#> [39] BiocGenerics_0.58.0 rappdirs_0.3.4
#> [41] circlize_0.4.18 IRanges_2.46.0
#> [43] S4Vectors_0.50.0 ggrepel_0.9.8
#> [45] irlba_2.3.7 listenv_0.10.1
#> [47] spatstat.utils_3.2-2 rentrez_1.2.4
#> [49] maketools_1.3.2 goftest_1.2-3
#> [51] RSpectra_0.16-2 spatstat.random_3.4-5
#> [53] fitdistrplus_1.2-6 parallelly_1.47.0
#> [55] codetools_0.2-20 DelayedArray_0.38.0
#> [57] xml2_1.5.2 tidyselect_1.2.1
#> [59] shape_1.4.6.1 farver_2.1.2
#> [61] viridis_0.6.5 ScaledMatrix_1.20.0
#> [63] matrixStats_1.5.0 stats4_4.6.0
#> [65] spatstat.explore_3.8-0 Seqinfo_1.2.0
#> [67] jsonlite_2.0.0 GetoptLong_1.1.1
#> [69] progressr_0.19.0 ggridges_0.5.7
#> [71] survival_3.8-6 iterators_1.0.14
#> [73] systemfonts_1.3.2 foreach_1.5.2
#> [75] tools_4.6.0 ragg_1.5.2
#> [77] ica_1.0-3 Rcpp_1.1.1-1.1
#> [79] glue_1.8.1 gridExtra_2.3
#> [81] SparseArray_1.12.0 xfun_0.57
#> [83] distributional_0.7.0 MatrixGenerics_1.24.0
#> [85] ggthemes_5.2.0 dplyr_1.2.1
#> [87] withr_3.0.2 BiocManager_1.30.27
#> [89] fastmap_1.2.0 digest_0.6.39
#> [91] rsvd_1.0.5 parallelDist_0.2.7
#> [93] R6_2.6.1 mime_0.13
#> [95] textshaping_1.0.5 colorspace_2.1-2
#> [97] Cairo_1.7-0 scattermore_1.2
#> [99] tensor_1.5.1 spatstat.data_3.1-9
#> [101] tidyr_1.3.2 generics_0.1.4
#> [103] data.table_1.18.2.1 httr_1.4.8
#> [105] htmlwidgets_1.6.4 S4Arrays_1.12.0
#> [107] uwot_0.2.4 pkgconfig_2.0.3
#> [109] gtable_0.3.6 ComplexHeatmap_2.28.0
#> [111] lmtest_0.9-40 S7_0.2.2
#> [113] SingleCellExperiment_1.34.0 XVector_0.52.0
#> [115] sys_3.4.3 htmltools_0.5.9
#> [117] dotCall64_1.2 zigg_0.0.2
#> [119] clue_0.3-68 SeuratObject_5.4.0
#> [121] scales_1.4.0 Biobase_2.72.0
#> [123] png_0.1-9 spatstat.univar_3.1-7
#> [125] knitr_1.51 tzdb_0.5.0
#> [127] reshape2_1.4.5 rjson_0.2.23
#> [129] curl_7.1.0 nlme_3.1-169
#> [131] proxy_0.4-29 cachem_1.1.0
#> [133] zoo_1.8-15 GlobalOptions_0.1.4
#> [135] stringr_1.6.0 KernSmooth_2.23-26
#> [137] parallel_4.6.0 miniUI_0.1.2
#> [139] GEOquery_2.80.0 pillar_1.11.1
#> [141] grid_4.6.0 vctrs_0.7.3
#> [143] RANN_2.6.2 promises_1.5.0
#> [145] BiocSingular_1.28.0 beachmat_2.28.0
#> [147] xtable_1.8-8 cluster_2.1.8.2
#> [149] evaluate_1.0.5 readr_2.2.0
#> [151] cli_3.6.6 compiler_4.6.0
#> [153] rlang_1.2.0 crayon_1.5.3
#> [155] future.apply_1.20.2 labeling_0.4.3
#> [157] plyr_1.8.9 stringi_1.8.7
#> [159] viridisLite_0.4.3 deldir_2.0-4
#> [161] BiocParallel_1.46.0 assertthat_0.2.1
#> [163] lazyeval_0.2.3 spatstat.geom_3.7-3
#> [165] Matrix_1.7-5 RcppHNSW_0.6.0
#> [167] hms_1.1.4 patchwork_1.3.2
#> [169] future_1.70.0 conflicted_1.2.0
#> [171] ggplot2_4.0.3 statmod_1.5.1
#> [173] shiny_1.13.0 SummarizedExperiment_1.42.0
#> [175] ROCR_1.0-12 Rfast_2.1.5.2
#> [177] memoise_2.0.1 igraph_2.3.0
#> [179] RcppParallel_5.1.11-2 bslib_0.10.0