Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Results in AnnData and Seurat

When a job finishes, the client writes the results into your object: one label per cell in the cell metadata, and the per-cluster results next to it. The interactive report holds the full evidence for every cluster; this page covers what lands in your data and how to use it.

The examples use the cluster column leiden in Python and seurat_clusters in R, with the default results_prefix = "cytetype". Column and key names change with both.

Keep the returned object

Python
R

run() returns the annotated AnnData. It is the object you passed to CyteType(), except when label_na=True relabeled missing cluster labels: then it is a new object, and only that object carries the results. Always assign the return value.

adata = annotator.run(study_context="Healthy human PBMCs, 10x Genomics 3' scRNA-seq")

Cell-level columns

Annotation happens per cluster, so every cell in a cluster gets the same values.

Python
R

run() adds five categorical columns to adata.obs, named {results_prefix}_<field>_{group_key}:

ColumnContent
cytetype_annotation_leidenThe cluster’s cell type label, for example Classical Monocyte.
cytetype_cellOntologyTerm_leidenThe Cell Ontology term name for the label, for example classical monocyte. Can be empty when no term matched.
cytetype_cellOntologyTermID_leidenThe Cell Ontology ID, for example CL:0000860. Can be empty when no term matched.
cytetype_cellState_leidenThe cell state, for example proliferating. Empty when no state was called.
cytetype_reviewerConfidence_leidenThe reviewer’s confidence: High, Moderate or Low, or Unknown when the result has none.

The categories of the confidence column are listed as High, Moderate, Low, Unknown (only those that occur), so legends follow that order; the categorical is not ordered, so comparisons such as > do not apply. Cells in clusters without a result get Unknown (an empty value for the cell state).

Cell Ontology IDs use the CL:0000860 form (occasionally PCL:, the Provisional Cell Ontology). Results from older jobs can use the CL_0000860 form.

Cluster IDs and your labels

The cell-level columns are already mapped to cells. Per-cluster results, the report and its CSV export identify clusters by CyteType’s cluster ID, which can differ from the labels in your cluster column. In the report and the CSV, cluster_id is CyteType’s ID and author_label is your label.

Python
R

If every label in the cluster column has at most three characters (for example Leiden numbers), CyteType uses the labels themselves as IDs. Otherwise it numbers the clusters 0, 1, 2, ... in natural sort order of the labels. With label_na=True, the added label Unknown is longer than three characters, so all clusters are then numbered. annotator.cluster_map maps each of your labels to its ID:

id_to_label = {cluster_id: label for label, cluster_id in annotator.cluster_map.items()}

Per-cluster results

Python
R

get_results() returns a dictionary:

results = annotator.get_results()
KeyContent
annotationsA list with one dictionary per annotated cluster (fields below).
clusterCategoriesDisplay groups: a list of dictionaries with categoryName, clusterIds and color. They define the report’s Biological Order and the grouping in marker_dotplot().
studyContextCyteType’s description of the study, shown as Study Overview in the report.
summaryA study-level assessment from earlier pipeline versions; empty for current jobs.
raw_annotationsThe server’s per-cluster results, unchanged (see Raw results).

Each entry in annotations has these keys:

KeyTypeContent
clusterIdstrCyteType’s cluster ID.
annotationstrThe cell type label (as in the obs annotation column).
ontologyTermstrThe Cell Ontology term name.
ontologyTermIDstrThe Cell Ontology ID.
granularAnnotationstrA one-sentence description that combines the cell type, its state and key genes.
cellStatestrThe cell state; empty when none was called.
justificationstrA summary of the evidence for the label.
supportingMarkerslist of strGenes whose expression supports the label.
conflictingMarkerslist of strGenes expressed in the cluster that are inconsistent with the label.
missingExpressionstrGenes expected for the label that are weak or absent, with an explanation.
unexpectedExpressionstrAn explanation of the inconsistent genes.
reviewerConfidencestr or NoneHigh, Moderate or Low.
isHeterogeneousbool or NoneWhether the reviewer judged that the cluster contains distinct populations.
heterogeneityTypestr or Nonedistinct_populations, mixed_lineage, doublet_artifact, transitional_state or unresolved. None for homogeneous clusters and when the type was not classified.

Interpret and review results explains how to read the confidence and heterogeneity values.

adata.uns holds two entries:

KeyContent
cytetype_results{"job_id": ..., "result": ...}, where result is the get_results() dictionary stored as a JSON string so that the AnnData can be written to h5ad.
cytetype_jobDetailsjob_id, report_url and api_url, plus visibility when you passed it to run().

get_results() returns the results stored in adata.uns without contacting the server. If there are none but adata.uns holds the job details, for example because run() stopped waiting, it checks the job once and, if the job has finished, downloads the results and adds the columns as run() would. It returns None when nothing is available yet, and raises AuthenticationError when it needs the server but finds no API key. Run, monitor and rerun jobs covers recovering results this way.

Raw results

Some details exist only in the raw results, such as the annotation levels L1 to L5 (see Identity, levels and state), the classification and the reviewer’s reasoning. They are in raw_annotations of the full results: results["raw_annotations"] in Python, a dictionary keyed by CyteType cluster ID, and attr(obj@misc$cytetype_results, "result")$raw_annotations in R, a named list. Each value has latest (the cluster’s newest result) and history (earlier results for the same cluster, usually empty). The fields people use most:

