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