Transfer

What it does

Labels cells from a labelled reference without changing the model. The reference and the query are embedded with the frozen model, a lightweight classifier is fitted on the reference embeddings and their labels, and the query is predicted.

Because no parameter of the model is updated, this works identically for every method that produces an embedding, and it runs in minutes. When the reference is tiny — one cell per class — it is the few-shot setting; when the reference is a fully annotated atlas, it is ordinary reference mapping. The mechanics are the same.

It is also the right comparison for models whose authors never published a weight-updating recipe (SCimilarity, GenePT, UCE, scPRINT): --classifier mlp trains the same post-hoc head on top of any of them.

Supported methods

scfoundry list methods --task transfer:

c2s cellama cellfm cellplm geneformer genept langcell scbert sccello scfoundation
scgpt scimilarity scprint uce scvi

pca, novae and the integration methods are not available here.

Classifiers

--classifier

Definition

logreg (default)

Embeddings are z-scored per dimension on the reference; an L2-regularised multinomial logistic regression (C = 1, at most 2,000 iterations) is fitted. Robust across reference sizes, which is why it is the default.

prototype

One prototype per class: the mean of the L2-normalised reference embeddings of that class. A query cell is scored by cosine similarity to every prototype; probabilities are softmax(similarity / 0.1). The classic few-shot classifier.

knn

The k nearest reference cells by cosine similarity (L2-normalised embeddings), majority vote; probabilities are vote fractions. --knn-k defaults to 15 and is capped at the reference size.

mlp

A two-layer MLP head trained with a stratified validation split (20% of each class, at least one cell held out per class) and early stopping. Needs at least one class with five or more reference cells; refuses to run otherwise.

Note

knn uses a plain majority vote. On a small reference, set --knn-k yourself — with one cell per class, k larger than 1 mixes classes by construction, and a k larger than the smallest class can never predict that class unanimously. scFoundry warns when k exceeds the smallest class or had to be capped, but it does not second-guess your choice.

Inputs

Two files, both satisfying the standard input contract:

  • Reference — must carry labels in obs["cell_type"] (or --label-key).

  • Query — labels not required. If present they are ignored, which is convenient for scoring afterwards.

Both need a unique obs["barcode"]; the prediction tables are indexed by it.

Warning

Input must be raw counts over the full transcriptome. Passing log-normalised values, or an object already subset to highly variable genes, does not raise an error — it produces a plausible-looking embedding that is silently wrong. See Input data format.

Every example on this page runs against the four demo files, downloaded once into a demo/ directory of the workspace as shown in Demo datasets:

demo/colon_1000.h5ad            1,000 human colon cells, labelled, 7 batches
demo/colon_50.h5ad                 50 human colon cells, labelled
demo/liver_1shot_support.h5ad   one labelled cell per class (5 cells)
demo/liver_1shot_query.h5ad     75 liver cells to annotate

The liver pair is a genuine one-shot setup: the reference contains exactly five cells, one per class.

Running it

Note

Run scfoundry commands from inside a workspace created by scfoundry init (any subdirectory works — the workspace is found by walking upwards, like a git repository), or pass --workspace DIR. Weights, the image cache, results and run logs all live there. See The workspace.

scfoundry transfer --method scgpt \
  --reference demo/liver_1shot_support.h5ad \
  --query demo/liver_1shot_query.h5ad \
  --classifier prototype
[PROCESS 3b/0a7f1e] TRANSFER:EMBED:embed_by_scgpt (liver_1shot_support)
[PROCESS 91/c2d8b0] TRANSFER:EMBED:embed_by_scgpt (liver_1shot_query)
[PROCESS e7/5f13aa] TRANSFER:transfer_fit (scgpt prototype liver_1shot_support)
[PROCESS 2c/9d40b7] TRANSFER:transfer_predict (scgpt prototype liver_1shot_query)

[SUCCESS] completed=4 failed=0 cached=0

Both files are embedded in one pass; the fitted model and the predictions are both saved.

scfoundry transfer --method scgpt --reference atlas.h5ad
results/transfer/models/scgpt/logreg/atlas/

Fit once, then predict several query sets against the same reference.

scfoundry transfer --method scgpt \
  --query new_sample.h5ad \
  --fitted results/transfer/models/scgpt/logreg/atlas

--fitted is the model directory written by the fit stage. The classifier and the method are read from its meta.json, so they need not be repeated.

Note

