The New Azimuth Might Just Be the Best Cell Annotation Tool Out There

· 10 min read · Analysis Notes

The New Azimuth Might Just Be the Best Cell Annotation Tool Out There

Image

At Single Cell Genomics Day a few months ago (see the replay: Rahul Satija Summarizes 8 Key Developments in Single-Cell for 2025), the Satija Lab released their latest single-cell annotation package: Pan-Human Azimuth. In this post, we'll take a deep dive into this new tool with a hands-on walkthrough!

01

R Version: Installation & Usage

⚠️ Apple computers with M-series chips will throw errors when using the R package version. There is currently no workaround — consider using the Python version instead.

# Install from GitHub
devtools::install_github("satijalab/AzimuthAPI")
library(AzimuthAPI)
# Assuming your Seurat object is called obj
obj <- CloudAzimuth(obj)

# Annotation runs in the cloud, so local memory usage is minimal
# Once complete, the annotated obj is returned with results stored in meta.data
colnames([email protected])

...
[11] "full_hierarchical_labels"
[14] "final_level_labels"        "final_level_softmax_prob"  "full_consistent_hierarchy"
[17] "azimuth_broad"             "azimuth_medium"            "azimuth_fine"
[20] "azimuth_label"

Let's benchmark it (dataset: 16,222 cells × 20,262 genes; test platform: Ubuntu 24.04 LTS, 2× Intel Xeon E5-2690 v4 + entry-level Quadro K2200):

timing_result <- system.time({
    obj  <- CloudAzimuth(obj)
})

cat("User CPU time:", timing_result["user.self"], "seconds\n")
cat("System CPU time:", timing_result["sys.self"], "seconds\n")
cat("Total elapsed time:", timing_result["elapsed"], "seconds\n")

Running Pan-Human Azimuth on the cloud!
Uploading dataset
Uploading file and listening for updates...
File successfully uploaded
Running Pan-Human Azimuth:

Reference model and parameters:
Model name: M0.2
Evaluation batch size: 40960
Extract embeddings: True
Run umap: False
Refine labels in postprocessing: True
Integer counts detected by panhumanpy.ANNotate : False
Log-normalization performed by panhumanpy.ANNotate: False
Query object:
Total number of cells: 16222
Total number of features: 4528
Overlap w. feature reference: 4528 (~89%)

Splitting query data into 1 evaluation batches.

Running model:

  1/507 ━━━━━━━━━━━━━━━━━━━━ 5:09 611ms/step
...
507/507 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step
Interpreting label predictions for consistent granularity at broad level.

Interpreting label predictions for consistent granularity at medium level.

Interpreting label predictions for consistent granularity at fine level.

Extracting azimuth embeddings:

  1/507 ━━━━━━━━━━━━━━━━━━━━ 22s 45ms/step
...
507/507 ━━━━━━━━━━━━━━━━━━━━ 2s 3ms/step

An object of class Seurat
4528 features across 16222 samples within 1 assay
Active assay: RNA (4528 features, 0 variable features)
1 layer present: data
1 dimensional reduction calculated: azimuth_embed
R script finished successfully
Output file ready for download
Downloading the output file...

User CPU time: 5.724 seconds
System CPU time: 0.595 seconds
Total elapsed time: 100.491 seconds

Comparing the annotation results against manual annotations, they're quite accurate. (Here I'm only showing the azimuth_broad level as an example — in practice there are three granularity levels from coarse to fine that you can choose from as needed.)
Result exampleResult example

Here's a handy trick: since Azimuth annotates at the individual cell level, you can tally the cell types within each cluster and rank them by count to guide your final annotation decisions. This approach works with any cell-level annotation tool:

library(dplyr)
for(i in levels(obj$seurat_clusters)){
  print(paste("Cluster", i))
  [email protected] %>%
    filter(seurat_clusters == i)%>%
    # You can also use annotations at other granularity levels
    group_by(final_level_labels)%>%
    summarize(count = n())%>%
    arrange(desc(count))%>%
    head(n =5)%>%
    print()
}

[1] "Cluster 0"
# A tibble: 5 × 2
  final_level_labels                 count
  <chr>                              <int>
1 PIP mammary luminal cell            1035
2 Secretoglobin mammary luminal cell   317
3 Secretory cell                       219
4 CXCL2/3+ surface mucous cell         140
5 MMP7+ basal cell of bladder          137
[1] "Cluster 1"
# A tibble: 5 × 2
  final_level_labels    count
  <chr>                 <int>
1 GZMB CD8 T cell         531
2 Memory CD4 T cell       383
3 KLRB1 CD8 T cell        334
4 Memory CD8 T cell       294
5 GZMK+IL7R+ CD8 T cell   129
[1] "Cluster 2"
# A tibble: 5 × 2
  final_level_labels                 count
  <chr>                              <int>
