Auditing donor influence in pseudobulk analysis

Introduction

scDonorAudit reports how a fixed two-condition pseudobulk analysis changes when one biological donor is omitted. It uses edgeR for the specified contrast and links the planned omissions to design viability, execution events, and observed gene-effect changes.

Installation

For the current source version, follow the installation instructions in the repository README.

After acceptance and a successful Bioconductor build, use the command below in the matching R/Bioconductor environment. The package will first be available in devel and later in the applicable release; see the Bioconductor installation guide.

if (!requireNamespace("BiocManager", quietly = TRUE)) install.packages("BiocManager")
BiocManager::install("scDonorAudit")
library(scDonorAudit)

Aggregate single-cell counts

This small example shows how cell counts become pseudobulk counts and how the sample registry records missing combinations. Two cells from s1 are summed into its T-cell column; registered sample s3 has no observed cells.

sce_counts <- matrix(c(1L, 4L, 2L, 5L, 3L, 6L, 4L, 7L), nrow = 2L,
    dimnames = list(c("g1", "g2"), paste0("cell", 1:4)))
sce_cells <- S4Vectors::DataFrame(
    sample_id = c("s1", "s1", "s2", "s2"),
    cell_type = c("T", "T", "T", "B"))
sce_registry <- data.frame(sample_id = c("s1", "s2", "s3"),
    donor_id = c("d1", "d2", "d3"),
    condition = c("ctrl", "stim", "ctrl"))
sce_input <- SingleCellExperiment::SingleCellExperiment(
    assays = list(counts = sce_counts), colData = sce_cells)
cell_pb <- scDonorAudit::preparePseudobulk(sce_input, "sample_id", "donor_id",
    "condition", "cell_type", sample_table = sce_registry)
SummarizedExperiment::assay(cell_pb, "counts")
#> 2 x 3 Matrix of class "dgeMatrix"
#>    pb_00001 pb_00002 pb_00003
#> g1        3        4        3
#> g2        9        7        6
knitr::kable(as.data.frame(SummarizedExperiment::colData(cell_pb))[
    , c("sample_id", "cell_type", "n_cells")])
sample_id cell_type n_cells
pb_00001 s1 T 2
pb_00002 s2 B 1
pb_00003 s2 T 1
knitr::kable(S4Vectors::metadata(cell_pb)$scdonoraudit$coverage)
sample_id donor_id condition cell_type observed n_cells
s1 d1 ctrl B FALSE NA
s1 d1 ctrl T TRUE 2
s2 d2 stim B TRUE 1
s2 d2 stim T TRUE 1
s3 d3 ctrl B FALSE NA
s3 d3 ctrl T FALSE NA

The three pseudobulk columns correspond to the observed sample/cell-type combinations. The coverage table has six rows: registered samples crossed with B and T. Missing combinations have observed = FALSE and n_cells = NA, with no extra count columns.

Construct an auditable example

The simulated counts below demonstrate registered coverage and paired-design checks. Each column of the SummarizedExperiment is one observed sample and cell type. Donor d4 lacks a B-cell pseudobulk under stim. The full sample registry makes that absence visible.

set.seed(1103)
sample_table <- data.frame(sample_id = paste0("s", seq_len(8)), donor_id = rep(paste0("d",
    seq_len(4)), each = 2), condition = rep(c("ctrl", "stim"), 4))
source_rows <- rbind(transform(sample_table, cell_type = "T"), transform(sample_table[seq_len(7),
    ], cell_type = "B"))
counts <- matrix(rnbinom(250 * nrow(source_rows), mu = 25, size = 5), nrow = 250,
    dimnames = list(paste0("g", seq_len(250)), paste0("column", seq_len(nrow(source_rows)))))
source_rows$n_cells <- 30L
se <- SummarizedExperiment::SummarizedExperiment(assays = list(counts = counts),
    colData = S4Vectors::DataFrame(source_rows))
