An end-to-end single-cell RNA-seq workflow in scanpy, taking raw 10x UMI counts through quality control, normalisation, dimensionality reduction, Leiden clustering, marker-gene detection and cell-type annotation on the canonical 10x Genomics PBMC3k dataset (~2,700 peripheral blood mononuclear cells).
The project is available both as a narrative Jupyter notebook and as a modular, reproducible pipeline package with a command-line entry point.
load -> QC (MAD-based outlier filtering) -> normalise & log1p ->
highly-variable genes -> PCA -> neighbours -> UMAP -> Leiden ->
marker genes (Wilcoxon) -> signature-based cell-type annotation
Figures below are from the offline validation run (see Data note). Running the notebook with internet access reproduces the equivalent figures on the real PBMC3k data.
Quality control, before and after MAD-based filtering:
UMAP embedding coloured by Leiden cluster, annotated cell type, and ground-truth label:
Canonical marker expression by annotated cell type:
By default the pipeline loads the real 10x PBMC3k dataset via
scanpy.datasets.pbmc3k(). If that host is unreachable (offline or a
restricted network / CI environment), it falls back to a synthetic
PBMC-structured dataset (src/make_synthetic.py) that reproduces the same
shape of problem: raw UMI counts, canonical PBMC markers, mitochondrial genes,
low-quality cells and doublets. The analysis workflow is identical for both.
This keeps the repository fully runnable anywhere, including continuous
integration, and the committed example figures come from that synthetic run.
single-cell-pbmc/
├── notebooks/
│ └── pbmc3k_analysis.ipynb # narrative walkthrough
├── src/
│ ├── pipeline.py # modular pipeline + CLI
│ └── make_synthetic.py # offline / CI data generator
├── figures/ # example figures (from validation run)
├── requirements.txt
└── README.md
Set up the environment:
pip install -r requirements.txtNotebook:
jupyter lab notebooks/pbmc3k_analysis.ipynbCommand-line pipeline (writes figures to figures/ and a processed .h5ad):
python -m src.pipeline --figdir figures --out data/pbmc_processed.h5ad
python -m src.pipeline --synthetic # force the offline synthetic dataset- QC. Per-cell QC metrics (total counts, genes detected, mitochondrial fraction) are computed, and low-quality cells are removed using median absolute deviation (MAD) based outlier detection on each metric rather than fixed cutoffs, plus a hard mitochondrial cap. Filtering is intentionally permissive to preserve rare populations.
- Normalisation. Counts are normalised to a common total and log1p
transformed; raw counts are retained in
layers['counts']and the log-normalised matrix inadata.raw. - Clustering and annotation. Structure is captured with the top 2,000 highly variable genes, PCA, a kNN graph and UMAP, with Leiden clustering. Clusters are annotated by scoring each cell against canonical PBMC signatures and assigning each cluster its highest-scoring type.
The embedding step can be swapped for a manifold-learning method such as Topometry (Sidarta-Oliveira et al., eLife, 2026) for richer topological structure, while the QC, normalisation and annotation steps stay the same. Spatial data can be handled with the same scanpy foundation via squidpy.
Fixed random seeds throughout, raw counts preserved alongside the processed
matrix, and a pinned environment in requirements.txt.