1 Secretoglobin mammary luminal cell   454
2 PIP mammary luminal cell             314
3 Goblet cell                          176
4 Secretory cell                       129
5 CXCL2/3+ surface mucous cell          94
[1] "Cluster 3"
# A tibble: 5 × 2
  final_level_labels count
  <chr>              <int>
1 Cycling proB cell    140
2 Cycling preB cell    133
3 Secretory cell       127
4 Club cell             99
5 Memory CD8 T cell     99
[1] "Cluster 4"
# A tibble: 5 × 2
  final_level_labels                 count
  <chr>                              <int>
1 PIP mammary luminal cell             481
2 Secretory cell                       133
3 Tuft cell                            108
4 Secretoglobin mammary luminal cell    99
5 Goblet cell                           97
[1] "Cluster 5"
# A tibble: 5 × 2
  final_level_labels count
  <chr>              <int>
1 Memory CD4 T cell    301
2 Treg cell            219
3 CD4 T cell           121
4 Naive CD4 T cell      85
5 Memory CD8 T cell     81

In my experience, the immune cell annotations are remarkably accurate and authoritative. One important caveat: the reference dataset was built from healthy donors, so if results don't match your expectations, consider whether disease-related changes might be the cause.

02Python (Command Line) Version: Installation & Usage

The R package only supports cloud-based annotation, which can occasionally be unstable or suffer from connectivity issues. The Python version can run locally — either from the command line or in a Jupyter notebook. Note that the GPU-accelerated version requires CUDA, which is currently not supported on Mac.

# (Optional) Recommended: install into a dedicated conda environment with Python 3.9+
# conda create -n azimuth python=3.12
# conda activate azimuth

# Basic installation (CPU only)
pip install git+https://github.com/satijalab/panhumanpy.git

# GPU support (requires CUDA; not available on M-series Macs)
pip install 'git+https://github.com/satijalab/panhumanpy.git#egg=panhumanpy[gpu]'

Command Line Usage

# View options and help
annotate --help

# Run annotation — note that a new AnnData file obj_ANN.h5ad will be generated
annotate obj.h5ad

Let's time it:

time annotate obj.h5ad

Here are the benchmark results using the same dataset (16,222 cells × 20,262 genes) on two platforms (CPU only). If you have GPU resources, give the accelerated version a try!

# Ubuntu 24.04 LTS, Intel Xeon E5-2690 v4
real    1m20.639s
user    2m57.626s
sys    0m17.212s

# macOS Sequoia 15.5, M4
32.59s user
7.87s system
139% cpu
29.049 total

The speed is quite impressive — 1 minute 20 seconds on an aging server, and under 30 seconds on a Mac. For moderately sized datasets like this, running locally is actually faster than using the cloud.

Running in Python

You can also run the annotation directly within a Python environment.

import panhumanpy as ph
import anndata as ad

# View documentation (advanced API)
help(ph.AzimuthNN)

# View documentation (basic API)
help(ph.AzimuthNN_base)

# Read the AnnData object (you can also pass a file path directly)
obj = ad.read_h5ad("obj.h5ad")

