Phenotype transfer of non-pre-gated cells with cyDefine
The purpose of pre-gating is to ensure the downstream analyses solely concern intact, living, singlet cells.
In this vignette, we demonstrate that cyDefine is capable of automating this process by labeling otherwise pre-gated cells as unassigned. Thus, cyDefine can alleviate another tedious process.
For the demonstration, we replicate a pre-gating strategy shown in this primer on biosurf.org using the same data. The first step is to download the sample 081216-Mike-HIMC ctrls-001_01_normalized.fcs from FlowRepository (ID: FR-FCM-ZYAJ).
Dependencies
library(MASS)
library(dplyr, warn.conflicts = FALSE)
library(ggplot2)
suppressPackageStartupMessages(library(patchwork))
library(grid)
suppressPackageStartupMessages(library(ComplexHeatmap))
suppressPackageStartupMessages(library(circlize))
library(cyCombine, warn.conflicts = FALSE)
library(cyDefine, warn.conflicts = FALSE)Prepare data
We load the sample using functions from cyCombine.
# Define markers
markers <- c("CD57", "CD19", "CD4", "CD8", "IgD", "CD11c", "CD16", "CD3", "CD38", "CD27", "CD14", "CXCR5", "CCR7", "CD45RA", "CD20", "CD127", "CD33", "CD28", "CD161", "TCRgd", "CD123", "CD56", "HLADR", "CD25")
pregating_channels <- c("Bead", "DNA1", "DNA2", "Dead", "Event_length")
# Compile fcs file into flowSet
fs <- compile_fcs("../data/ZYAJ", pattern = "001_01_normalized.fcs")
#> Reading 1 files to a flowSet..
#> uneven number of tokens: 525
#> The last keyword is dropped.
#> uneven number of tokens: 525
#> The last keyword is dropped.
# Create panel from flowSet parameters
panel <- tibble(channel = fs[[1]]@parameters@data$name,
antigen = fs[[1]]@parameters@data$desc) |>
mutate(antigen = case_when(is.na(antigen) ~ channel,
TRUE ~ antigen))
# Convert to data.frame
raw <- prepare_data(
flowset = fs,
panel = panel, panel_antigen = "antigen", panel_channel = "channel",
markers = c(markers, pregating_channels[1:4]), # Include pregating channels in the transformation
.keep = TRUE, # Keep all pregating channels
down_sample = FALSE,
derand = FALSE # FALSE for plotting
)
#> Converting flowset to data frame
#> Extracting expression data..
#> Your flowset is now converted into a dataframe.
#> Transforming data using asinh with a cofactor of 5..
#> Done!
raw
#> # A tibble: 250,000 × 51
#> id Time Eventlength CD57 Dead `120Sn` `127I` `131Xe` `138Ba` Bead
#> <int> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 1 9.97 14 0 1.25 0 227. 0 11.3 0
#> 2 2 10.5 24 0 0 0 0 0 1.71 7.26
#> 3 3 15.1 16 0.103 1.20 0 6.66 0.729 48.0 0
#> 4 4 35.3 16 1.91 0 3.73 1.18 0 62.4 0
#> 5 5 47.4 17 0 0 0 7.39 0 19.4 0
#> 6 6 58.5 17 0 0.0645 0 141. 0 17.0 0
#> 7 7 88.5 17 0 0 0 9.19 0 37.9 0
#> 8 8 90.8 14 0 0 0 2.26 1.67 31.6 0
#> 9 9 107. 17 0 0.332 0 0 0 20.3 0.143
#> 10 10 108. 15 3.48 0.641 0 0 0 21.7 0
#> # ℹ 249,990 more rows
#> # ℹ 41 more variables: CD19 <dbl>, CD4 <dbl>, CD8 <dbl>, IgD <dbl>,
#> # CD85j <dbl>, CD11c <dbl>, CD16 <dbl>, CD3 <dbl>, CD38 <dbl>, CD27 <dbl>,
#> # CD11b <dbl>, CD14 <dbl>, CCR6 <dbl>, CD94 <dbl>, CD86 <dbl>, CXCR5 <dbl>,
#> # CXCR3 <dbl>, CCR7 <dbl>, CD45RA <dbl>, CD20 <dbl>, CD127 <dbl>, CD33 <dbl>,
#> # CD28 <dbl>, CD24 <dbl>, ICOS <dbl>, CD161 <dbl>, TCRgd <dbl>, PD1 <dbl>,
#> # CD123 <dbl>, CD56 <dbl>, HLADR <dbl>, CD25 <dbl>, `190BCKG` <dbl>, …Pre-gating
In this pre-gating, we keep gated cells, but label them as either Beads, Debris, Dead, or Doublets for the analysis.
Gating for cells
Gating on the DNA1 and Bead markers, we can remove beads, and some bead/cell doublets and debris.
# Gating values
left <- 0; right <- 2; lower <- 5; upper <- 8.5
p <- ggplot2::ggplot(raw |> dplyr::sample_n(20000), aes(x = Bead, y = DNA1)) +
geom_point(color = 'grey', size = 0.05, alpha = 0.6) +
geom_density2d(h = max(bandwidth.nrd(raw$Bead), bandwidth.nrd(raw$DNA1))) +
theme_bw()
p + geom_rect(xmin = left, xmax = right, ymin = lower, ymax = upper, fill = NA, linetype = 1, color = 'red') +
annotate('text', x = right + 1, y = mean(c(lower, upper)),
label = paste0("Cells: ", round(mean(raw$Bead < right & raw$DNA1 > lower & raw$DNA1 < upper) * 100, 2), "%"),
color = 'red')
#> Warning: Computation failed in `stat_density2d()`.
#> Caused by error in `precompute_2d_bw()`:
#> ! `h` must be a vector of length 2, not length 1.Gating for intact cells
Gating on DNA1 and DNA2 helps identify intact cells.
# Gating values
left <- 5.75; right <- 7.2; lower <- 6.35; upper <- 7.8
p <- raw |>
filter(pregating == "Cells") |>
dplyr::sample_n(10000) |>
ggplot2::ggplot(aes(x = DNA1, y = DNA2)) +
geom_point(color = 'grey', size = 0.05, alpha = 0.6) +
coord_cartesian(xlim = c(5,8), ylim = c(5.5,8.5)) +
geom_density2d() +
theme_bw()
p + geom_rect(xmin = left, xmax = right, ymin = lower, ymax = upper, fill = NA, linetype = 1, color = 'red') +
annotate('text', x = right + 0.4, y = mean(c(lower, upper)),
label = paste0("Intact cells: ", round(mean(raw$DNA1[raw$pregating == "Cells"] > left &
raw$DNA1[raw$pregating == "Cells"] < right &
raw$DNA2[raw$pregating == "Cells"] > lower &
raw$DNA2[raw$pregating == "Cells"] < upper) * 100, 2), "%"),
color = 'red')Gating for singlets
Event length helps remove doublets.
# Gating values
left <- 12; lower <- 5.65; right <- 24; upper <- 7.35
intact_idx <- raw$pregating == "Cells"
p <- raw |>
filter(intact_idx) |>
dplyr::sample_n(10000) |>
ggplot2::ggplot(aes(x = Eventlength, y = DNA1)) +
geom_point(color = 'grey', size = 0.05, alpha = 0.6) +
coord_cartesian(ylim = c(0,8)) +
geom_density2d() +
theme_bw()
pct_singlets <- mean(raw$Eventlength[intact_idx] > left &
raw$Eventlength[intact_idx] < right) * 100
p + geom_rect(xmin = left, xmax = right, ymin = lower, ymax = upper, fill = NA, linetype = 1, color = 'red') +
annotate('text', x = mean(c(left, right)), y = lower - 0.5,
label = paste0("Intact singlets: ", round(pct_singlets, 2), "%"),
color = 'red')Gating for live cells
Finally, we identify deads cells using the viability stain.
# Gating values
left <- 0; lower <- 5.65; right <- 3.5; upper <- 7.35
singlets_idx <- raw$pregating == "Cells"
p <- raw |>
filter(singlets_idx) |>
dplyr::sample_n(20000) |>
ggplot2::ggplot(aes(x = Dead, y = DNA1)) +
geom_point(color = 'grey', size = 0.05, alpha = 0.6) +
coord_cartesian(ylim = c(0,8)) +
geom_density2d() +
theme_bw()
pct_live <- mean(raw$Dead[singlets_idx] < right) * 100
p + geom_rect(xmin = left, xmax = right, ymin = lower, ymax = upper, fill = NA, linetype = 1, color = 'red') +
annotate('text', x = mean(c(left, right)), y = lower - 0.5,
label = paste0("Live intact singlets: ", round(pct_live, 2), "%"),
color = 'red')
raw <- raw |>
mutate(pregating = case_when(
pregating != "Cells" ~ pregating,
Dead >= right ~ "Dead",
TRUE ~ "Cells"
))This pre-gating leaves us with the following number of kept and discarded cells:
Adapt PBMC reference
With the pre-gating in place, we select a reference for our analysis. Having a manually gated sample from the same experiment would be ideal. However, cyDefine enables us to use a universal reference. Therefore, we use the universal PBMC reference prebuilt in cyDefine.
In this step, we adapt the universal PBMC reference with our sample because the panels are not identical. In the adaptation, we define the random forest parameter mtry as half the available markers. mtry determines how many markers that are randomly selected at each branching point in the model.
# Load reference
reference <- get_reference(path = "../data/")
#> Retrieving reference..
# Map marker names to reference markers
raw_mapped <- map_marker_names(ref_markers = pbmc_markers,
using_pbmc = TRUE, query = raw,
query_markers = markers
)
#> Renaming CXCR5 to CD185
#> Renaming TCRgd to gdTCR
#> Renaming HLADR to HLA-DR
#> Warning in map_marker_names(ref_markers = pbmc_markers, using_pbmc = TRUE, : The following markers were not detected to be in the reference and were excluded from the data:
#> CCR7, CD33
#> Markers of the Universal PBMC reference can be found in 'pbmc_markers'. If needed, markers can be manually mapped to reference markers using the 'map_specfic_from' and 'map_specfic_to' arguments
# Redefine the marker list
non_markers <- c(non_markers, colnames(raw)[!colnames(raw) %in% markers])
markers <- get_markers(raw_mapped)
# Define RF paramters
mtry <- ceiling(length(markers)/2)
# Adapt reference
reference_adapted <- adapt_reference(
reference = reference,
markers = markers,
using_pbmc = TRUE,
num.threads = 4,
# min_f1 = 0.7,
mtry = mtry
)
#> # ------- Population merging - Round 1 ------- #
#> Running classification to identify similar populations
#> Merging groups of similar populations
#> Merging:
#> ASDC, pDC
#> CD4 Proliferating, CD4 TEM, CD4 TCM
#> CD8 Proliferating, CD8 TCM, CD8 TEM
#> cDC1, cDC2
#> NK Proliferating, NK
#> # ------- Population merging - Round 2 ------- #
#> Running classification to identify similar populations
#> Merging groups of similar populations
#> Merging:
#> B intermediate, B memory
#> # ------- Population merging - Round 3 ------- #
#> Running classification to identify similar populations
#>
#> Reference adapted!
# Define colors for adapted cell types
celltype_colors <- get_distinct_colors(unique(reference_adapted$celltype),
add_unassigned = TRUE)
cyDefine::plot_umap(
reference_adapted,
markers = markers,
col = "celltype",
title = "Universal PBMC Reference",
colors = celltype_colors,
down_sample = TRUE,
sample_n = 25000
)
#> Generating UMAPIn this case, five groups of cell populations could not be accurately separated using the smaller marker panel. Therefore, cyDefine merges those populations. You can plot a diagram of the merged populations:
Classify all live cells
Now, we classify all live cells, i.e., cells retained after the pre-gating. We leave identify_unassigned = TRUE to detect outliers, e.g., beads, debris, dead, and doublets.
classified_live <- cyDefine(
reference = reference_adapted,
query = raw_mapped |> filter(pregating == "Cells"),
markers = markers,
using_pbmc = TRUE,
adapt_reference = FALSE,
batch_correct = TRUE,
norm_method = "scale",
xdim = 6, ydim = 6,
identify_unassigned = FALSE,
# use.weights = TRUE,
# identify_type = "probability",
# probability_threshold = 0.5,
num.threads = 4,
mtry = mtry,
train_on_unassigned = FALSE,
# MAD_factor = 2.5,
seed = 332,
verbose = TRUE
)
#> Sample information is not provided for reference - assuming one sample.
#> Batch information is not provided for reference - assuming one batch.
#> Batch information is not provided for query - assuming one batch.
#> Batch correcting using a SOM grid of dimensions 6x6
#> Scaling expression data..
#> Creating SOM grid..
#> Batch correcting data..
#> Batch correction took 126.02 seconds
#> Training random forest model using 4 threads
#> Growing trees.. Progress: 41%. Estimated remaining time: 44 seconds.
#> Growing trees.. Progress: 90%. Estimated remaining time: 6 seconds.
#> Model training took 68.54 seconds
#> Predicting..
#> Classification took 95.64 seconds
# plot_umap(classified_live$query, markers = markers, col = "mode_prediction")Classify non-pre-gated cells
Then, we do the exact same, but without removing cells from the pre-gating. We use the exact same settings to make it as comparable as possible.
classified_raw <- cyDefine(
reference = reference_adapted,
query = raw_mapped,
markers = markers,
using_pbmc = TRUE,
adapt_reference = FALSE,
batch_correct = TRUE,
norm_method = "scale",
xdim = 6, ydim = 6,
identify_unassigned = FALSE,
num.threads = 4,
mtry = mtry,
train_on_unassigned = FALSE,
seed = 332,
)
#> Sample information is not provided for reference - assuming one sample.
#> Batch information is not provided for reference - assuming one batch.
#> Batch information is not provided for query - assuming one batch.
#> Batch correcting using a SOM grid of dimensions 6x6
#> Scaling expression data..
#> Creating SOM grid..
#> Batch correcting data..
#> Batch correction took 278.22 seconds
#> Training random forest model using 4 threads
#> Growing trees.. Progress: 44%. Estimated remaining time: 40 seconds.
#> Growing trees.. Progress: 86%. Estimated remaining time: 10 seconds.
#> Model training took 71.75 seconds
#> Predicting..
#> Classification took 105.98 seconds
# plot_umap(classified_raw$query, markers = markers)Identify unassigned
<– per-celltype mahalanobis distance threshold with a 2.5 MAD factor and using a –!> After the classification, we identify unassigned using a simple random forest probability threshold of 80% certainty. This is a rather tight gating, but that is reasonable when trying to capture debris, doublets, and dead cells.
classified_live$query <- identify_unassigned(
classified_live,
markers = markers,
identify_type = "probability",#c("maha", "probability"),
probability_threshold = 0.8,
# MAD_factor = 2.5
)
classified_raw$query <- identify_unassigned(
classified_raw,
markers = markers,
identify_type = "probability",#c("maha", "probability"),
probability_threshold = 0.8,
# MAD_factor = 2.5,
)UMAP of celltype predictions
The first visualization we use is a UMAP comparing the predicted celltypes. The UMAPs will look different because the filtered and unfiltered dataset are integrated separately with the reference, and the plots are generated using separate samplings.
# Define celltype colors
umap_live <- cyDefine::plot_umap(
classified_live$query,
markers = markers,
col = "predicted_celltype",
title = "Pre-gated cells",
colors = celltype_colors,
down_sample = TRUE,
sample_n = 25000
)
#> Generating UMAPumap_raw <- cyDefine::plot_umap(
classified_raw$query,
markers = markers,
col = "predicted_celltype",
title = "All cells",
colors = celltype_colors,
down_sample = TRUE,
sample_n = 25000
)
#> Generating UMAPUMAP of pre-gated cells
Here, we visually compare the pre-gating labels with the celltype predictions. We hope to see that most pre-gated cells are grouped in UMAP space, and those groups are correctly identified as “unassigned”.
That is exactly what we see, especially for beads and the clusters of doublets and dead cells. Debris and many dead cells appear harder to discern, at least from a UMAP.
pregating_colors <- get_distinct_colors(unique(c(names(celltype_colors), classified_raw$query$pregating)), add_unassigned = TRUE)
umap_pregating <- cyDefine::plot_umap(
reference = classified_raw$query,
query = classified_raw$query,
markers = markers,
build_umap_on = "reference",
ref_col = "predicted_celltype",
query_col = "pregating",
title = c("Prediction", "Pre-gating"),
colors = pregating_colors,
down_sample = TRUE,
sample_n = 25000)
#> Generating UMAP
#> Computing UMAP embedding only of reference cells. Be aware that this can hide potential novel populations in the query!
#> Projecting query cells onto reference UMAP embedding
umap_pregatingCelltype abundances
We also hope to see that the proportion of cell types remains nearly identical between the two analysis strategies. We remove unassigned cells, as the raw dataset contain many more of those, which would skew the proportions.
ab_live <- classified_live$query |>
filter(predicted_celltype != "unassigned") |>
pull(predicted_celltype) |>
plot_abundance(colors = celltype_colors, title = "Cell type abundances - pre-gated")
#> Visualizing abundance of predicted cell types
ab_raw <- classified_raw$query |>
filter(predicted_celltype != "unassigned") |>
# filter(pregating == "Cells") |>
pull(predicted_celltype) |>
plot_abundance(colors = celltype_colors, title = "Cell type abundances - non-pre-gated")
#> Visualizing abundance of predicted cell types
(ab_live / ab_raw) & ggplot2::theme(
axis.text.x = ggplot2::element_text(angle = 45, hjust = 1, vjust = 1))Heatmap of prediction differences
Lastly, we demonstrate the accuracy of the predictions with a heatmap comparing the predictions of live and raw, where the live pre-gated cells are appended with labels.
The bottom row in the heatmap shows which celltypes were identified as unassigned in the raw data, compared to how the manual pre-gating and prediction on live cells classifies those cells. It appears the dead cells are particularly difficult to identify, which makes sense, as the viability stain is not used.
query <- classified_live$query |>
rename(celltype = predicted_celltype) |>
dplyr::select(id, celltype)
query <- classified_raw$query |>
left_join(query, by = "id") |>
mutate(celltype = case_when(
is.na(celltype) ~ pregating,
TRUE ~ celltype
)) |>
filter(celltype != "Cells")
regular_levels <- setdiff(
unique(query$celltype),
c("Doublets", "Beads", "Dead", "Debris", "unassigned")
)
ordered_levels <- c("unassigned", "Doublets", "Beads", "Dead", "Debris", sort(regular_levels))
# Turn `celltype` into a factor with that order
query$celltype <- factor(query$celltype, levels = ordered_levels)
conf_mat <- table(query$celltype, query$predicted_celltype)
row_totals <- rowSums(conf_mat)
pct_mat <- sweep(conf_mat, 1, row_totals, FUN = "/") * 100
pct_mat <- round(pct_mat, 1)
plot_mat <- t(pct_mat)
display_mat <- matrix(
ifelse(plot_mat == 0, "",
paste0(round(plot_mat, 1), "%")),
nrow = nrow(plot_mat),
dimnames = dimnames(plot_mat)
)
col_fun <- colorRamp2(
c(0, 25, 50, 75, 100),
c("#f7fbff", "#9ecae1", "#4292c6", "#2171b5", "#08519c")
)
ht <- Heatmap(
plot_mat,
col = col_fun,
na_col = "#f7fbff",
cell_fun = function(j, i, x, y, width, height, fill) {
val <- plot_mat[i, j]
if (val != 0) {
grid.text(
paste0(round(val, 1), "%"),
x, y,
gp = gpar(fontsize = 10, col = "black")
)
}
},
cluster_rows = FALSE,
cluster_columns = FALSE,
row_names_side = "left",
column_title = "Predicted celltypes of live cells + pre-gated labels",
# column_title_side = "bottom",
column_title_gp = gpar(fontsize = 12),
row_title = "Predicted celltypes on non-pre-gated cells",
row_title_side = "right",
row_title_gp = gpar(fontsize = 12),
column_names_rot = 45,
column_names_gp = gpar(fontsize = 12),
row_names_gp = gpar(fontsize = 12),
# width = unit(18 * ncol(plot_mat), "pt"),
# height = unit(18 * nrow(plot_mat), "pt"),
rect_gp = gpar(col = "white", lwd = 1),
heatmap_legend_param = list(
title = "Percentage of celltype\n agreement",
at = c(25, 50, 75, 100),
labels = c("25", "50", "75", "100"),
color_bar = "continuous",
legend_direction = "vertical"
),
)
# png("../figs/Pregating_Heatmap.png",
# width = 2400*2, # Width in pixels
# height = 1600*2, # Height in pixels
# res = 300)
# pdf("../figs/Pregating_Heatmap.pdf", width = 24, height = 16)
draw(ht,
column_title = "Cell type prediction on non-pre-gated vs pre-gated data",
column_title_gp = gpar(fontsize = 18, fontface = "bold"),
padding = unit(c(5, 20, 5, 5), "mm"))Session info
sessionInfo()
#> R version 4.4.3 (2025-02-28)
#> Platform: aarch64-apple-darwin20
#> Running under: macOS 26.3.1
#>
#> Matrix products: default
#> BLAS: /Library/Frameworks/R.framework/Versions/4.4-arm64/Resources/lib/libRblas.0.dylib
#> LAPACK: /Library/Frameworks/R.framework/Versions/4.4-arm64/Resources/lib/libRlapack.dylib; LAPACK version 3.12.0
#>
#> locale:
#> [1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8
#>
#> time zone: Europe/Copenhagen
#> tzcode source: internal
#>
#> attached base packages:
#> [1] grid stats graphics grDevices utils datasets methods
#> [8] base
#>
#> other attached packages:
#> [1] cyDefine_0.0.0.9000 cyCombine_0.3.0 circlize_0.4.17
#> [4] ComplexHeatmap_2.22.0 patchwork_1.3.2 ggplot2_4.0.2
#> [7] dplyr_1.2.0 MASS_7.3-65
#>
#> loaded via a namespace (and not attached):
#> [1] DBI_1.3.0 kohonen_3.0.13 rlang_1.1.7
#> [4] magrittr_2.0.4 clue_0.3-67 GetoptLong_1.1.0
#> [7] RcppAnnoy_0.0.23 otel_0.2.0 matrixStats_1.5.0
#> [10] compiler_4.4.3 RSQLite_2.4.6 mgcv_1.9-4
#> [13] reshape2_1.4.5 png_0.1-8 pbmcapply_1.5.1
#> [16] vctrs_0.7.1 sva_3.54.0 stringr_1.6.0
#> [19] pkgconfig_2.0.3 shape_1.4.6.1 crayon_1.5.3
#> [22] fastmap_1.2.0 XVector_0.46.0 labeling_0.4.3
#> [25] utf8_1.2.6 rmarkdown_2.30 UCSC.utils_1.2.0
#> [28] bit_4.6.0 xfun_0.56 zlibbioc_1.52.0
#> [31] cachem_1.1.0 GenomeInfoDb_1.42.3 jsonlite_2.0.0
#> [34] blob_1.3.0 tweenr_2.0.3 BiocParallel_1.40.2
#> [37] parallel_4.4.3 cluster_2.1.8.2 R6_2.6.1
#> [40] bslib_0.10.0 stringi_1.8.7 RColorBrewer_1.1-3
#> [43] ranger_0.18.0 limma_3.62.2 genefilter_1.88.0
#> [46] jquerylib_0.1.4 Rcpp_1.1.1 iterators_1.0.14
#> [49] knitr_1.51 IRanges_2.40.1 flowCore_2.18.0
#> [52] Matrix_1.7-4 splines_4.4.3 igraph_2.2.2
#> [55] tidyselect_1.2.1 rstudioapi_0.18.0 yaml_2.3.12
#> [58] doParallel_1.0.17 codetools_0.2-20 curl_7.0.0
#> [61] plyr_1.8.9 lattice_0.22-9 tibble_3.3.1
#> [64] Biobase_2.66.0 withr_3.0.2 KEGGREST_1.46.0
#> [67] S7_0.2.1 zen4R_0.10.4 evaluate_1.0.5
#> [70] survival_3.8-6 polyclip_1.10-7 isoband_0.3.0
#> [73] xml2_1.5.2 Biostrings_2.74.1 pillar_1.11.1
#> [76] DiagrammeR_1.0.11 MatrixGenerics_1.18.1 foreach_1.5.2
#> [79] stats4_4.4.3 generics_0.1.4 S4Vectors_0.44.0
#> [82] scales_1.4.0 xtable_1.8-8 glue_1.8.0
#> [85] tools_4.4.3 RSpectra_0.16-2 annotate_1.84.0
#> [88] locfit_1.5-9.12 visNetwork_2.1.4 XML_3.99-0.22
#> [91] RProtoBufLib_2.18.0 AnnotationDbi_1.68.0 edgeR_4.4.2
#> [94] colorspace_2.1-2 nlme_3.1-168 GenomeInfoDbData_1.2.13
#> [97] ggforce_0.5.0 cli_3.6.5 cytolib_2.18.2
#> [100] keyring_1.4.1 uwot_0.2.4 gtable_0.3.6
#> [103] sass_0.4.10 digest_0.6.39 BiocGenerics_0.52.0
#> [106] ggrepel_0.9.6.9999 htmlwidgets_1.6.4 rjson_0.2.23
#> [109] farver_2.1.2 memoise_2.0.1 htmltools_0.5.9
#> [112] lifecycle_1.0.5 prettydoc_0.4.1 httr_1.4.8
#> [115] GlobalOptions_0.1.3 statmod_1.5.1 bit64_4.6.0-1