CellDEEP_vignette.RmdSingle-cell RNA sequencing (scRNA-seq) provides gene expression measurements at single-cell resolution, but its high sparsity and dropout rates can make differential expression (DE) analysis challenging.
CellDEEP reduces scRNA-seq sparsity by pooling cells into pseudocells before differential expression testing. It was developed to bridge the gap between robust but coarse pseudobulk approaches and sensitive but potentially biased cell-level methods.
CellDEEP provides two main workflows:
FindMarker.CellDEEP(). This is the recommended workflow
when DE testing is your final goal.CellDEEP.Kmean() or CellDEEP.Random(). This is
useful when you want to inspect or reuse the pooled object in downstream
analyses.These are alternative workflows. You do not need to
run the pooling functions before using
FindMarker.CellDEEP().
Load CellDEEP and the example sim dataset:
The example object contains the metadata columns used throughout this vignette:
Status: biological groupDonorID: sample or donorcluster_id: cell clusterThese column names are passed directly to CellDEEP, so your own Seurat object does not need to use these exact names.
The simplest way to use CellDEEP is with
FindMarker.CellDEEP().
This function handles metadata preparation, pseudocell pooling, and differential expression testing in a single workflow.
Important arguments include:
group_id: metadata column defining the biological
groups to comparesample_id: metadata column identifying samples or
donorscluster_id: metadata column identifying cell
clustersident.1 and ident.2: groups to
comparecell_selection: method used to construct pseudocells
("kmean" or "random")readcounts: method used to aggregate expression within
pseudocells ("sum" or "mean")n_cells: number of cells contributing to each
pseudocellmin_cells_per_subgroup: minimum number of cells
required within each sample-cluster subgroup before poolingtest.use: differential expression method passed to the
DE analysislogfc.threshold: minimum log-fold-change thresholdFor example, we can compare Case and Control cells using random pooling:
de_genes <- FindMarker.CellDEEP(
sim,
group_id = "Status",
sample_id = "DonorID",
cluster_id = "cluster_id",
Pool = TRUE,
test.use = "MAST",
n_cells = 3,
min_cells_per_subgroup = 1,
cell_selection = "random",
readcounts = "sum",
logfc.threshold = 0.25,
ident.1 = "Case",
ident.2 = "Control"
)
#> Start Pooling.....
#> Pooling...
#> Warning: Data is of class matrix. Coercing to dgCMatrix.
#> Pooling summary (random):
#> Input cells: 200
#> Cells kept in pooled pseudocells: 180
#> Cells not kept (approx): 20
#> Skipped empty groups: 0
#> Skipped empty clusters: 0
#> Skipped empty samples: 20
#> Skipped subgroups (<= min_cells_per_subgroup): 0
#> Dropped remainder cells (< n_cells) after random pooling: 20
#> FindMarker running.....
#> 1st ident is:
#> Case
#> 2nd ident is:
#> Control
#> group by:
#> group_id
#> Normalizing layer: counts
#> Finding variable features for layer counts
#> Centering and scaling data matrix
#>
#> Done!
#> Combining coefficients and standard errors
#> Calculating log-fold changes
#> Calculating likelihood ratio tests
#> Refitting on reduced model...
#>
#> Done!
#> 27
#> Gene1728Gene1992Gene1864Gene1715Gene1807Gene1626
head(de_genes)
#> [1] "Gene1728" "Gene1992" "Gene1864" "Gene1715" "Gene1807" "Gene1626"By default, CellDEEP returns the names of genes passing the differential expression filters.
FindMarker.CellDEEP() allows you to control how much
information is returned.
Depending on your downstream analysis, you can obtain:
By default, name.only = TRUE.
Therefore, the standard CellDEEP call returns the names of genes passing the DE filters:
de_genes <- FindMarker.CellDEEP(
sim,
group_id = "Status",
sample_id = "DonorID",
cluster_id = "cluster_id",
Pool = TRUE,
test.use = "MAST",
n_cells = 3,
min_cells_per_subgroup = 1,
cell_selection = "random",
readcounts = "sum",
logfc.threshold = 0.25,
ident.1 = "Case",
ident.2 = "Control"
)
#> Start Pooling.....
#> Pooling...
#> Warning: Data is of class matrix. Coercing to dgCMatrix.
#> Pooling summary (random):
#> Input cells: 200
#> Cells kept in pooled pseudocells: 180
#> Cells not kept (approx): 20
#> Skipped empty groups: 0
#> Skipped empty clusters: 0
#> Skipped empty samples: 20
#> Skipped subgroups (<= min_cells_per_subgroup): 0
#> Dropped remainder cells (< n_cells) after random pooling: 20
#> FindMarker running.....
#> 1st ident is:
#> Case
#> 2nd ident is:
#> Control
#> group by:
#> group_id
#> Normalizing layer: counts
#> Finding variable features for layer counts
#> Centering and scaling data matrix
#>
#> Done!
#> Combining coefficients and standard errors
#> Calculating log-fold changes
#> Calculating likelihood ratio tests
#> Refitting on reduced model...
#>
#> Done!
#> 27
#> Gene1728Gene1992Gene1864Gene1715Gene1807Gene1626
head(de_genes)
#> [1] "Gene1728" "Gene1992" "Gene1864" "Gene1715" "Gene1807" "Gene1626"This output is convenient when you only need a list of differentially expressed genes for downstream analyses or visualization.
If you want to retain the complete statistics for genes passing the DE filters, set:
name.only = FALSEFor example:
de_results <- FindMarker.CellDEEP(
sim,
group_id = "Status",
sample_id = "DonorID",
cluster_id = "cluster_id",
Pool = TRUE,
test.use = "MAST",
n_cells = 3,
min_cells_per_subgroup = 1,
cell_selection = "random",
readcounts = "sum",
logfc.threshold = 0.25,
ident.1 = "Case",
ident.2 = "Control",
name.only = FALSE
)
#> Start Pooling.....
#> Pooling...
#> Warning: Data is of class matrix. Coercing to dgCMatrix.
#> Pooling summary (random):
#> Input cells: 200
#> Cells kept in pooled pseudocells: 180
#> Cells not kept (approx): 20
#> Skipped empty groups: 0
#> Skipped empty clusters: 0
#> Skipped empty samples: 20
#> Skipped subgroups (<= min_cells_per_subgroup): 0
#> Dropped remainder cells (< n_cells) after random pooling: 20
#> FindMarker running.....
#> 1st ident is:
#> Case
#> 2nd ident is:
#> Control
#> group by:
#> group_id
#> Normalizing layer: counts
#> Finding variable features for layer counts
#> Centering and scaling data matrix
#>
#> Done!
#> Combining coefficients and standard errors
#> Calculating log-fold changes
#> Calculating likelihood ratio tests
#> Refitting on reduced model...
#>
#> Done!
head(de_results)
#> p_val avg_log2FC pct.1 pct.2 p_val_adj
#> Gene1728 2.135401e-18 3.493846 1.0 0.567 4.270801e-15
#> Gene1992 5.520147e-15 -11.401061 0.0 0.900 1.104029e-11
#> Gene1864 5.149952e-14 -10.188828 0.0 0.867 1.029990e-10
#> Gene1715 5.149952e-14 -9.855602 0.0 0.867 1.029990e-10
#> Gene1807 3.904984e-13 -9.667764 0.0 0.833 7.809967e-10
#> Gene1626 7.039584e-13 -3.879532 0.2 1.000 1.407917e-09This output is useful when you want to inspect quantities such as p-values, adjusted p-values, and log-fold changes.
It is also the appropriate format for creating DE plots or comparing CellDEEP results with another differential expression method.
In some analyses, filtering genes before downstream analysis is undesirable.
For example, methods such as Gene Set Enrichment Analysis (GSEA) generally require a ranked statistic across the full set of tested genes.
To retain the complete DE output without significance filtering, use:
name.only = FALSE
full_list = TRUEFor example:
de_full <- FindMarker.CellDEEP(
sim,
group_id = "Status",
sample_id = "DonorID",
cluster_id = "cluster_id",
Pool = TRUE,
test.use = "MAST",
n_cells = 3,
min_cells_per_subgroup = 1,
cell_selection = "random",
readcounts = "sum",
logfc.threshold = 0.25,
ident.1 = "Case",
ident.2 = "Control",
name.only = FALSE,
full_list = TRUE
)
#> Start Pooling.....
#> Pooling...
#> Warning: Data is of class matrix. Coercing to dgCMatrix.
#> Pooling summary (random):
#> Input cells: 200
#> Cells kept in pooled pseudocells: 180
#> Cells not kept (approx): 20
#> Skipped empty groups: 0
#> Skipped empty clusters: 0
#> Skipped empty samples: 20
#> Skipped subgroups (<= min_cells_per_subgroup): 0
#> Dropped remainder cells (< n_cells) after random pooling: 20
#> FindMarker running.....
#> 1st ident is:
#> Case
#> 2nd ident is:
#> Control
#> group by:
#> group_id
#> Normalizing layer: counts
#> Finding variable features for layer counts
#> Centering and scaling data matrix
#>
#> Done!
#> Combining coefficients and standard errors
#> Calculating log-fold changes
#> Calculating likelihood ratio tests
#> Refitting on reduced model...
#>
#> Done!
head(de_full)
#> p_val avg_log2FC pct.1 pct.2 p_val_adj
#> Gene1728 2.135401e-18 3.493846 1.0 0.567 4.270801e-15
#> Gene1992 5.520147e-15 -11.401061 0.0 0.900 1.104029e-11
#> Gene1864 5.149952e-14 -10.188828 0.0 0.867 1.029990e-10
#> Gene1715 5.149952e-14 -9.855602 0.0 0.867 1.029990e-10
#> Gene1807 3.904984e-13 -9.667764 0.0 0.833 7.809967e-10
#> Gene1626 7.039584e-13 -3.879532 0.2 1.000 1.407917e-09We can compare the number of genes returned by the filtered and full outputs:
de_results contains genes retained after CellDEEP’s DE
filtering, whereas de_full contains the complete set of
tested genes.
| Setting | Output | Typical use |
|---|---|---|
name.only = TRUE |
Significant gene names only | Gene lists and quick downstream analyses |
name.only = FALSE |
Filtered DE table with statistics | Inspecting p-values, logFC, and DE results |
name.only = FALSE, full_list = TRUE |
Full DE table without significance filtering | GSEA, gene ranking, and method comparisons |
FindMarker.CellDEEP() is the simplest option when
differential expression is your final goal.
However, CellDEEP can also be used to generate pseudocells without immediately performing differential expression testing.
This workflow is useful when you want to:
This is an alternative to the workflow above; it is not a
required preprocessing step for
FindMarker.CellDEEP().
The standalone pooling functions use standardized metadata fields:
sample_idgroup_idcluster_idUse prepare_data() to create these fields from the
corresponding columns in your Seurat object:
pool_input <- prepare_data(
sim,
sample_id = "DonorID",
group_id = "Status",
cluster_id = "cluster_id"
)The resulting object can then be passed to either of the CellDEEP pooling methods.
CellDEEP.Kmean() constructs pseudocells using a
k-means-based cell selection strategy.
pooled_kmean <- CellDEEP.Kmean(
pool_input,
readcounts = "sum",
n_cells = 3,
min_cells_per_subgroup = 1,
assay_name = "RNA"
)
#> Pooling...
#> Warning: Data is of class matrix. Coercing to dgCMatrix.
#> Drop out cell number during kmean pooling is:
#> 24
#> Pooling summary (kmean):
#> Input cells: 200
#> Cells kept in pooled pseudocells: 176
#> Cells not kept (approx): 24
#> Skipped empty groups: 0
#> Skipped empty clusters: 0
#> Skipped empty samples: 20
#> Skipped subgroups (<= min_cells_per_subgroup): 0
#> Dropped singleton cells after kmeans split: 24
pooled_kmean
#> An object of class Seurat
#> 2000 features across 56 samples within 1 assay
#> Active assay: RNA (2000 features, 0 variable features)
#> 1 layer present: countsHere:
n_cells = 3 specifies the number of cells contributing
to each pseudocell;readcounts = "sum" sums their counts; andmin_cells_per_subgroup = 1 controls the minimum number
of cells required within each sample-cluster subgroup before
pooling.Alternatively, CellDEEP.Random() constructs pseudocells
by randomly selecting cells within the corresponding subgroups.
pooled_random <- CellDEEP.Random(
pool_input,
readcounts = "sum",
n_cells = 5,
min_cells_per_subgroup = 1,
assay_name = "RNA"
)
#> Pooling...
#> Warning: Data is of class matrix. Coercing to dgCMatrix.
#> Pooling summary (random):
#> Input cells: 200
#> Cells kept in pooled pseudocells: 160
#> Cells not kept (approx): 40
#> Skipped empty groups: 0
#> Skipped empty clusters: 0
#> Skipped empty samples: 20
#> Skipped subgroups (<= min_cells_per_subgroup): 0
#> Dropped remainder cells (< n_cells) after random pooling: 40
pooled_random
#> An object of class Seurat
#> 2000 features across 32 samples within 1 assay
#> Active assay: RNA (2000 features, 0 variable features)
#> 1 layer present: countsThe resulting pooled objects can be inspected or used independently in downstream analyses.
If your goal is differential expression, start with:
This handles the required preparation and pooling internally.
If you only need the final DE gene names, use the default
name.only = TRUE.
If you need p-values, log-fold changes, and other DE statistics, use:
name.only = FALSEIf you need the complete gene ranking, for example for GSEA or comparisons with other DE methods, use:
name.only = FALSE
full_list = TRUEIf your goal is instead to construct and inspect pseudocells, use:
or:
This separation allows CellDEEP to be used either as a complete differential expression workflow or as a standalone pseudocell-generation method.