Signature smoothing by kNN#

Single-cell data are sparse, and gene expression is often not uniformly detected, even in cell types where specific markers should be present. Multi-gene signatures are already a lot more robust than relying on individual genes for phenotype characterization. However, they can still be impacted by data sparsity - especially short signatures composed of relatively few genes.

To mitigate sparsity, we can smooth signature scores across nearest neighbors in some relevant transcriptional space (e.g. PCA space or directly in gene expression space). pyUCell implements k-nearest neighbor (kNN) smoothing on the UCell scores stored in adata.obs, and returns smoothed scores that are less impacted by data sparsity. Below is a small example.

Prepare data and signatures#

import scanpy as sc
import matplotlib.pyplot as plt
import pyucell as uc

To run pyUCell, you will need two things: 1. a single-cell data object in AnnData format, and 2. a list of gene signatures.

First, load a test dataset:

adata = sc.datasets.pbmc3k()

We will also calculate a PCA reduction of the expression data, which will be used for nearest-neighbor smoothing.

sc.pp.normalize_total(adata, target_sum=1e4)
sc.pp.log1p(adata)
sc.pp.scale(adata, max_value=10)
sc.tl.pca(adata, svd_solver="arpack", n_comps=50)

Define two simple signatures to test

signatures = {"Bcell": ["CD19", "MS4A1", "CD79A"], "Myeloid": ["LYZ", "SPI1"]}

Run pyUCell#

First we calculate UCell scores for these gene signatures:

uc.compute_ucell_scores(adata, signatures=signatures, chunk_size=500)

kNN smoothing#

We can then smooth scores using for example the 10 nearest neighbors in PCA space:

uc.smooth_knn_scores(adata, k=10, use_rep="X_pca", obs_columns=["Bcell_UCell", "Myeloid_UCell"], suffix="_kNN")
adata.obs
Bcell_UCell Myeloid_UCell Myeloid_UCell_kNN Bcell_UCell_kNN
index
AAACATACAACCAC-1 0.000000 0.000000 0.101080 0.020556
AAACATTGAGCTAC-1 0.421006 0.233567 0.074687 0.513137
AAACATTGATCAGC-1 0.000000 0.150150 0.069996 0.011671
AAACCGTGCTTCCG-1 0.000000 0.591258 0.577401 0.023835
AAACCGTGTATGCG-1 0.000000 0.000000 0.049323 0.000000
... ... ... ... ...
TTTCGAACTCTCAT-1 0.000000 0.604938 0.581622 0.014074
TTTCTACTGAGGCA-1 0.401202 0.000000 0.023058 0.392815
TTTCTACTTCCTCG-1 0.536271 0.000000 0.114894 0.578955
TTTGCATGAGAGGC-1 0.923676 0.000000 0.108258 0.517280
TTTGCATGCCTCAC-1 0.000000 0.000000 0.085377 0.003931

2700 rows × 4 columns

Visualize results on UMAP#

Let us visually compare the effect of kNN smoothing on the distribution of UCell scores for the two signatures.

sc.pp.neighbors(adata)
sc.tl.umap(adata)
fig, axes = plt.subplots(1, 2, figsize=(10, 5))
sc.pl.umap(adata, color="Bcell_UCell", cmap="viridis", ax=axes[0], size=20, show=False)
sc.pl.umap(adata, color="Bcell_UCell_kNN", cmap="viridis", ax=axes[1], size=20, show=False)
plt.tight_layout()
plt.show()
../_images/3c00fc06cefc8f70465d7b64ff8a0747340cc8fc418ed82b5709606e20e52fbf.png
fig, axes = plt.subplots(1, 2, figsize=(10, 5))
sc.pl.umap(adata, color="Myeloid_UCell", cmap="viridis", ax=axes[0], size=20, show=False)
sc.pl.umap(adata, color="Myeloid_UCell_kNN", cmap="viridis", ax=axes[1], size=20, show=False)
plt.tight_layout()
plt.show()
../_images/82d31bee023c4880764f0b9943ef57bf0c3a2a2571457d7063d193b2d1ecb8e2.png

Additional resources#

For more advanced use cases and a discussion of the parameters available, refer to this tutorial

An implementation of the UCell algorithm for R is available at Bioconductor and on GitHub.

References#

If you used UCell in your research, please cite:

UCell and pyUCell: single-cell gene signature scoring for R and Python. Massimo Andreatta & Santiago J Carmona (2026) Bioinformatics - doi.org/10.1093/bioinformatics/btag055

UCell: robust and scalable single-cell gene signature scoring. Massimo Andreatta & Santiago J Carmona (2021) CSBJ - doi.org/10.1016/j.csbj.2021.06.043