pb <- scDonorAudit::preparePseudobulk(se, "sample_id", "donor_id", "condition",
    "cell_type", sample_table = sample_table, n_cells = "n_cells")
contrast <- c(numerator = "stim", denominator = "ctrl")

The named contrast fixes the sign: positive log2 fold-change means higher expression in stim than ctrl. The count assay must contain raw, non-negative integer counts. The package checks numeric validity; users must confirm that the assay contains unnormalized counts. Factor coding is fixed inside the design, so a user’s global options("contrasts") does not change the named effect’s direction or scale.

Supported assays are in-memory numeric base matrices or numeric Matrix objects. Logical/pattern matrices and DelayedArray or disk-backed assays are unsupported. Sparse cell counts remain sparse during aggregation; only the smaller cell-type pseudobulk matrices are converted for edgeR fitting. For inputs that cannot fit in memory, aggregate raw counts upstream and use the preaggregated SummarizedExperiment entry point, retaining the full sample registry and n_cells. Output memory also grows with genes and planned fits.

Review the design before fitting

For either pairing policy, the registry must contain one sample per condition for every donor. complete_pairs handles incomplete cell-type eligibility; the registry itself must remain paired.

The default paired policy is strict. An incomplete eligible pair blocks that cell type without affecting other cell types.

audit <- scDonorAudit::auditDesign(pb, "paired", contrast)
as.data.frame(audit[["spec"]])
#>   cell_type plan_status n_planned n_samples n_donors baseline_status
#> 1         B not_created        NA         0        0 INCOMPLETE_PAIR
#> 2         T     created         4         8        4              OK
subset(as.data.frame(audit[["sample_membership"]]), cell_type == "B" &
    reason_code != "OK")
#>   cell_type sample_id donor_id condition observed n_cells in_baseline
#> 1         B        s1       d1      ctrl     TRUE      30       FALSE
#> 2         B        s2       d1      stim     TRUE      30       FALSE
#> 3         B        s3       d2      ctrl     TRUE      30       FALSE
#> 4         B        s4       d2      stim     TRUE      30       FALSE
#> 5         B        s5       d3      ctrl     TRUE      30       FALSE
#> 6         B        s6       d3      stim     TRUE      30       FALSE
#> 7         B        s7       d4      ctrl     TRUE      30       FALSE
#> 8         B        s8       d4      stim    FALSE      NA       FALSE
#>         reason_code
#> 1 STRICT_PAIR_BLOCK
#> 2 STRICT_PAIR_BLOCK
#> 3 STRICT_PAIR_BLOCK
#> 4 STRICT_PAIR_BLOCK
#> 5 STRICT_PAIR_BLOCK
#> 6 STRICT_PAIR_BLOCK
#> 7 STRICT_PAIR_BLOCK
#> 8 MISSING_CELL_TYPE

plan_status = "not_created" means a deletion plan was never established for the blocked cell type. It does not mean that all donor deletions succeeded or that the result was stable.

Fit and inspect the ledger

result <- scDonorAudit::assessDonorInfluence(pb, "paired", contrast)
subset(as.data.frame(result[["fits"]]), select = c(cell_type, run_id, omitted_donor,
    execution_status, reason_code))
#>   cell_type   run_id omitted_donor execution_status     reason_code
#> 1         B baseline          <NA>          skipped INCOMPLETE_PAIR
#> 2         T baseline          <NA>        completed              OK
#> 3         T omit_001            d1        completed              OK
#> 4         T omit_002            d2        completed              OK
#> 5         T omit_003            d3        completed              OK
#> 6         T omit_004            d4        completed              OK
head(as.data.frame(scDonorAudit::summarizeInfluence(result, effect_threshold = 0.5))[,
    c("cell_type", "gene_id", "n_planned", "n_effect_valid", "max_abs_delta_observed")])
#>   cell_type gene_id n_planned n_effect_valid max_abs_delta_observed
#> 1         T      g1         4              4              0.2878988
#> 2         T     g10         4              4              0.6444123
#> 3         T    g100         4              4              0.4444523
#> 4         T    g101         4              4              0.4274512
#> 5         T    g102         4              4              0.5059251
#> 6         T    g103         4              4              0.9924754

