Status: v0.2 beta.
- python >=3.9
- scanpy>=1.9
- anndata
- numpy
- pandas
- scipy
Juxtaposition & doublet detection for segmentation-based spatial transcriptomics (Xenium, MERFISH, CosMx). This is meant to be run it after annotation to find cells whose transcriptome mixes two identities.The pipeline labels each by the mixing/juxtaposed pair (e.g. Juxtaposed T cell - Macrophage).
This will work with your own broad + granular annotations (as this is a POST-ANNOTATION pipeline). While JuxtaScope does its own marker selection, it alternatively accepts custom marker signatures. Marker genes are identified for broad cellular identities (epithelial, vascular, immune, etc.) as well as within-compartment cellular identities (B cells, T cells, etc) to identify cross-compartment doublets (e.g., Immmune - Vascular) and within-compartment doublets (e.g., Dendritic cell - T cell).
A cell is only called a confident doublet when it is both:
- high in a foreign identity's markers (co-expresses another cell type's program), AND
- oversized — larger cell area or more transcripts than the pXX (default: p95 for cross-compartment and p99 for within-compartment) cutoff.
This AND-gate is deliberate, and it separates two physically different things that co-expression alone cannot tell apart:
doublet— high co-expression and oversized. Two cells merged into one segment: a segmentation error producing one chimeric "cell." In most cases, these should be excluded, unless biologically interesting (.ambiguous_embedded— high co-expression of multiple cell types but normal size. One real, correctly-sized cell whose transcriptome picks up a physically adjacent neighbor's signal because it sits embedded in another tissue — one example would be intraepithelial lymphocytes (IELs). In other cases, these cells just need a second-pass / another evaluation. Could be rare cell types that need to be re-annotated, or signal from an adjacent cell. After manual evaluation/curation, they could be re-classified as juxtaposed or as a singular cell type.misclassified— a strong foreign single identity, low co-expression, normal size: one cell that simply carries the wrong label.
Because embedded cells are frequently real, JuxtaScope keeps them separate from doublets by default. Set embedded_as_doublet=True if you want the stricter filter that treats them as doublets too. Preserve the information by default; collapse only when you choose to.
Size gating can be turned off entirely (size_gate=False) — then high co-expression alone flags a cell as ambiguous_embedded.
- Cross-compartment (broad key) — default contamination+size cutoff p95.
- Within-compartment (granular key) — default p99.
Both, plus the co-expression z thresholds, are user-settable:
cross_pctile, within_pctile, cross_z, within_z, size_pctile.
import scanpy as sc, juxtascope as js
ad = sc.read_h5ad("tissue.h5ad")
ad = js.detect(ad,
compartment_key="Compartment", # broad identity -> cross-compartment (p95)
celltype_key="cell_type", # granular identity -> within (p99)
size_gate=True, # require oversized for a doublet (default)
embedded_as_doublet=False, # keep IEL-type embedded cells separate (default)
save_to="tissue_juxta.h5ad", # write results to disk (omit to not save)
# custom_broad={...}, custom_fine={...}, # optional marker overrides
)
js.report(ad, celltype_key="cell_type", compartment_key="Compartment",
outdir="juxtascope_report")
# NOTE: detect() adds js_* columns to the RETURNED object. The input .h5ad on
# disk is unchanged unless you pass save_to=... (above) or save yourself:
# ad.write_h5ad("tissue_juxta.h5ad")obs columns (js_): js_status (clean / doublet / ambiguous_embedded /
misclassified), js_pair (the labeled mixing pair), js_level (cross/within),
js_own, js_foreign, js_cross_score, js_coexpr_markers.
report/ folder:
umap_broad,umap_granular,umap_juxtaposition(png+pdf)juxtaposition_pairs.csv— which type-pairs mix most, counts, co-expressed markerschosen_markers.csv— markers used to delineate each broad & granular typecelltype_metrics.csv— per cell type AND per pair: mean cell area, nucleus count, transcriptsmetric_*.png— bar charts of those metrics
ad = js.detect(ad, compartment_key="Compartment", celltype_key="cell_type",
custom_broad={"Immune":["Ptprc","Cd3e"], "Epithelial":["Epcam","Krt8"], ...},
custom_fine={"CD8 T":["Cd8a","Gzmb"], "Macrophage":["Cd68","Adgre1"], ...})Area/nucleus anomaly scoring is separate from the size gate and is OFF by default — in Xenium colon it did not add doublet signal beyond co-expression + the size gate. Check before enabling:
js.check_morphology_discriminates(ad, celltype_key="cell_type",
reference_flag_key="doublet_status")pip install -e . # from this directory (contains setup.py)