Path under raw_annotations[cluster_id]Content
latest.annotation.fullOutput.cellType.annotationLevelsThe hierarchy from L1 (broadest) to L5 (most specific). Each level has a label; levels can be missing.
latest.annotation.fullOutput.cellType.classificationHow the label relates to the reference hierarchy: catalogue, outOfCatalog or unresolved for current jobs (see Classification).
latest.annotation.fullOutput.cellType.keySupportingGenes, missingGenes, unexpectedGenesThe gene lists behind the evidence fields.
latest.annotation.fullOutput.cellStateGeneGroupsGene sets that support the cell state.
latest.review.fullOutput.confidenceAssessmentThe reviewer’s confidence label and its reasoning.
latest.review.fullOutput.heterogeneityAssessmentlabel, reasoning, heterogeneityType and heterogeneityTypeReasoning.
latest.review.labelComparisonScoreThe similarity between CyteType’s label and your cluster label, 0 to 100. labelComparisonLlmScore holds the fallback estimate when this is empty.
Python
R

Build a table of the annotation levels:

import pandas as pd

rows = []
for cluster_id, cluster in results["raw_annotations"].items():
    annotation = (cluster.get("latest") or {}).get("annotation") or {}
    cell_type = (annotation.get("fullOutput") or {}).get("cellType") or {}
    levels = cell_type.get("annotationLevels") or {}
    rows.append({
        "clusterId": cluster_id,
        **{name: (levels.get(name) or {}).get("label", "") for name in ["L1", "L2", "L3", "L4", "L5"]},
    })
levels_table = pd.DataFrame(rows)

Save and reload

Python
R

The results live in adata.obs and adata.uns, so they are saved with the AnnData. Call annotator.cleanup() first if you want to remove the upload files and the temporary gene symbol column that CyteType added to adata.var.

import json
import anndata as ad

adata.write_h5ad("annotated.h5ad")

adata = ad.read_h5ad("annotated.h5ad")
results = json.loads(adata.uns["cytetype_results"]["result"])
report_url = adata.uns["cytetype_jobDetails"]["report_url"]

Parsing the JSON returns the results exactly as they were stored. Files written by 0.21.0 already contain reviewerConfidence, isHeterogeneous and heterogeneityType and the filled evidence fields. For files written by older versions, get_results() and marker_dotplot() in 0.21.0 normalize the stored results when they read them: they fill the evidence fields and add the three review keys (as None for files from version 0.7.0 and older, which have no raw results). The reviewerConfidence column is not added to adata.obs of older files. get_results() is a method of the CyteType object, so in a new session parsing the JSON is the simplest way to read the results.

Plot the results

Python
R

Color the embedding by the label or by the reviewer’s confidence:

import scanpy as sc

sc.pl.umap(adata, color="cytetype_annotation_leiden")

confidence_colors = {"High": "#1a9850", "Moderate": "#fdae61", "Low": "#d73027", "Unknown": "#bdbdbd"}
sc.pl.umap(adata, color="cytetype_reviewerConfidence_leiden", palette=confidence_colors)

marker_dotplot() draws a Scanpy dot plot of the genes that support each label, with the labels ordered and the genes grouped by display group (the clusterCategories):

from cytetype import marker_dotplot

marker_dotplot(adata, group_key="leiden")
marker_dotplot(adata, group_key="leiden", n_top_markers=5, standard_scale="var")
ArgumentDefaultMeaning
adataThe annotated AnnData; the results are read from adata.uns.
group_keyThe cluster column used for the run, for example "leiden".
results_prefix"cytetype"Must match the results_prefix of the run.
n_top_markers3Number of key supporting genes shown per cluster.
gene_symbolsNoneA column of adata.var with gene symbols, if var_names are IDs. It is passed to sc.pl.dotplot.
**kwargsPassed to sc.pl.dotplot, for example standard_scale, cmap, use_raw, layer, show, save or return_fig.

Gene names are matched without regard to case, because CyteType reports upper-case symbols (for example CD8A for mouse Cd8a). Genes that are not found are skipped with a warning. Like Scanpy, the function uses adata.raw when it exists and you pass no layer. It needs Scanpy, and it raises an error when the results have no display groups (some older jobs) or no raw results (files from version 0.7.0 and older).

Export a per-cluster table

The report’s Download as CSV button exports one row per cluster with the label, confidence, heterogeneity, supporting genes and annotation levels (see Download as CSV). To build a table from your own object:

Python
R

From get_results(), with your cluster labels added:

import pandas as pd

results = annotator.get_results()
table = pd.DataFrame(results["annotations"])
for column in ["supportingMarkers", "conflictingMarkers"]:
    table[column] = table[column].apply(lambda genes: ";".join(genes) if isinstance(genes, list) else "")

id_to_label = {cluster_id: label for label, cluster_id in annotator.cluster_map.items()}
table.insert(1, "leiden", table["clusterId"].map(id_to_label))
table.to_csv("cytetype_clusters.csv", index=False)

From the obs columns, which also works on a reloaded file without the CyteType object:

group_key = "leiden"
fields = ["annotation", "cellOntologyTerm", "cellOntologyTermID", "cellState", "reviewerConfidence"]
columns = [f"cytetype_{field}_{group_key}" for field in fields]

per_cluster = adata.obs.groupby(group_key, observed=True)[columns].first()
per_cluster["n_cells"] = adata.obs[group_key].value_counts()
per_cluster.to_csv("cytetype_clusters.csv")

See also