Tutorial 2: Recovery of cell states and ecotypes in scRNA-seq data¶
Goal: assign single cells to the cell states of a prebuilt model and, if cells come with a
sample label, compute per-sample ecotype abundances. R equivalent: EcoTyper_recovery_scRNA.R.
Tip
In short, with the scverse API (any AnnData of cells x genes, raw counts, sparse is fine):
import ecotyper as et
et.recovery(adata, "Carcinoma", cell_type_key="cell_type", sample_key="patient", n_jobs=8)
adata.obs["ecotyper_state_Fibroblasts"]; adata.uns["ecotyper"]["ecotype_abundance"]
Only the model genes are densified, so sparse objects with many cells stay cheap.
Input requirements¶
Expression: genes x cells, linear scale (counts, CPM or TPM; not log), gene symbols in the first column. The file is read with
data.table::freadsemantics (read_scrna_matrix()): cell IDs are kept verbatim, and rows with NA or duplicated gene names are dropped.Annotation:
ID(matching the matrix columns),CellType(one of the model’s cell types, e.g.Fibroblasts,CD8.T.cells), and optionallySample(enables ecotype recovery). Cells of other types are ignored.
2.1 Carcinoma model on colorectal cancer cells¶
Step 1: Load the data¶
from ecotyper.io import read_annotation, read_scrna_matrix, to_anndata
expr = read_scrna_matrix("ecotyper/example_data/scRNA_CRC_data.txt") # 19751 genes x 13780 cells
ann = read_annotation("ecotyper/example_data/scRNA_CRC_annotation.txt")
ann["CellType"].value_counts() # up to 1,500 cells per cell type
ann["Sample"].nunique() # 33 samples (tumor and normal)
adata = to_anndata(expr, ann) # 13780 cells x 19751 genes
If you start from your own AnnData object, put the raw-scale matrix in X or a layer (pass
layer=), and make sure adata.obs has CellType and Sample.
Step 2: Recover states¶
from ecotyper import workflows as wf
states, eco = wf.recover_scrna(adata, "Carcinoma", discovery_root="ecotyper/EcoTyper", n_jobs=8)
For each cell type, the cells of that type are log2(x+1) transformed, restricted to the model genes and scaled. NMF with the model’s W then gives every cell a state abundance profile, and the cell is assigned to its top state.
Step 3: Inspect the results¶
adata.obs["ecotyper_state_Fibroblasts"].value_counts(dropna=False) # NaN for non-fibroblasts
adata.obsm["ecotyper_abundance_Fibroblasts"]
# ecotypes are per sample, not per cell: they are returned, not stored in adata
eco.ecotype_abundance # ecotypes x samples
eco.initial_assignment # every sample: ID, MaxEcotype, AssignmentP/Q, Ecotype
eco.assignment # only samples assigned to an ecotype
For each state, the per-sample value is the fraction of that sample’s cells of the state’s cell type that are in the state. A sample’s ecotype abundance is the mean of these fractions over the ecotype’s states, normalized to sum to 1 across ecotypes.
Step 4 (optional): R-compatible output files¶
ecotyper-py recover-scrna -m ecotyper/example_data/scRNA_CRC_data.txt \
-a ecotyper/example_data/scRNA_CRC_annotation.txt -d Carcinoma \
--models ecotyper/EcoTyper -o RecoveryOut -t 8
RecoveryOut/RecoveryOutput/scRNA_CRC_data/
├── <cell type>/state_assignment.txt, state_abundances.txt, heatmap_data.txt, heatmap_top_ann.txt
└── Ecotypes/ecotype_abundance.txt, ecotype_assignment.txt
Use -s N (subsample=N) to cap the number of cells per cell type for very large datasets
(sampled with R’s RNG, seed 1234, as in R; values below 50 keep all cells).
2.2 Lymphoma model on lymphoma cells¶
The lymphoma example has no Sample column, so only cell states are recovered:
expr = read_scrna_matrix("ecotyper/example_data/scRNA_lymphoma_data.txt") # 14643 x 1000
ann = read_annotation("ecotyper/example_data/scRNA_lymphoma_annotation.txt") # ID, CellType, Tissue
adata = to_anndata(expr, ann)
states, eco = wf.recover_scrna(adata, "Lymphoma", discovery_root="ecotyper/EcoTyper")
eco is None # True: no Sample column
ecotyper-py recover-scrna -m ecotyper/example_data/scRNA_lymphoma_data.txt \
-a ecotyper/example_data/scRNA_lymphoma_annotation.txt -d Lymphoma \
--models ecotyper/EcoTyper -o RecoveryOut
The R tutorial passes -c Tissue to add the Tissue column to its heatmaps. The port does not
draw heatmaps, and all annotation columns are kept in adata.obs anyway.