2. QC and Filtering#

Pipeline phase: Phase 1 — Data Ingestion and QC

What you will learn:

  • Calculate per-spot QC metrics (total counts, n_genes, % mitochondrial)

  • Filter low-quality spots and genes

  • Generate QC plots (2x2 grid, violin, scatter)

  • Run sample-level QC classification (pass/fail with MAD outlier detection)

  • Produce an HTML QC report

import numpy as np
import anndata as ad
import sc_tools.qc as qc
import matplotlib.pyplot as plt

Synthetic data#

We create a small dataset with a handful of mitochondrial genes (prefixed MT-).

np.random.seed(0)
n_spots, n_genes = 300, 150

X = np.random.negative_binomial(3, 0.4, size=(n_spots, n_genes)).astype(float)

# Add some mitochondrial genes
mt_genes = [f"MT-{g}" for g in ["CO1", "CO2", "ND1", "ND2", "ATP6"]]
other_genes = [f"GENE_{i}" for i in range(n_genes - len(mt_genes))]
gene_names = mt_genes + other_genes

adata = ad.AnnData(
    X=X,
    obs={"sample": [f"sample_{i % 3}" for i in range(n_spots)],
         "library_id": [f"lib_{i % 3}" for i in range(n_spots)]},
)
adata.var_names = gene_names
adata.obs_names = [f"spot_{i}" for i in range(n_spots)]

print(adata)

Calculate QC metrics#

calculate_qc_metrics wraps scanpy’s pp.calculate_qc_metrics and adds optional MT/HB percentage columns when patterns are provided.

qc.calculate_qc_metrics(
    adata,
    mt_pattern="^MT-",
    hb_pattern="^HB",
)

print("QC columns:", [c for c in adata.obs.columns if "count" in c or "gene" in c or "pct" in c])

QC plots#

2x2 grid (pre-filter)#

fig = qc.qc_2x2_grid(adata)
plt.show()

Violin metrics#

fig = qc.qc_violin_metrics(adata, groupby="sample")
plt.show()

Scatter: total counts vs n_genes#

fig = qc.qc_scatter_counts_genes(adata)
plt.show()

Filter cells and genes#

print(f"Before: {adata.shape}")

qc.filter_cells(adata, min_genes=5)
qc.filter_genes(adata, min_cells=3)

print(f"After:  {adata.shape}")

Sample-level QC#

Compute per-sample aggregate metrics and classify samples as pass/fail using absolute thresholds combined with adaptive MAD outlier detection.

sample_metrics = qc.compute_sample_metrics(adata, sample_col="sample")
print(sample_metrics[["n_spots", "median_counts", "median_genes"]].to_string())
results = qc.classify_samples(
    sample_metrics,
    modality="visium",
    mad_multiplier=3.0,
)
print("Pass/fail summary:")
print(results[["sample", "qc_pass", "fail_reasons"]].to_string())

HTML QC report#

In a real project, generate the HTML report and save to figures/QC/qc_report.html:

qc.generate_qc_report(
    adata,
    sample_metrics=sample_metrics,
    classification=results,
    output_path="figures/QC/qc_report.html",
)

Or run the CLI script that produces all QC figures and the report in one step:

python scripts/run_qc_report.py \
    --input results/adata.raw.p1.h5ad \
    --output-dir figures/QC \
    --modality visium \
    --apply-filter