To time the run in a Jupyter notebook, use the %%time magic command on the first line (skip it if you don't need timing):

%%time
# Build the AzimuthNN object
azimuth = ph.AzimuthNN(obj)
# Run annotation
azimuth.azimuth_refine()

# Extract annotation results
cell_metadata = azimuth.cells_meta
embeddings = azimuth.azimuth_embed()
umap = azimuth.azimuth_umap()

# Package the annotated AnnData and save
annotated_query = azimuth.pack_adata(save_path="obj_ANN.h5ad")

Timing results:

# Mac
CPU times: user 20.6 s, sys: 3.57 s, total: 24.2 s
Wall time: 16.1 s

# Server
CPU times: user 2min 9s, sys: 7.13 s, total: 2min 16s
Wall time: 54.4 s

Note that the AnnData structure after annotation differs from the original:

print("Data shape:", annotated_query.shape)
print("obsm keys:", list(annotated_query.obsm.keys()))
print("obs columns:", annotated_query.obs.columns.tolist()[:10])  # Show the first 10 column names

Data shape: (16222, 20262)
obsm keys: ['azimuth_embed', 'azimuth_umap']
obs columns: ['full_hierarchical_labels', 'level_zero_labels', 'final_level_labels', 'final_level_confidence', 'full_consistent_hierarchy', 'orig.ident', 'nCount_RNA', 'nFeature_RNA', 'scDblFinder.score', 'scDblFinder.weighted']

Let's visualize the results:

import scanpy as sc
annotated_query.obsm['X_umap'] = annotated_query.obsm['azimuth_umap']
sc.pl.umap(annotated_query, color=['azimuth_broad','final_level_confidence'],
           ncols=2, wspace=0.5)

03

Frequently Asked Questions

Here's a summary of the official FAQ:

What data types are supported?

Pan-Human Azimuth was trained on data from 23 human tissues: adipose tissue, adrenal gland, bladder, blood, bone marrow, brain, breast, eye, intestine, heart, kidney, liver, lung, lymph node, ovary, pancreas, placenta, skeletal muscle, skin, spleen, stomach, testis, and thymus.

  • • The training dataset includes both single-cell RNA-seq (scRNA-seq) and single-nucleus RNA-seq (snRNA-seq) data, spanning multiple experimental platforms including droplet-based, plate-based, and combinatorial indexing technologies.
  • • Cancer datasets were excluded during training.
  • • In general, query datasets that fall within the scope of the above parameters should yield good annotation results with Pan-Human Azimuth.

What preprocessing steps are required?

  • • The R interface requires a Seurat object as input; the Python interface requires an AnnData object.
  • • Users should perform quality control before annotation (i.e., filter cells based on UMI counts and mitochondrial gene percentage thresholds).
  • • R users need to perform standard log-normalization with NormalizeData prior to annotation — see the official tutorial for details.
  • • Python users can either normalize beforehand or provide raw integer counts (the package will handle normalization automatically).
  • • Variable gene selection is not required — users should retain all quantified genes in the object before annotation.

What does the package output?

  • • full_hierarchical_labels: Hierarchical cell type annotations with predictions at multiple levels of the cell type taxonomy. Example: Immune cell | Lymphocyte | T/NK cell | T cell | CD4 T cell | Naive CD4 T cell
  • • final_level_softmax_prob: Prediction probabilities associated with the hierarchical labels.
  • • azimuth_embed: 128-dimensional cell embeddings generated by the neural network encoder, which are decoded to produce annotation results.
  • • For easier interpretation, labels are post-processed to be more concise and readable, with granularity levels standardized across cells. These include final_level_labels, azimuth_fine, azimuth_medium, and azimuth_broad. See the official tutorial for details.

How should I QC the annotation results?

  • • final_level_softmax_prob is a useful QC metric. We typically exclude cells with scores < 0.5.
  • • No QC metric is perfect, so we strongly recommend identifying and visualizing differentially expressed genes for each annotated cell population as a manual sanity check. To help with this, the R API package provides the make_azimuth_QC_heatmaps function, demonstrated in the tutorial.

Can Pan-Human Azimuth annotate disease data?

  • Yes. We routinely use Pan-Human Azimuth to annotate disease data. In many — if not most — cases, malignant cells still retain partial molecular signatures of their normal counterparts. Reference mapping workflows, including Pan-Human Azimuth, are widely used to annotate both healthy and diseased cells to a common set of cell types, followed by within-cell-type differential expression analysis to identify malignant transformations.

  • However, the Pan-Human Azimuth reference does not include disease-specific cell types. Malignant cells will be mapped to the most similar normal cell type. The QC measures discussed above help ensure that these annotations remain useful for comparison with healthy samples and for identifying disease-specific signatures.

  • The Pan-Human Azimuth training dataset does not contain cancer samples. Tumor cells often undergo substantial molecular reprogramming that diverges significantly from normal cells, which may lead to poor mapping performance.

Is there a size limit for mappable datasets?

  • • Both R and Python workflows are highly scalable. The R workflow runs in the cloud at approximately 50,000–100,000 cells/minute. The Python workflow achieves similar throughput on a standard laptop.
  • • Pan-Human Azimuth annotates and embeds each cell independently, so large datasets can be chunked and mapped serially. Using this strategy, we mapped over 80 million cells from the scBaseCamp repository in less than a day.

Future Plans

Yes! We're excited about upcoming features, including:

  • • Improved quality control: Enhanced QC metrics with better calibration than softmax probabilities, capturing uncertainty across multiple levels of the hierarchy.
  • • Low-quality cell detection: In the current version, low-quality cells (e.g., those with UMI < 100) tend to map to cell types with inherently low RNA content, such as plasma cells and αβT (entry) cells. We are working to explicitly flag these as low quality in future releases.
  • • Higher-resolution annotations: We are continuously updating the reference dataset to provide finer-grained annotations and incorporate more recently discovered cell types.

References

  1. 1. https://satijalab.org/pan_human_azimuth/
  2. 2. R tutorial: https://satijalab.org/panhuman_r_vignette
  3. 3. Python tutorial: https://github.com/satijalab/panhumanpy/blob/main/tutorial_panhumanpy.ipynb

04

Afterword

We previously published an article on using large language models to assist with single-cell annotation: The Future Is Here: The Era of LLM × Single-Cell Has Begun. What do you think is the future of single-cell annotation — reference-based approaches or large language models? What's the best annotation tool you've used? Let us know in the comments! To stay updated with our study notes, give us a ✨:

Past highlights ⬇️:

Publication-Quality RNA-seq Figures with tidyplots: A Practical Guide

Stop Using .rds for Single-Cell Data — Try qs Instead!

STAR Has a Bug?? A Full Post-Mortem


Originally posted on WeChat Official Account: bioinfo_studyroom

Single Cell Seurat Downstream