The first run of any method pulls its container image, which can take several minutes and a few gigabytes. The image is cached under cache/.shared/nxf_singularity/ in the workspace (or wherever --cache-dir points) and reused by every later run, so this cost is paid once per method.

Parameters

Option

Type

Default

Description

--method

string

required

Which model provides the embeddings.

--reference

path

none

Labelled reference cells. Required to fit.

--query

path

none

Cells to label. Required to predict.

--fitted

path

none

Model directory from an earlier fit. Required when --reference is absent.

--classifier

string

logreg

logreg, prototype, knn or mlp.

--label-key

string

cell_type

obs column on the reference holding labels.

--knn-k

integer

15

Neighbours for --classifier knn.

--model, --batch-size

per method

Passed to the embedding step, as in Embed.

Every embedding parameter also applies, since embedding is the first step.

Outputs

results/transfer/
├── models/<method>/<classifier>/<reference>/
│   ├── meta.json                    classes, dimensions, hyperparameters
│   └── model.npz | mlp/             the fitted classifier
└── predictions/<method>/<classifier>/
    ├── <query>_predicted_labels.tsv
    └── <query>_predicted_probs.tsv

For the one-shot command above:

results/transfer/models/scgpt/prototype/liver_1shot_support/meta.json
results/transfer/models/scgpt/prototype/liver_1shot_support/model.npz
results/transfer/predictions/scgpt/prototype/liver_1shot_query_predicted_labels.tsv
results/transfer/predictions/scgpt/prototype/liver_1shot_query_predicted_probs.tsv

The embeddings of both files are published to results/embeddings/<method>/ as a side effect, so they need not be recomputed for benchmark or geometry.

The two TSVs are indexed by barcode:

  • _predicted_labels.tsv — one column, predicted_label, the argmax class.

  • _predicted_probs.tsv — one column per class, rows summing to 1.

meta.json records what the model was fitted on:

{
  "format": "scfoundry_transfer/1",
  "classifier": "logreg",
  "method": "scgpt",
  "label_key": "cell_type",
  "classes": ["cholangiocyte", "mast cell", "mononuclear phagocyte",
              "natural killer cell", "plasma cell"],
  "dim": 512,
  "n_reference": 5,
  "cells_per_class": {"cholangiocyte": 1, "mast cell": 1, "...": 1},
  "hyperparameters": {"C": 1.0, "max_iter": 2000, "scaling": "z-score", "n_iter": 22}
}

Reading results in Python

import anndata as ad
import pandas as pd

base = "results/transfer/predictions/scgpt/prototype/liver_1shot_query"
labels = pd.read_csv(f"{base}_predicted_labels.tsv", sep="\t", index_col=0)
probs  = pd.read_csv(f"{base}_predicted_probs.tsv",  sep="\t", index_col=0)

# Confidence of the winning call, useful for flagging uncertain cells.
labels["confidence"] = probs.max(axis=1)
print(labels.head())

Scoring against held-out truth:

truth = ad.read_h5ad("demo/liver_1shot_query.h5ad").obs["cell_type"]
accuracy = (labels["predicted_label"] == truth.reindex(labels.index)).mean()
print(f"accuracy: {accuracy:.1%}")

Gotchas

Every class you expect must be in the reference. A class absent from the reference can never be predicted — query cells of that type are silently assigned to whichever class is nearest. Compare the label sets before you trust a prediction.

Fitted models are method-specific. A classifier fitted on scGPT embeddings is meaningless applied to Geneformer embeddings. meta.json records the method, and --fitted refuses a mismatch when you pass --method explicitly, but it cannot tell you that your query was embedded with different settings.

Class imbalance in the reference is not corrected. Prototypes are plain class means and logreg is unweighted, so a class with 100 reference cells is better estimated than one with 1. That is usually what you want; just be aware it is not normalised away.

Cosine similarity ignores magnitude. prototype and knn only look at embedding direction. For models whose embeddings encode information in vector norm, this discards it; logreg and mlp do not.

Note

Every launch gets its own run directory under runs/<task>/, and the workspace nextflow.config sets cleanup = true, so the task work directory is deleted once the run succeeds. After a failure it is kept: fix the cause, add --resume to the same command, and Nextflow reuses every task that already completed. scfoundry runs lists the run directories with their status; scfoundry runs --task <task> narrows the list.

Next steps

  • Fine-tune — when you have enough labels to update the model itself.

  • Embed — the representation this task builds on.