Library / SDK
Teichlab/celltypist avatar
Teichlab/celltypist

CellTypist: logistic regression label transfer for scRNA-seq, and where it stops

A tool for semi-automatic cell type classification

503 stars59 forksPythonMIT

At a glance

What is it?
CellTypist assigns cell type labels to single-cell data using logistic regression classifiers trained on built-in or custom models. It is a fast first pass for immune-focused datasets, not a replacement for manual annotation or for evidence that your query data resembles the training reference.
Who is it for?
Adopt CellTypist if you have a raw UMI count matrix of a tissue the built-in models cover, mainly immune sub-populations, and you want a reproducible label pass you can inspect cell by cell. Do not adopt it as the only annotation step for a tissue no available model was trained on, and do not feed it normalised or log-transformed matrices, since the README asks for raw counts.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 116 days ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 15, 2026, and from our analysis. They are not legal advice.

DEEP OPEN-SOURCE ANALYSIS

The annotation bottleneck CellTypist targets

Manual cell type annotation does not scale. A researcher clusters a single-cell RNA-seq dataset, then reads marker genes cluster by cluster and assigns names. That works for a few thousand cells and one tissue. It breaks when a study covers dozens of samples, when clusters sit on a continuum rather than in clean groups, or when two analysts label the same cluster differently. The labels also carry no reusable artefact: the next dataset starts the process again.

CellTypist approaches this as a supervised classification problem rather than a clustering problem. It takes a count matrix and a pre-trained model and returns a predicted label per cell. The README frames the goal as assisting "in the accurate classification of different cell types and subtypes", with built-in models that currently focus on immune sub-populations. That focus matters. The tool is aimed at people who already have clusters or cell-level data and want a consistent, transferable label assignment, not at people who need an unsupervised method to discover structure from nothing.

Logistic regression, SGD, and a pickle file as the unit of transfer

The mechanism is stated plainly in the README: logistic regression classifiers optimised by stochastic gradient descent. Each model is a set of per-cell-type linear decision boundaries over a fixed gene feature space. Inference is a matrix operation, which is why the tool can label cells without refitting anything on your data.

A model is a pickle file that bundles three things you can inspect. The README shows loading it through the Model class and reading model.cell_types for the label set and model.features for the genes the classifier expects. That feature list is the contract between model and input. If your matrix uses different gene identifiers, or lacks genes the model was trained on, those features simply contribute nothing. The README also advises keeping non-expressed genes in the input table, on the reasoning that their absence from a cell is itself a negative signal the model can use.

Label transfer, then, is not a shared embedding or an alignment step. There is no batch correction, no joint embedding of reference and query, and no graph matching. The model is frozen; only your query matrix moves. This is the design's main strength and its main risk. It is fast and reproducible, and it assumes the reference and your data live in comparable expression space.

Getting a prediction: install, download, annotate

Two install paths are documented. Via pip: pip install celltypist. Via conda: conda install -c bioconda -c conda-forge celltypist. The README badge states Python 3.6 and above.

Models are not bundled. You download them with the models module. The README gives models.models_description() to list what is available, then models.download_models(model = 'Immune_All_Low.pkl'), or a list of names, or no argument at all to fetch everything. The README notes each model averages about 1 MB and explicitly encourages downloading all of them. Models land in a .celltypist/ folder in your home directory by default, and the README shows overriding that location with an environment variable in your shell profile: export CELLTYPIST_FOLDER='/path/to/model/folder/'. Because the models are downloaded rather than shipped, a pipeline that depends on a specific model version needs that download step pinned and cached, not left to whatever the server serves on the day a job runs.

Annotation itself is one call. predictions = celltypist.annotate(input_file, model = 'Immune_All_Low.pkl'), or pass a previously loaded Model instance instead of the filename. Input is a count table in .txt, .csv, .tsv, .tab, .mtx or .mtx.gz, cell-by-gene by default. For a gene-by-cell table, pass transpose_input = True. For .mtx input you must also supply gene_file and cell_file, since the matrix alone carries no names. The README is explicit that a raw count matrix of reads or UMIs is required, and the demo sample is described as a UMI count csv with cells as rows and gene symbols as columns. That is a hard input constraint, not a suggestion.

