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.


raw <- raw |>
  mutate(pregating = case_when(
    Bead > right ~ "Beads",
    DNA1 > upper ~ "Doublets",
    DNA1 < lower ~ "Debris",
    TRUE         ~ "Cells"
  ))

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')


raw <- raw |>
  mutate(pregating = case_when(
    pregating != "Cells"          ~ pregating,
    DNA1 > right | DNA2 > upper   ~ "Doublets",
    DNA1 < left  | DNA2 < lower   ~ "Debris",
    TRUE                          ~ "Cells"
  ))

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')


raw <- raw |>
  mutate(pregating = case_when(
    pregating != "Cells"  ~ pregating,
    Eventlength >= right  ~ "Doublets",
    Eventlength <= left   ~ "Debris",
    TRUE                  ~ pregating
  ))

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:

table(raw$pregating)
#> 
#>    Beads    Cells     Dead   Debris Doublets 
#>    10023   213172     7305     5570    13930

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 UMAP

In 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:

plot_diagram(reference_adapted)

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 UMAP
umap_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 UMAP
(umap_live + umap_raw) + plot_layout(guides = "collect")

UMAP 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_pregating

Celltype 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"))


# dev.off()

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