COTAN 2.11.4
library(COTAN)
library(zeallot)
# necessary to solve precedence of overloads
conflicted::conflict_prefer("%<-%", "zeallot")
# enable multi-processing (not on Windows)
prevOptState <- options(parallelly.fork.enable = TRUE)
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/RtmpRABiog/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/RtmpRABiog/COTAN_vignette_data/vignette_DEA.log
Download 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 2000
Initialize 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.5
Assign 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.49046087265015
#> 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: DONE
TODO:
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: 2.27090072631836
#> Differential Expression Analysis - DONE
vignette.merge.coexDF <-
DEAOnClusters(obj, clusters = vignette.merge.clusters)
#> Differential Expression Analysis - START
#> *********
#> Total calculations elapsed time: 2.29036808013916
#> 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 124
COTAN 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: 8.1580331325531
#> Log Fold Change Analysis - DONE
#> Total calculations elapsed time: 8.24346661567688
#> 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 5
merge 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: 10.5938677787781
#> 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.112128973007202
#> Differential Expression Analysis - DONE
#> Log Fold Change Analysis - START
#> **
#> Total calculations elapsed time: 1.71864628791809
#> Log Fold Change Analysis - DONE
#> Total calculations elapsed time: 1.85508584976196
#> 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.8404755
We 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/RtmpRABiog/COTAN_vignette_data/vignette_uniform_clustering.log', reason
#> 'No such file or directory'
#> [1] FALSE
options(prevOptState)
Sys.time()
#> [1] "2026-03-29 17:03:08 EDT"
sessionInfo()
#> R Under development (unstable) (2026-03-05 r89546)
#> Platform: x86_64-pc-linux-gnu
#> Running under: Ubuntu 24.04.4 LTS
#>
#> Matrix products: default
#> BLAS: /home/biocbuild/bbs-3.23-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] stats graphics grDevices utils datasets methods base
#>
#> other attached packages:
#> [1] zeallot_0.2.0 COTAN_2.11.4 BiocStyle_2.39.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.5 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.4
#> [17] limma_3.67.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.4.0 sctransform_0.4.3
#> [27] spam_2.11-3 sp_2.2-1
#> [29] spatstat.sparse_3.1-0 reticulate_1.45.0
#> [31] cowplot_1.2.0 pbapply_1.7-4
#> [33] RColorBrewer_1.1-3 abind_1.4-8
#> [35] Rtsne_0.17 GenomicRanges_1.63.1
#> [37] purrr_1.2.1 BiocGenerics_0.57.0
#> [39] rappdirs_0.3.4 circlize_0.4.17
#> [41] IRanges_2.45.0 S4Vectors_0.49.0
#> [43] ggrepel_0.9.8 irlba_2.3.7
#> [45] listenv_0.10.1 spatstat.utils_3.2-2
#> [47] rentrez_1.2.4 goftest_1.2-3
#> [49] RSpectra_0.16-2 spatstat.random_3.4-5
#> [51] fitdistrplus_1.2-6 parallelly_1.46.1
#> [53] codetools_0.2-20 DelayedArray_0.37.0
#> [55] xml2_1.5.2 tidyselect_1.2.1
#> [57] shape_1.4.6.1 farver_2.1.2
#> [59] ScaledMatrix_1.19.0 viridis_0.6.5
#> [61] matrixStats_1.5.0 stats4_4.6.0
#> [63] spatstat.explore_3.8-0 Seqinfo_1.1.0
#> [65] jsonlite_2.0.0 GetoptLong_1.1.0
#> [67] progressr_0.18.0 ggridges_0.5.7
#> [69] survival_3.8-6 iterators_1.0.14
#> [71] systemfonts_1.3.2 foreach_1.5.2
#> [73] tools_4.6.0 ragg_1.5.2
#> [75] ica_1.0-3 Rcpp_1.1.1
#> [77] glue_1.8.0 gridExtra_2.3
#> [79] SparseArray_1.11.11 xfun_0.57
#> [81] distributional_0.7.0 MatrixGenerics_1.23.0
#> [83] ggthemes_5.2.0 dplyr_1.2.0
#> [85] withr_3.0.2 BiocManager_1.30.27
#> [87] fastmap_1.2.0 digest_0.6.39
#> [89] rsvd_1.0.5 parallelDist_0.2.7
#> [91] R6_2.6.1 mime_0.13
#> [93] textshaping_1.0.5 colorspace_2.1-2
#> [95] Cairo_1.7-0 scattermore_1.2
#> [97] tensor_1.5.1 dichromat_2.0-0.1
#> [99] spatstat.data_3.1-9 tidyr_1.3.2
#> [101] generics_0.1.4 data.table_1.18.2.1
#> [103] httr_1.4.8 htmlwidgets_1.6.4
#> [105] S4Arrays_1.11.1 uwot_0.2.4
#> [107] pkgconfig_2.0.3 gtable_0.3.6
#> [109] ComplexHeatmap_2.27.1 lmtest_0.9-40
#> [111] S7_0.2.1 SingleCellExperiment_1.33.2
#> [113] XVector_0.51.0 htmltools_0.5.9
#> [115] dotCall64_1.2 bookdown_0.46
#> [117] zigg_0.0.2 clue_0.3-68
#> [119] SeuratObject_5.3.0 scales_1.4.0
#> [121] Biobase_2.71.0 png_0.1-9
#> [123] spatstat.univar_3.1-7 knitr_1.51
#> [125] tzdb_0.5.0 reshape2_1.4.5
#> [127] rjson_0.2.23 curl_7.0.0
#> [129] nlme_3.1-169 proxy_0.4-29
#> [131] cachem_1.1.0 zoo_1.8-15
#> [133] GlobalOptions_0.1.3 stringr_1.6.0
#> [135] KernSmooth_2.23-26 parallel_4.6.0
#> [137] miniUI_0.1.2 GEOquery_2.79.0
#> [139] pillar_1.11.1 grid_4.6.0
#> [141] vctrs_0.7.1 RANN_2.6.2
#> [143] promises_1.5.0 BiocSingular_1.27.1
#> [145] beachmat_2.27.3 xtable_1.8-8
#> [147] cluster_2.1.8.2 evaluate_1.0.5
#> [149] magick_2.9.1 tinytex_0.59
#> [151] readr_2.2.0 cli_3.6.5
#> [153] compiler_4.6.0 rlang_1.1.7
#> [155] crayon_1.5.3 future.apply_1.20.2
#> [157] labeling_0.4.3 plyr_1.8.9
#> [159] stringi_1.8.7 viridisLite_0.4.3
#> [161] deldir_2.0-4 BiocParallel_1.45.0
#> [163] assertthat_0.2.1 lazyeval_0.2.2
#> [165] spatstat.geom_3.7-3 Matrix_1.7-5
#> [167] RcppHNSW_0.6.0 hms_1.1.4
#> [169] patchwork_1.3.2 future_1.70.0
#> [171] conflicted_1.2.0 ggplot2_4.0.2
#> [173] statmod_1.5.1 shiny_1.13.0
#> [175] SummarizedExperiment_1.41.1 ROCR_1.0-12
#> [177] Rfast_2.1.5.2 memoise_2.0.1
#> [179] igraph_2.2.2 RcppParallel_5.1.11-2
#> [181] bslib_0.10.0