OmicVerse Bulk RNA-seq — Cell-Type Deconvolution
Goal
Take a bulk RNA-seq cohort (AnnData, samples × genes) plus a single-cell reference (AnnData, cells × genes, with cell-type labels) and infer cell-type fractions per bulk sample. One unified class wraps four established methods under a method=... switch:
- TAPE (Cao 2022) — deep-learning autoencoder; default; CPU-friendly.
- Scaden (Menden 2020) — deep neural network; trains on pseudo-bulk mixtures; benefits from GPU.
- BayesPrism (Chu 2022) — full Bayesian model; multi-core CPU; produces posterior fractions.
- OmicsTweezer — joint reference-correction deconvolution.
Output is a pd.DataFrame indexed by bulk sample with one column per cell type — directly stackable as a per-sample bar chart, or summarisable per phenotype group via ov.pl.plot_grouped_fractions.
This skill is the opposite direction of bulk-to-single-deconvolution (Bulk2Single, which generates synthetic single cells from bulk). They share the word "deconvolution" but solve inverse problems — don't confuse them.
Quick Workflow
- Load bulk expression + single-cell reference, both as
AnnData. The COVID-19 PBMC demo ships with both: bulk_ad = ov.datasets.decov_bulk_covid_bulk(), single_ad_ref = ov.datasets.decov_bulk_covid_single().
- Eyeball the reference: UMAP coloured by
cell.type.coarse / cell.type.fine and by phenotype (disease); per-group composition with ov.pl.cellproportion(adata=single_ad_ref, celltype_clusters='cell.type.coarse', groupby='disease').
- Construct:
deconv_obj = ov.bulk.Deconvolution(adata_bulk=bulk_ad, adata_single=single_ad_ref, max_single_cells=10000, celltype_key='cell.type.coarse', cellstate_key='cell.type.fine', gpu=0). The cellstate_key (sub-celltype) is needed by some backends (BayesPrism's hierarchical model) and ignored by others.
- Run BayesPrism (multi-core CPU, full Bayesian):
res = deconv_obj.deconvolution(method='bayesprism', n_cores=8, fast_mode=True) → pd.DataFrame (samples × cell types).
- Run Scaden (GPU-friendly DNN): re-instantiate with
gpu='mps' (Apple) / gpu=0 (CUDA index): res2 = deconv_obj.deconvolution(method='scaden', scaler='ss', scale=True, datatype='counts', pseudobulk_size=2000).
- Re-order columns to the canonical celltype order from the reference:
res = res[single_ad_ref.obs['cell.type.coarse'].cat.categories].
- Plot stacked bars: build a colour dict from the reference's
uns['cell.type.coarse_colors'], then res.plot(kind='bar', stacked=True, figsize=(12, 3), color=color_dict).
- (Optional) Group-aware summary with
ov.pl.plot_grouped_fractions(res, obs=bulk_ad.obs, group_key='disease', color_dict=color_dict) — averages fractions per phenotype group with normalisation.
- Compare two methods on the same cohort: build both
res (BayesPrism) and res2 (Scaden) DataFrames, plot side-by-side; the per-sample correlation reveals method-specific biases.
Interface Summary
ov.bulk.Deconvolution(
adata_bulk: AnnData,
adata_single: AnnData,
max_single_cells: int = 5000,
celltype_key: str = 'celltype',
cellstate_key: str | None = None,
gpu: int | str = 0,
)
# Constructor: subsamples adata_single if larger than max_single_cells,
# builds the signature matrix from celltype_key (and cellstate_key for
# hierarchical methods).
deconv_obj.deconvolution(
method: str = 'tape', # 'tape' | 'scaden' | 'bayesprism' | 'omicstweezer'
sep: str = '\t',
scaler: str = 'mms', # TAPE / Scaden input scaling
datatype: str = 'counts',
genelenfile: str | None = None,
mode: str = 'overall', # TAPE running mode
adaptive: bool = True, # TAPE adaptive feature selection
variance_threshold: float = 0.98,
save_model_name: str | None = None,
batch_size: int = 128, # NN-based methods
epochs: int = 128, # NN-based methods
seed: int = 1,
scale_size: int = 2,
scale: bool = True,
n_cores: int = 4, # BayesPrism only
fast_mode: bool = True, # BayesPrism approx updates
pseudobulk_size: int = 2000, # Scaden / TAPE training mixtures
**kwargs,
) -> pd.DataFrame # samples × cell types
Built-in datasets:
ov.datasets.decov_bulk_covid_bulk() → AnnData — Decov et al. 2020 COVID-19 PBMC bulk RNA-seq.
ov.datasets.decov_bulk_covid_single() → AnnData — paired single-cell reference with cell.type.coarse, cell.type.fine, disease annotations.
Plotting helpers:
ov.pl.cellproportion(adata, celltype_clusters, groupby, legend=True, ax=None) — stacked-bar of celltype proportions per group; reused for both reference QC (cells / disease) and downstream presentation of inferred fractions.
ov.pl.plot_grouped_fractions(res, obs, group_key, color_dict=None, agg='mean', normalize=True, figsize=(4, 4)) — phenotype-group-averaged stacked bars.
Boundary
Inside scope:
- All four
ov.bulk.Deconvolution backends (TAPE, Scaden, BayesPrism, OmicsTweezer).
- Reference subsampling via
max_single_cells.
- Hierarchical celltype + cellstate handling (BayesPrism).
- Stacked-bar visualisation per sample / per phenotype group.
- Method comparison on the same cohort.
Outside scope — separate skill:
- Bulk2Single (generating synthetic single cells from bulk; the opposite direction) — see
bulk-to-single-deconvolution.
- Spatial transcriptomics deconvolution (Tangram / cell2location / starfysh / FlashDeconv) — see
spatial-tutorials.
- Building the upstream single-cell reference (preprocessing, annotation) — see existing single-cell skills.
- TCGA bulk preprocessing — see
tcga-preprocessing.
- Differential expression on the bulk side — see
bulk-deg-analysis / bulk-deseq2-analysis.
Branch Selection
Method by compute environment + interpretation needs
| method |
Engine |
Compute |
Strengths |
Weaknesses |
tape |
DNN (AE) |
CPU OK |
Default; sensible on most cohorts; adaptive feature selection |
Less transparent than Bayesian methods |
scaden |
DNN |
GPU helps |
Robust on diverse cohorts; large benchmark history |
Slower on CPU; trains per-cohort |
bayesprism |
Bayesian |
Multi-core CPU |
Posterior fractions; hierarchical; handles low-quality references |
No GPU acceleration; slowest on large cohorts |
omicstweezer |
Joint |
CPU OK |
Reference-correction integrated |
Newer; less benchmark history |
Default rule of thumb: report at least two methods (e.g. BayesPrism + Scaden) and inspect agreement. Per-sample correlation between methods >0.8 across cell types is a healthy floor; <0.5 indicates the deconvolution is unstable for that sample (often due to a cell type that's poorly represented in the reference).
max_single_cells
- 5000 (default) — fast; appropriate for a 50k-cell reference (10 % subsample).
- 10000 (tutorial) — better resolution at moderate compute cost.
- Above 50000 — diminishing returns; BayesPrism in particular slows quadratically.
celltype_key vs cellstate_key
celltype_key: required (the coarse annotation; output column granularity).
cellstate_key: optional; when set, BayesPrism builds a hierarchical signature with sub-states inside each celltype. Improves accuracy when within-celltype heterogeneity is biologically meaningful.
- For TAPE / Scaden / OmicsTweezer,
cellstate_key is ignored; only celltype_key matters.
gpu
gpu=0 (CUDA device 0) — default on a CUDA box; needed for fast Scaden / TAPE.
gpu='mps' — Apple-Silicon GPU; works for Scaden in the tutorial.
gpu=-1 — force CPU; only practical for BayesPrism (which is CPU-native).
- BayesPrism ignores this — always CPU; tune
n_cores instead.
BayesPrism-specific knobs
n_cores=8 — parallel chains; scale to physical cores.
fast_mode=True — approximate updates; ~10x faster than full Gibbs with minor accuracy loss. Use fast_mode=False only for publication-grade inference when runtime allows.
Scaden / TAPE-specific knobs
scaler: 'ss' (StandardScaler) vs 'mms' (MinMaxScaler — TAPE default). For Scaden the tutorial uses 'ss'; for TAPE keep 'mms'.
pseudobulk_size=2000 — number of synthetic mixtures generated to train the DNN. 2000 is fine for 5–15 cell types; raise to 5000+ for 30+ cell types.
batch_size=128, epochs=128 — defaults work on cohorts up to a few hundred bulk samples; raise epochs for larger cohorts.
datatype='counts' (Scaden expects raw counts) vs 'tpm' (TAPE accepts either).
Re-ordering output columns is a recurring tutorial pattern:
res = res[single_ad_ref.obs[celltype_key].cat.categories]
Cell types in the result come back in arbitrary order; reordering matches the reference's canonical category order so the colour dict and stacked-bar legend stay consistent.
Input Contract
adata_bulk: AnnData with samples in obs, genes in var. Counts or normalised expression both work; the wrapper handles scaling per-method.
adata_single: AnnData with obs[celltype_key] populated and (optionally) obs[cellstate_key]. Gene names in var_names must overlap with adata_bulk.var_names — the wrapper uses the intersection.
- For
BayesPrism: install with pip install BayesPrism (or via the bundled extras).
- For
Scaden / TAPE: bundled in OmicVerse via omicverse.external.
cell.type.coarse_colors (or whatever the celltype palette key is) in single_ad_ref.uns — used to build the stacked-bar colour dict. Run sc.pl.umap(adata, color=celltype_key) once with scanpy if the colours aren't already populated; that writes the _colors array.
Minimal Execution Patterns
import omicverse as ov
ov.plot_set()
# 1) Built-in COVID-19 PBMC demo (replace with your own AnnData)
bulk_ad = ov.datasets.decov_bulk_covid_bulk()
single_ad_ref = ov.datasets.decov_bulk_covid_single()
# 2) Reference QC visuals
ov.pl.embedding(single_ad_ref, basis='X_umap',
color=['cell.type.fine', 'cell.type.coarse'], ncols=1)
ov.pl.embedding(single_ad_ref, basis='X_umap', color='disease')
fig, ax = ov.plt.subplots(figsize=(1.5, 3))
ov.pl.cellproportion(
adata=single_ad_ref,
celltype_clusters='cell.type.coarse',
groupby='disease', legend=True, ax=ax,
)
# 3) BayesPrism — multi-core CPU, full Bayesian
deconv_obj = ov.bulk.Deconvolution(
adata_bulk=bulk_ad,
adata_single=single_ad_ref,
max_single_cells=10000,
celltype_key='cell.type.coarse',
cellstate_key='cell.type.fine',
)
res = deconv_obj.deconvolution(
method='bayesprism',
n_cores=8, fast_mode=True,
)
res = res[single_ad_ref.obs['cell.type.coarse'].cat.categories] # canonical order
print(res.head()) # samples × cell types
# Stacked-bar visualisation
color_dict = dict(zip(
single_ad_ref.obs['cell.type.coarse'].cat.categories,
single_ad_ref.uns['cell.type.coarse_colors'],
))
ax = res.plot(kind='bar', stacked=True, figsize=(12, 3), color=color_dict)
ax.set(xlabel='Sample', ylabel='Cell Fraction',
title='BayesPrism predicted fractions')
ov.plt.legend(bbox_to_anchor=(1.05, 1), ncol=1)
ov.plt.show()
# 4) Scaden — GPU-friendly DNN; re-instantiate with gpu='mps' / 0 / -1
deconv_obj = ov.bulk.Deconvolution(
adata_bulk=bulk_ad,
adata_single=single_ad_ref,
max_single_cells=10000,
celltype_key='cell.type.coarse',
cellstate_key='cell.type.fine',
gpu='mps', # or gpu=0 for CUDA 0
)
res2 = deconv_obj.deconvolution(
method='scaden',
scaler='ss', scale=True, datatype='counts',
pseudobulk_size=2000,
)
res2 = res2[single_ad_ref.obs['cell.type.coarse'].cat.categories]
ax = res2.plot(kind='bar', stacked=True, figsize=(12, 3), color=color_dict)
ax.set(xlabel='Sample', ylabel='Cell Fraction',
title='Scaden predicted fractions')
ov.plt.legend(bbox_to_anchor=(1.05, 1), ncol=1)
ov.plt.show()
# 5) Group-averaged comparison (e.g. healthy vs disease)
ov.pl.plot_grouped_fractions(
res,
obs=bulk_ad.obs,
group_key='disease',
color_dict=color_dict,
agg='mean', normalize=True,
figsize=(4, 4),
)
# 6) Cross-method agreement check
import numpy as np
common = res.columns.intersection(res2.columns)
per_sample_corr = np.array([
np.corrcoef(res.loc[s, common].values,
res2.loc[s, common].values)[0, 1]
for s in res.index
])
print(f'BayesPrism vs Scaden per-sample correlation: '
f'{per_sample_corr.mean():.3f} ± {per_sample_corr.std():.3f}')
print(f'Samples with corr < 0.5: {(per_sample_corr < 0.5).sum()} / {len(per_sample_corr)}')
Validation
- After construction:
deconv_obj.adata_single.n_obs <= max_single_cells. The wrapper subsamples; if it doesn't shrink, the reference was already small enough.
- After
.deconvolution(...): result rows sum to ~1 (approximate compositional constraint). Violations of more than a few % are a red flag — re-check celltype_key mapping.
- After re-ordering columns:
set(res.columns) == set(single_ad_ref.obs[celltype_key].cat.categories). If columns are missing, the reference had a category with zero cells.
- Method agreement: per-sample correlation between two methods should mostly be >0.8 across cell types. Per-cell-type bias (e.g. BayesPrism systematically reports more T cells than Scaden) is a known method-level artefact; report it explicitly rather than picking the more "favourable" method.
- Cohort-size check: with
<10 bulk samples, neural-network methods (TAPE, Scaden) may overfit; prefer BayesPrism for small cohorts.
- Cell-type representation: cell types with
<50 reference cells produce noisy fraction estimates — collapse them into a sibling celltype before the run, or set cellstate_key so BayesPrism can borrow strength.
- Don't compare fractions across runs with different
max_single_cells — the reference subsample changes and so does the signature matrix.
Resource Map
- See
reference.md for compact copy-paste snippets per method.
- See
references/source-grounding.md for verified ov.bulk.Deconvolution constructor + .deconvolution signatures and the four-method dispatch.
- For the opposite-direction Bulk2Single workflow (synthetic single-cell generation from bulk), see
bulk-to-single-deconvolution.
- For spatial transcriptomics deconvolution (Tangram / cell2location / starfysh / FlashDeconv), see
spatial-tutorials.
- For TCGA bulk preprocessing as an upstream input, see
tcga-preprocessing.
Examples
- "Run BayesPrism on
decov_bulk_covid_bulk() with the bundled scRNA-seq reference; report 8-core fast-mode fractions and a stacked-bar plot."
- "Re-run with Scaden on Apple Silicon (
gpu='mps') and report the per-sample correlation against BayesPrism."
- "Group-average fractions by
disease (healthy vs COVID) and plot with ov.pl.plot_grouped_fractions."
- "Decide which deconvolution method to pick for a 30-bulk-sample CPU-only cohort with 12 cell types in the reference."
References
- Tutorial notebook:
t_decov_bulk — COVID-19 PBMC deconvolution with BayesPrism + Scaden.
- BayesPrism: Chu et al. 2022, Nature Cancer — "Cell type and gene expression deconvolution with BayesPrism".
- Scaden: Menden et al. 2020, Science Advances — "Deep learning–based cell composition analysis from tissue expression profiles".
- TAPE: Cao et al. 2022, Nature Communications — "TAPE: an interpretable autoencoder for cell-type deconvolution".
- Live API verified — see
references/source-grounding.md.
1---2name: omicverse-bulk-celltype-deconvolution3description: Cell-type composition deconvolution of bulk RNA-seq using a single-cell reference. Wraps `ov.bulk.Deconvolution` with four interchangeable backends — TAPE, Scaden, BayesPrism, OmicsTweezer — under one constructor / one `.deconvolution(method=...)` call. Use when inferring cell-type fractions from bulk samples given a paired scRNA-seq atlas, when reproducing `t_decov_bulk`, or when comparing deep-learning vs. Bayesian deconvolution methods on the same cohort.4---56# OmicVerse Bulk RNA-seq — Cell-Type Deconvolution78## Goal910Take a bulk RNA-seq cohort (`AnnData`, samples × genes) plus a single-cell reference (`AnnData`, cells × genes, with cell-type labels) and infer **cell-type fractions per bulk sample**. One unified class wraps four established methods under a `method=...` switch:1112- **TAPE** (Cao 2022) — deep-learning autoencoder; default; CPU-friendly.13- **Scaden** (Menden 2020) — deep neural network; trains on pseudo-bulk mixtures; benefits from GPU.14- **BayesPrism** (Chu 2022) — full Bayesian model; multi-core CPU; produces posterior fractions.15- **OmicsTweezer** — joint reference-correction deconvolution.1617Output is a `pd.DataFrame` indexed by bulk sample with one column per cell type — directly stackable as a per-sample bar chart, or summarisable per phenotype group via `ov.pl.plot_grouped_fractions`.1819This skill is the **opposite direction** of `bulk-to-single-deconvolution` (Bulk2Single, which generates synthetic single cells from bulk). They share the word "deconvolution" but solve inverse problems — don't confuse them.2021## Quick Workflow22231. Load bulk expression + single-cell reference, both as `AnnData`. The COVID-19 PBMC demo ships with both: `bulk_ad = ov.datasets.decov_bulk_covid_bulk()`, `single_ad_ref = ov.datasets.decov_bulk_covid_single()`.242. Eyeball the reference: UMAP coloured by `cell.type.coarse` / `cell.type.fine` and by phenotype (`disease`); per-group composition with `ov.pl.cellproportion(adata=single_ad_ref, celltype_clusters='cell.type.coarse', groupby='disease')`.253. **Construct**: `deconv_obj = ov.bulk.Deconvolution(adata_bulk=bulk_ad, adata_single=single_ad_ref, max_single_cells=10000, celltype_key='cell.type.coarse', cellstate_key='cell.type.fine', gpu=0)`. The `cellstate_key` (sub-celltype) is needed by some backends (BayesPrism's hierarchical model) and ignored by others.264. **Run BayesPrism** (multi-core CPU, full Bayesian): `res = deconv_obj.deconvolution(method='bayesprism', n_cores=8, fast_mode=True)` → `pd.DataFrame` (samples × cell types).275. **Run Scaden** (GPU-friendly DNN): re-instantiate with `gpu='mps'` (Apple) / `gpu=0` (CUDA index): `res2 = deconv_obj.deconvolution(method='scaden', scaler='ss', scale=True, datatype='counts', pseudobulk_size=2000)`.286. **Re-order columns** to the canonical celltype order from the reference: `res = res[single_ad_ref.obs['cell.type.coarse'].cat.categories]`.297. **Plot stacked bars**: build a colour dict from the reference's `uns['cell.type.coarse_colors']`, then `res.plot(kind='bar', stacked=True, figsize=(12, 3), color=color_dict)`.308. (Optional) **Group-aware summary** with `ov.pl.plot_grouped_fractions(res, obs=bulk_ad.obs, group_key='disease', color_dict=color_dict)` — averages fractions per phenotype group with normalisation.319. Compare two methods on the same cohort: build both `res` (BayesPrism) and `res2` (Scaden) DataFrames, plot side-by-side; the per-sample correlation reveals method-specific biases.3233## Interface Summary3435```python36ov.bulk.Deconvolution(37 adata_bulk: AnnData,38 adata_single: AnnData,39 max_single_cells: int = 5000,40 celltype_key: str = 'celltype',41 cellstate_key: str | None = None,42 gpu: int | str = 0,43)44# Constructor: subsamples adata_single if larger than max_single_cells,45# builds the signature matrix from celltype_key (and cellstate_key for46# hierarchical methods).4748deconv_obj.deconvolution(49 method: str = 'tape', # 'tape' | 'scaden' | 'bayesprism' | 'omicstweezer'50 sep: str = '\t',51 scaler: str = 'mms', # TAPE / Scaden input scaling52 datatype: str = 'counts',53 genelenfile: str | None = None,54 mode: str = 'overall', # TAPE running mode55 adaptive: bool = True, # TAPE adaptive feature selection56 variance_threshold: float = 0.98,57 save_model_name: str | None = None,58 batch_size: int = 128, # NN-based methods59 epochs: int = 128, # NN-based methods60 seed: int = 1,61 scale_size: int = 2,62 scale: bool = True,63 n_cores: int = 4, # BayesPrism only64 fast_mode: bool = True, # BayesPrism approx updates65 pseudobulk_size: int = 2000, # Scaden / TAPE training mixtures66 **kwargs,67) -> pd.DataFrame # samples × cell types68```6970Built-in datasets:71- `ov.datasets.decov_bulk_covid_bulk() → AnnData` — Decov *et al.* 2020 COVID-19 PBMC bulk RNA-seq.72- `ov.datasets.decov_bulk_covid_single() → AnnData` — paired single-cell reference with `cell.type.coarse`, `cell.type.fine`, `disease` annotations.7374Plotting helpers:75- `ov.pl.cellproportion(adata, celltype_clusters, groupby, legend=True, ax=None)` — stacked-bar of celltype proportions per group; reused for both reference QC (cells / disease) and downstream presentation of inferred fractions.76- `ov.pl.plot_grouped_fractions(res, obs, group_key, color_dict=None, agg='mean', normalize=True, figsize=(4, 4))` — phenotype-group-averaged stacked bars.7778## Boundary7980**Inside scope:**81- All four `ov.bulk.Deconvolution` backends (TAPE, Scaden, BayesPrism, OmicsTweezer).82- Reference subsampling via `max_single_cells`.83- Hierarchical celltype + cellstate handling (BayesPrism).84- Stacked-bar visualisation per sample / per phenotype group.85- Method comparison on the same cohort.8687**Outside scope — separate skill:**88- Bulk2Single (generating synthetic single cells from bulk; the opposite direction) — see `bulk-to-single-deconvolution`.89- Spatial transcriptomics deconvolution (Tangram / cell2location / starfysh / FlashDeconv) — see `spatial-tutorials`.90- Building the upstream single-cell reference (preprocessing, annotation) — see existing single-cell skills.91- TCGA bulk preprocessing — see `tcga-preprocessing`.92- Differential expression on the bulk side — see `bulk-deg-analysis` / `bulk-deseq2-analysis`.9394## Branch Selection9596**Method by compute environment + interpretation needs**9798| method | Engine | Compute | Strengths | Weaknesses |99|---------------|-----------|---------------|---------------------------------------------|-----------------------------------------|100| `tape` | DNN (AE) | CPU OK | Default; sensible on most cohorts; adaptive feature selection | Less transparent than Bayesian methods |101| `scaden` | DNN | GPU helps | Robust on diverse cohorts; large benchmark history | Slower on CPU; trains per-cohort |102| `bayesprism` | Bayesian | Multi-core CPU | Posterior fractions; hierarchical; handles low-quality references | No GPU acceleration; slowest on large cohorts |103| `omicstweezer`| Joint | CPU OK | Reference-correction integrated | Newer; less benchmark history |104105**Default rule of thumb**: report at least **two** methods (e.g. BayesPrism + Scaden) and inspect agreement. Per-sample correlation between methods >0.8 across cell types is a healthy floor; <0.5 indicates the deconvolution is unstable for that sample (often due to a cell type that's poorly represented in the reference).106107**`max_single_cells`**108- 5000 (default) — fast; appropriate for a 50k-cell reference (10 % subsample).109- 10000 (tutorial) — better resolution at moderate compute cost.110- Above 50000 — diminishing returns; BayesPrism in particular slows quadratically.111112**`celltype_key` vs `cellstate_key`**113- `celltype_key`: required (the coarse annotation; output column granularity).114- `cellstate_key`: optional; when set, BayesPrism builds a hierarchical signature with sub-states inside each celltype. Improves accuracy when within-celltype heterogeneity is biologically meaningful.115- For TAPE / Scaden / OmicsTweezer, `cellstate_key` is ignored; only `celltype_key` matters.116117**`gpu`**118- `gpu=0` (CUDA device 0) — default on a CUDA box; needed for fast Scaden / TAPE.119- `gpu='mps'` — Apple-Silicon GPU; works for Scaden in the tutorial.120- `gpu=-1` — force CPU; only practical for BayesPrism (which is CPU-native).121- BayesPrism ignores this — always CPU; tune `n_cores` instead.122123**BayesPrism-specific knobs**124- `n_cores=8` — parallel chains; scale to physical cores.125- `fast_mode=True` — approximate updates; ~10x faster than full Gibbs with minor accuracy loss. Use `fast_mode=False` only for publication-grade inference when runtime allows.126127**Scaden / TAPE-specific knobs**128- `scaler`: `'ss'` (StandardScaler) vs `'mms'` (MinMaxScaler — TAPE default). For Scaden the tutorial uses `'ss'`; for TAPE keep `'mms'`.129- `pseudobulk_size=2000` — number of synthetic mixtures generated to train the DNN. 2000 is fine for 5–15 cell types; raise to 5000+ for 30+ cell types.130- `batch_size=128`, `epochs=128` — defaults work on cohorts up to a few hundred bulk samples; raise epochs for larger cohorts.131- `datatype='counts'` (Scaden expects raw counts) vs `'tpm'` (TAPE accepts either).132133**Re-ordering output columns** is a recurring tutorial pattern:134```python135res = res[single_ad_ref.obs[celltype_key].cat.categories]136```137Cell types in the result come back in arbitrary order; reordering matches the reference's canonical category order so the colour dict and stacked-bar legend stay consistent.138139## Input Contract140141- `adata_bulk`: `AnnData` with samples in `obs`, genes in `var`. Counts or normalised expression both work; the wrapper handles scaling per-method.142- `adata_single`: `AnnData` with `obs[celltype_key]` populated and (optionally) `obs[cellstate_key]`. Gene names in `var_names` must overlap with `adata_bulk.var_names` — the wrapper uses the intersection.143- For `BayesPrism`: install with `pip install BayesPrism` (or via the bundled extras).144- For `Scaden` / `TAPE`: bundled in OmicVerse via `omicverse.external`.145- `cell.type.coarse_colors` (or whatever the celltype palette key is) in `single_ad_ref.uns` — used to build the stacked-bar colour dict. Run `sc.pl.umap(adata, color=celltype_key)` once with scanpy if the colours aren't already populated; that writes the `_colors` array.146147## Minimal Execution Patterns148149```python150import omicverse as ov151ov.plot_set()152153# 1) Built-in COVID-19 PBMC demo (replace with your own AnnData)154bulk_ad = ov.datasets.decov_bulk_covid_bulk()155single_ad_ref = ov.datasets.decov_bulk_covid_single()156157# 2) Reference QC visuals158ov.pl.embedding(single_ad_ref, basis='X_umap',159 color=['cell.type.fine', 'cell.type.coarse'], ncols=1)160ov.pl.embedding(single_ad_ref, basis='X_umap', color='disease')161162fig, ax = ov.plt.subplots(figsize=(1.5, 3))163ov.pl.cellproportion(164 adata=single_ad_ref,165 celltype_clusters='cell.type.coarse',166 groupby='disease', legend=True, ax=ax,167)168```169170```python171# 3) BayesPrism — multi-core CPU, full Bayesian172deconv_obj = ov.bulk.Deconvolution(173 adata_bulk=bulk_ad,174 adata_single=single_ad_ref,175 max_single_cells=10000,176 celltype_key='cell.type.coarse',177 cellstate_key='cell.type.fine',178)179res = deconv_obj.deconvolution(180 method='bayesprism',181 n_cores=8, fast_mode=True,182)183res = res[single_ad_ref.obs['cell.type.coarse'].cat.categories] # canonical order184print(res.head()) # samples × cell types185186# Stacked-bar visualisation187color_dict = dict(zip(188 single_ad_ref.obs['cell.type.coarse'].cat.categories,189 single_ad_ref.uns['cell.type.coarse_colors'],190))191ax = res.plot(kind='bar', stacked=True, figsize=(12, 3), color=color_dict)192ax.set(xlabel='Sample', ylabel='Cell Fraction',193 title='BayesPrism predicted fractions')194ov.plt.legend(bbox_to_anchor=(1.05, 1), ncol=1)195ov.plt.show()196```197198```python199# 4) Scaden — GPU-friendly DNN; re-instantiate with gpu='mps' / 0 / -1200deconv_obj = ov.bulk.Deconvolution(201 adata_bulk=bulk_ad,202 adata_single=single_ad_ref,203 max_single_cells=10000,204 celltype_key='cell.type.coarse',205 cellstate_key='cell.type.fine',206 gpu='mps', # or gpu=0 for CUDA 0207)208res2 = deconv_obj.deconvolution(209 method='scaden',210 scaler='ss', scale=True, datatype='counts',211 pseudobulk_size=2000,212)213res2 = res2[single_ad_ref.obs['cell.type.coarse'].cat.categories]214215ax = res2.plot(kind='bar', stacked=True, figsize=(12, 3), color=color_dict)216ax.set(xlabel='Sample', ylabel='Cell Fraction',217 title='Scaden predicted fractions')218ov.plt.legend(bbox_to_anchor=(1.05, 1), ncol=1)219ov.plt.show()220```221222```python223# 5) Group-averaged comparison (e.g. healthy vs disease)224ov.pl.plot_grouped_fractions(225 res,226 obs=bulk_ad.obs,227 group_key='disease',228 color_dict=color_dict,229 agg='mean', normalize=True,230 figsize=(4, 4),231)232```233234```python235# 6) Cross-method agreement check236import numpy as np237common = res.columns.intersection(res2.columns)238per_sample_corr = np.array([239 np.corrcoef(res.loc[s, common].values,240 res2.loc[s, common].values)[0, 1]241 for s in res.index242])243print(f'BayesPrism vs Scaden per-sample correlation: '244 f'{per_sample_corr.mean():.3f} ± {per_sample_corr.std():.3f}')245print(f'Samples with corr < 0.5: {(per_sample_corr < 0.5).sum()} / {len(per_sample_corr)}')246```247248## Validation249250- After construction: `deconv_obj.adata_single.n_obs <= max_single_cells`. The wrapper subsamples; if it doesn't shrink, the reference was already small enough.251- After `.deconvolution(...)`: result rows sum to ~1 (approximate compositional constraint). Violations of more than a few % are a red flag — re-check `celltype_key` mapping.252- After re-ordering columns: `set(res.columns) == set(single_ad_ref.obs[celltype_key].cat.categories)`. If columns are missing, the reference had a category with zero cells.253- Method agreement: per-sample correlation between two methods should mostly be >0.8 across cell types. Per-cell-type bias (e.g. BayesPrism systematically reports more T cells than Scaden) is a known method-level artefact; report it explicitly rather than picking the more "favourable" method.254- Cohort-size check: with `<10` bulk samples, neural-network methods (TAPE, Scaden) may overfit; prefer BayesPrism for small cohorts.255- Cell-type representation: cell types with `<50` reference cells produce noisy fraction estimates — collapse them into a sibling celltype before the run, or set `cellstate_key` so BayesPrism can borrow strength.256- Don't compare fractions across runs with different `max_single_cells` — the reference subsample changes and so does the signature matrix.257258## Resource Map259260- See [`reference.md`](reference.md) for compact copy-paste snippets per method.261- See [`references/source-grounding.md`](references/source-grounding.md) for verified `ov.bulk.Deconvolution` constructor + `.deconvolution` signatures and the four-method dispatch.262- For the opposite-direction Bulk2Single workflow (synthetic single-cell generation from bulk), see `bulk-to-single-deconvolution`.263- For spatial transcriptomics deconvolution (Tangram / cell2location / starfysh / FlashDeconv), see `spatial-tutorials`.264- For TCGA bulk preprocessing as an upstream input, see `tcga-preprocessing`.265266## Examples267- "Run BayesPrism on `decov_bulk_covid_bulk()` with the bundled scRNA-seq reference; report 8-core fast-mode fractions and a stacked-bar plot."268- "Re-run with Scaden on Apple Silicon (`gpu='mps'`) and report the per-sample correlation against BayesPrism."269- "Group-average fractions by `disease` (healthy vs COVID) and plot with `ov.pl.plot_grouped_fractions`."270- "Decide which deconvolution method to pick for a 30-bulk-sample CPU-only cohort with 12 cell types in the reference."271272## References273- Tutorial notebook: [`t_decov_bulk`](https://omicverse.readthedocs.io/en/latest/Tutorials-bulk/t_decov_bulk/) — COVID-19 PBMC deconvolution with BayesPrism + Scaden.274- BayesPrism: Chu *et al.* 2022, *Nature Cancer* — "Cell type and gene expression deconvolution with BayesPrism".275- Scaden: Menden *et al.* 2020, *Science Advances* — "Deep learning–based cell composition analysis from tissue expression profiles".276- TAPE: Cao *et al.* 2022, *Nature Communications* — "TAPE: an interpretable autoencoder for cell-type deconvolution".277- Live API verified — see [`references/source-grounding.md`](references/source-grounding.md).