← All projects

Single-cell data analysis

Live University coursework
Python scanpy scVI CellTypist
Goal
Identify the cell types in a subsampled human intestinal atlas and produce a clean, batch-integrated, annotated map
Inputs
~15,000 cells x ~33,500 genes (terminal ileum, 4 donors) as .h5ad, plus per-sample technical metadata
Outputs
A batch-corrected UMAP with CellTypist labels, cross-checked against marker genes and Leiden clusters
Tools
Python, scanpy, scVI, CellTypist

What it does

It takes a subsampled human intestinal atlas from raw counts to an annotated, batch-corrected single-cell map, and then checks that map three independent ways before trusting it.

Why it exists

It was set as an assignment for the Genomics and Transcriptomics Analysis lecture: identify the cell types present in a provided dataset. The interesting part is not running the standard steps, it is deciding which of them are actually correct for this data, and then showing that the answer is not an artefact of the method that produced it.

How it works

The cell-type calls are confirmed by triangulating three independent views. CellTypist assigns labels from a reference model of the adult human intestine; a hand-picked panel of known marker genes is checked against those labels; and unsupervised Leiden clustering is run on the same neighbourhood graph. Where all three agree, the structure is real rather than an artefact of any one method. That cross-check is the point of the notebook.

Getting to that point: genes detected in fewer than three cells are dropped, QC metrics are computed, and doublets are scored with Scrublet per donor. Cells are then filtered on doublet score and total counts. Batch effects across the four donors are removed with scVI, and the neighbour graph is rebuilt from the scVI latent space rather than from PCA, so both clustering and UMAP operate on the batch-corrected representation.

What was hard

Three decisions did most of the work.

The mitochondrial QC was silently doing nothing. This dataset's var_names are Ensembl IDs, so flagging mitochondrial genes by matching MT- against them matched zero genes, and every cell reported a pct_counts_mt of zero. That reads as unusually clean data rather than as a broken filter, which is what makes it dangerous. Flagging against the gene symbols instead makes the metric reflect real mitochondrial content.

scVI was being trained on the wrong matrix. Training on the full ~33,500-gene matrix was slow enough that it had to be capped at max_epochs=150 - which stops training at an arbitrary point rather than at convergence. Selecting 2,000 highly variable genes from raw counts with flavor="seurat_v3" and training on that subset lets the run converge on its own.

Dimensionality reductions do not update themselves. Once cells are filtered, HVG selection, PCA, the neighbour graph and UMAP are all still functions of the old cell set, so all four are recomputed on the cleaned data.

Current state

Complete for the task it was set. The CellTypist reference model is included in the repository, but the JupyterLab environment it ran on was provided by the course and is not, so the notebook is currently easier to read than to re-run.