3. Preprocessing#
Pipeline phase: Phase 3
What you will learn:
Back up raw counts before normalization
Normalize and log-transform
Select highly variable genes
Run PCA, neighbors, UMAP, and Leiden clustering
Use the modality-aware
preprocessrecipe
Note on integration: The default integration for Visium is scVI.
This tutorial uses Harmony (lighter dependency) to stay dependency-free.
See the API docs for all sc_tools.pp options.
import numpy as np
import anndata as ad
import sc_tools.pp as pp
Create synthetic data#
np.random.seed(7)
n_spots, n_genes = 400, 300
X = np.random.negative_binomial(4, 0.45, size=(n_spots, n_genes)).astype(float)
gene_names = [f"GENE_{i}" for i in range(n_genes)]
adata = ad.AnnData(
X=X,
obs={
"sample": [f"S{i % 2}" for i in range(n_spots)],
"library_id": [f"lib_{i % 2}" for i in range(n_spots)],
},
)
adata.var_names = gene_names
print(adata)
Option A: Step-by-step preprocessing#
This gives you full control over each step.
# 1. Backup raw counts before any transformation
pp.backup_raw(adata)
print("adata.raw backed up:", adata.raw is not None)
# 2. Optionally filter mitochondrial / ribosomal genes before normalization
pp.filter_genes_by_pattern(adata, patterns=["^MT-", "^RPL", "^RPS"])
print(f"After MT/RP filter: {adata.shape}")
# 3. Normalize to 10,000 counts per spot, then log1p transform
pp.normalize_total(adata, target_sum=1e4)
pp.log_transform(adata)
print("X max after log1p:", adata.X.max().round(3))
# 4. PCA on top-N genes (seurat_v3 HVG by default)
pp.pca(adata, n_comps=30, n_top_genes=150)
print("PCA done. obsm keys:", list(adata.obsm.keys()))
# 5. Build neighbor graph and compute UMAP
pp.neighbors(adata, n_neighbors=15)
pp.umap(adata)
print("UMAP done. Shape:", adata.obsm["X_umap"].shape)
# 6. Leiden clustering
pp.leiden(adata, resolution=0.5)
print("Clusters:", adata.obs["leiden"].value_counts().to_dict())
Option B: Recipe (one-call preprocessing)#
The preprocess recipe handles all steps above for a given modality.
For IMC data, it uses arcsinh_transform instead of log_transform.
# Re-create fresh adata
adata2 = ad.AnnData(X=X.copy())
adata2.var_names = gene_names
adata2.obs["library_id"] = [f"lib_{i % 2}" for i in range(n_spots)]
pp.preprocess(
adata2,
modality="visium",
integration="harmony", # scvi is default; harmony used here for lighter deps
batch_key="library_id",
n_top_genes=150,
n_pcs=30,
resolution=0.5,
)
print("Done. obsm:", list(adata2.obsm.keys()))
print("Clusters:", adata2.obs["leiden"].value_counts().to_dict())
Save Phase 3 checkpoint#
adata.write_h5ad("results/adata.normalized.p3.h5ad")
The checkpoint must have:
adata.rawbacked up (raw counts)A batch-corrected embedding (e.g.
obsm['X_scvi']orobsm['X_pca_harmony'])obs['leiden']cluster labels
See Architecture.md §2.2 in the repository for the full requirement table.