The summary’s n_planned includes all eligible donor deletions, even failed or skipped refits. Read the maximum effect change alongside valid-effect coverage: it is the maximum among refits with comparable effects. Missing effects remain missing, rather than becoming zero changes or non-significant tests. Adjusted p values use a fixed baseline gene family within each cell type; they do not control error across cell types or deletion runs.

The analysis first fixes the eligible samples and complete-pair policy, then plans all whole-donor deletions. Genes retained by the full-data filter remain the comparison family in each refit. The named contrast and covariate meanings also stay fixed. Each successful deletion refit re-estimates library-size normalization and the edgeR model on its remaining samples.

Read each gene in this order: is its baseline effect valid; how many deletions were planned; how many effects are comparable; then how large were the observed changes? n_sign_reversal counts even tiny crossings of zero. With a positive effect_threshold, n_material_reversal requires both effects to cross that threshold in opposite directions, and baseline_near_zero flags a baseline for which a raw sign change is especially easy to overread. The minimum and maximum deletion effects are an observed range, not a confidence interval. An unavailable effect may reflect design failure, backend failure, or a gene-level status; it is not evidence of stability.

Effect changes describe sensitivity to donor omission. A large change can reflect donor heterogeneity, limited precision, or other data properties; it does not establish that a donor is invalid or should be excluded. The p values come from the specified edgeR model, not a test that a donor is aberrant.

Output dictionary

Component Meaning
spec Per-cell-type plan status and number of planned donor omissions.
sample_membership / coverage Registered sample-by-cell-type combinations and the actual baseline inclusion decisions.
fits / fit_samples Baseline and deletion ledger. design_reason_code preserves the original design diagnosis; execution_status, reason_code, and stage describe the later execution outcome. The separate fit_samples table aligns sample IDs with library sizes and normalization factors for each completed run.
gene_filter / results Fixed baseline gene family and gene-by-run effects, tests, validity flags, and gene status codes.
issues Design problems and execution warnings or errors; a run can have both a backend warning and an error.
config / provenance Requested design, contrast and thresholds; source preparation, package/R/edgeR versions, and the actual named contrast.

The complete field/type/status dictionary is installed with the package:

system.file("OUTPUT_SCHEMA.md", package = "scDonorAudit")
#> [1] "/tmp/RtmpFjCgU6/Rinstd3777b167b0/scDonorAudit/OUTPUT_SCHEMA.md"

It also documents the 16 typed summary columns, which remain available in zero-row summaries when all genes are filtered or all cell types are blocked.

Design rank deficiency can coexist with an estimable target contrast. A rank problem is still a reason to skip fitting and is not relabeled as target non-estimability. When a baseline cannot run, the planned deletion rows remain in fits; design_reason_code keeps their independent audit results even if their execution outcome becomes BASELINE_UNAVAILABLE.

To analyze the B-cell data from complete pairs, request the alternative policy explicitly. This changes the analysis cohort, so report the excluded pair when presenting the result.

reduced <- scDonorAudit::assessDonorInfluence(pb, "paired", contrast, pair_policy = "complete_pairs")
subset(as.data.frame(reduced[["spec"]]), select = c(cell_type, plan_status,
    n_planned, n_samples))
#>   cell_type plan_status n_planned n_samples
#> 1         B     created         3         6
#> 2         T     created         4         8

Inspect figures

The coverage plot shows included and excluded sample/cell-type combinations. Gene plots use the explicit, stable gene IDs supplied by the user.

scDonorAudit::plotInfluence(result, "coverage")

scDonorAudit::plotInfluence(result, "influence", cell_type = "T", gene_id = c("g1",
    "g2", "g3"))

scDonorAudit::plotInfluence(result, "effect", cell_type = "T", gene_id = "g1")

