5. Cell-type Deconvolution#

Pipeline phase: Phase 3.5b

What you will learn:

  • Understand the deconvolution module API

  • Prepare a reference scRNA-seq AnnData

  • Extract reference profiles with extract_reference_profiles

  • Call deconvolution with a supported backend

  • Inspect obsm['cell_type_proportions'] and obs['{method}_argmax']

Backends supported: cell2location, tangram, destvi

Install: pip install sc-tools[deconvolution]

Note: This tutorial shows the API interface and expected data shapes. Running a full deconvolution requires a GPU-enabled machine with the optional [deconvolution] extras installed. The code cells below are illustrative; they will raise ImportError without those extras.

import numpy as np
import pandas as pd
import anndata as ad
import sc_tools.tl as tl

Prepare spatial AnnData (target)#

The spatial AnnData must have raw counts in adata.X and spatial coordinates in adata.obsm['spatial']. The library_id column in adata.obs is used for per-library batching to avoid OOM on large datasets.

np.random.seed(99)
n_spots, n_genes = 300, 200
gene_names = [f"GENE_{i}" for i in range(n_genes)]

row, col = np.divmod(np.arange(n_spots), 20)
spatial_coords = np.column_stack([col * 100.0, row * 100.0])

spatial_adata = ad.AnnData(
    X=np.random.poisson(5, size=(n_spots, n_genes)).astype(float),
    obs={"library_id": [f"lib_{i % 2}" for i in range(n_spots)]},
    obsm={"spatial": spatial_coords},
)
spatial_adata.var_names = gene_names
print("Spatial AnnData:", spatial_adata)

Prepare reference scRNA-seq AnnData#

The reference must have:

  • Raw counts in adata.X

  • Cell-type labels in adata.obs['celltype']

  • Overlapping gene names with the spatial AnnData

n_cells = 500
cell_types = ["Epithelial", "Macrophage", "T_cell", "Fibroblast", "Endothelial"]

ref_adata = ad.AnnData(
    X=np.random.poisson(3, size=(n_cells, n_genes)).astype(float),
    obs={"celltype": pd.Categorical([cell_types[i % len(cell_types)] for i in range(n_cells)])},
)
ref_adata.var_names = gene_names
print("Reference AnnData:", ref_adata)
print("Cell types:", ref_adata.obs["celltype"].value_counts().to_dict())

Extract reference profiles#

extract_reference_profiles computes per-cell-type mean expression profiles optimized for Cell2location (negative binomial regression model). It handles memory-efficient batch processing for large reference datasets.

# In a real run (with scvi-tools installed):
# inf_aver = tl.extract_reference_profiles(
#     ref_adata,
#     celltype_col="celltype",
#     batch_key=None,
#     use_gpu=False,   # set True on GPU machine
# )
# print("Reference profiles shape:", inf_aver.shape)

# Simulated output shape for illustration:
print("Expected inf_aver shape: (n_genes, n_celltypes) =", (n_genes, len(cell_types)))

Run deconvolution#

The deconvolution function accepts backend="cell2location", "tangram", or "destvi". It processes each library_id separately to avoid OOM on large spatial datasets.

# In a real run:
# tl.deconvolution(
#     spatial_adata,
#     reference=ref_adata,
#     celltype_col="celltype",
#     backend="cell2location",
#     library_key="library_id",
#     use_gpu=False,
#     max_epochs=100,
# )

# After deconvolution, the following are populated:
# - spatial_adata.obsm['cell_type_proportions']  -- shape (n_spots, n_celltypes)
# - spatial_adata.obs['cell2location_argmax']     -- dominant cell type per spot

print("Output: obsm['cell_type_proportions'] shape = (n_spots, n_celltypes)")
print("Output: obs['cell2location_argmax'] = dominant cell type per spot")

Inspect proportions (synthetic example)#

# Simulate deconvolution output for illustration
proportions = np.random.dirichlet(np.ones(len(cell_types)), size=n_spots)
spatial_adata.obsm["cell_type_proportions"] = pd.DataFrame(
    proportions, index=spatial_adata.obs_names, columns=cell_types
)
spatial_adata.obs["cell2location_argmax"] = pd.Categorical(
    spatial_adata.obsm["cell_type_proportions"].idxmax(axis=1)
)

print("Proportions (first 3 spots):")
print(spatial_adata.obsm["cell_type_proportions"].head(3).round(3).to_string())

print("\nDominant cell type distribution:")
print(spatial_adata.obs["cell2location_argmax"].value_counts())

Save and downstream use#

# Save method-specific checkpoint
spatial_adata.write_h5ad("results/adata.deconvolution.cell2location.h5ad")

# Spatial plots (Phase 5)
import sc_tools.pl as pl
pl.spatial.plot_spatial_continuous(
    spatial_adata,
    key="cell_type_proportions",
    column="Macrophage",
    library_id="lib_0",
)

For project-specific deconvolution scripts see:

  • projects/visium/ggo_visium/scripts/run_deconvolution.py

  • projects/visium_hd/robin/scripts/run_deconvolution.py