Where the frozen-model assumption breaks

The failure mode follows directly from the architecture. A logistic regression model trained on one reference compresses cell identity into a fixed gene space. Query cells that belong to a type the model has never seen have nowhere to go except the nearest known class. The classifier will still return a label with a probability attached, and a plausible-looking probability on an out-of-reference cell is the most dangerous output the tool produces.

Three practical consequences. First, tissue mismatch: the built-in models have a stated focus on immune sub-populations, so applying an immune model to, say, a developmental atlas asks the classifier to place cells into categories that may not exist in its label set. Second, identifier mismatch: the model expects specific gene symbols, and a matrix built from a different annotation source silently loses features rather than raising an error. Third, preprocessing mismatch: the README requires raw counts, so a matrix that has already been normalised or log-transformed violates the documented input contract. None of these produce a crash. They produce labels.

The README points at this problem indirectly through the tutorials it links: one on multi-label classification, and one titled "Best practice in large-scale cross-dataset label transfer using CellTypist". The existence of a cross-dataset best-practice notebook is an admission that naive transfer across datasets is not the intended default. Anyone running annotate() on a new tissue without reading that notebook is skipping the part the maintainers thought worth writing down.

What you give up compared with reference-mapping tools

The obvious comparison is with reference-mapping approaches such as Seurat's anchor-based transfer or Scanpy's ingest, where query cells are projected into a reference-defined space, often after an integration step that corrects for batch effects between reference and query. The difference is architectural. Those methods model the relationship between two datasets and produce a joint representation; CellTypist models only the decision boundary and applies it to your data unchanged.

That choice buys speed and simplicity. There is no integration hyperparameter to tune, no anchor set to filter, no risk of over-correction distorting the query. It costs you any correction for the technical gap between reference and query. If your data was generated on a different platform, chemistry, or dissociation protocol than the training reference, the classifier has no mechanism to compensate. A reference-mapping pipeline can absorb some of that shift in the integration step. CellTypist cannot, and its output will not tell you the shift happened.

The second alternative is simply not using a classifier: cluster, score markers, and label manually. That remains the right choice for a tissue with no suitable reference, or for a study where the novel cell state is the finding. CellTypist is a labelling shortcut, and a shortcut is only useful when the destination is already known.

Maintenance, model versioning, and the MIT licence

The code is MIT licensed, which permits commercial and closed-source use with the usual requirement to retain the licence and copyright notice. The models are a separate question. The README describes downloading them from a remote source via download_models() and updating them with force_update = True, and it does not state a licence for the model artefacts themselves. If you redistribute a model file or ship it inside a container, check the terms attached to the models rather than assuming the repository's MIT licence covers them.

Maintenance cost concentrates in two places. Model drift is one: models.download_models(force_update = True) re-fetches the latest versions, which means a pipeline that does not pin and cache model files can silently change its predictions between runs. The README's own framing, that you should update "if you think they may be outdated", leaves the decision to you. Version pinning is the other: the project released 1.6.3, then 1.7.0 and 1.7.1 in June 2025, with a gap of roughly a year between 1.6.3 and 1.7.0. That cadence is normal for an academic tool but means you should treat the package version and the model version as two independent things to record alongside any result you publish.

The repository is not archived and the last push is recent, so the project is active. That says nothing about whether a specific model is appropriate for your tissue, which is the question that actually determines whether the output is usable.

Editorial conclusion

Adopt CellTypist if you have a raw UMI count matrix of a tissue the built-in models cover, mainly immune sub-populations, and you want a reproducible label pass you can inspect cell by cell. Do not adopt it as the only annotation step for a tissue no available model was trained on, and do not feed it normalised or log-transformed matrices, since the README asks for raw counts. Before trusting a run, check three things: whether the model's feature list overlaps your gene symbols, what fraction of cells fall below the prediction confidence threshold, and whether the majority-vote option changes labels you care about. If your markers disagree with the transferred labels, the disagreement is the result, not an error to smooth over.

Official sources

  1. License: MIT
  2. Project website
  3. README
  4. Releases
  5. Teichlab/celltypist on GitHub
Community notes

Community notes