Before interpreting a gene, inspect result[["fits"]], result[["sample_membership"]], and the number of valid deletion runs. A failed fit or unestimable contrast limits the observed sensitivity analysis. Grey influence tiles mean the effect is unavailable; consult the fit ledger and gene status for the specific cause.

Report missing coverage, design failure and valid effects together

This independent-donor example has eight donors, a missing B pseudobulk for d8, and a batch category observed only in d1. The baseline models are estimable. Omitting d1 leaves the frozen batch column all zero, causing rank deficiency; the target contrast can still be estimable. The failed design is recorded as a skipped run. Other deletions provide valid effects.

set.seed(995201)
mixed_registry <- data.frame(sample_id = paste0("s", seq_len(8)), donor_id = paste0("d",
    seq_len(8)), condition = rep(c("ctrl", "stim"), each = 4), batch = c("special",
    rep("common", 7)))
mixed_rows <- rbind(transform(mixed_registry, cell_type = "T"), transform(mixed_registry[seq_len(7),
    ], cell_type = "B"))
mixed_rows$n_cells <- 30L
mixed_counts <- matrix(rnbinom(250 * nrow(mixed_rows), mu = 25, size = 5),
    nrow = 250, dimnames = list(paste0("g", seq_len(250)), NULL))
mixed_source <- SummarizedExperiment::SummarizedExperiment(assays = list(counts = mixed_counts),
    colData = S4Vectors::DataFrame(mixed_rows))
mixed_pb <- scDonorAudit::preparePseudobulk(mixed_source, "sample_id",
    "donor_id", "condition", "cell_type", sample_vars = "batch", sample_table = mixed_registry,
    n_cells = "n_cells")
mixed_result <- scDonorAudit::assessDonorInfluence(mixed_pb, "independent",
    contrast, covariates = "batch")
mixed_fits <- as.data.frame(mixed_result[["fits"]])
stopifnot(all(mixed_fits$execution_status[mixed_fits$omitted_donor %in%
    "d1"] == "skipped"), all(mixed_fits$reason_code[mixed_fits$omitted_donor %in%
    "d1"] == "DESIGN_RANK_DEFICIENT"))
knitr::kable(subset(as.data.frame(mixed_result[["sample_membership"]]),
    !in_baseline))
cell_type sample_id donor_id condition observed n_cells in_baseline reason_code
8 B s8 d8 stim FALSE NA FALSE MISSING_CELL_TYPE
knitr::kable(mixed_fits[, c("cell_type", "run_id", "omitted_donor", "execution_status",
    "design_reason_code", "target_estimable")])
cell_type run_id omitted_donor execution_status design_reason_code target_estimable
B baseline NA completed OK TRUE
B omit_001 d1 skipped DESIGN_RANK_DEFICIENT TRUE
B omit_002 d2 completed OK TRUE
B omit_003 d3 completed OK TRUE
B omit_004 d4 completed OK TRUE
B omit_005 d5 completed OK TRUE
B omit_006 d6 completed OK TRUE
B omit_007 d7 completed OK TRUE
T baseline NA completed OK TRUE
T omit_001 d1 skipped DESIGN_RANK_DEFICIENT TRUE
T omit_002 d2 completed OK TRUE
T omit_003 d3 completed OK TRUE
T omit_004 d4 completed OK TRUE
T omit_005 d5 completed OK TRUE
T omit_006 d6 completed OK TRUE
T omit_007 d7 completed OK TRUE
T omit_008 d8 completed OK TRUE
mixed_summary <- as.data.frame(scDonorAudit::summarizeInfluence(mixed_result))
knitr::kable(subset(mixed_summary, gene_id == "g1", select = c(cell_type,
    gene_id, n_planned, n_effect_valid, complete_effect_coverage, max_abs_delta_observed)),
    digits = 3)
cell_type gene_id n_planned n_effect_valid complete_effect_coverage max_abs_delta_observed
1 B g1 7 6 FALSE 0.224
251 T g1 8 7 FALSE 0.276
scDonorAudit::plotInfluence(mixed_result, "influence", cell_type = "T",
    gene_id = c("g1", "g2", "g3"))

For T, eight omissions are planned but only seven can supply effects; for B, seven are planned and six can supply effects. The missing d8/B combination was never eligible for that cell type’s deletion plan. Omitting d1 remains in each plan and appears as unavailable in the plot. The observed maximum covers the valid deletions; the unavailable deletion has no measured effect change. Backend failures use execution_status="failed", reason_code="BACKEND_ERROR" and an error event; the same planned-denominator and missing-effect rules apply.

Reproduce a real single-nucleus example

The package includes a compact pseudobulk derived from the public Crowell19 mouse cortex dataset (ExperimentHub EH3297). Raw counts from 1,740 Astrocyte nuclei were summed within each of eight mice, retaining all 11,076 genes. Four mice received LPS and four received Vehicle. The original single-nucleus object is available through ExperimentHub and is not bundled here. The included extdata/README-crowell19.md records the transformation, attribution, and source’s CC BY 4.0 license.

count_file <- system.file("extdata", "crowell19_astrocyte_counts.csv",
    package = "scDonorAudit")
sample_file <- system.file("extdata", "crowell19_astrocyte_samples.csv",
    package = "scDonorAudit")
real_counts <- as.matrix(utils::read.csv(count_file, row.names = 1, check.names = FALSE))
real_samples <- utils::read.csv(sample_file)
real_source <- SummarizedExperiment::SummarizedExperiment(assays = list(counts = real_counts),
    colData = S4Vectors::DataFrame(real_samples))
real_registry <- real_samples[, c("sample_id", "donor_id", "condition")]
real_pb <- scDonorAudit::preparePseudobulk(real_source, "sample_id", "donor_id",
    "condition", "cell_type", sample_table = real_registry, n_cells = "n_cells")
real_contrast <- c(numerator = "LPS", denominator = "Vehicle")
real_audit <- scDonorAudit::auditDesign(real_pb, "independent", real_contrast,
    min_cells = 20L)
as.data.frame(real_audit[["spec"]])
#>    cell_type plan_status n_planned n_samples n_donors baseline_status
#> 1 Astrocytes     created         8         8        8              OK
knitr::kable(as.data.frame(real_audit[["sample_membership"]])[, c("sample_id",
    "donor_id", "condition", "n_cells", "in_baseline")])
sample_id donor_id condition n_cells in_baseline
LC016 LC016 Vehicle 151 TRUE
LC017 LC017 LPS 105 TRUE
LC019 LC019 Vehicle 231 TRUE
LC020 LC020 LPS 299 TRUE
LC022 LC022 Vehicle 270 TRUE
LC023 LC023 LPS 209 TRUE
LC025 LC025 Vehicle 114 TRUE
LC026 LC026 LPS 361 TRUE
real_result <- scDonorAudit::assessDonorInfluence(real_pb, "independent",
    real_contrast, min_cells = 20L)
table(as.data.frame(real_result[["fits"]])$execution_status)
#> 
#> completed 
#>         9
knitr::kable(as.data.frame(real_result[["fits"]])[, c("run_id", "omitted_donor",
    "execution_status", "design_reason_code", "reason_code")])
run_id omitted_donor execution_status design_reason_code reason_code
baseline NA completed OK OK
omit_001 LC016 completed OK OK
omit_002 LC017 completed OK OK
omit_003 LC019 completed OK OK
omit_004 LC020 completed OK OK
omit_005 LC022 completed OK OK
omit_006 LC023 completed OK OK
omit_007 LC025 completed OK OK
omit_008 LC026 completed OK OK
head(as.data.frame(scDonorAudit::summarizeInfluence(real_result))[, c("gene_id",
    "n_planned", "n_effect_valid", "max_abs_delta_observed")])
#>              gene_id n_planned n_effect_valid max_abs_delta_observed
#> 1 ENSMUSG00000000001         8              8              0.2306918
#> 2 ENSMUSG00000000056         8              8              0.1135039
#> 3 ENSMUSG00000000078         8              8              0.1688491
#> 4 ENSMUSG00000000085         8              8              0.1130049
#> 5 ENSMUSG00000000088         8              8              0.2293102
#> 6 ENSMUSG00000000127         8              8              0.2121334

Eight mice offer limited replication, especially after one is omitted. Read the observed changes with this sample size and the specified model in mind.

Read the donor-deletion results

The fit ledger above has one full-data fit and eight donor-deletion fits. To illustrate what the summary adds, consider genes with valid effects in all eight deletions and an absolute full-data log2 fold-change of at least 1. Order these genes by their largest observed absolute change, breaking ties by gene ID, and show the genes at the 10th and 90th percentile ranks. This rule selects examples with different sensitivity for interpreting the audit output.

real_summary <- as.data.frame(scDonorAudit::summarizeInfluence(real_result))
candidates <- real_summary[real_summary$complete_effect_coverage & is.finite(real_summary$baseline_logFC) &
    abs(real_summary$baseline_logFC) >= 1 & is.finite(real_summary$max_abs_delta_observed),
    ]
candidates <- candidates[order(candidates$max_abs_delta_observed, candidates$gene_id),
    ]
if (nrow(candidates) < 2L) {
    stop("The Crowell19 example requires at least two genes to illustrate ",
        "different levels of donor-deletion sensitivity; the stated ",
        "selection rule returned ", nrow(candidates), ".")
}
chosen <- candidates[c(ceiling(0.1 * nrow(candidates)), ceiling(0.9 * nrow(candidates))),
    ]
chosen$illustration <- c("lower change", "higher change")
knitr::kable(chosen[, c("illustration", "gene_id", "baseline_logFC", "n_planned",
    "n_effect_valid", "max_abs_delta_observed")], digits = 3)
illustration gene_id baseline_logFC n_planned n_effect_valid max_abs_delta_observed
3832 lower change ENSMUSG00000035086 1.094 8 8 0.163
6228 higher change ENSMUSG00000085245 -2.102 8 8 0.713

For the higher-change example, inspect every deletion alongside the maximum. A missing or failed deletion would appear in the fit ledger and reduce n_effect_valid.

real_se <- real_result[["results"]][[chosen$cell_type[2L]]]
gene_row <- match(chosen$gene_id[2L], rownames(real_se))
refits <- as.data.frame(SummarizedExperiment::colData(real_se))
refits$logFC <- as.numeric(SummarizedExperiment::assay(real_se, "logFC")[gene_row,
    ])
refits$change_from_full <- refits$logFC - refits$logFC[1L]
largest <- which.max(abs(refits$change_from_full[-1L])) + 1L
knitr::kable(refits[, c("run_id", "omitted_donor", "logFC", "change_from_full")],
    digits = 3)
run_id omitted_donor logFC change_from_full
baseline baseline NA -2.102 0.000
omit_001 omit_001 LC016 -2.335 -0.233
omit_002 omit_002 LC017 -2.272 -0.169
omit_003 omit_003 LC019 -1.390 0.713
omit_004 omit_004 LC020 -1.918 0.185
omit_005 omit_005 LC022 -2.189 -0.086
omit_006 omit_006 LC023 -1.922 0.180
omit_007 omit_007 LC025 -2.321 -0.219
omit_008 omit_008 LC026 -2.334 -0.231

In this dataset, 504 genes meet the stated selection rule. For ENSMUSG00000085245, the full-data LPS-versus-Vehicle log2 fold-change is -2.102; omitting LC019 changes it by 0.713 to -1.390. Of the 8 planned deletion effects, 8 are valid here. By comparison, the lower-change example ENSMUSG00000035086 has a maximum absolute change of 0.163. These observations describe sensitivity to donor omission for the specified model, these eight mice, and the selected genes. The two genes illustrate the stated percentile ranks; conclusions about other genes or datasets require their own analysis.

Method and data references

  • Chen Y, Chen L, Lun ATL, Baldoni PL, Smyth GK (2025). edgeR v4: powerful differential analysis of sequencing data with expanded functionality and improved support for small counts and larger datasets. Nucleic Acids Research 53(2), gkaf018. doi:10.1093/nar/gkaf018.
  • Crowell HL et al. (2020). muscat detects subpopulation-specific state transitions from multi-sample multi-condition single-cell transcriptomics data. Nature Communications 11, 6077. doi:10.1038/s41467-020-19894-4. Dataset attribution and transformations are in extdata/README-crowell19.md.

Use citation("edgeR") for citations matching the installed backend version and citation("scDonorAudit") for the package citation. The help-example data are synthetic; their generation is documented in extdata/README-example.md.

Session information

sessionInfo()
#> R version 4.6.1 (2026-06-24)
#> Platform: x86_64-pc-linux-gnu
#> Running under: Ubuntu 26.04.1 LTS
#> 
#> Matrix products: default
#> BLAS:   /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3 
#> LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.32.so;  LAPACK version 3.12.0
#> 
#> locale:
#>  [1] LC_CTYPE=en_US.UTF-8       LC_NUMERIC=C              
#>  [3] LC_TIME=en_US.UTF-8        LC_COLLATE=en_US.UTF-8    
#>  [5] LC_MONETARY=en_US.UTF-8    LC_MESSAGES=en_US.UTF-8   
#>  [7] LC_PAPER=en_US.UTF-8       LC_NAME=C                 
#>  [9] LC_ADDRESS=C               LC_TELEPHONE=C            
#> [11] LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C       
#> 
#> time zone: Etc/UTC
#> tzcode source: system (glibc)
#> 
#> attached base packages:
#> [1] stats     graphics  grDevices utils     datasets  methods   base     
#> 
#> other attached packages:
#> [1] BiocStyle_2.41.0
#> 
#> loaded via a namespace (and not attached):
#>  [1] sass_0.4.10                 generics_0.1.4             
#>  [3] SparseArray_1.13.4          lattice_0.23-1             
#>  [5] digest_0.6.39               RColorBrewer_1.1-3         
#>  [7] evaluate_1.0.5              grid_4.6.1                 
#>  [9] fastmap_1.2.0               jsonlite_2.0.0             
#> [11] Matrix_1.7-6                limma_3.99.0               
#> [13] BiocManager_1.30.27         SingleCellExperiment_1.35.2
#> [15] scales_1.4.0                jquerylib_0.1.4            
#> [17] abind_1.4-8                 cli_3.6.6                  
#> [19] rlang_1.3.0                 XVector_0.53.0             
#> [21] Biobase_2.73.2              withr_3.0.3                
#> [23] cachem_1.1.0                DelayedArray_0.39.8        
#> [25] yaml_2.3.12                 otel_0.2.0                 
#> [27] S4Arrays_1.13.2             tools_4.6.1                
#> [29] ggplot2_4.0.3               locfit_1.5-9.12            
#> [31] SummarizedExperiment_1.43.0 BiocGenerics_0.59.12       
#> [33] vctrs_0.7.3                 buildtools_1.0.0           
#> [35] R6_2.6.1                    matrixStats_1.5.0          
#> [37] stats4_4.6.1                lifecycle_1.0.5            
#> [39] Seqinfo_1.3.2               S4Vectors_0.51.10          
#> [41] edgeR_4.99.6                IRanges_2.47.5             
#> [43] bslib_0.12.0                gtable_0.3.6               
#> [45] glue_1.8.1                  statmod_1.5.2              
#> [47] xfun_0.61                   GenomicRanges_1.65.4       
#> [49] sys_3.4.3                   MatrixGenerics_1.25.0      
#> [51] knitr_1.52                  farver_2.1.2               
#> [53] htmltools_0.5.9             labeling_0.4.3             
#> [55] rmarkdown_2.32              maketools_1.3.2            
#> [57] scDonorAudit_0.99.9         compiler_4.6.1             
#> [59] S7_0.2.2